1617 lines
57 KiB
JavaScript
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;
|
|
}
|