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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user