Files
UnrealEngine/Engine/Source/ThirdParty/MaterialX/MaterialX-1.38.10/javascript/MaterialXView/source/viewer.js
2025-05-18 13:04:45 +08:00

1617 lines
57 KiB
JavaScript

//
// Copyright Contributors to the MaterialX Project
// SPDX-License-Identifier: Apache-2.0
//
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
import { prepareEnvTexture, getLightRotation, findLights, registerLights, getUniformValues } from './helper.js'
import { Group } from 'three';
import GUI from 'lil-gui';
const ALL_GEOMETRY_SPECIFIER = "*";
const NO_GEOMETRY_SPECIFIER = "";
const DAG_PATH_SEPERATOR = "/";
// Logging toggle
var logDetailedTime = false;
/*
Scene management
*/
export class Scene
{
constructor()
{
this._geometryURL = new URLSearchParams(document.location.search).get("geom");
if (!this._geometryURL)
{
this._geometryURL = 'Geometry/shaderball.glb';
}
}
initialize()
{
this._scene = new THREE.Scene();
this._scene.background = new THREE.Color(this.#_backgroundColor);
this._scene.background.convertSRGBToLinear();
const aspectRatio = window.innerWidth / window.innerHeight;
const cameraNearDist = 0.05;
const cameraFarDist = 100.0;
const cameraFOV = 60.0;
this._camera = new THREE.PerspectiveCamera(cameraFOV, aspectRatio, cameraNearDist, cameraFarDist);
this.#_gltfLoader = new GLTFLoader();
this.#_normalMat = new THREE.Matrix3();
this.#_viewProjMat = new THREE.Matrix4();
this.#_worldViewPos = new THREE.Vector3();
}
// Set whether to flip UVs in V for loaded geometry
setFlipGeometryV(val)
{
this.#_flipV = val;
}
// Get whether to flip UVs in V for loaded geometry
getFlipGeometryV()
{
return this.#_flipV;
}
// Utility to perform geometry file load
loadGeometryFile(geometryFilename, loader)
{
return new Promise((resolve, reject) =>
{
loader.load(geometryFilename, data => resolve(data), null, reject);
});
}
//
// Load in geometry specified by a given file name,
// then update the scene geometry and camera.
//
async loadGeometry(viewer, orbitControls)
{
var startTime = performance.now();
var geomLoadTime = startTime;
const gltfData = await this.loadGeometryFile(this.getGeometryURL(), this.#_gltfLoader);
const scene = this.getScene();
while (scene.children.length > 0)
{
scene.remove(scene.children[0]);
}
this.#_rootNode = null;
const model = gltfData.scene;
if (!model)
{
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0xdddddd });
const cube = new THREE.Mesh(geometry, material);
obj = new Group();
obj.add(geometry);
}
else
{
this.#_rootNode = model;
}
scene.add(model);
console.log("- Scene load time: ", performance.now() - geomLoadTime, "ms");
// Always reset controls based on camera for each load.
orbitControls.reset();
this.updateScene(viewer, orbitControls);
console.log("Total geometry load time: ", performance.now() - startTime, " ms.");
viewer.getMaterial().clearSoloMaterialUI();
viewer.getMaterial().updateMaterialAssignments(viewer, "");
this.setUpdateTransforms();
}
//
// Update the geometry buffer, assigned materials, and camera controls.
//
updateScene(viewer, orbitControls)
{
var startUpdateSceneTime = performance.now();
var uvTime = 0;
var normalTime = 0;
var tangentTime = 0;
var streamTime = 0;
var bboxTime = 0;
var startBboxTime = performance.now();
const bbox = new THREE.Box3().setFromObject(this._scene);
const bsphere = new THREE.Sphere();
bbox.getBoundingSphere(bsphere);
bboxTime = performance.now() - startBboxTime;
let theScene = viewer.getScene();
let flipV = theScene.getFlipGeometryV();
this._scene.traverse((child) =>
{
if (child.isMesh)
{
var startUVTime = performance.now();
if (!child.geometry.attributes.uv)
{
const posCount = child.geometry.attributes.position.count;
const uvs = [];
const pos = child.geometry.attributes.position.array;
for (let i = 0; i < posCount; i++)
{
uvs.push((pos[i * 3] - bsphere.center.x) / bsphere.radius);
uvs.push((pos[i * 3 + 1] - bsphere.center.y) / bsphere.radius);
}
child.geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(uvs), 2));
}
else if (flipV)
{
const uvCount = child.geometry.attributes.position.count;
const uvs = child.geometry.attributes.uv.array;
for (let i = 0; i < uvCount; i++)
{
let v = 1.0 - (uvs[i * 2 + 1]);
uvs[i * 2 + 1] = v;
}
}
uvTime += performance.now() - startUVTime;
if (!child.geometry.attributes.normal)
{
var startNormalTime = performance.new();
child.geometry.computeVertexNormals();
normalTime += performance.now() - startNormalTime;
}
if (child.geometry.getIndex())
{
if (!child.geometry.attributes.tangent)
{
var startTangentTime = performance.now();
child.geometry.computeTangents();
tangentTime += performance.now() - startTangentTime;
}
}
// Use default MaterialX naming convention.
var startStreamTime = performance.now();
child.geometry.attributes.i_position = child.geometry.attributes.position;
child.geometry.attributes.i_normal = child.geometry.attributes.normal;
child.geometry.attributes.i_tangent = child.geometry.attributes.tangent;
child.geometry.attributes.i_texcoord_0 = child.geometry.attributes.uv;
streamTime += performance.now() - startStreamTime;
}
});
console.log("- Stream update time: ", performance.now() - startUpdateSceneTime, "ms");
if (logDetailedTime)
{
console.log(' - UV time: ', uvTime);
console.log(' - Normal time: ', normalTime);
console.log(' - Tangent time: ', tangentTime);
console.log(' - Stream Update time: ', streamTime);
console.log(' - Bounds compute time: ', bboxTime);
}
// Update the background
this._scene.background = this.getBackground();
// Fit camera to model
const camera = this.getCamera();
camera.position.y = bsphere.center.y;
camera.position.z = bsphere.radius * 2.0;
camera.updateProjectionMatrix();
orbitControls.target = bsphere.center;
orbitControls.update();
}
setUpdateTransforms()
{
this.#_updateTransforms = true;
}
updateTransforms()
{
// Only update on demand versus continuously.
// Call setUpdateTransforms() to trigger an update here.
// Required for: scene geometry, camera change and viewport resize.
if (!this.#_updateTransforms)
{
return;
}
this.#_updateTransforms = false;
const scene = this.getScene();
const camera = this.getCamera();
scene.traverse((child) =>
{
if (child.isMesh)
{
const uniforms = child.material.uniforms;
if (uniforms)
{
uniforms.u_worldMatrix.value = child.matrixWorld;
uniforms.u_viewProjectionMatrix.value = this.#_viewProjMat.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
if (uniforms.u_viewPosition)
uniforms.u_viewPosition.value = camera.getWorldPosition(this.#_worldViewPos);
if (uniforms.u_worldInverseTransposeMatrix)
uniforms.u_worldInverseTransposeMatrix.value =
new THREE.Matrix4().setFromMatrix3(this.#_normalMat.getNormalMatrix(child.matrixWorld));
}
}
});
}
// Determine string DAG path based on individual node names.
getDagPath(node)
{
const rootNode = this.#_rootNode;
let path = [node.name];
while (node.parent)
{
node = node.parent;
if (node)
{
// Stop at the root of the scene read in.
if (node == rootNode)
{
break;
}
path.unshift(node.name);
}
}
return path;
}
// Assign material shader to associated geometry
updateMaterial(matassign)
{
let assigned = 0;
const shader = matassign.getShader();
const material = matassign.getMaterial().getName();
const geometry = matassign.getGeometry();
const collection = matassign.getCollection();
const scene = this.getScene();
const camera = this.getCamera();
scene.traverse((child) =>
{
if (child.isMesh)
{
const dagPath = this.getDagPath(child).join('/');
// Note that this is a very simplistic
// assignment resolve and assumes basic
// regular expression name match.
let matches = (geometry == ALL_GEOMETRY_SPECIFIER);
if (!matches)
{
if (collection)
{
if (collection.matchesGeomString(dagPath))
{
matches = true;
}
}
else
{
if (geometry != NO_GEOMETRY_SPECIFIER)
{
const paths = geometry.split(',');
for (let path of paths)
{
if (dagPath.match(path))
{
matches = true;
break;
}
}
}
}
}
if (matches)
{
child.material = shader;
assigned++;
}
}
});
return assigned;
}
updateCamera()
{
const camera = this.getCamera();
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
}
getScene()
{
return this._scene;
}
getCamera()
{
return this._camera;
}
getGeometryURL()
{
return this._geometryURL;
}
setGeometryURL(url)
{
this._geometryURL = url;
}
setBackgroundTexture(texture)
{
this.#_backgroundTexture = texture;
}
getShowBackgroundTexture()
{
return this.#_showBackgroundTexture;
}
setShowBackgroundTexture(enable)
{
this.#_showBackgroundTexture = enable;
}
getBackground()
{
if (this.#_backgroundTexture && this.#_showBackgroundTexture)
{
return this.#_backgroundTexture;
}
var color = new THREE.Color(this.#_backgroundColor);
color.convertSRGBToLinear();
return color;
}
toggleBackgroundTexture()
{
this.#_showBackgroundTexture = !this.#_showBackgroundTexture;
this._scene.background = this.getBackground();
}
// Geometry file
#_geometryURL = '';
// Geometry loader
#_gltfLoader = null;
// Flip V coordinate of texture coordinates.
// Set to true to be consistent with desktop viewer.
#_flipV = true;
// Scene
#_scene = null;
// Camera
#_camera = null;
// Background color
#_backgroundColor = 0x4c4c52;
// Background texture
#_backgroundTexture = null;
#_showBackgroundTexture = true;
// Transform matrices
#_normalMat = new THREE.Matrix3();
#_viewProjMat = new THREE.Matrix4();
#_worldViewPos = new THREE.Vector3();
#_updateTransforms = true;
// Root node of imported scene
#_rootNode = null;
}
/*
Property editor
*/
export class Editor
{
// Initialize the editor, clearing any elements from previous materials.
initialize()
{
Array.from(document.getElementsByClassName('lil-gui')).forEach(
function (element, index, array)
{
if (element.className)
{
element.remove();
}
}
);
this._gui = new GUI({ title: "Property Editor" });
this._gui.close();
}
// Update ui properties
// - Hide close button
// - Update transparency so scene shows through if overlapping
updateProperties(targetOpacity = 1)
{
// Set opacity
Array.from(document.getElementsByClassName('dg')).forEach(
function (element, index, array)
{
element.style.opacity = targetOpacity;
}
);
}
getGUI()
{
return this._gui;
}
_gui = null;
}
class MaterialAssign
{
constructor(material, geometry, collection)
{
this._material = material;
this._geometry = geometry;
this._collection = collection;
this._shader = null;
this._materialUI = null;
}
setMaterialUI(value)
{
this._materialUI = value;
}
getMaterialUI()
{
return this._materialUI;
}
setShader(shader)
{
this._shader = shader;
}
getShader()
{
return this._shader;
}
getMaterial()
{
return this._material;
}
getGeometry()
{
return this._geometry;
}
setGeometry(value)
{
this._geometry = value;
}
getCollection()
{
return this._collection;
}
// MaterialX material node name
_material;
// MaterialX assignment geometry string
_geometry;
// MaterialX assignment collection
_collection;
// THREE.JS shader
_shader;
}
export class Material
{
constructor()
{
this._materials = [];
this._defaultMaterial = null;
this._soloMaterial = "";
}
clearMaterials()
{
this._materials.length = 0;
this._defaultMaterial = null;
this._soloMaterial = "";
}
setSoloMaterial(value)
{
this._soloMaterial = value;
}
getSoloMaterial()
{
return this._soloMaterial;
}
// If no material file is selected, we programmatically create a default material as a fallback
static createFallbackMaterial(doc)
{
let ssNode = doc.getChild('Generated_Default_Shader');
if (ssNode)
{
return ssNode;
}
const ssName = 'Generated_Default_Shader';
ssNode = doc.addChildOfCategory('standard_surface', ssName);
ssNode.setType('surfaceshader');
const smNode = doc.addChildOfCategory('surfacematerial', 'Default');
smNode.setType('material');
const shaderElement = smNode.addInput('surfaceshader');
shaderElement.setType('surfaceshader');
shaderElement.setNodeName(ssName);
return ssNode;
}
async loadMaterialFile(loader, materialFilename)
{
return new Promise((resolve, reject) =>
{
loader.load(materialFilename, data => resolve(data), null, reject);
});
}
async loadMaterials(viewer, materialFilename)
{
var startTime = performance.now();
const mx = viewer.getMx();
// Re-initialize document
var startDocTime = performance.now();
var doc = mx.createDocument();
doc.importLibrary(viewer.getLibrary());
viewer.setDocument(doc);
const fileloader = viewer.getFileLoader();
let mtlxMaterial = await viewer.getMaterial().loadMaterialFile(fileloader, materialFilename);
// Load lighting setup into document
doc.importLibrary(viewer.getLightRig());
console.log("- Material document load time: ", performance.now() - startDocTime, "ms.");
// Set search path. Assumes images are relative to current file
// location.
if (!materialFilename) materialFilename = "/";
const paths = materialFilename.split('/');
paths.pop();
const searchPath = paths.join('/');
// Load material
if (mtlxMaterial)
await mx.readFromXmlString(doc, mtlxMaterial, searchPath);
else
Material.createFallbackMaterial(doc);
// Check if there are any looks defined in the document
// If so then traverse the looks for all material assignments.
// Generate code and compile for any associated surface shader
// and assign to the associated geometry. If there are no looks
// then the first material is found and assignment to all the
// geometry.
this.clearMaterials();
var looks = doc.getLooks();
if (looks.length)
{
for (let look of looks)
{
const materialAssigns = look.getMaterialAssigns();
for (let materialAssign of materialAssigns)
{
let matName = materialAssign.getMaterial();
if (matName)
{
let mat = doc.getChild(matName);
var shader;
if (mat)
{
var shaders = mx.getShaderNodes(mat);
if (shaders.length)
{
shader = shaders[0];
}
}
let collection = materialAssign.getCollection();
let geom = materialAssign.getGeom();
let newAssignment;
if (collection || geom)
{
// Remove leading "/" from collection and geom for
// later assignment comparison checking
if (collection && collection.charAt(0) == "/")
{
collection = collection.slice(1);
}
if (geom && geom.charAt(0) == "/")
{
geom = geom.slice(1);
}
newAssignment = new MaterialAssign(shader, geom, collection);
}
else
{
newAssignment = new MaterialAssign(shader, NO_GEOMETRY_SPECIFIER, null);
}
if (newAssignment)
{
this._materials.push(newAssignment);
}
}
}
}
}
else
{
// Search for any surface shaders. The first found
// is assumed to be assigned to the entire scene
// The identifier used is "*" to mean the entire scene.
const materialNodes = doc.getMaterialNodes();
let shaderList = [];
let foundRenderable = false;
for (let i = 0; i < materialNodes.length; ++i)
{
let materialNode = materialNodes[i];
if (materialNode)
{
console.log('Scan material: ', materialNode.getNamePath());
let shaderNodes = mx.getShaderNodes(materialNode)
if (shaderNodes.length > 0)
{
let shaderNodePath = shaderNodes[0].getNamePath()
if (!shaderList.includes(shaderNodePath))
{
let assignment = NO_GEOMETRY_SPECIFIER;
if (foundRenderable == false)
{
assignment = ALL_GEOMETRY_SPECIFIER;
foundRenderable = true;
}
console.log('-- add shader: ', shaderNodePath);
shaderList.push(shaderNodePath);
this._materials.push(new MaterialAssign(shaderNodes[0], assignment));
}
}
}
}
const nodeGraphs = doc.getNodeGraphs();
for (let i = 0; i < nodeGraphs.length; ++i)
{
let nodeGraph = nodeGraphs[i];
if (nodeGraph)
{
if (nodeGraph.hasAttribute('nodedef') || nodeGraph.hasSourceUri())
{
continue;
}
// Skip any nodegraph that is connected to something downstream
if (nodeGraph.getDownstreamPorts().length > 0)
{
continue
}
let outputs = nodeGraph.getOutputs();
for (let j = 0; j < outputs.length; ++j)
{
let output = outputs[j];
{
let assignment = NO_GEOMETRY_SPECIFIER;
if (foundRenderable == false)
{
assignment = ALL_GEOMETRY_SPECIFIER;
foundRenderable = true;
}
let newMat = new MaterialAssign(output, assignment, null);
this._materials.push(newMat);
}
}
}
}
const outputs = doc.getOutputs();
for (let i = 0; i < outputs.length; ++i)
{
let output = outputs[i];
if (output)
{
let assignment = NO_GEOMETRY_SPECIFIER;
if (foundRenderable == false)
{
assignment = ALL_GEOMETRY_SPECIFIER;
foundRenderable = true;
}
this._materials.push(new MaterialAssign(output, assignment));
}
}
const shaderNodes = [];
for (let i = 0; i < shaderNodes.length; ++i)
{
let shaderNode = shaderNodes[i];
let shaderNodePath = shaderNode.getNamePath()
if (!shaderList.includes(shaderNodePath))
{
let assignment = NO_GEOMETRY_SPECIFIER;
if (foundRenderable == false)
{
assignment = ALL_GEOMETRY_SPECIFIER;
foundRenderable = true;
}
shaderList.push(shaderNodePath);
this._materials.push(new MaterialAssign(shaderNode, assignment));
}
}
}
// Assign to default material if none found
if (this._materials.length == 0)
{
const defaultNode = Material.createFallbackMaterial(doc);
this._materials.push(new MaterialAssign(defaultNode, ALL_GEOMETRY_SPECIFIER));
}
// Create a new shader for each material node.
// Only create the shader once even if assigned more than once.
var startGenTime = performance.now();
let shaderMap = new Map();
let closeUI = false;
for (let matassign of this._materials)
{
// Need to use path vs name to get a unique key.
let materialName = matassign.getMaterial().getNamePath();
let shader = shaderMap[materialName];
if (!shader)
{
shader = viewer.getMaterial().generateMaterial(matassign, viewer, searchPath, closeUI);
shaderMap[materialName] = shader;
}
matassign.setShader(shader);
closeUI = true;
}
console.log("- Generate (", this._materials.length, ") shader(s) time: ", performance.now() - startGenTime, " ms.",);
// Update scene shader assignments
this.updateMaterialAssignments(viewer, "");
// Mark transform update
viewer.getScene().setUpdateTransforms();
console.log("Total material time: ", (performance.now() - startTime), "ms");
}
//
// Update the assignments for scene objects based on the
// material assignment information stored in the viewer.
// Note: If none of the MaterialX assignments match the geometry
// in the scene, then the first material assignment shader is assigned
// to the entire scene.
//
async updateMaterialAssignments(viewer, soloMaterial)
{
console.log("Update material assignments. Solo=", soloMaterial);
var startTime = performance.now();
let assigned = 0;
let assignedSolo = false;
for (let matassign of this._materials)
{
if (matassign.getShader())
{
if (soloMaterial.length)
{
if (matassign.getMaterial().getNamePath() == soloMaterial)
{
let temp = matassign.getGeometry();
matassign.setGeometry(ALL_GEOMETRY_SPECIFIER);
assigned += viewer.getScene().updateMaterial(matassign);
matassign.setGeometry(temp);
assignedSolo = true;
break
}
}
else
{
assigned += viewer.getScene().updateMaterial(matassign);
}
}
}
if (assigned == 0 && this._materials.length)
{
this._defaultMaterial = new MaterialAssign(this._materials[0].getMaterial(), ALL_GEOMETRY_SPECIFIER);
this._defaultMaterial.setShader(this._materials[0].getShader());
viewer.getScene().updateMaterial(this._defaultMaterial);
}
if (assigned > 0)
{
console.log('Material assignment time: ', performance.now() - startTime, " ms.");
}
}
//
// Generate a new material for a given element
//
generateMaterial(matassign, viewer, searchPath, closeUI)
{
var elem = matassign.getMaterial();
var startGenerateMat = performance.now();
const mx = viewer.getMx();
const textureLoader = new THREE.TextureLoader();
const lights = viewer.getLights();
const lightData = viewer.getLightData();
const radianceTexture = viewer.getRadianceTexture();
const irradianceTexture = viewer.getIrradianceTexture();
const gen = viewer.getGenerator();
const genContext = viewer.getGenContext();
// Perform transparency check on renderable item
var startTranspCheckTime = performance.now();
const isTransparent = mx.isTransparentSurface(elem, gen.getTarget());
genContext.getOptions().hwTransparency = isTransparent;
// Always set to complete.
// Can consider option to set to reduced as the parsing of large numbers of uniforms (e.g. on shading models)
// can be quite expensive.
genContext.getOptions().shaderInterfaceType = mx.ShaderInterfaceType.SHADER_INTERFACE_COMPLETE;
if (logDetailedTime)
console.log(" - Transparency check time: ", performance.now() - startTranspCheckTime, "ms");
// Generate GLES code
var startMTLXGenTime = performance.now();
let shader = gen.generate(elem.getNamePath(), elem, genContext);
if (logDetailedTime)
console.log(" - MaterialX gen time: ", performance.now() - startMTLXGenTime, "ms");
var startUniformUpdate = performance.now();
// Get shaders and uniform values
let vShader = shader.getSourceCode("vertex");
let fShader = shader.getSourceCode("pixel");
let theScene = viewer.getScene();
let flipV = theScene.getFlipGeometryV();
let uniforms = {
...getUniformValues(shader.getStage('vertex'), textureLoader, searchPath, flipV),
...getUniformValues(shader.getStage('pixel'), textureLoader, searchPath, flipV),
}
Object.assign(uniforms, {
u_numActiveLightSources: { value: lights.length },
u_lightData: { value: lightData },
u_envMatrix: { value: getLightRotation() },
u_envRadiance: { value: radianceTexture },
u_envRadianceMips: { value: Math.trunc(Math.log2(Math.max(radianceTexture.image.width, radianceTexture.image.height))) + 1 },
u_envRadianceSamples: { value: 16 },
u_envIrradiance: { value: irradianceTexture },
u_refractionEnv: { value: true }
});
// Create Three JS Material
let newMaterial = new THREE.RawShaderMaterial({
uniforms: uniforms,
vertexShader: vShader,
fragmentShader: fShader,
transparent: isTransparent,
blendEquation: THREE.AddEquation,
blendSrc: THREE.OneMinusSrcAlphaFactor,
blendDst: THREE.SrcAlphaFactor,
side: THREE.DoubleSide
});
if (logDetailedTime)
console.log(" - Three material update time: ", performance.now() - startUniformUpdate, "ms");
// Update property editor
const gui = viewer.getEditor().getGUI();
this.updateEditor(matassign, shader, newMaterial, gui, closeUI, viewer);
if (logDetailedTime)
console.log("- Per material generate time: ", performance.now() - startGenerateMat, "ms");
return newMaterial;
}
clearSoloMaterialUI()
{
for (let i = 0; i < this._materials.length; ++i)
{
let matassign = this._materials[i];
let matUI = matassign.getMaterialUI();
if (matUI)
{
let matTitle = matUI.domElement.getElementsByClassName('title')[0];
matTitle.classList.remove('peditor_material_assigned');
let img = matTitle.getElementsByTagName('img')[0];
img.src = 'public/shader_ball.svg';
//matTitle.classList.remove('peditor_material_unassigned');
}
}
}
static updateSoloMaterial(viewer, elemPath, materials, event)
{
// Prevent the event from being passed to parent folder
event.stopPropagation();
for (let i = 0; i < materials.length; ++i)
{
let matassign = materials[i];
// Need to use path vs name to get a unique key.
let materialName = matassign.getMaterial().getNamePath();
var matUI = matassign.getMaterialUI();
let matTitle = matUI.domElement.getElementsByClassName('title')[0];
let img = matTitle.getElementsByTagName('img')[0];
if (materialName == elemPath)
{
if (this._soloMaterial == elemPath)
{
img.src = 'public/shader_ball.svg';
matTitle.classList.remove('peditor_material_assigned');
this._soloMaterial = "";
}
else
{
img.src = 'public/shader_ball2.svg';
matTitle.classList.add('peditor_material_assigned');
this._soloMaterial = elemPath;
}
}
else
{
img.src = 'public/shader_ball.svg';
matTitle.classList.remove('peditor_material_assigned');
}
}
viewer.getMaterial().updateMaterialAssignments(viewer, this._soloMaterial);
viewer.getScene().setUpdateTransforms();
}
//
// Update property editor for a given MaterialX element, it's shader, and
// Three material
//
updateEditor(matassign, shader, material, gui, closeUI, viewer)
{
var elem = matassign.getMaterial();
var materials = this._materials;
const DEFAULT_MIN = 0;
const DEFAULT_MAX = 100;
var startTime = performance.now();
const elemPath = elem.getNamePath();
// Create and cache associated UI
var matUI = gui.addFolder(elemPath);
matassign.setMaterialUI(matUI);
let matTitle = matUI.domElement.getElementsByClassName('title')[0];
// Add a icon to the title to allow for assigning the material to geometry
// Clicking on the icon will "solo" the material to the geometry.
// Clicking on the title will open/close the material folder.
matTitle.innerHTML = "<img id='" + elemPath + "' src='public/shader_ball.svg' width='16' height='16' style='vertical-align:middle; margin-right: 5px;'>" + elem.getNamePath();
let img = matTitle.getElementsByTagName('img')[0];
if (img)
{
// Add event listener to icon to call updateSoloMaterial function
img.addEventListener('click', function (event)
{
Material.updateSoloMaterial(viewer, elemPath, materials, event);
});
}
if (closeUI)
{
matUI.close();
}
const uniformBlocks = Object.values(shader.getStage('pixel').getUniformBlocks());
var uniformToUpdate;
const ignoreList = ['u_envRadianceMips', 'u_envRadianceSamples', 'u_alphaThreshold'];
var folderList = new Map();
folderList[elemPath] = matUI;
uniformBlocks.forEach(uniforms =>
{
if (!uniforms.empty())
{
for (let i = 0; i < uniforms.size(); ++i)
{
const variable = uniforms.get(i);
const value = variable.getValue()?.getData();
let name = variable.getVariable();
if (ignoreList.includes(name))
{
continue;
}
let currentFolder = matUI;
let currentElemPath = variable.getPath();
if (!currentElemPath || currentElemPath.length == 0)
{
continue;
}
let currentElem = elem.getDocument().getDescendant(currentElemPath);
if (!currentElem)
{
continue;
}
// Skip non-input types and anything > 2 levels deep
if (!currentElem.asAInput() || currentElem.getNamePath().split('/').length > 2)
{
continue;
}
let currentNode = null;
if (currentElem.getParent() && currentElem.getParent().getNamePath() != "")
{
currentNode = currentElem.getParent();
}
let uiname = "";
let nodeDefInput = null;
if (currentNode)
{
let currentNodePath = currentNode.getNamePath();
var pathSplit = currentNodePath.split('/');
if (pathSplit.length)
{
currentNodePath = pathSplit[0];
}
currentFolder = folderList[currentNodePath];
if (!currentFolder)
{
currentFolder = matUI.addFolder(currentNodePath);
folderList[currentNodePath] = currentFolder;
}
// Check for ui attributes
var nodeDef = currentNode.getNodeDef();
if (nodeDef)
{
// Remove node name from shader uniform name for non root nodes
let lookup_name = name.replace(currentNode.getName() + '_', '');
nodeDefInput = nodeDef.getActiveInput(lookup_name);
if (nodeDefInput)
{
uiname = nodeDefInput.getAttribute('uiname');
let uifolderName = nodeDefInput.getAttribute('uifolder');
if (uifolderName && uifolderName.length)
{
let newFolderName = currentNodePath + '/' + uifolderName;
currentFolder = folderList[newFolderName];
if (!currentFolder)
{
currentFolder = matUI.addFolder(uifolderName);
currentFolder.domElement.classList.add('peditorfolder');
folderList[newFolderName] = currentFolder;
}
}
}
}
}
// Determine UI name to use
let path = name;
let interfaceName = currentElem.getAttribute("interfacename");
if (interfaceName && interfaceName.length)
{
const graph = currentNode.getParent();
if (graph)
{
const graphInput = graph.getInput(interfaceName);
if (graphInput)
{
let uiname = graphInput.getAttribute('uiname');
if (uiname.length)
{
path = uiname;
}
else
{
path = graphInput.getName();
}
}
}
else
{
path = interfaceName;
}
}
else
{
if (!uiname)
{
uiname = currentElem.getAttribute('uiname');
}
if (uiname && uiname.length)
{
path = uiname;
}
}
// Skip if already added to current folder
let found = false;
for (let i = 0; i < currentFolder.children.length; ++i)
{
if (currentFolder.children[i]._name == path)
{
found = true;
break;
}
}
if (found)
{
continue;
}
switch (variable.getType().getName())
{
case 'float':
uniformToUpdate = material.uniforms[name];
if (uniformToUpdate && value != null)
{
var minValue = DEFAULT_MIN;
if (value < minValue)
{
minValue = value;
}
var maxValue = DEFAULT_MAX;
if (value > maxValue)
{
maxValue = value;
}
var step = 0;
if (nodeDefInput)
{
if (nodeDefInput.hasAttribute('uisoftmin'))
minValue = parseFloat(nodeDefInput.getAttribute('uisoftmin'));
else if (nodeDefInput.hasAttribute('uimin'))
minValue = parseFloat(nodeDefInput.getAttribute('uimin'));
if (nodeDefInput.hasAttribute('uisoftmax'))
maxValue = parseFloat(nodeDefInput.getAttribute('uisoftmax'));
else if (nodeDefInput.hasAttribute('uimax'))
maxValue = parseFloat(nodeDefInput.getAttribute('uimax'));
if (nodeDefInput.hasAttribute('uistep'))
step = parseFloat(nodeDefInput.getAttribute('uistep'));
}
if (step == 0)
{
step = (maxValue - minValue) / 1000.0;
}
const w = currentFolder.add(material.uniforms[name], 'value', minValue, maxValue, step).name(path);
w.domElement.classList.add('peditoritem');
}
break;
case 'integer':
uniformToUpdate = material.uniforms[name];
if (uniformToUpdate && value != null)
{
var minValue = DEFAULT_MIN;
if (value < minValue)
{
minValue = value;
}
var maxValue = DEFAULT_MAX;
if (value > maxValue)
{
maxValue = value;
}
var step = 0;
var enumList = []
var enumValues = []
if (nodeDefInput)
{
if (nodeDefInput.hasAttribute('enum'))
{
// Get enum and enum values attributes (if present)
enumList = nodeDefInput.getAttribute('enum').split(',');
if (nodeDefInput.hasAttribute('enumvalues'))
{
enumValues = nodeDefInput.getAttribute('enumvalues').split(',').map(Number);
}
}
else
{
if (nodeDefInput.hasAttribute('uisoftmin'))
minValue = parseInt(nodeDefInput.getAttribute('uisoftmin'));
else if (nodeDefInput.hasAttribute('uimin'))
minValue = parseInt(nodeDefInput.getAttribute('uimin'));
if (nodeDefInput.hasAttribute('uisoftmax'))
maxValue = parseInt(nodeDefInput.getAttribute('uisoftmax'));
else if (nodeDefInput.hasAttribute('uimax'))
maxValue = parseInt(nodeDefInput.getAttribute('uimax'));
if (nodeDefInput.hasAttribute('uistep'))
step = parseInt(nodeDefInput.getAttribute('uistep'));
}
}
if (enumList.length == 0)
{
if (step == 0)
{
step = 1 / (maxValue - minValue);
step = Math.ceil(step);
if (step == 0)
{
step = 1;
}
}
}
if (enumList.length == 0)
{
let w = currentFolder.add(material.uniforms[name], 'value', minValue, maxValue, step).name(path);
w.domElement.classList.add('peditoritem');
}
else
{
// Map enumList strings to values
// Map to 0..N if no values are specified via enumvalues attribute
if (enumValues.length == 0)
{
for (let i = 0; i < enumList.length; ++i)
{
enumValues.push(i);
}
}
const enumeration = {};
enumList.forEach((str, index) =>
{
enumeration[str] = enumValues[index];
});
// Function to handle enum drop-down
function handleDropdownChange(value)
{
if (material.uniforms[name])
{
material.uniforms[name].value = value;
}
}
const defaultOption = enumList[value]; // Set the default selected option
const dropdownController = currentFolder.add(enumeration, defaultOption, enumeration).name(path);
dropdownController.onChange(handleDropdownChange);
dropdownController.domElement.classList.add('peditoritem');
}
}
break;
case 'boolean':
uniformToUpdate = material.uniforms[name];
if (uniformToUpdate && value != null)
{
let w = currentFolder.add(material.uniforms[name], 'value').name(path);
w.domElement.classList.add('peditoritem');
}
break;
case 'vector2':
case 'vector3':
case 'vector4':
uniformToUpdate = material.uniforms[name];
if (uniformToUpdate && value != null)
{
var minValue = [DEFAULT_MIN, DEFAULT_MIN, DEFAULT_MIN, DEFAULT_MIN];
var maxValue = [DEFAULT_MAX, DEFAULT_MAX, DEFAULT_MAX, DEFAULT_MAX];
var step = [0, 0, 0, 0];
if (nodeDefInput)
{
if (nodeDefInput.hasAttribute('uisoftmin'))
minValue = nodeDefInput.getAttribute('uisoftmin').split(',').map(Number);
else if (nodeDefInput.hasAttribute('uimin'))
minValue = nodeDefInput.getAttribute('uimin').split(',').map(Number);
if (nodeDefInput.hasAttribute('uisoftmax'))
maxValue = nodeDefInput.getAttribute('uisoftmax').split(',').map(Number);
else if (nodeDefInput.hasAttribute('uimax'))
maxValue = nodeDefInput.getAttribute('uimax').split(',').map(Number);
if (nodeDefInput.hasAttribute('uistep'))
step = nodeDefInput.getAttribute('uistep').split(',').map(Number);
}
for (let i = 0; i < 4; ++i)
{
if (step[i] == 0)
{
step[i] = 1 / (maxValue[i] - minValue[i]);
}
}
const keyString = ["x", "y", "z", "w"];
let vecFolder = currentFolder.addFolder(path);
Object.keys(material.uniforms[name].value).forEach((key) =>
{
let w = vecFolder.add(material.uniforms[name].value,
key, minValue[key], maxValue[key], step[key]).name(keyString[key]);
w.domElement.classList.add('peditoritem');
})
}
break;
case 'color3':
// Irksome way to map arrays to colors and back
uniformToUpdate = material.uniforms[name];
if (uniformToUpdate && value != null)
{
var dummy =
{
color: 0xFF0000
};
const color3 = new THREE.Color(dummy.color);
color3.fromArray(material.uniforms[name].value);
dummy.color = color3.getHex();
let w = currentFolder.addColor(dummy, 'color').name(path)
.onChange(function (value)
{
const color3 = new THREE.Color(value);
material.uniforms[name].value.set(color3.toArray());
});
w.domElement.classList.add('peditoritem');
}
break;
case 'color4':
break;
case 'matrix33':
case 'matrix44':
case 'samplerCube':
case 'filename':
break;
case 'string':
if (value != null)
{
var dummy =
{
thevalue: value
}
let item = currentFolder.add(dummy, 'thevalue');
item.name(path);
item.disable(true);
item.domElement.classList.add('peditoritem');
}
break;
default:
break;
}
}
}
});
if (logDetailedTime)
{
console.log(" - Editor update time: ", performance.now() - startTime, "ms");
}
}
// List of material assignments: { MaterialX node, geometry assignment string, and hardware shader }
_materials;
// Fallback material if nothing was assigned explicitly
_defaultMaterial;
}
/*
Viewer class
Keeps track of local scene, and property editor as well as current MaterialX document
and assocaited material, shader and lighting information.
*/
export class Viewer
{
static create()
{
return new Viewer();
}
constructor()
{
this.scene = new Scene();
this.editor = new Editor();
this.materials.push(new Material());
this.fileLoader = new THREE.FileLoader();
this.hdrLoader = new RGBELoader();
}
//
// Create shader generator, generation context and "base" document which
// contains the standard definition libraries and lighting elements.
//
async initialize(mtlxIn, renderer, radianceTexture, irradianceTexture, lightRigXml)
{
this.mx = mtlxIn;
// Initialize base document
this.generator = new this.mx.EsslShaderGenerator();
this.genContext = new this.mx.GenContext(this.generator);
this.document = this.mx.createDocument();
this.stdlib = this.mx.loadStandardLibraries(this.genContext);
this.document.importLibrary(this.stdlib);
this.initializeLighting(renderer, radianceTexture, irradianceTexture, lightRigXml);
radianceTexture.mapping = THREE.EquirectangularReflectionMapping;
this.getScene().setBackgroundTexture(radianceTexture);
}
//
// Load in lighting rig document and register lights with generation context
// Initialize environment lighting (IBLs).
//
async initializeLighting(renderer, radianceTexture, irradianceTexture, lightRigXml)
{
// Load lighting setup into document
const mx = this.getMx();
this.lightRigDoc = mx.createDocument();
await mx.readFromXmlString(this.lightRigDoc, lightRigXml);
this.document.importLibrary(this.lightRigDoc);
// Register lights with generation context
this.lights = findLights(this.document);
this.lightData = registerLights(mx, this.lights, this.genContext);
this.radianceTexture = prepareEnvTexture(radianceTexture, renderer.capabilities);
this.irradianceTexture = prepareEnvTexture(irradianceTexture, renderer.capabilities);
}
getEditor()
{
return this.editor;
}
getScene()
{
return this.scene;
}
getMaterial()
{
return this.materials[0];
}
getFileLoader()
{
return this.fileLoader;
}
getHdrLoader()
{
return this.hdrLoader;
}
setDocument(doc)
{
this.doc = doc;
}
getDocument()
{
return this.doc;
}
getLibrary()
{
return this.stdlib;
}
getLightRig()
{
return this.lightRigDoc;
}
getMx()
{
return this.mx;
}
getGenerator()
{
return this.generator;
}
getGenContext()
{
return this.genContext;
}
getLights()
{
return this.lights;
}
getLightData()
{
return this.lightData;
}
getRadianceTexture()
{
return this.radianceTexture;
}
getIrradianceTexture()
{
return this.irradianceTexture;
}
// Three scene and materials.
scene = null;
materials = [];
// Property editor
editor = null;
// Utility loaders
fileloader = null;
hdrLoader = null;
// MaterialX module, current document and support documents.
mx = null;
doc = null;
stdlib = null;
lightRigDoc = null;
// MaterialX code generator and context
generator = null;
genContext = null;
// Lighting information
lights = null;
lightData = null;
radianceTexture = null;
irradianceTexture = null;
}