@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
533 lines (448 loc) • 18.9 kB
text/typescript
import {Color, Texture, Vector2, Vector3, Vector4} from "three";
import {
formatsWithAlphaChannel,
makeNameSafeForUSD
} from "../ThreeUSDZExporter.js";
const materialRoot = '</StageRoot/Materials';
declare type TextureMap = {[name: string]: {texture: Texture, scale?: Vector4}};
type MeshPhysicalNodeMaterial = import("three/src/materials/nodes/MeshPhysicalNodeMaterial.js").default;
function buildNodeMaterial( nodeMaterial: MeshPhysicalNodeMaterial, materialName: string, textures: TextureMap ) {
const collectedNodeTypes = new Map();
const getUniqueNodeName = (node) => {
const type = node["type___needle"];
const typeMap = collectedNodeTypes.get(type) || new Map();
collectedNodeTypes.set(type, typeMap);
if (!typeMap.has(node)) {
const name = `${type}${typeMap.size ? `_${typeMap.size}` : ""}`;
typeMap.set(node, name);
}
return typeMap.get(node);
};
const colorNodesToBeExported = nodeMaterial.colorNode ? getNodesToExport(nodeMaterial.colorNode) : [];
const colorOutputString = nodeMaterial.colorNode
? `color3f inputs:diffuseColor.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(colorNodesToBeExported.values().next().value)}.outputs:out>`
: "";
const roughnessNodesToBeExported = nodeMaterial.roughnessNode ? getNodesToExport(nodeMaterial.roughnessNode) : [];
const roughnessOutputString = nodeMaterial.roughnessNode
? `float inputs:roughness.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(roughnessNodesToBeExported.values().next().value)}.outputs:out>`
: "";
const normalNodesToBeExported = nodeMaterial.normalNode ? getNodesToExport(nodeMaterial.normalNode) : [];
const normalOutputString = nodeMaterial.normalNode
? `float3 inputs:normal.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(normalNodesToBeExported.values().next().value)}.outputs:out>`
: "";
const metallicNodesToBeExported = nodeMaterial.metalnessNode ? getNodesToExport(nodeMaterial.metalnessNode) : [];
const metallicOutputString = nodeMaterial.metalnessNode
? `float inputs:metallic.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(metallicNodesToBeExported.values().next().value)}.outputs:out>`
: "";
const combinedSetOfAllNodesToExport = new Set([...colorNodesToBeExported, ...roughnessNodesToBeExported, ...normalNodesToBeExported, ...metallicNodesToBeExported])
const shaderOutput = getShadersFromNodes(combinedSetOfAllNodesToExport, materialName, textures, getUniqueNodeName);
console.debug(shaderOutput);
return `
def Material "${materialName}" ${nodeMaterial.name ? `(
displayName = "${nodeMaterial.name}"
)` : ''}
{
token outputs:mtlx:surface.connect = ${materialRoot}/${materialName}/N_mtlxsurface.outputs:surface>
def Shader "N_mtlxsurface"
{
uniform token info:id = "ND_UsdPreviewSurface_surfaceshader"
${colorOutputString}
${roughnessOutputString}
${normalOutputString}
${metallicOutputString}
token outputs:surface
}
${shaderOutput}
}`;
}
function getNodesToExport( rootNode: any ): Set<any> {
const getNodeType = (node) => {
if (node.nodeType) return node.nodeType;
switch (node.type) {
case "TimerNode": return "float";
case "TextureNode": return undefined;
case "ConvertNode": return node.convertTo;
default: return undefined;
}
};
const getSetOfAllNodes = (rootNode: any): Set<any> => {
const setOfAllNodes:Set<any> = new Set();
const collectNode = (node) => {
if (!node.isNode || setOfAllNodes.has(node)) return;
if (!node["nodeType___needle"]){
node["nodeType___needle"] = getNodeType(node);
}
if (node.shaderNode){
node["type___needle"] = "ShaderCallNodeInternal";
node["shaderNodeLayoutName___needle"] = node.shaderNode.layout.name.slice(3);
}
else {
node["type___needle"] = node.type;
}
setOfAllNodes.add(node);
for (const key in node) {
if (node[key]?.isNode) {
collectNode(node[key]);
node["nodeType___needle"] ||= node[key]["nodeType___needle"];
}
if (Array.isArray(node[key])) {
node[key].forEach(child => {
if (child.isNode) {
collectNode(child)
node["nodeType___needle"] ||= child["nodeType___needle"];
}
});
}
}
};
collectNode(rootNode);
return setOfAllNodes;
}
const isSelfConversionNode = (node) => {
if (node.type === "ConvertNode"){
if (node.convertTo === node.node["nodeType___needle"])
{
return true;
}
else if (node.node.type === "ConstNode")
{
if (node.convertTo === "vec4" && node.node.value.isVector4){
return true;
}
else if (node.convertTo === "vec3" && node.node.value.isVector3){
return true;
}
else if (node.convertTo === "vec2" && node.node.value.isVector2){
return true;
}
else if (node.convertTo === "color" && node.node.value.isColor){
return true;
}
else if (node.convertTo === "float" && typeof node.node.value === 'number'){
return true;
}
}
else if (node.node.type == "SplitNode"){
if (node.convertTo == "float" && node.node.components.length === 1){
return true;
}
}
}
return false;
}
const getNextValidNode = (node) => {
while (nodeShouldNotBeExported(node)) {
if (!node.node && node.shaderNode){
node = node.inputNodes[0];
}
else{
node = node.node ?? node.aNode ?? node.bNode ?? node.cNode;
}
}
return node;
};
const nodeShouldNotBeExported = (node: any | undefined): boolean => {
const ignorableNodeTypes = ["UniformNode", "UniformGroupNode", "ShaderNodeInternal"]
return !node || isSelfConversionNode(node) || ignorableNodeTypes.includes(node["type___needle"]) || node["type___needle"] === undefined;
}
const getParent = (currNode, nodeSet) => {
for (const node of nodeSet) {
for (const key in node) {
if (node[key]?.isNode && node[key] === currNode) {
return { parent: node, label: key };
}
if (Array.isArray(node[key])) {
const child = node[key].find(childNode => childNode.isNode && childNode === currNode);
if (child) return { parent: node, label: key };
}
}
}
return null;
};
const updateNodeReferences = (node, refKeys) => {
if (node.shaderNode){
node.inputNodes[0] = getNextValidNode(node.inputNodes[0]);
}
else if (Array.isArray(node.nodes)) {
for(let i = 0; i < node.nodes.length; i++){
if (node.nodes[i] && nodeShouldNotBeExported(node.nodes[i])) {
node.nodes[i] = getNextValidNode(node.nodes[i]);
}
}
}
else{
refKeys.forEach(key => {
if (node[key] && nodeShouldNotBeExported(node[key])) {
node[key] = getNextValidNode(node[key]);
}
});
}
};
const setMixNodeMixToFloat = (node) => {
if (node.type === "MathNode" && node.method === "mix"){
node.cNode["nodeType___needle"] = "float";
if (node.cNode.type === "ConvertNode"){
node.cNode.convertTo = "float";
}
}
}
const setConstNodeTypeToParentType = (node, parentResponse) => {
if (!(parentResponse.label === 'cNode' && parentResponse.parent.type === "MathNode" && parentResponse.parent.method === "mix")){
if (parentResponse.parent.type === "JoinNode"){
node["nodeType___needle"] = "float";
}
else {
node["nodeType___needle"] = parentResponse.parent["nodeType___needle"];
}
}
}
const isConvertVector4ToColorNode = (node) => (
node?.type === "ConvertNode" && node["nodeType___needle"] === "color" && node.node["nodeType___needle"] === "vec4"
);
const createVec3ToColorConversionNode = (node, allNodes) => {
node.convertTo = "vec3";
node["nodeType___needle"] = "vec3";
const newNode = {
type: "ConvertNode",
convertTo: "color",
node: node,
isNode: true,
nodeType___needle: "color",
type___needle: "ConvertNode"
};
const parentInfo = getParent(node, allNodes);
if (parentInfo?.parent) {
parentInfo.parent[parentInfo.label] = newNode;
}
return newNode;
}
const isConvertFromTextureNode = (node) => (
node?.type === "ConvertNode" && node.node.type === "TextureNode" && node["nodeType___needle"] !== node.node["nodeType___needle"]
);
const pruneNodes = (allNodes: Set<any>) : Set<any> => {
const nodesToBeExported: Set<any> = new Set();
for (let node of allNodes) {
if (nodeShouldNotBeExported(node)) continue;
// Math mix nodes take a mix input that should always be a float, the typing gets messed up here because
// the other inputs that are being mixed can be anything and if we have a convert or a const that goes
// into the mix input, it gets set as whatever the output of the mix should be, not float. Here we make
// sure it will be float
setMixNodeMixToFloat(node);
if (node.type == "SplitNode"){
const parentResponse = getParent(node, allNodes);
if (node.components.length === 1){
node["nodeType___needle"] = "float";
}
else if(parentResponse) {
// TODO: this may not be sufficient and it may be better to always count the component lengths
node["nodeType___needle"] = parentResponse.parent["nodeType___needle"];
}
else throw new Error("SplitNode without parent found, this should not happen");
}
// Here we check child nodes to update the connections if they will be ignored later
updateNodeReferences(node, ["node", "aNode", "bNode", "cNode"]);
// Const nodes don't always have a type and sometimes they need to be converted from a value of 0 -> [0, 0, 0]
// this function does that conversion
if (node.type == "ConstNode" && node.nodeType == null){
setConstNodeTypeToParentType(node, getParent(node, allNodes));
}
if (isConvertVector4ToColorNode(node)) {
nodesToBeExported.add(createVec3ToColorConversionNode(node, allNodes));
}
// We want Texture nodes to export the type they need, rather than something else and then convert
// here if we have a convert above a Texture that we missed on the first pass because there was
// some other node in between to prune, set the type on the Texture correctly and kill the convert
if (isConvertFromTextureNode(node)){
node.node["nodeType___needle"] = node.convertTo;
const parentInfo = getParent(node, allNodes);
if (parentInfo?.parent) {
parentInfo.parent[parentInfo.label] = node.node;
}
node = node.node;
}
nodesToBeExported.add(node);
}
return nodesToBeExported;
}
const setOfAllNodes = getSetOfAllNodes(rootNode);
const prunedNodes = pruneNodes(setOfAllNodes);
return prunedNodes;
}
function getConstValueString(value: any, type: string){
switch (type) {
case "float4":
return value.isVector4
? `(${value.x}, ${value.y}, ${value.z}, ${value.w})`
: `(${value}, ${value}, ${value}, ${value})`;
case "float3":
return value.isVector3
? `(${value.x}, ${value.y}, ${value.z})`
: `(${value}, ${value}, ${value})`;
case "float2":
return value.isVector2
? `(${value.x}, ${value.y})`
: `(${value}, ${value})`;
case "color3f":
return value.isColor
? `(${value.r}, ${value.g}, ${value.b})`
: `(${value}, ${value}, ${value})`;
default:
return (value.isVector4 || value.isVector3 || value.isVector2)
? `${value.x}`
: value.isColor
? `${value.r}`
: `${value}`;
}
}
function TSLNodeToUsdShadeString(node:any, materialName:string, getUniqueNodeName, textures: TextureMap) {
const pad = ' ';
const getType = (nodeType) => {
const types = {
float: "float",
vec2: "vector2",
vec3: "vector3",
vec4: "vector4",
color: "color3"
};
return types[nodeType] || "float";
};
const getUsdType = (nodeType) => {
const usdTypes = {
float: "float",
vec2: "float2",
vec3: "float3",
vec4: "float4",
color: "color3f"
};
return usdTypes[nodeType] || "float";
};
const type = node["type___needle"];
const ndType = node["nodeType___needle"];
const mtlxNdType = getType(ndType);
let usdNdType = getUsdType(ndType);
let usdShadeNodeName = "";
const inputs = new Array<string>();
switch (type) {
case "UniformGroupNode":
case "UniformNode":
// break out of node loop
return "";
case "TimerNode":
usdShadeNodeName = "time_float";
break;
case "ConstNode":
usdShadeNodeName = "constant_" + mtlxNdType;
inputs.push(`${usdNdType} inputs:value = ${getConstValueString(node.value, usdNdType)}`);
break;
case "JoinNode":
usdShadeNodeName = "combine" + node.nodes.length + "_" + mtlxNdType;
let i = 1;
for (const childNode of node.nodes) {
inputs.push(`float inputs:in${i++}.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(childNode)}.outputs:out>`);
}
break;
case "ConvertNode":
const inputType = getType(node.node["nodeType___needle"]);
usdShadeNodeName = "convert_" + inputType + "_" + mtlxNdType;
if (node.node)
inputs.push(`${getUsdType(node.node["nodeType___needle"])} inputs:in.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(node.node)}.outputs:out>`);
break;
case "MathNode":
usdShadeNodeName = node.method + "_" + mtlxNdType;
if (node.aNode && !node.bNode)
inputs.push(`${getUsdType(node.aNode["nodeType___needle"])} inputs:in.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(node.aNode)}.outputs:out>`);
if (node.aNode && node.bNode && !node.cNode) {
inputs.push(`${getUsdType(node.aNode["nodeType___needle"])} inputs:in1.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(node.aNode)}.outputs:out>`);
inputs.push(`${getUsdType(node.bNode["nodeType___needle"])} inputs:in2.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(node.bNode)}.outputs:out>`);
}
if (node.aNode && node.bNode && node.cNode && node.method == "clamp") {
inputs.push(`${getUsdType(node.aNode["nodeType___needle"])} inputs:in.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(node.aNode)}.outputs:out>`);
inputs.push(`${getUsdType(node.bNode["nodeType___needle"])} inputs:low.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(node.bNode)}.outputs:out>`);
inputs.push(`${getUsdType(node.cNode["nodeType___needle"])} inputs:high.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(node.cNode)}.outputs:out>`);
}
if (node.aNode && node.bNode && node.cNode && node.method == "mix") {
inputs.push(`${getUsdType(node.aNode["nodeType___needle"])} inputs:fg.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(node.bNode)}.outputs:out>`);
inputs.push(`${getUsdType(node.bNode["nodeType___needle"])} inputs:bg.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(node.aNode)}.outputs:out>`);
inputs.push(`float inputs:mix.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(node.cNode)}.outputs:out>`);
}
break;
case "OperatorNode":
let opName = "";
switch (node.op) {
case "*" : opName = "multiply"; break;
case "/" : opName = "divide"; break;
case "+" : opName = "add"; break;
case "-" : opName = "subtract"; break;
}
usdShadeNodeName = opName + "_" + mtlxNdType;
if (node.aNode && !node.bNode)
inputs.push(`${getUsdType(node.aNode["nodeType___needle"])} inputs:in.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(node.aNode)}.outputs:out>`);
if (node.aNode && node.bNode) {
const aNodeType = getUsdType(node.aNode["nodeType___needle"])
const bNodeType = getUsdType(node.bNode["nodeType___needle"])
// todo: make this more generic / support all combinations
if (aNodeType === 'color3f' && bNodeType === 'float' || bNodeType === 'float' && bNodeType === 'color3f') {
usdShadeNodeName = opName + "_color3FA";
}
inputs.push(`${aNodeType} inputs:in1.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(node.aNode)}.outputs:out>`);
inputs.push(`${bNodeType} inputs:in2.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(node.bNode)}.outputs:out>`);
}
break;
case "TextureNode":
if (node.uvNode){
usdShadeNodeName = "tiledimage_" + mtlxNdType;
inputs.push(`float2 inputs:texcoord.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(node.uvNode)}.outputs:out>`);
}
else{
usdShadeNodeName = "image_" + mtlxNdType;
}
const texture = node._value;
const isRGBA = formatsWithAlphaChannel.includes( texture.format );
const textureName = texName(texture);
inputs.push( `asset inputs:file = @textures/${textureName}.${isRGBA ? 'png' : 'jpg'}@` );
textures[ textureName ] = { texture, scale: undefined };
break;
case "NormalMapNode":
usdNdType = "float3"
usdShadeNodeName = "normalmap";
inputs.push(`${usdNdType} inputs:in.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(node.node)}.outputs:out>`);
break;
case "AttributeNode":
usdShadeNodeName = "geompropvalue_" + mtlxNdType;
inputs.push(`string inputs:geomprop = "st"`);
break;
case "ShaderCallNodeInternal":
usdShadeNodeName = node["shaderNodeLayoutName___needle"] + "_" + mtlxNdType;
inputs.push(`${usdNdType} inputs:in.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(node.inputNodes[0])}.outputs:out>`);
break;
case "SplitNode":
usdShadeNodeName = "swizzle_" + getType(node.node["nodeType___needle"]) + "_" + mtlxNdType;
inputs.push(`${getUsdType(node.node["nodeType___needle"])} inputs:in.connect = ${materialRoot}/${materialName}/${getUniqueNodeName(node.node)}.outputs:out>`);
inputs.push(`string inputs:channels = "${node.components}"`);
break;
}
// todo: better way to pad here for sure...
return `
${pad}def Shader "${getUniqueNodeName(node)}"
${pad}{
${pad}uniform token info:id = "ND_${usdShadeNodeName}"
${pad}${usdNdType} outputs:out
${pad}${inputs.length > 0 ? inputs.join("\n ") : ""}
${pad}}
`;
}
function getShadersFromNodes( nodes: Set<any>, materialName: string, textures: TextureMap, getUniqueNodeName){
let shaderOutput = "";
for (const node of nodes) {
shaderOutput += TSLNodeToUsdShadeString(node, materialName, getUniqueNodeName, textures);
}
return shaderOutput;
}
function texName(tex: Texture) {
// If we have a source, we only want to use the source's id, not the texture's id
// to avoid exporting the same underlying data multiple times.
return makeNameSafeForUSD(tex.name) + '_' + (tex.source?.id ?? tex.id);
}
export {
buildNodeMaterial
};