feat(web): complete PBR shader, asset library and offscreen render chain
- generate a desktop-Blender PBR asset library and seed catalog v2 into OPFS - add React asset browser, material preview sphere and Web PBR render panel - extend shader graph/compiler to the finite 14-node set and native properties - add image colour-space editing, scene-camera still rendering and asset repair - add focused E2E/unit coverage and implementation status notes Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -15243,6 +15243,68 @@ bool web_engine_blend_main_set_material_image(WebBlendMainState *state,
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool web_apply_shader_node_property(WebBlendMainState *state,
|
||||
bNodeTree *tree,
|
||||
bNode *node,
|
||||
const std::string &property,
|
||||
const json &value,
|
||||
std::string &error)
|
||||
{
|
||||
StructRNA *rna_struct = node->type_legacy == SH_NODE_MIX ? RNA_ShaderNodeMix :
|
||||
node->type_legacy == SH_NODE_MIX_RGB_LEGACY ? RNA_ShaderNodeMixRGB :
|
||||
node->type_legacy == SH_NODE_MAPPING ? RNA_ShaderNodeMapping :
|
||||
node->type_legacy == SH_NODE_MATH ? RNA_ShaderNodeMath :
|
||||
node->type_legacy == SH_NODE_SEPARATE_COLOR ? RNA_ShaderNodeSeparateColor :
|
||||
node->type_legacy == SH_NODE_NORMAL_MAP ? RNA_ShaderNodeNormalMap :
|
||||
node->type_legacy == SH_NODE_BUMP ? RNA_ShaderNodeBump :
|
||||
node->type_legacy == SH_NODE_UVMAP ? RNA_ShaderNodeUVMap :
|
||||
node->type_legacy == SH_NODE_TEX_IMAGE ? RNA_ShaderNodeTexImage : nullptr;
|
||||
static const std::unordered_map<int, std::unordered_set<std::string>> allowed_properties = {
|
||||
{SH_NODE_MIX, {"data_type", "blend_type", "clamp_factor", "clamp_result", "factor_mode"}},
|
||||
{SH_NODE_MIX_RGB_LEGACY, {"blend_type", "use_clamp", "use_alpha"}},
|
||||
{SH_NODE_MAPPING, {"vector_type"}},
|
||||
{SH_NODE_MATH, {"operation"}},
|
||||
{SH_NODE_SEPARATE_COLOR, {"mode"}},
|
||||
{SH_NODE_NORMAL_MAP, {"space", "uv_map"}},
|
||||
{SH_NODE_BUMP, {"invert"}},
|
||||
{SH_NODE_UVMAP, {"uv_map"}},
|
||||
{SH_NODE_TEX_IMAGE, {"interpolation", "extension", "projection"}},
|
||||
};
|
||||
const auto allowed = allowed_properties.find(node->type_legacy);
|
||||
if (rna_struct == nullptr || allowed == allowed_properties.end() || !allowed->second.count(property)) {
|
||||
error = "SHADER_PROPERTY_UNSUPPORTED";
|
||||
return false;
|
||||
}
|
||||
PointerRNA pointer = RNA_pointer_create_discrete(&tree->id, rna_struct, node);
|
||||
PropertyRNA *prop = RNA_struct_find_property(&pointer, property.c_str());
|
||||
if (prop == nullptr || !RNA_property_editable(&pointer, prop)) {
|
||||
error = "SHADER_PROPERTY_NOT_EDITABLE";
|
||||
return false;
|
||||
}
|
||||
const PropertyType type = RNA_property_type(prop);
|
||||
bool written = false;
|
||||
if (type == PROP_ENUM) {
|
||||
if (value.is_number_integer()) { RNA_property_enum_set(&pointer, prop, value.get<int>()); written = true; }
|
||||
else if (value.is_string()) {
|
||||
int enum_value = 0;
|
||||
if (RNA_property_enum_value(nullptr, &pointer, prop, value.get<std::string>().c_str(), &enum_value)) {
|
||||
RNA_property_enum_set(&pointer, prop, enum_value);
|
||||
written = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (type == PROP_STRING && value.is_string()) { RNA_property_string_set(&pointer, prop, value.get<std::string>().c_str()); written = true; }
|
||||
else if (type == PROP_BOOLEAN && value.is_boolean()) { RNA_property_boolean_set(&pointer, prop, value.get<bool>()); written = true; }
|
||||
else if (type == PROP_INT && value.is_number_integer()) { RNA_property_int_set(&pointer, prop, value.get<int>()); written = true; }
|
||||
if (!written) {
|
||||
error = "SHADER_PROPERTY_VALUE_INVALID";
|
||||
return false;
|
||||
}
|
||||
BKE_ntree_update_tag_all(tree);
|
||||
BKE_ntree_update_after_single_tree_change(*state->main, *tree);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool web_engine_blend_main_edit_shader_node(WebBlendMainState *state,
|
||||
const char *material_id,
|
||||
const char *edit_json,
|
||||
@@ -15298,51 +15360,30 @@ bool web_engine_blend_main_edit_shader_node(WebBlendMainState *state,
|
||||
node->id = &image->id;
|
||||
id_us_plus(&image->id);
|
||||
}
|
||||
else if (action=="SET_PROPERTY") {
|
||||
const std::string property = edit.value("property", "");
|
||||
const json &value = edit.at("value");
|
||||
StructRNA *rna_struct = node->type_legacy == SH_NODE_MIX ? RNA_ShaderNodeMix :
|
||||
node->type_legacy == SH_NODE_MIX_RGB_LEGACY ? RNA_ShaderNodeMixRGB :
|
||||
node->type_legacy == SH_NODE_MAPPING ? RNA_ShaderNodeMapping :
|
||||
node->type_legacy == SH_NODE_NORMAL_MAP ? RNA_ShaderNodeNormalMap :
|
||||
node->type_legacy == SH_NODE_BUMP ? RNA_ShaderNodeBump :
|
||||
node->type_legacy == SH_NODE_UVMAP ? RNA_ShaderNodeUVMap :
|
||||
node->type_legacy == SH_NODE_TEX_IMAGE ? RNA_ShaderNodeTexImage : nullptr;
|
||||
static const std::unordered_map<int, std::unordered_set<std::string>> allowed_properties = {
|
||||
{SH_NODE_MIX, {"data_type", "blend_type", "clamp_factor", "clamp_result", "factor_mode"}},
|
||||
{SH_NODE_MIX_RGB_LEGACY, {"blend_type", "use_clamp", "use_alpha"}},
|
||||
{SH_NODE_MAPPING, {"vector_type"}},
|
||||
{SH_NODE_NORMAL_MAP, {"space", "uv_map"}},
|
||||
{SH_NODE_BUMP, {"invert"}},
|
||||
{SH_NODE_UVMAP, {"uv_map"}},
|
||||
{SH_NODE_TEX_IMAGE, {"interpolation", "extension", "projection"}},
|
||||
};
|
||||
const auto allowed = allowed_properties.find(node->type_legacy);
|
||||
if (rna_struct == nullptr || allowed == allowed_properties.end() || !allowed->second.count(property)) {
|
||||
error = "SHADER_PROPERTY_UNSUPPORTED";
|
||||
return false;
|
||||
}
|
||||
PointerRNA pointer = RNA_pointer_create_discrete(&tree->id, rna_struct, node);
|
||||
PropertyRNA *prop = RNA_struct_find_property(&pointer, property.c_str());
|
||||
if (prop == nullptr || !RNA_property_editable(&pointer, prop)) { error = "SHADER_PROPERTY_NOT_EDITABLE"; return false; }
|
||||
const PropertyType type = RNA_property_type(prop);
|
||||
bool written = false;
|
||||
if (type == PROP_ENUM) {
|
||||
if (value.is_number_integer()) { RNA_property_enum_set(&pointer, prop, value.get<int>()); written = true; }
|
||||
else if (value.is_string()) {
|
||||
int enum_value = 0;
|
||||
if (RNA_property_enum_value(nullptr, &pointer, prop, value.get<std::string>().c_str(), &enum_value)) {
|
||||
RNA_property_enum_set(&pointer, prop, enum_value);
|
||||
written = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (type == PROP_STRING && value.is_string()) { RNA_property_string_set(&pointer, prop, value.get<std::string>().c_str()); written = true; }
|
||||
else if (type == PROP_BOOLEAN && value.is_boolean()) { RNA_property_boolean_set(&pointer, prop, value.get<bool>()); written = true; }
|
||||
else if (type == PROP_INT && value.is_number_integer()) { RNA_property_int_set(&pointer, prop, value.get<int>()); written = true; }
|
||||
if (!written) { error = "SHADER_PROPERTY_VALUE_INVALID"; return false; }
|
||||
else if (action=="SET_IMAGE_COLORSPACE") {
|
||||
if (node->type_legacy != SH_NODE_TEX_IMAGE || node->id == nullptr) { error = "SHADER_IMAGE_NODE_REQUIRED"; return false; }
|
||||
Image *image = reinterpret_cast<Image *>(node->id);
|
||||
if (image->id.lib != nullptr) { error = "SHADER_IMAGE_LIBRARY_LINKED"; return false; }
|
||||
const std::string requested = edit.value("colorSpace", "");
|
||||
const std::string colorspace = requested == "SRGB" ? "sRGB" :
|
||||
requested == "NON_COLOR" ? "Non-Color" :
|
||||
requested == "LINEAR" ? "Linear Rec.709" : "";
|
||||
if (colorspace.empty()) { error = "SHADER_COLORSPACE_UNSUPPORTED"; return false; }
|
||||
PointerRNA image_pointer = RNA_id_pointer_create(&image->id);
|
||||
PropertyRNA *settings_prop = RNA_struct_find_property(&image_pointer, "colorspace_settings");
|
||||
if (settings_prop == nullptr) { error = "SHADER_COLORSPACE_UNSUPPORTED"; return false; }
|
||||
PointerRNA settings = RNA_property_pointer_get(&image_pointer, settings_prop);
|
||||
PropertyRNA *name_prop = RNA_struct_find_property(&settings, "name");
|
||||
if (name_prop == nullptr || !RNA_property_editable(&settings, name_prop)) { error = "SHADER_COLORSPACE_NOT_EDITABLE"; return false; }
|
||||
int enum_value = 0;
|
||||
if (!RNA_property_enum_value(nullptr, &settings, name_prop, colorspace.c_str(), &enum_value)) { error = "SHADER_COLORSPACE_UNSUPPORTED"; return false; }
|
||||
RNA_property_enum_set(&settings, name_prop, enum_value);
|
||||
BKE_ntree_update_tag_all(tree);
|
||||
BKE_ntree_update_after_single_tree_change(*state->main, *tree);
|
||||
material->id.recalc |= ID_RECALC_SYNC_TO_EVAL;
|
||||
}
|
||||
else if (action=="SET_PROPERTY") {
|
||||
if (!web_apply_shader_node_property(state, tree, node, edit.value("property", ""), edit.at("value"), error)) return false;
|
||||
}
|
||||
else if (action=="SET_SOCKET") {
|
||||
const bool output=edit.value("direction","")=="OUTPUT";
|
||||
@@ -15433,6 +15474,7 @@ bool web_engine_blend_main_set_shader_graph(WebBlendMainState *state,
|
||||
std::string name;
|
||||
Image *image = nullptr;
|
||||
int math_operation = NODE_MATH_ADD;
|
||||
json properties = json::object();
|
||||
std::unordered_map<std::string, SocketSpec> sockets;
|
||||
};
|
||||
std::unordered_map<std::string, NodeSpec> specs;
|
||||
@@ -15481,10 +15523,12 @@ bool web_engine_blend_main_set_shader_graph(WebBlendMainState *state,
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (node.contains("properties") &&
|
||||
(!node["properties"].is_object() || !node["properties"].empty())) {
|
||||
error = "SHADER_NODE_UNSUPPORTED: node properties are not writable in schema 1";
|
||||
return false;
|
||||
else if (node.contains("properties")) {
|
||||
if (!node["properties"].is_object()) {
|
||||
error = "SHADER_INVALID_GRAPH: node properties must be an object";
|
||||
return false;
|
||||
}
|
||||
spec.properties = node["properties"];
|
||||
}
|
||||
if (spec.type == "MATERIAL_OUTPUT") output_count++;
|
||||
if (spec.type == "IMAGE_TEXTURE") {
|
||||
@@ -15590,6 +15634,9 @@ bool web_engine_blend_main_set_shader_graph(WebBlendMainState *state,
|
||||
STRNCPY(node->name, spec.name.c_str());
|
||||
STRNCPY(node->label, spec.name.c_str());
|
||||
if (spec.type == "MATH") node->custom1 = spec.math_operation;
|
||||
for (const auto &item : spec.properties.items()) {
|
||||
if (!web_apply_shader_node_property(state, tree, node, item.key(), item.value(), error)) return false;
|
||||
}
|
||||
if (spec.image != nullptr) {
|
||||
node->id = &spec.image->id;
|
||||
id_us_plus(&spec.image->id);
|
||||
|
||||
Reference in New Issue
Block a user