feat(web): extend scene render audio and unit authoring
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

This commit is contained in:
mes123456
2026-08-29 13:13:04 +08:00
parent b1ec8d29f6
commit ab03be9046
118 changed files with 7549 additions and 44 deletions

View File

@@ -1592,7 +1592,7 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
type == "insertGreasePencilFrame" || type == "removeGreasePencilFrame" ||
type == "setGreasePencilStrokes" || type == "setVertexColors" ||
type == "setVertexWeights" || type == "setCameraProperties" || type == "setLightProperties" ||
type == "setWorldProperties" ||
type == "setWorldProperties" || type == "setSceneProperties" || type == "setSceneRenderSettings" || type == "setSceneAudioSettings" || type == "setSceneUnitSettings" ||
type == "setSculptMeshAttributes" ||
type == "sculptStroke") {
if (!engine->authoritative_commands || engine->authoritative_main == nullptr) {
@@ -2196,12 +2196,34 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
command.value("normalize", false), command.value("limit", 0u), command.value("mirror", false),
command.value("mirrorAxis", 0), command.value("mirrorTolerance", 1e-4f), main_error);
}
else if (type == "setCameraProperties" || type == "setLightProperties" || type == "setWorldProperties")
else if (type == "setCameraProperties" || type == "setLightProperties" || type == "setWorldProperties" || type == "setSceneProperties" || type == "setSceneRenderSettings" || type == "setSceneAudioSettings" || type == "setSceneUnitSettings")
{
const json properties = command.value("properties", json::object());
applied = web_engine_blend_main_set_render_properties(
engine->authoritative_main, command.value("dataId", "").c_str(),
properties.dump().c_str(), main_error);
if (type == "setSceneProperties") {
applied = web_engine_blend_main_set_scene_properties(
engine->authoritative_main, command.value("dataId", "").c_str(),
properties.dump().c_str(), main_error);
}
else if (type == "setSceneRenderSettings") {
applied = web_engine_blend_main_set_scene_render_settings(
engine->authoritative_main, command.value("dataId", "").c_str(),
properties.dump().c_str(), main_error);
}
else if (type == "setSceneAudioSettings") {
applied = web_engine_blend_main_set_scene_audio_settings(
engine->authoritative_main, command.value("dataId", "").c_str(),
properties.dump().c_str(), main_error);
}
else if (type == "setSceneUnitSettings") {
applied = web_engine_blend_main_set_scene_unit_settings(
engine->authoritative_main, command.value("dataId", "").c_str(),
properties.dump().c_str(), main_error);
}
else {
applied = web_engine_blend_main_set_render_properties(
engine->authoritative_main, command.value("dataId", "").c_str(),
properties.dump().c_str(), main_error);
}
}
else if (type == "setSculptMeshAttributes") {
const json attributes = command.value("attributes", json::object());

View File

@@ -5249,15 +5249,37 @@ json scene_ir_from_blend(const ParsedBlend &blend,
scene_fps = int(read_integer(*blend.sdna, *render, "frs_sec").value_or(24));
scene_fps_base = read_float(*blend.sdna, *render, "frs_sec_base").value_or(1.0f);
scene["renderEngine"] = read_string(*blend.sdna, *render, "engine");
scene["renderSettings"] = {{"resolutionX", read_integer(*blend.sdna, *render, "xsch").value_or(1920)},
{"resolutionY", read_integer(*blend.sdna, *render, "ysch").value_or(1080)},
{"resolutionPercentage", read_integer(*blend.sdna, *render, "size").value_or(100)},
{"frameStart", frame_start},
{"frameEnd", frame_end},
{"outputPath", read_string(*blend.sdna, *render, "pic")}};
const int64_t render_flags = read_integer(*blend.sdna, *render, "flag").value_or(0);
if ((render_flags & SCER_PRV_RANGE) != 0) {
scene["previewRange"] = {{"start", read_integer(*blend.sdna, *render, "psfra").value_or(frame_start)},
{"end", read_integer(*blend.sdna, *render, "pefra").value_or(frame_end)}};
}
}
if (const std::optional<ElementRef> audio = embedded_element(*blend.sdna, element, "audio")) {
const int64_t audio_flags = read_integer(*blend.sdna, *audio, "flag").value_or(AUDIO_SYNC);
scene["audioSettings"] = {{"volume", read_float(*blend.sdna, *audio, "volume").value_or(1.0f)},
{"speedOfSound", read_float(*blend.sdna, *audio, "speed_of_sound").value_or(343.3f)},
{"dopplerFactor", read_float(*blend.sdna, *audio, "doppler_factor").value_or(1.0f)},
{"distanceModel", read_integer(*blend.sdna, *audio, "distance_model").value_or(2)},
{"muted", (audio_flags & AUDIO_MUTE) != 0},
{"sync", (audio_flags & AUDIO_SYNC) != 0}};
}
if (const std::optional<ElementRef> units = embedded_element(*blend.sdna, element, "unit")) {
unit_system = int(read_integer(*blend.sdna, *units, "system").value_or(unit_system));
unit_scale = read_float(*blend.sdna, *units, "scale_length").value_or(unit_scale);
scene["unitSettings"] = {{"system", read_integer(*blend.sdna, *units, "system").value_or(0)},
{"scaleLength", read_float(*blend.sdna, *units, "scale_length").value_or(0.0f)},
{"rotationSystem", read_integer(*blend.sdna, *units, "system_rotation").value_or(0)},
{"lengthUnit", read_integer(*blend.sdna, *units, "length_unit").value_or(0)},
{"massUnit", read_integer(*blend.sdna, *units, "mass_unit").value_or(0)},
{"timeUnit", read_integer(*blend.sdna, *units, "time_unit").value_or(0)},
{"temperatureUnit", read_integer(*blend.sdna, *units, "temperature_unit").value_or(0)}};
}
if (const std::optional<ElementRef> view = embedded_element(*blend.sdna, element, "view_settings")) {
json color_management = {{"viewTransform", read_string(*blend.sdna, *view, "view_transform")},

View File

@@ -350,6 +350,14 @@ World *find_world(Main *main, const char *data_id)
return nullptr;
}
Scene *find_scene(Main *main, const char *data_id)
{
if (main == nullptr || data_id == nullptr || strncmp(data_id, "scene:", 6) != 0) return nullptr;
const std::string name(data_id + 6);
for (Scene &scene : main->scenes) if (id_name(scene.id) == name) return &scene;
return nullptr;
}
GreasePencil *find_grease_pencil(Main *main, const char *data_id)
{
if (main == nullptr || data_id == nullptr || strncmp(data_id, "grease-pencil:", 14) != 0) return nullptr;
@@ -5696,6 +5704,286 @@ bool web_engine_blend_main_set_render_properties(WebBlendMainState *state,
return false;
}
bool web_engine_blend_main_set_scene_properties(WebBlendMainState *state,
const char *target_id,
const char *properties_json,
std::string &error)
{
if (state == nullptr || state->main == nullptr || target_id == nullptr || properties_json == nullptr) {
error = "RENDER_PROPERTY_INVALID: scene property target is missing";
return false;
}
Scene *scene = find_scene(state->main, target_id);
if (scene == nullptr || scene->id.lib != nullptr) {
error = "LINKED_DATA_MUTATION_BLOCKED: Scene is not editable";
return false;
}
const json properties = json::parse(properties_json, nullptr, false);
if (!properties.is_object() || properties.empty()) {
error = "RENDER_PROPERTY_INVALID: scene properties must be a non-empty object";
return false;
}
const std::unordered_set<std::string> allowed = {"renderEngine", "viewTransform", "look", "exposure", "gamma"};
for (const auto &[key, value] : properties.items()) {
(void)value;
if (!allowed.contains(key)) {
error = "RENDER_PROPERTY_INVALID: unsupported scene property " + key;
return false;
}
}
const auto read_string = [&](const char *key, char *target, size_t capacity) {
if (!properties.contains(key)) return true;
if (!properties[key].is_string()) {
error = std::string("RENDER_PROPERTY_INVALID: ") + key + " must be a string";
return false;
}
const std::string value = properties[key].get<std::string>();
if (value.empty() || value.size() >= capacity) {
error = std::string("RENDER_PROPERTY_INVALID: ") + key + " exceeds the bounded string size";
return false;
}
BLI_strncpy_utf8(target, value.c_str(), capacity);
return true;
};
const auto read_float = [&](const char *key, float minimum, float maximum, float &target) {
if (!properties.contains(key)) return true;
if (!properties[key].is_number()) {
error = std::string("RENDER_PROPERTY_INVALID: ") + key + " must be numeric";
return false;
}
const float value = properties[key].get<float>();
if (!std::isfinite(value) || value < minimum || value > maximum) {
error = std::string("RENDER_PROPERTY_INVALID: ") + key + " is outside the bounded range";
return false;
}
target = value;
return true;
};
if (!read_string("renderEngine", scene->r.engine, sizeof(scene->r.engine)) ||
!read_string("viewTransform", scene->view_settings.view_transform, sizeof(scene->view_settings.view_transform)) ||
!read_string("look", scene->view_settings.look, sizeof(scene->view_settings.look)) ||
!read_float("exposure", -1000.0f, 1000.0f, scene->view_settings.exposure) ||
!read_float("gamma", 0.0f, 1000.0f, scene->view_settings.gamma)) {
return false;
}
scene->id.recalc |= ID_RECALC_PARAMETERS;
return true;
}
bool web_engine_blend_main_set_scene_render_settings(WebBlendMainState *state,
const char *target_id,
const char *properties_json,
std::string &error)
{
if (state == nullptr || state->main == nullptr || target_id == nullptr || properties_json == nullptr) {
error = "RENDER_PROPERTY_INVALID: scene render target is missing";
return false;
}
Scene *scene = find_scene(state->main, target_id);
if (scene == nullptr || scene->id.lib != nullptr) {
error = "LINKED_DATA_MUTATION_BLOCKED: Scene is not editable";
return false;
}
const json properties = json::parse(properties_json, nullptr, false);
if (!properties.is_object() || properties.empty()) {
error = "RENDER_PROPERTY_INVALID: scene render settings must be a non-empty object";
return false;
}
const std::unordered_set<std::string> allowed = {"resolutionX", "resolutionY", "resolutionPercentage", "frameStart", "frameEnd", "outputPath"};
for (const auto &[key, value] : properties.items()) {
(void)value;
if (!allowed.contains(key)) {
error = "RENDER_PROPERTY_INVALID: unsupported scene render property " + key;
return false;
}
}
const auto read_int = [&](const char *key, int minimum, int maximum, int &target) {
if (!properties.contains(key)) return true;
if (!properties[key].is_number_integer()) {
error = std::string("RENDER_PROPERTY_INVALID: ") + key + " must be an integer";
return false;
}
const int64_t value = properties[key].get<int64_t>();
if (value < minimum || value > maximum) {
error = std::string("RENDER_PROPERTY_INVALID: ") + key + " is outside the bounded range";
return false;
}
target = int(value);
return true;
};
int resolution_percentage = scene->r.size;
if (!read_int("resolutionX", 1, 65536, scene->r.xsch) ||
!read_int("resolutionY", 1, 65536, scene->r.ysch) ||
!read_int("resolutionPercentage", 1, 1000, resolution_percentage) ||
!read_int("frameStart", -1000000, 1000000, scene->r.sfra) ||
!read_int("frameEnd", -1000000, 1000000, scene->r.efra)) {
return false;
}
scene->r.size = short(resolution_percentage);
if (scene->r.efra < scene->r.sfra) {
error = "RENDER_PROPERTY_INVALID: frameEnd must not precede frameStart";
return false;
}
if (properties.contains("outputPath")) {
if (!properties["outputPath"].is_string()) {
error = "RENDER_PROPERTY_INVALID: outputPath must be a string";
return false;
}
const std::string output_path = properties["outputPath"].get<std::string>();
if (output_path.empty() || output_path.size() >= sizeof(scene->r.pic)) {
error = "RENDER_PROPERTY_INVALID: outputPath exceeds the bounded path size";
return false;
}
BLI_strncpy_utf8(scene->r.pic, output_path.c_str(), sizeof(scene->r.pic));
}
scene->id.recalc |= ID_RECALC_PARAMETERS;
return true;
}
bool web_engine_blend_main_set_scene_audio_settings(WebBlendMainState *state,
const char *target_id,
const char *properties_json,
std::string &error)
{
if (state == nullptr || state->main == nullptr || target_id == nullptr || properties_json == nullptr) {
error = "RENDER_PROPERTY_INVALID: scene audio target is missing";
return false;
}
Scene *scene = find_scene(state->main, target_id);
if (scene == nullptr || scene->id.lib != nullptr) {
error = "LINKED_DATA_MUTATION_BLOCKED: Scene is not editable";
return false;
}
const json properties = json::parse(properties_json, nullptr, false);
if (!properties.is_object() || properties.empty()) {
error = "RENDER_PROPERTY_INVALID: scene audio settings must be a non-empty object";
return false;
}
const std::unordered_set<std::string> allowed = {"volume", "speedOfSound", "dopplerFactor", "distanceModel", "muted", "sync"};
for (const auto &[key, value] : properties.items()) {
(void)value;
if (!allowed.contains(key)) {
error = "RENDER_PROPERTY_INVALID: unsupported scene audio property " + key;
return false;
}
}
const auto read_float = [&](const char *key, float minimum, float maximum, float &target) {
if (!properties.contains(key)) return true;
if (!properties[key].is_number()) {
error = std::string("RENDER_PROPERTY_INVALID: ") + key + " must be numeric";
return false;
}
const float value = properties[key].get<float>();
if (!std::isfinite(value) || value < minimum || value > maximum) {
error = std::string("RENDER_PROPERTY_INVALID: ") + key + " is outside the bounded range";
return false;
}
target = value;
return true;
};
if (!read_float("volume", 0.0f, 10.0f, scene->audio.volume) ||
!read_float("speedOfSound", 0.0f, 10000.0f, scene->audio.speed_of_sound) ||
!read_float("dopplerFactor", 0.0f, 100.0f, scene->audio.doppler_factor)) {
return false;
}
if (properties.contains("distanceModel")) {
if (!properties["distanceModel"].is_number_integer()) {
error = "RENDER_PROPERTY_INVALID: distanceModel must be an integer";
return false;
}
const int64_t model = properties["distanceModel"].get<int64_t>();
if (model < 0 || model > 32) {
error = "RENDER_PROPERTY_INVALID: distanceModel is outside the bounded range";
return false;
}
scene->audio.distance_model = int(model);
}
const auto read_flag = [&](const char *key, short flag) {
if (!properties.contains(key)) return true;
if (!properties[key].is_boolean()) {
error = std::string("RENDER_PROPERTY_INVALID: ") + key + " must be boolean";
return false;
}
if (properties[key].get<bool>()) scene->audio.flag |= flag;
else scene->audio.flag &= ~flag;
return true;
};
if (!read_flag("muted", AUDIO_MUTE) || !read_flag("sync", AUDIO_SYNC)) return false;
scene->id.recalc |= ID_RECALC_PARAMETERS;
return true;
}
bool web_engine_blend_main_set_scene_unit_settings(WebBlendMainState *state,
const char *target_id,
const char *properties_json,
std::string &error)
{
if (state == nullptr || state->main == nullptr || target_id == nullptr || properties_json == nullptr) {
error = "RENDER_PROPERTY_INVALID: scene unit target is missing";
return false;
}
Scene *scene = find_scene(state->main, target_id);
if (scene == nullptr || scene->id.lib != nullptr) {
error = "LINKED_DATA_MUTATION_BLOCKED: Scene is not editable";
return false;
}
const json properties = json::parse(properties_json, nullptr, false);
if (!properties.is_object() || properties.empty()) {
error = "RENDER_PROPERTY_INVALID: scene unit settings must be a non-empty object";
return false;
}
const std::unordered_set<std::string> allowed = {"system", "scaleLength", "rotationSystem", "lengthUnit", "massUnit", "timeUnit", "temperatureUnit"};
for (const auto &[key, value] : properties.items()) {
(void)value;
if (!allowed.contains(key)) {
error = "RENDER_PROPERTY_INVALID: unsupported scene unit property " + key;
return false;
}
}
const auto read_int = [&](const char *key, int minimum, int maximum, int &target) {
if (!properties.contains(key)) return true;
if (!properties[key].is_number_integer()) {
error = std::string("RENDER_PROPERTY_INVALID: ") + key + " must be an integer";
return false;
}
const int64_t value = properties[key].get<int64_t>();
if (value < minimum || value > maximum) {
error = std::string("RENDER_PROPERTY_INVALID: ") + key + " is outside the bounded range";
return false;
}
target = int(value);
return true;
};
const auto read_char = [&](const char *key, int minimum, int maximum, char &target) {
int value = target;
if (!read_int(key, minimum, maximum, value)) return false;
target = char(value);
return true;
};
if (!read_char("system", 0, 8, scene->unit.system) ||
!read_char("rotationSystem", 0, 8, scene->unit.system_rotation) ||
!read_char("lengthUnit", 0, 32, scene->unit.length_unit) ||
!read_char("massUnit", 0, 32, scene->unit.mass_unit) ||
!read_char("timeUnit", 0, 32, scene->unit.time_unit) ||
!read_char("temperatureUnit", 0, 32, scene->unit.temperature_unit)) {
return false;
}
if (properties.contains("scaleLength")) {
if (!properties["scaleLength"].is_number()) {
error = "RENDER_PROPERTY_INVALID: scaleLength must be numeric";
return false;
}
const float scale = properties["scaleLength"].get<float>();
if (!std::isfinite(scale) || scale < 0.0f || scale > 1.0e6f) {
error = "RENDER_PROPERTY_INVALID: scaleLength is outside the bounded range";
return false;
}
scene->unit.scale_length = scale;
}
scene->id.recalc |= ID_RECALC_PARAMETERS;
return true;
}
bool web_engine_blend_main_write(WebBlendMainState *state,
std::vector<uint8_t> &bytes,
std::string &error)

View File

@@ -516,6 +516,22 @@ bool web_engine_blend_main_set_render_properties(WebBlendMainState *state,
const char *target_id,
const char *properties_json,
std::string &error);
bool web_engine_blend_main_set_scene_properties(WebBlendMainState *state,
const char *target_id,
const char *properties_json,
std::string &error);
bool web_engine_blend_main_set_scene_render_settings(WebBlendMainState *state,
const char *target_id,
const char *properties_json,
std::string &error);
bool web_engine_blend_main_set_scene_audio_settings(WebBlendMainState *state,
const char *target_id,
const char *properties_json,
std::string &error);
bool web_engine_blend_main_set_scene_unit_settings(WebBlendMainState *state,
const char *target_id,
const char *properties_json,
std::string &error);
bool web_engine_blend_main_write(WebBlendMainState *state,
std::vector<uint8_t> &bytes,
std::string &error);