|
|
|
|
@@ -16,9 +16,16 @@ export type ShaderTextureColorSpace = "SRGB" | "NON_COLOR" | "LINEAR";
|
|
|
|
|
export const SHADER_COMPILE_ALLOWLIST = [
|
|
|
|
|
"RGB",
|
|
|
|
|
"VALUE",
|
|
|
|
|
"MIX",
|
|
|
|
|
"MATH",
|
|
|
|
|
"MAPPING",
|
|
|
|
|
"TEX_COORD",
|
|
|
|
|
"UV_MAP",
|
|
|
|
|
"SEPARATE_COLOR",
|
|
|
|
|
"OBJECT_INFO",
|
|
|
|
|
"IMAGE_TEXTURE",
|
|
|
|
|
"NORMAL_MAP",
|
|
|
|
|
"BUMP",
|
|
|
|
|
"PRINCIPLED",
|
|
|
|
|
"OUTPUT",
|
|
|
|
|
] as const;
|
|
|
|
|
@@ -51,6 +58,19 @@ export interface ShaderCompiledMaterial {
|
|
|
|
|
normalImageId?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type ShaderTextureUsage = "BASE_COLOR" | "NORMAL" | "BUMP";
|
|
|
|
|
export interface ShaderTextureBindingIR {
|
|
|
|
|
imageId: string;
|
|
|
|
|
usage: ShaderTextureUsage;
|
|
|
|
|
strength?: number;
|
|
|
|
|
uvMap?: string;
|
|
|
|
|
uvTransform?: {
|
|
|
|
|
offset: [number, number];
|
|
|
|
|
rotation: number;
|
|
|
|
|
scale: [number, number];
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ShaderCompileReport {
|
|
|
|
|
schemaVersion: typeof SHADER_COMPILE_SCHEMA;
|
|
|
|
|
taskId: typeof SHADER_COMPILE_TASK;
|
|
|
|
|
@@ -61,7 +81,7 @@ export interface ShaderCompileReport {
|
|
|
|
|
compileKey: string | null;
|
|
|
|
|
nodeOrder: string[];
|
|
|
|
|
compiledNodeTypes: CompileNodeType[];
|
|
|
|
|
textureBindings: Array<{ imageId: string; usage: "BASE_COLOR" | "NORMAL" }>;
|
|
|
|
|
textureBindings: ShaderTextureBindingIR[];
|
|
|
|
|
instructions: Array<{ nodeId: string; type: CompileNodeType; operation?: string }>;
|
|
|
|
|
material?: ShaderCompiledMaterial;
|
|
|
|
|
issues: Issue[];
|
|
|
|
|
@@ -83,7 +103,7 @@ export interface ShaderCompileKeyInput {
|
|
|
|
|
rendererBackend: string;
|
|
|
|
|
textures: ReadonlyArray<{
|
|
|
|
|
imageId: string;
|
|
|
|
|
usage: "BASE_COLOR" | "NORMAL";
|
|
|
|
|
usage: ShaderTextureUsage;
|
|
|
|
|
assetId: string | null;
|
|
|
|
|
sha256: string | null;
|
|
|
|
|
colorSpace: ShaderTextureColorSpace;
|
|
|
|
|
@@ -153,7 +173,7 @@ function normalizeSocket(value: string): string {
|
|
|
|
|
|
|
|
|
|
function scalarSocket(value: string): boolean {
|
|
|
|
|
const normalized = normalizeSocket(value);
|
|
|
|
|
return normalized === "value" || normalized === "value001" || normalized === "a" || normalized === "b";
|
|
|
|
|
return normalized === "value" || normalized === "value001" || normalized === "a" || normalized === "b" || normalized === "factor";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function canonicalGraph(material: MaterialIR): string {
|
|
|
|
|
@@ -250,7 +270,7 @@ function sourceLink(incoming: ReadonlyMap<string, MaterialLinkIR>, nodeId: strin
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isScalarSource(node: MaterialNodeIR | undefined): boolean {
|
|
|
|
|
return node?.type === "VALUE" || node?.type === "MATH";
|
|
|
|
|
return node?.type === "VALUE" || node?.type === "MATH" || node?.type === "MIX" || node?.type === "SEPARATE_COLOR";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function targetScalarSocket(socket: string): boolean {
|
|
|
|
|
@@ -321,7 +341,6 @@ export function compileMaterialGraph(material: MaterialIR, context: ShaderCompil
|
|
|
|
|
byId.set(node.id, node);
|
|
|
|
|
if (!(SHADER_COMPILE_ALLOWLIST as readonly string[]).includes(node.type)) base.issues.push(issue("SHADER_NODE_UNSUPPORTED", `shader node ${node.type} is outside the M10-07 compiler allowlist`, `nodes.${index}.type`));
|
|
|
|
|
if (node.type === "MATH" && !["ADD", "SUBTRACT", "MULTIPLY", "DIVIDE", "MINIMUM", "MAXIMUM"].includes(node.properties?.operation ?? "")) base.issues.push(issue("SHADER_NODE_UNSUPPORTED", "Math operation is outside the M10-07 compiler allowlist", `nodes.${index}.properties.operation`));
|
|
|
|
|
if (node.type !== "MATH" && node.properties && Object.keys(node.properties).length > 0) base.issues.push(issue("SHADER_NODE_UNSUPPORTED", `properties are not compiled for ${node.type}`, `nodes.${index}.properties`));
|
|
|
|
|
}
|
|
|
|
|
const incoming = new Map<string, MaterialLinkIR>();
|
|
|
|
|
const edges = new Map<string, string[]>();
|
|
|
|
|
@@ -363,7 +382,7 @@ export function compileMaterialGraph(material: MaterialIR, context: ShaderCompil
|
|
|
|
|
const output = outputs[0];
|
|
|
|
|
const shader = principled[0];
|
|
|
|
|
if (output && shader && !sourceLink(incoming, output.id, "Surface")) base.issues.push(issue("SHADER_INVALID_GRAPH", "Material Output Surface is not connected", "links"));
|
|
|
|
|
const textures = new Map<string, { imageId: string; usage: "BASE_COLOR" | "NORMAL" }>();
|
|
|
|
|
const textures = new Map<string, ShaderTextureBindingIR>();
|
|
|
|
|
const compiled: ShaderCompiledMaterial = {
|
|
|
|
|
baseColor: material.baseColor,
|
|
|
|
|
roughness: material.roughness,
|
|
|
|
|
@@ -377,6 +396,72 @@ export function compileMaterialGraph(material: MaterialIR, context: ShaderCompil
|
|
|
|
|
emissionStrength: material.emissionStrength,
|
|
|
|
|
emissionColor: material.emissionColor,
|
|
|
|
|
};
|
|
|
|
|
const socketNumber = (node: MaterialNodeIR, names: string[]): number | undefined => {
|
|
|
|
|
for (const socket of node.sockets ?? []) {
|
|
|
|
|
if (socket.direction !== "INPUT") continue;
|
|
|
|
|
if (!names.includes(normalizeSocket(socket.id)) && !names.includes(normalizeSocket(socket.name))) continue;
|
|
|
|
|
if (finite(socket.value)) return socket.value;
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
};
|
|
|
|
|
const socketVector2 = (node: MaterialNodeIR, names: string[]): [number, number] | undefined => {
|
|
|
|
|
for (const socket of node.sockets ?? []) {
|
|
|
|
|
if (socket.direction !== "INPUT") continue;
|
|
|
|
|
if (!names.includes(normalizeSocket(socket.id)) && !names.includes(normalizeSocket(socket.name))) continue;
|
|
|
|
|
if (Array.isArray(socket.value) && socket.value.length >= 2 && finite(socket.value[0]) && finite(socket.value[1])) {
|
|
|
|
|
return [socket.value[0], socket.value[1]];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
};
|
|
|
|
|
const meshUvMap = (node: MaterialNodeIR): string | undefined => {
|
|
|
|
|
const properties = node.properties as Record<string, unknown> | undefined;
|
|
|
|
|
const value = properties?.uvMap ?? properties?.uv_map;
|
|
|
|
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
|
|
|
};
|
|
|
|
|
const textureUv = (imageNode: MaterialNodeIR): { uvMap?: string; uvTransform?: ShaderTextureBindingIR["uvTransform"] } => {
|
|
|
|
|
const vectorLink = sourceLink(incoming, imageNode.id, "Vector");
|
|
|
|
|
if (!vectorLink) return {};
|
|
|
|
|
const source = byId.get(vectorLink.fromNodeId);
|
|
|
|
|
if (!source) return {};
|
|
|
|
|
if (source.type === "UV_MAP") return { uvMap: meshUvMap(source) };
|
|
|
|
|
if (source.type === "TEX_COORD") return {};
|
|
|
|
|
if (source.type !== "MAPPING") {
|
|
|
|
|
base.issues.push(issue("SHADER_NODE_UNSUPPORTED", "Texture Vector must come from Mapping, UV Map, or Texture Coordinate", `nodes.${imageNode.id}.Vector`));
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
const offset = socketVector2(source, ["location"]) ?? [0, 0];
|
|
|
|
|
const scale = socketVector2(source, ["scale"]) ?? [1, 1];
|
|
|
|
|
const rotation = socketVector2(source, ["rotation"])?.[0] ?? 0;
|
|
|
|
|
const uvLink = sourceLink(incoming, source.id, "Vector");
|
|
|
|
|
const uvNode = uvLink ? byId.get(uvLink.fromNodeId) : undefined;
|
|
|
|
|
let uvMap: string | undefined;
|
|
|
|
|
if (uvNode?.type === "UV_MAP") uvMap = meshUvMap(uvNode);
|
|
|
|
|
else if (uvNode && uvNode.type !== "TEX_COORD") {
|
|
|
|
|
base.issues.push(issue("SHADER_NODE_UNSUPPORTED", "Mapping Vector must come from UV Map or Texture Coordinate", `nodes.${source.id}.Vector`));
|
|
|
|
|
}
|
|
|
|
|
return { uvMap, uvTransform: { offset, rotation, scale } };
|
|
|
|
|
};
|
|
|
|
|
const registerTexture = (imageNode: MaterialNodeIR, usage: ShaderTextureUsage, strength?: number): ShaderTextureBindingIR | undefined => {
|
|
|
|
|
if (!imageNode.imageId) return undefined;
|
|
|
|
|
if (context.imageIds && !context.imageIds.has(imageNode.imageId)) {
|
|
|
|
|
base.issues.push(issue("SHADER_EXTERNAL_RESOURCE_MISSING", `shader image is not present: ${imageNode.imageId}`, `nodes.${imageNode.id}.imageId`));
|
|
|
|
|
}
|
|
|
|
|
if (context.blockedImageIds?.has(imageNode.imageId)) {
|
|
|
|
|
base.issues.push(issue("SHADER_EXTERNAL_RESOURCE_MISSING", `shader image is blocked: ${imageNode.imageId}`, `nodes.${imageNode.id}.imageId`));
|
|
|
|
|
}
|
|
|
|
|
const uv = textureUv(imageNode);
|
|
|
|
|
const binding: ShaderTextureBindingIR = {
|
|
|
|
|
imageId: imageNode.imageId,
|
|
|
|
|
usage,
|
|
|
|
|
...(strength !== undefined ? { strength } : {}),
|
|
|
|
|
...(uv.uvMap ? { uvMap: uv.uvMap } : {}),
|
|
|
|
|
...(uv.uvTransform ? { uvTransform: uv.uvTransform } : {}),
|
|
|
|
|
};
|
|
|
|
|
textures.set(`${usage}:${imageNode.imageId}`, binding);
|
|
|
|
|
if (usage === "BASE_COLOR") compiled.baseColorImageId = imageNode.imageId;
|
|
|
|
|
return binding;
|
|
|
|
|
};
|
|
|
|
|
const evaluated = new Map<string, number>();
|
|
|
|
|
const evaluating = new Set<string>();
|
|
|
|
|
const evalScalar = (nodeId: string, depth: number): number | undefined => {
|
|
|
|
|
@@ -394,12 +479,8 @@ export function compileMaterialGraph(material: MaterialIR, context: ShaderCompil
|
|
|
|
|
else if (node.type === "MATH") {
|
|
|
|
|
const first = sourceLink(incoming, node.id, "Value") ?? sourceLink(incoming, node.id, "A");
|
|
|
|
|
const second = sourceLink(incoming, node.id, "Value_001") ?? sourceLink(incoming, node.id, "B");
|
|
|
|
|
const socketDefault=(id:string):number|undefined=>{
|
|
|
|
|
const value=node.sockets?.find(socket=>socket.direction==='INPUT'&&normalizeSocket(socket.id)===normalizeSocket(id))?.value;
|
|
|
|
|
return finite(value)?value:undefined;
|
|
|
|
|
};
|
|
|
|
|
const left = first ? evalScalar(first.fromNodeId, depth + 1) : socketDefault('Value');
|
|
|
|
|
const right = second ? evalScalar(second.fromNodeId, depth + 1) : socketDefault('Value_001');
|
|
|
|
|
const left = first ? evalScalar(first.fromNodeId, depth + 1) : socketNumber(node, ["value"]);
|
|
|
|
|
const right = second ? evalScalar(second.fromNodeId, depth + 1) : socketNumber(node, ["value001"]);
|
|
|
|
|
if (left !== undefined && right !== undefined) {
|
|
|
|
|
switch (node.properties?.operation) {
|
|
|
|
|
case "ADD": value = left + right; break;
|
|
|
|
|
@@ -411,56 +492,113 @@ export function compileMaterialGraph(material: MaterialIR, context: ShaderCompil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (node.type === "MIX") {
|
|
|
|
|
const factorLink = sourceLink(incoming, node.id, "Factor");
|
|
|
|
|
const aLink = sourceLink(incoming, node.id, "A");
|
|
|
|
|
const bLink = sourceLink(incoming, node.id, "B");
|
|
|
|
|
const factor = factorLink ? evalScalar(factorLink.fromNodeId, depth + 1) : socketNumber(node, ["factor"]);
|
|
|
|
|
const left = aLink ? evalScalar(aLink.fromNodeId, depth + 1) : socketNumber(node, ["a"]);
|
|
|
|
|
const right = bLink ? evalScalar(bLink.fromNodeId, depth + 1) : socketNumber(node, ["b"]);
|
|
|
|
|
if (factor !== undefined && left !== undefined && right !== undefined) {
|
|
|
|
|
const blend = String(node.properties?.blendType ?? "MIX");
|
|
|
|
|
if (blend === "MIX") value = left * (1 - factor) + right * factor;
|
|
|
|
|
else if (blend === "ADD") value = left + right * factor;
|
|
|
|
|
else if (blend === "MULTIPLY") value = left * (1 - factor + right * factor);
|
|
|
|
|
else base.issues.push(issue("SHADER_NODE_UNSUPPORTED", `Mix blend ${blend} is not a compiled scalar constant`, `nodes.${node.id}`));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
evaluating.delete(nodeId);
|
|
|
|
|
if (value !== undefined && finite(value)) evaluated.set(nodeId, value);
|
|
|
|
|
return value;
|
|
|
|
|
};
|
|
|
|
|
const evalColor = (link: MaterialLinkIR): [number, number, number, number] | undefined => {
|
|
|
|
|
const evalColorLink = (link: MaterialLinkIR, depth: number): [number, number, number, number] | undefined => {
|
|
|
|
|
if (depth > SHADER_COMPILE_BUDGET.maxDepth) return undefined;
|
|
|
|
|
const node = byId.get(link.fromNodeId);
|
|
|
|
|
if (node?.type === "RGB" && finiteColor(node.defaultValue)) {
|
|
|
|
|
if (!node) return undefined;
|
|
|
|
|
if (node.type === "RGB" && finiteColor(node.defaultValue)) {
|
|
|
|
|
return [node.defaultValue[0], node.defaultValue[1], node.defaultValue[2], node.defaultValue[3] ?? 1];
|
|
|
|
|
}
|
|
|
|
|
if (node?.type === "IMAGE_TEXTURE" && node.imageId) {
|
|
|
|
|
if (context.imageIds && !context.imageIds.has(node.imageId)) {
|
|
|
|
|
base.issues.push(issue("SHADER_EXTERNAL_RESOURCE_MISSING", `shader image is not present: ${node.imageId}`, `nodes.${node.id}.imageId`));
|
|
|
|
|
}
|
|
|
|
|
if (context.blockedImageIds?.has(node.imageId)) base.issues.push(issue("SHADER_EXTERNAL_RESOURCE_MISSING", `shader image is blocked: ${node.imageId}`, `nodes.${node.id}.imageId`));
|
|
|
|
|
textures.set(`BASE_COLOR:${node.imageId}`, { imageId: node.imageId, usage: "BASE_COLOR" });
|
|
|
|
|
compiled.baseColorImageId = node.imageId;
|
|
|
|
|
if (node.type === "IMAGE_TEXTURE") {
|
|
|
|
|
registerTexture(node, "BASE_COLOR");
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
if (node.type === "MIX") {
|
|
|
|
|
const factorLink = sourceLink(incoming, node.id, "Factor");
|
|
|
|
|
const aLink = sourceLink(incoming, node.id, "A");
|
|
|
|
|
const bLink = sourceLink(incoming, node.id, "B");
|
|
|
|
|
const factor = factorLink ? evalScalar(factorLink.fromNodeId, depth + 1) : socketNumber(node, ["factor"]);
|
|
|
|
|
const left = aLink ? evalColorLink(aLink, depth + 1) : undefined;
|
|
|
|
|
const right = bLink ? evalColorLink(bLink, depth + 1) : undefined;
|
|
|
|
|
if (factor === undefined || !left || !right) {
|
|
|
|
|
base.issues.push(issue("SHADER_NODE_UNSUPPORTED", "Mix color inputs must be bounded constants", `nodes.${node.id}`));
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
const blend = String(node.properties?.blendType ?? "MIX");
|
|
|
|
|
const mix = (a: number, b: number): number => blend === "MULTIPLY" ? a * b : blend === "ADD" ? Math.min(1, a + b) : a * (1 - factor) + b * factor;
|
|
|
|
|
return [mix(left[0], right[0]), mix(left[1], right[1]), mix(left[2], right[2]), mix(left[3], right[3])];
|
|
|
|
|
}
|
|
|
|
|
if (node.type === "SEPARATE_COLOR") {
|
|
|
|
|
const colorLink = sourceLink(incoming, node.id, "Color");
|
|
|
|
|
return colorLink ? evalColorLink(colorLink, depth + 1) : undefined;
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
};
|
|
|
|
|
const evalScalarLink = (link: MaterialLinkIR, depth: number): number | undefined => {
|
|
|
|
|
const node = byId.get(link.fromNodeId);
|
|
|
|
|
if (node?.type === "SEPARATE_COLOR") {
|
|
|
|
|
const colorLink = sourceLink(incoming, node.id, "Color");
|
|
|
|
|
const color = colorLink ? evalColorLink(colorLink, depth + 1) : undefined;
|
|
|
|
|
if (!color) return undefined;
|
|
|
|
|
const channel = normalizeSocket(link.fromSocket);
|
|
|
|
|
return channel === "red" || channel === "r" ? color[0]
|
|
|
|
|
: channel === "green" || channel === "g" ? color[1]
|
|
|
|
|
: channel === "blue" || channel === "b" ? color[2]
|
|
|
|
|
: channel === "alpha" || channel === "a" ? color[3]
|
|
|
|
|
: undefined;
|
|
|
|
|
}
|
|
|
|
|
return evalScalar(link.fromNodeId, depth);
|
|
|
|
|
};
|
|
|
|
|
if (shader) {
|
|
|
|
|
for (const [socket, field] of [["Roughness", "roughness"], ["Metallic", "metallic"], ["Alpha", "alpha"], ["IOR", "ior"], ["Specular IOR Level", "specularIORLevel"], ["Transmission Weight", "transmissionWeight"], ["Coat Weight", "coatWeight"], ["Coat Roughness", "coatRoughness"], ["Emission Strength", "emissionStrength"]] as const) {
|
|
|
|
|
const link = sourceLink(incoming, shader.id, socket);
|
|
|
|
|
if (!link) continue;
|
|
|
|
|
const value = evalScalar(link.fromNodeId, 0);
|
|
|
|
|
const value = evalScalarLink(link, 0);
|
|
|
|
|
const normalized = normalizeSocket(field);
|
|
|
|
|
if (value === undefined || !boundedMaterialValue(normalized, value)) base.issues.push(issue("SHADER_INVALID_GRAPH", `${socket} input is not a finite bounded constant`, `links.${socket}`));
|
|
|
|
|
else (compiled as unknown as Record<string, unknown>)[field] = value;
|
|
|
|
|
}
|
|
|
|
|
const colorLink = sourceLink(incoming, shader.id, "Base Color");
|
|
|
|
|
if (colorLink) {
|
|
|
|
|
const color = evalColor(colorLink);
|
|
|
|
|
const source = byId.get(colorLink.fromNodeId);
|
|
|
|
|
const color = evalColorLink(colorLink, 0);
|
|
|
|
|
if (color) compiled.baseColor = color;
|
|
|
|
|
else if (byId.get(colorLink.fromNodeId)?.type !== "IMAGE_TEXTURE") base.issues.push(issue("SHADER_INVALID_GRAPH", "Base Color must be an RGB constant or Image Texture", "links.BaseColor"));
|
|
|
|
|
else if (source?.type !== "IMAGE_TEXTURE" && source?.type !== "MIX") base.issues.push(issue("SHADER_INVALID_GRAPH", "Base Color must be an RGB constant, Mix constant, or Image Texture", "links.BaseColor"));
|
|
|
|
|
}
|
|
|
|
|
const normalLink = sourceLink(incoming, shader.id, "Normal");
|
|
|
|
|
if (normalLink) {
|
|
|
|
|
const normalNode = byId.get(normalLink.fromNodeId);
|
|
|
|
|
if (normalNode?.type !== "NORMAL_MAP") base.issues.push(issue("SHADER_NODE_UNSUPPORTED", "Principled Normal must be driven by the declared Normal Map node", "links.Normal"));
|
|
|
|
|
else {
|
|
|
|
|
if (normalNode?.type === "NORMAL_MAP") {
|
|
|
|
|
const imageLink = sourceLink(incoming, normalNode.id, "Color");
|
|
|
|
|
const imageNode = imageLink ? byId.get(imageLink.fromNodeId) : undefined;
|
|
|
|
|
if (!imageLink || imageNode?.type !== "IMAGE_TEXTURE" || !imageNode.imageId) base.issues.push(issue("SHADER_INVALID_GRAPH", "Normal Map requires an Image Texture Color input", `nodes.${normalNode.id}`));
|
|
|
|
|
else {
|
|
|
|
|
if (context.imageIds && !context.imageIds.has(imageNode.imageId)) base.issues.push(issue("SHADER_EXTERNAL_RESOURCE_MISSING", `shader image is not present: ${imageNode.imageId}`, `nodes.${imageNode.id}.imageId`));
|
|
|
|
|
if (context.blockedImageIds?.has(imageNode.imageId)) base.issues.push(issue("SHADER_EXTERNAL_RESOURCE_MISSING", `shader image is blocked: ${imageNode.imageId}`, `nodes.${imageNode.id}.imageId`));
|
|
|
|
|
textures.set(`NORMAL:${imageNode.imageId}`, { imageId: imageNode.imageId, usage: "NORMAL" });
|
|
|
|
|
const strengthLink = sourceLink(incoming, normalNode.id, "Strength");
|
|
|
|
|
const strength = strengthLink ? evalScalarLink(strengthLink, 0) : socketNumber(normalNode, ["strength"]);
|
|
|
|
|
registerTexture(imageNode, "NORMAL", strength);
|
|
|
|
|
compiled.normalImageId = imageNode.imageId;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (normalNode?.type === "BUMP") {
|
|
|
|
|
const heightLink = sourceLink(incoming, normalNode.id, "Height");
|
|
|
|
|
const imageNode = heightLink ? byId.get(heightLink.fromNodeId) : undefined;
|
|
|
|
|
if (!heightLink || imageNode?.type !== "IMAGE_TEXTURE" || !imageNode.imageId) base.issues.push(issue("SHADER_INVALID_GRAPH", "Bump requires an Image Texture Height input", `nodes.${normalNode.id}`));
|
|
|
|
|
else {
|
|
|
|
|
const strengthLink = sourceLink(incoming, normalNode.id, "Strength");
|
|
|
|
|
const strength = strengthLink ? evalScalarLink(strengthLink, 0) : socketNumber(normalNode, ["strength"]);
|
|
|
|
|
registerTexture(imageNode, "BUMP", strength);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else base.issues.push(issue("SHADER_NODE_UNSUPPORTED", "Principled Normal must be driven by Normal Map or Bump", "links.Normal"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const supportedLink = (link: MaterialLinkIR): boolean => {
|
|
|
|
|
@@ -471,12 +609,34 @@ export function compileMaterialGraph(material: MaterialIR, context: ShaderCompil
|
|
|
|
|
const toSocket = normalizeSocket(link.toSocket);
|
|
|
|
|
if (to.type === "OUTPUT") return from.type === "PRINCIPLED" && fromSocket === "bsdf" && toSocket === "surface";
|
|
|
|
|
if (to.type === "PRINCIPLED") {
|
|
|
|
|
if (toSocket === "basecolor") return (from.type === "RGB" && fromSocket === "color") || (from.type === "IMAGE_TEXTURE" && fromSocket === "color");
|
|
|
|
|
if (toSocket === "normal") return from.type === "NORMAL_MAP" && fromSocket === "normal";
|
|
|
|
|
if (toSocket === "basecolor") {
|
|
|
|
|
return (from.type === "RGB" || from.type === "MIX" || from.type === "IMAGE_TEXTURE" || from.type === "SEPARATE_COLOR") && (fromSocket === "color" || fromSocket === "result");
|
|
|
|
|
}
|
|
|
|
|
if (toSocket === "normal") return (from.type === "NORMAL_MAP" || from.type === "BUMP") && fromSocket === "normal";
|
|
|
|
|
return targetScalarSocket(toSocket) && isScalarSource(from) && (from.type !== "MATH" || fromSocket === "value");
|
|
|
|
|
}
|
|
|
|
|
if (to.type === "NORMAL_MAP") return from.type === "IMAGE_TEXTURE" && fromSocket === "color" && toSocket === "color";
|
|
|
|
|
if (to.type === "BUMP") return from.type === "IMAGE_TEXTURE" && fromSocket === "color" && toSocket === "height";
|
|
|
|
|
if (to.type === "MATH") return isScalarSource(from) && scalarSocket(toSocket) && fromSocket === "value";
|
|
|
|
|
if (to.type === "IMAGE_TEXTURE") {
|
|
|
|
|
return toSocket === "vector" && (
|
|
|
|
|
(from.type === "MAPPING" && fromSocket === "vector") ||
|
|
|
|
|
(from.type === "UV_MAP" && (fromSocket === "uv" || fromSocket === "uvmap")) ||
|
|
|
|
|
(from.type === "TEX_COORD" && fromSocket === "uv")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (to.type === "MAPPING") {
|
|
|
|
|
return toSocket === "vector" && (
|
|
|
|
|
(from.type === "UV_MAP" && (fromSocket === "uv" || fromSocket === "uvmap")) ||
|
|
|
|
|
(from.type === "TEX_COORD" && fromSocket === "uv")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (to.type === "MIX") {
|
|
|
|
|
if (toSocket === "factor") return isScalarSource(from);
|
|
|
|
|
if (toSocket === "a" || toSocket === "b") return isScalarSource(from) || from.type === "RGB" || from.type === "IMAGE_TEXTURE";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (to.type === "SEPARATE_COLOR") return toSocket === "color" && from.type === "RGB";
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
for (const [index, link] of links.entries()) if (!supportedLink(link)) base.issues.push(issue("SHADER_NODE_UNSUPPORTED", `Shader link is outside the M10-07 compiled closure: ${link.fromNodeId}.${link.fromSocket} -> ${link.toNodeId}.${link.toSocket}`, `links.${index}`));
|
|
|
|
|
|