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

@@ -13,6 +13,7 @@ export type ShaderNodeEdit =
| {action:'SET_SOCKET';nodeId:string;socketId:string;direction:'INPUT'|'OUTPUT';value:number|boolean|number[]}
| {action:'SET_PROPERTY';nodeId:string;property:string;value:string|number|boolean}
| {action:'SET_IMAGE';nodeId:string;imageId:string}
| {action:'SET_IMAGE_COLORSPACE';nodeId:string;colorSpace:'SRGB'|'NON_COLOR'|'LINEAR'}
| {action:'LINK'|'UNLINK';nodeId:string;socketId:string;fromNodeId:string;fromSocketId:string};
export interface ShaderSocketIR {
@@ -83,8 +84,33 @@ function socket(value: unknown, path: string): value is ShaderSocketIR {
return true;
}
const supportedTypes = new Set<ShaderNodeType>(["RGB", "VALUE", "MATH", "IMAGE_TEXTURE", "NORMAL_MAP", "PRINCIPLED", "MATERIAL_OUTPUT"]);
const supportedTypes = new Set<ShaderNodeType>([
"RGB", "VALUE", "MIX", "MATH", "MAPPING", "TEX_COORD", "UV_MAP", "SEPARATE_COLOR",
"OBJECT_INFO", "IMAGE_TEXTURE", "NORMAL_MAP", "BUMP", "PRINCIPLED", "MATERIAL_OUTPUT",
]);
const supportedMathOperations = new Set(["ADD", "SUBTRACT", "MULTIPLY", "DIVIDE", "MINIMUM", "MAXIMUM"]);
const integerInRange = (value: unknown, minimum: number, maximum: number): boolean =>
typeof value === "number" && Number.isInteger(value) && value >= minimum && value <= maximum;
const nodePropertyRules: Partial<Record<ShaderNodeType, Record<string, (value: unknown) => boolean>>> = {
MATH: { operation: (value) => typeof value === "string" && supportedMathOperations.has(value) },
MIX: {
data_type: (value) => typeof value === "string",
blend_type: (value) => typeof value === "string",
clamp_factor: (value) => typeof value === "boolean",
clamp_result: (value) => typeof value === "boolean",
factor_mode: (value) => integerInRange(value, 0, 1),
},
MAPPING: { vector_type: (value) => typeof value === "string" },
NORMAL_MAP: { space: (value) => typeof value === "string", uv_map: (value) => typeof value === "string" },
BUMP: { invert: (value) => typeof value === "boolean" },
UV_MAP: { uv_map: (value) => typeof value === "string" },
SEPARATE_COLOR: { mode: (value) => value === "RGB" || value === "HSV" || value === "HSL" },
IMAGE_TEXTURE: {
interpolation: (value) => integerInRange(value, 0, 2),
extension: (value) => integerInRange(value, 0, 3),
projection: (value) => integerInRange(value, 0, 6),
},
};
export function parseShaderGraph(value: unknown): ShaderGraphIR {
if (!record(value) || value.schemaVersion !== SHADER_GRAPH_SCHEMA) throw new ShaderGraphError("PROTOCOL_MISMATCH", "Unsupported ShaderGraph schema");
@@ -117,13 +143,24 @@ export function validateShaderGraph(graph: ShaderGraphIR, context: ShaderGraphVa
nodes.set(node.id, node);
if (node.type === "UNSUPPORTED" || !supportedTypes.has(node.type)) issues.push({ code: "SHADER_NODE_UNSUPPORTED", message: `unsupported shader node: ${node.type}`, path: `nodes.${index}.type` });
const propertyKeys = Object.keys(node.properties ?? {});
if (node.type === "MATH") {
const operation = node.properties?.operation;
if (propertyKeys.length !== 1 || typeof operation !== "string" || !supportedMathOperations.has(operation)) {
const rules = node.type === "UNSUPPORTED" ? undefined : nodePropertyRules[node.type];
if (node.type === "MATH" && !rules) {
issues.push({ code: "SHADER_NODE_UNSUPPORTED", message: "Math has no writable property schema", path: `nodes.${index}.properties` });
}
else if (propertyKeys.length > 0 && !rules) {
issues.push({ code: "SHADER_NODE_UNSUPPORTED", message: `shader node properties are not writable for ${node.type}`, path: `nodes.${index}.properties` });
}
else if (rules) {
for (const key of propertyKeys) {
const check = rules[key];
if (!check || !check(node.properties?.[key])) {
issues.push({ code: "SHADER_NODE_UNSUPPORTED", message: `unsupported ${node.type} property: ${key}`, path: `nodes.${index}.properties.${key}` });
}
}
if (node.type === "MATH" && !propertyKeys.includes("operation")) {
issues.push({ code: "SHADER_NODE_UNSUPPORTED", message: "Math requires one supported operation property", path: `nodes.${index}.properties.operation` });
}
}
else if (propertyKeys.length > 0) issues.push({ code: "SHADER_NODE_UNSUPPORTED", message: `shader node properties are not writable for ${node.type}`, path: `nodes.${index}.properties` });
if (node.type === "IMAGE_TEXTURE" && (!node.imageId || !context.imageIds.has(node.imageId))) issues.push({ code: "SHADER_EXTERNAL_RESOURCE_MISSING", message: `shader node references a missing image: ${node.imageId ?? "none"}`, path: `nodes.${index}.imageId` });
else if (node.type === "IMAGE_TEXTURE" && node.imageId && context.blockedImageIds?.has(node.imageId)) issues.push({ code: "SHADER_EXTERNAL_RESOURCE_MISSING", message: `shader image is blocked by the library/path sandbox: ${node.imageId}`, path: `nodes.${index}.imageId` });
for (const item of node.sockets) {