feat(web): extend scene render audio and unit authoring
This commit is contained in:
@@ -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());
|
||||
|
||||
@@ -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")},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user