feat(web): complete PBR shader, asset library and offscreen render chain
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

- 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:
mes123456
2026-11-11 14:59:43 +08:00
parent d1e6b63d54
commit a078648297
65 changed files with 6083 additions and 157 deletions

View File

@@ -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);