mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-24 21:55:09 +00:00
stuff
This commit is contained in:
4
MaterialX/javascript/.gitignore
vendored
Normal file
4
MaterialX/javascript/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
build
|
||||
**/_build
|
||||
**/dist
|
||||
**/node_modules
|
||||
10
MaterialX/javascript/MaterialXTest/.babelrc
Normal file
10
MaterialX/javascript/MaterialXTest/.babelrc
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"exclude": ["transform-regenerator"]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
47
MaterialX/javascript/MaterialXTest/browser/karma.conf.js
Normal file
47
MaterialX/javascript/MaterialXTest/browser/karma.conf.js
Normal file
@@ -0,0 +1,47 @@
|
||||
module.exports = function (config)
|
||||
{
|
||||
config.set({
|
||||
basePath: '../', // base is the javascript folder
|
||||
files: [
|
||||
{ pattern: '_build/JsMaterialXGenShader.js', watched: false, included: true, served: true },
|
||||
{ pattern: '_build/JsMaterialXGenShader.wasm', watched: false, included: false, served: true },
|
||||
{ pattern: '_build/JsMaterialXGenShader.data', watched: false, included: false, served: true, nocache: true },
|
||||
{ pattern: 'browser/*.spec.js', watched: true, included: true, served: true },
|
||||
],
|
||||
mime: {
|
||||
'application/wasm': ['wasm'],
|
||||
'application/octet-stream; charset=UTF-8': ['data'],
|
||||
},
|
||||
proxies: {
|
||||
'/JsMaterialXGenShader.data': '/base/_build/JsMaterialXGenShader.data',
|
||||
},
|
||||
reporters: ['mocha'],
|
||||
client: {
|
||||
mocha: {
|
||||
reporter: 'html'
|
||||
}
|
||||
},
|
||||
browsers: ['Chrome'],
|
||||
port: 8080,
|
||||
autoWatch: true,
|
||||
concurrency: Infinity,
|
||||
// logLevel: config.LOG_DEBUG,
|
||||
frameworks: ['mocha', 'chai'],
|
||||
plugins: [
|
||||
'karma-chai',
|
||||
'karma-chrome-launcher',
|
||||
'karma-mocha',
|
||||
'karma-mocha-reporter',
|
||||
],
|
||||
customLaunchers: {
|
||||
ChromeHeadlessGL: {
|
||||
base: 'Chrome',
|
||||
flags: [
|
||||
'--headless=new',
|
||||
'--enable-gpu',
|
||||
'--enable-webgl',
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
// MaterialX is served through a script tag in the test setup.
|
||||
|
||||
function createStandardSurfaceMaterial(mx)
|
||||
{
|
||||
const doc = mx.createDocument();
|
||||
const ssName = 'SR_default';
|
||||
const 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);
|
||||
expect(doc.validate()).to.be.true;
|
||||
// Release local wrappers
|
||||
shaderElement.delete();
|
||||
smNode.delete();
|
||||
ssNode.delete();
|
||||
return doc;
|
||||
}
|
||||
|
||||
describe('Generate Shaders', function ()
|
||||
{
|
||||
let mx;
|
||||
const canvas = document.createElement('canvas');
|
||||
const gl = canvas.getContext('webgl2');
|
||||
|
||||
this.timeout(60000);
|
||||
|
||||
before(async function ()
|
||||
{
|
||||
mx = await MaterialX();
|
||||
});
|
||||
|
||||
it('Compile Shaders', () =>
|
||||
{
|
||||
const doc = createStandardSurfaceMaterial(mx);
|
||||
|
||||
const generators = []
|
||||
if (typeof mx.EsslShaderGenerator != 'undefined')
|
||||
generators.push(mx.EsslShaderGenerator.create());
|
||||
if (typeof mx.GlslShaderGenerator != 'undefined')
|
||||
generators.push(mx.GlslShaderGenerator.create());
|
||||
if (typeof mx.MslShaderGenerator != 'undefined')
|
||||
generators.push(mx.MslShaderGenerator.create());
|
||||
if (typeof mx.OslShaderGenerator != 'undefined')
|
||||
generators.push(mx.OslShaderGenerator.create());
|
||||
if (typeof mx.VkShaderGenerator != 'undefined')
|
||||
generators.push(mx.VkShaderGenerator.create());
|
||||
if (typeof mx.WgslShaderGenerator != 'undefined')
|
||||
generators.push(mx.WgslShaderGenerator.create());
|
||||
if (typeof mx.MdlShaderGenerator != 'undefined')
|
||||
generators.push(mx.MdlShaderGenerator.create());
|
||||
if (typeof mx.SlangShaderGenerator != 'undefined')
|
||||
generators.push(mx.SlangShaderGenerator.create());
|
||||
|
||||
const elem = mx.findRenderableElement(doc);
|
||||
for (let gen of generators)
|
||||
{
|
||||
console.log("Generating shader for " + gen.getTarget() + "...");
|
||||
|
||||
const genContext = new mx.GenContext(gen);
|
||||
const stdlib = mx.loadStandardLibraries(genContext);
|
||||
doc.importLibrary(stdlib);
|
||||
|
||||
try
|
||||
{
|
||||
const mxShader = gen.generate(elem.getNamePath(), elem, genContext);
|
||||
|
||||
const fShader = mxShader.getSourceCode("pixel");
|
||||
|
||||
if (gen.getTarget() == 'essl')
|
||||
{
|
||||
const vShader = mxShader.getSourceCode("vertex");
|
||||
|
||||
const glVertexShader = gl.createShader(gl.VERTEX_SHADER);
|
||||
gl.shaderSource(glVertexShader, vShader);
|
||||
gl.compileShader(glVertexShader);
|
||||
if (!gl.getShaderParameter(glVertexShader, gl.COMPILE_STATUS))
|
||||
{
|
||||
console.error("-------- VERTEX SHADER FAILED TO COMPILE: ----------------");
|
||||
console.error("--- VERTEX SHADER LOG ---");
|
||||
console.error(gl.getShaderInfoLog(glVertexShader));
|
||||
console.error("--- VERTEX SHADER START ---");
|
||||
console.error(fShader);
|
||||
console.error("--- VERTEX SHADER END ---");
|
||||
}
|
||||
expect(gl.getShaderParameter(glVertexShader, gl.COMPILE_STATUS)).to.equal(true);
|
||||
|
||||
const glPixelShader = gl.createShader(gl.FRAGMENT_SHADER);
|
||||
gl.shaderSource(glPixelShader, fShader);
|
||||
gl.compileShader(glPixelShader);
|
||||
if (!gl.getShaderParameter(glPixelShader, gl.COMPILE_STATUS))
|
||||
{
|
||||
console.error("-------- PIXEL SHADER FAILED TO COMPILE: ----------------");
|
||||
console.error("--- PIXEL SHADER LOG ---");
|
||||
console.error(gl.getShaderInfoLog(glPixelShader));
|
||||
console.error("--- PIXEL SHADER START ---");
|
||||
console.error(fShader);
|
||||
console.error("--- PIXEL SHADER END ---");
|
||||
}
|
||||
expect(gl.getShaderParameter(glPixelShader, gl.COMPILE_STATUS)).to.equal(true);
|
||||
// Cleanup GL shaders
|
||||
gl.deleteShader(glVertexShader);
|
||||
gl.deleteShader(glPixelShader);
|
||||
}
|
||||
// Cleanup shader wrapper
|
||||
mxShader.delete();
|
||||
}
|
||||
catch (errPtr)
|
||||
{
|
||||
console.error("-------- Failed code generation: ----------------");
|
||||
if (typeof mx.getExceptionMessage === 'function')
|
||||
{
|
||||
console.error(mx.getExceptionMessage(errPtr));
|
||||
}
|
||||
else
|
||||
{
|
||||
console.error(errPtr);
|
||||
}
|
||||
}
|
||||
// Cleanup per-generator wrappers
|
||||
stdlib.delete();
|
||||
genContext.delete();
|
||||
gen.delete();
|
||||
}
|
||||
// Cleanup element and document
|
||||
elem.delete();
|
||||
doc.delete();
|
||||
});
|
||||
});
|
||||
145
MaterialX/javascript/MaterialXTest/codeExamples.spec.js
Normal file
145
MaterialX/javascript/MaterialXTest/codeExamples.spec.js
Normal file
@@ -0,0 +1,145 @@
|
||||
import { expect } from 'chai';
|
||||
import Module from './_build/JsMaterialXCore.js';
|
||||
import { getMtlxStrings } from './testHelpers';
|
||||
|
||||
describe('Code Examples', () =>
|
||||
{
|
||||
it('Building a MaterialX Document', async () =>
|
||||
{
|
||||
const mx = await Module();
|
||||
// Create a document.
|
||||
const doc = mx.createDocument();
|
||||
|
||||
// Create a node graph with a single image node and output.
|
||||
const nodeGraph = doc.addNodeGraph();
|
||||
expect(doc.getNodeGraphs().length).to.equal(1);
|
||||
const image = nodeGraph.addNode('image');
|
||||
const nodes = nodeGraph.getNodes();
|
||||
expect(nodes.length).to.equal(1);
|
||||
expect(nodes[0]).to.eql(image);
|
||||
|
||||
image.setInputValueString('file', 'image1.tif', 'filename');
|
||||
const input = image.getInput('file');
|
||||
expect(input).to.not.be.null;
|
||||
expect(input.getValue().getData()).to.equal('image1.tif');
|
||||
|
||||
const output = nodeGraph.addOutput();
|
||||
const outputs = nodeGraph.getOutputs();
|
||||
expect(outputs.length).to.equal(1);
|
||||
expect(outputs[0]).to.eql(output);
|
||||
|
||||
output.setConnectedNode(image);
|
||||
const connectedNode = output.getConnectedNode();
|
||||
expect(connectedNode).to.not.be.null;
|
||||
expect(connectedNode instanceof mx.Node).to.be.true;
|
||||
|
||||
// Create a simple shader interface.
|
||||
const simpleSrf = doc.addNodeDef('ND_simpleSrf', 'surfaceshader', 'simpleSrf');
|
||||
const nodeDefs = doc.getNodeDefs();
|
||||
expect(nodeDefs.length).to.equal(1);
|
||||
expect(nodeDefs[0]).to.eql(simpleSrf);
|
||||
|
||||
simpleSrf.setInputValueColor3('diffColor', new mx.Color3(1.0, 1.0, 1.0));
|
||||
let inputValue = simpleSrf.getInputValue('diffColor');
|
||||
expect(inputValue).to.not.be.null;
|
||||
expect(inputValue.getData()).to.eql(new mx.Color3(1.0, 1.0, 1.0));
|
||||
|
||||
simpleSrf.setInputValueColor3('specColor', new mx.Color3(0.0, 0.0, 0.0));
|
||||
inputValue = simpleSrf.getInputValue('specColor');
|
||||
expect(inputValue).to.not.be.null;
|
||||
expect(inputValue.getData()).to.eql(new mx.Color3(0.0, 0.0, 0.0));
|
||||
|
||||
const roughness = simpleSrf.setInputValueFloat('roughness', 0.25);
|
||||
inputValue = simpleSrf.getInputValue('roughness');
|
||||
expect(inputValue).to.not.be.null;
|
||||
expect(inputValue.getData()).to.equal(0.25);
|
||||
|
||||
// // Create a material that instantiates the shader.
|
||||
// const material = doc.addMaterial();
|
||||
// const materials = doc.getMaterials();
|
||||
// expect(materials.length).to.equal(1);
|
||||
// expect(materials[0]).to.eql(material);
|
||||
// const refSimpleSrf = material.addShaderRef('SR_simpleSrf', 'simpleSrf');
|
||||
// const shaderRefs = material.getShaderRefs();
|
||||
// expect(shaderRefs.length).to.equal(1);
|
||||
// expect(shaderRefs[0]).to.eql(refSimpleSrf);
|
||||
// expect(shaderRefs[0].getName()).to.equal('SR_simpleSrf');
|
||||
|
||||
// // Bind roughness to a new value within this material.
|
||||
// const bindInput = refSimpleSrf.addBindInput('roughness');
|
||||
// const bindInputs = refSimpleSrf.getBindInputs();
|
||||
// expect(bindInputs.length).to.equal(1);
|
||||
// expect(bindInputs[0]).to.eql(bindInput);
|
||||
// bindInput.setValuefloat(0.5);
|
||||
// expect(bindInput.getValue()).to.not.be.null;
|
||||
// expect(bindInput.getValue().getData()).to.equal(0.5);
|
||||
|
||||
// // Validate the value of roughness in the context of this material.
|
||||
// expect(roughness.getBoundValue(material).getValueString()).to.equal('0.5');
|
||||
// Cleanup wrappers
|
||||
nodeDefs.forEach(nd => nd.delete());
|
||||
output.delete();
|
||||
image.delete();
|
||||
nodeGraph.delete();
|
||||
doc.delete();
|
||||
});
|
||||
|
||||
it('Traversing a Document Tree', async () =>
|
||||
{
|
||||
const xmlStr = getMtlxStrings(
|
||||
['standard_surface_greysphere_calibration.mtlx'],
|
||||
'../../resources/Materials/Examples/StandardSurface'
|
||||
)[0];
|
||||
const mx = await Module();
|
||||
|
||||
// Read a document from disk.
|
||||
const doc = mx.createDocument();
|
||||
await mx.readFromXmlString(doc, xmlStr);
|
||||
|
||||
// Traverse the document tree in depth-first order.
|
||||
const elements = doc.traverseTree();
|
||||
let imageCount = 0;
|
||||
for (let elem of elements)
|
||||
{
|
||||
if (elem.isANode('image'))
|
||||
{
|
||||
imageCount++;
|
||||
}
|
||||
}
|
||||
expect(imageCount).to.greaterThan(0);
|
||||
doc.delete();
|
||||
});
|
||||
|
||||
it('Building a MaterialX Document', async () =>
|
||||
{
|
||||
const xmlStr = getMtlxStrings(['standard_surface_marble_solid.mtlx'], '../../resources/Materials/Examples/StandardSurface')[0];
|
||||
const mx = await Module();
|
||||
|
||||
// Read a document from disk.
|
||||
const doc = mx.createDocument();
|
||||
await mx.readFromXmlString(doc, xmlStr);
|
||||
|
||||
// let materialCount = 0;
|
||||
// let shaderInputCount = 0;
|
||||
// // Iterate through 1.37 materials for which there should be none
|
||||
// const materials = doc.getMaterials();
|
||||
// materials.forEach((material) => {
|
||||
// materialCount++;
|
||||
|
||||
// // For each shader input, find all upstream images in the dataflow graph.
|
||||
// const primaryShaderInputs = material.getPrimaryShaderInputs();
|
||||
// primaryShaderInputs.forEach((input) => {
|
||||
// const graphIter = input.traverseGraph(material);
|
||||
// let edge = graphIter.next();
|
||||
// while (edge) {
|
||||
// shaderInputCount++;
|
||||
// edge = graphIter.next();
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
// expect(materialCount).to.equal(0);
|
||||
// expect(shaderInputCount).to.equal(0);
|
||||
doc.delete();
|
||||
});
|
||||
});
|
||||
250
MaterialX/javascript/MaterialXTest/customBindings.spec.js
Normal file
250
MaterialX/javascript/MaterialXTest/customBindings.spec.js
Normal file
@@ -0,0 +1,250 @@
|
||||
import { expect } from 'chai';
|
||||
import Module from './_build/JsMaterialXCore.js';
|
||||
import { getMtlxStrings } from './testHelpers';
|
||||
|
||||
describe('Custom Bindings', () =>
|
||||
{
|
||||
const examplesPath = '../../resources/Materials/Examples/StandardSurface';
|
||||
|
||||
let mx;
|
||||
before(async () =>
|
||||
{
|
||||
mx = await Module();
|
||||
});
|
||||
|
||||
it('Optional parameters work as expected', () =>
|
||||
{
|
||||
const doc = mx.createDocument();
|
||||
// Call a method without optional argument
|
||||
const nodeGraph = doc.addNodeGraph();
|
||||
expect(nodeGraph).to.be.instanceof(mx.NodeGraph);
|
||||
expect(nodeGraph.getName()).to.equal('nodegraph1'); // Auto-constructed default value
|
||||
// Call a method with optional argument
|
||||
const nodeGraph2 = doc.addNodeGraph('myGraph');
|
||||
expect(nodeGraph2).to.be.instanceof(mx.NodeGraph);
|
||||
expect(nodeGraph2.getName()).to.equal('myGraph');
|
||||
|
||||
// Call a method that requires at least one parameter
|
||||
const node = nodeGraph.addNode('node');
|
||||
expect(node).to.be.instanceof(mx.Node);
|
||||
|
||||
// Omitting non-optional parameter should throw
|
||||
expect(() => { nodeGraph.addNode(); }).to.throw;
|
||||
|
||||
// Cleanup
|
||||
node.delete();
|
||||
nodeGraph2.delete();
|
||||
nodeGraph.delete();
|
||||
doc.delete();
|
||||
});
|
||||
|
||||
it('Vector <-> Array conversion', () =>
|
||||
{
|
||||
// Functions that return vectors in C++ should return an array in JS
|
||||
const doc = mx.createDocument();
|
||||
const nodeGraph = doc.addNodeGraph();
|
||||
const nodeGraphB = doc.addNodeGraph();
|
||||
const nodeGraphs = doc.getNodeGraphs();
|
||||
expect(nodeGraphs).to.be.an.instanceof(Array);
|
||||
expect(nodeGraphs.length).to.equal(2);
|
||||
|
||||
// Elements fetched through the vector -> array conversion should be editable and changes should be reflected
|
||||
// in the original objects.
|
||||
// Note: We cannot simply compare these objects for equality, since they're separately constructed pointers
|
||||
// to the same object.
|
||||
const backdrop = nodeGraph.addBackdrop();
|
||||
const backDrops = nodeGraphs[0].getBackdrops();
|
||||
expect(backDrops.length).to.equal(1);
|
||||
nodeGraphs[0].addBackdrop();
|
||||
expect(nodeGraph.getBackdrops().length).to.equal(2);
|
||||
|
||||
// Functions that expect vectors as parameters in C++ should accept arrays in JS
|
||||
// Built-in types (strings)
|
||||
const pathSegments = ['path', 'to', 'something'];
|
||||
const namePath = mx.createNamePath(pathSegments);
|
||||
expect(namePath).to.equal(pathSegments.join(mx.NAME_PATH_SEPARATOR));
|
||||
|
||||
// Complex (smart pointer) types
|
||||
const node1 = nodeGraph.addNode('node');
|
||||
const node2 = nodeGraph.addNode('node');
|
||||
const node3 = nodeGraph.addNode('node', 'anotherNode');
|
||||
backdrop.setContainsElements([node1, node2, node3]);
|
||||
const nodes = backdrop.getContainsElements();
|
||||
expect(nodes.length).to.equal(3);
|
||||
expect(nodes[0].getName()).to.equal('node1'); // Name auto-constructed from category
|
||||
expect(nodes[1].getName()).to.equal('node2'); // Name auto-constructed from category
|
||||
expect(nodes[2].getName()).to.equal('anotherNode'); // Name set explicitly at creation time
|
||||
|
||||
// Cleanup created wrappers
|
||||
nodes.forEach(n => n.delete());
|
||||
backdrop.delete();
|
||||
node3.delete();
|
||||
node2.delete();
|
||||
node1.delete();
|
||||
nodeGraphB.delete();
|
||||
nodeGraph.delete();
|
||||
doc.delete();
|
||||
});
|
||||
|
||||
it('C++ exception handling', () =>
|
||||
{
|
||||
// Exceptions that are thrown and caught in C++ shouldn't bubble up to JS
|
||||
const doc = mx.createDocument();
|
||||
const nodeGraph1 = doc.addNodeGraph();
|
||||
const nodeGraph2 = doc.addNodeGraph();
|
||||
nodeGraph1.setInheritsFrom(nodeGraph2);
|
||||
nodeGraph2.setInheritsFrom(nodeGraph1);
|
||||
expect(nodeGraph1.hasInheritanceCycle()).to.not.throw;
|
||||
expect(nodeGraph1.hasInheritanceCycle()).to.be.true;
|
||||
|
||||
// Exceptions that are not caught in C++ should throw
|
||||
nodeGraph1.addNode('node', 'node1');
|
||||
expect(() => { nodeGraph1.addNode('node', 'node1'); }).to.throw;
|
||||
try
|
||||
{
|
||||
nodeGraph1.addNode('node', 'node1');
|
||||
} catch (err)
|
||||
{
|
||||
expect(mx.getExceptionMessage(err)).to.be.a('string');
|
||||
}
|
||||
// Cleanup
|
||||
nodeGraph2.delete();
|
||||
nodeGraph1.delete();
|
||||
doc.delete();
|
||||
});
|
||||
|
||||
it('getReferencedSourceUris', async () =>
|
||||
{
|
||||
const doc = mx.createDocument();
|
||||
const filename = 'standard_surface_look_brass_tiled.mtlx';
|
||||
await mx.readFromXmlFile(doc, filename, examplesPath);
|
||||
const sourceUris = doc.getReferencedSourceUris();
|
||||
expect(sourceUris).to.be.instanceof(Array);
|
||||
expect(sourceUris.length).to.equal(3);
|
||||
expect(sourceUris[0]).to.be.a('string');
|
||||
expect(sourceUris.includes('standard_surface_brass_tiled.mtlx')).to.be.true;
|
||||
doc.delete();
|
||||
});
|
||||
|
||||
it('Should invoke correct instance of \'validate\'', () =>
|
||||
{
|
||||
// We check whether the correct function is called by provoking an error message that is specific to the
|
||||
// function that we expect to be called.
|
||||
const message = {};
|
||||
|
||||
// Should invoke Document::validate.
|
||||
const doc = mx.createDocument();
|
||||
expect(doc.validate()).to.be.true;
|
||||
doc.removeAttribute(mx.InterfaceElement.VERSION_ATTRIBUTE);
|
||||
expect(doc.validate()).to.be.true;
|
||||
|
||||
// Should invoke Node::validate
|
||||
const node = doc.addNode('node');
|
||||
expect(node.validate()).to.be.true;
|
||||
node.setCategory('');
|
||||
expect(node.validate()).to.be.false;
|
||||
expect(node.validate(message)).to.be.false;
|
||||
expect(message.message).to.include('Node element is missing a category');
|
||||
|
||||
// Should invoke inherited ValueElement::validate
|
||||
const token = new mx.Token(node, 'token');
|
||||
expect(token.validate()).to.be.true;
|
||||
token.setUnitType('bogus');
|
||||
expect(token.validate()).to.be.false;
|
||||
expect(token.validate(message)).to.be.false;
|
||||
expect(message.message).to.include('Unit type definition does not exist in document')
|
||||
|
||||
// Cleanup
|
||||
token.delete();
|
||||
node.delete();
|
||||
doc.delete();
|
||||
});
|
||||
|
||||
it('StringResolver name substitution getters', () =>
|
||||
{
|
||||
const fnTestData = {
|
||||
fnKey: 'fnValue',
|
||||
fnKey1: 'fnValue1'
|
||||
};
|
||||
const fnTestKeys = Object.keys(fnTestData);
|
||||
|
||||
const gnTestData = {
|
||||
gnKey: 'gnValue',
|
||||
gnKey1: 'gnValue1'
|
||||
};
|
||||
const gnTestKeys = Object.keys(gnTestData);
|
||||
|
||||
const resolver = mx.StringResolver.create();
|
||||
|
||||
resolver.setFilenameSubstitution(fnTestKeys[0], fnTestData[fnTestKeys[0]]);
|
||||
resolver.setFilenameSubstitution(fnTestKeys[1], fnTestData[fnTestKeys[1]]);
|
||||
const fnSubs = resolver.getFilenameSubstitutions();
|
||||
expect(fnSubs).to.be.instanceof(Object);
|
||||
expect(Object.keys(fnSubs).length).to.equal(2);
|
||||
expect(fnSubs).to.deep.equal(fnTestData);
|
||||
|
||||
resolver.setGeomNameSubstitution(gnTestKeys[0], gnTestData[gnTestKeys[0]]);
|
||||
resolver.setGeomNameSubstitution(gnTestKeys[1], gnTestData[gnTestKeys[1]]);
|
||||
const gnSubs = resolver.getGeomNameSubstitutions();
|
||||
expect(gnSubs).to.be.instanceof(Object);
|
||||
expect(Object.keys(gnSubs).length).to.equal(2);
|
||||
expect(gnSubs).to.deep.equal(gnTestData);
|
||||
resolver.delete();
|
||||
});
|
||||
|
||||
it('getShaderNodes', async () =>
|
||||
{
|
||||
const doc = mx.createDocument();
|
||||
const fileNames = ['standard_surface_marble_solid.mtlx'];
|
||||
const mtlxStrs = getMtlxStrings(fileNames, examplesPath);
|
||||
await mx.readFromXmlString(doc, mtlxStrs[0]);
|
||||
let matNodes = doc.getMaterialNodes();
|
||||
expect(matNodes.length).to.equal(1);
|
||||
const matNode = matNodes[0];
|
||||
|
||||
// Should return a surface shader node but no displacement shader node
|
||||
let shaderNodes = mx.getShaderNodes(matNode);
|
||||
expect(shaderNodes).to.be.instanceof(Array);
|
||||
expect(shaderNodes.length).to.equal(1);
|
||||
expect(shaderNodes[0].getType()).to.equal(mx.SURFACE_SHADER_TYPE_STRING);
|
||||
shaderNodes = mx.getShaderNodes(matNode, mx.DISPLACEMENT_SHADER_TYPE_STRING);
|
||||
expect(shaderNodes).to.be.instanceof(Array);
|
||||
expect(shaderNodes.length).to.equal(0);
|
||||
|
||||
// Cleanup wrappers
|
||||
shaderNodes.forEach(s => s.delete());
|
||||
matNodes.forEach(n => n.delete());
|
||||
doc.delete();
|
||||
});
|
||||
|
||||
it('createValidName', () =>
|
||||
{
|
||||
const testString = '_Note_:Please,turn.this+-into*1#valid\nname for_me';
|
||||
const replaceRegex = /[^a-zA-Z0-9_:]/g
|
||||
expect(mx.createValidName(testString)).to.equal(testString.replace(replaceRegex, '_'));
|
||||
expect(mx.createValidName(testString, '-')).to.equal(testString.replace(replaceRegex, '-'));
|
||||
});
|
||||
|
||||
it('getVersionIntegers', () =>
|
||||
{
|
||||
const versionStringArr = mx.getVersionString().split('.').map((value) => parseInt(value, 10));
|
||||
|
||||
// Global getVersionIntegers
|
||||
const globalVersion = mx.getVersionIntegers();
|
||||
expect(globalVersion).to.be.instanceof(Array);
|
||||
expect(globalVersion.length).to.equal(3);
|
||||
expect(globalVersion).to.deep.equal(versionStringArr);
|
||||
|
||||
// Document.getVersionIntegers
|
||||
versionStringArr.pop();
|
||||
const doc = mx.createDocument();
|
||||
const docVersion = doc.getVersionIntegers();
|
||||
expect(docVersion).to.be.instanceof(Array);
|
||||
expect(docVersion.length).to.equal(2);
|
||||
expect(docVersion).to.deep.equal(versionStringArr);
|
||||
doc.delete();
|
||||
|
||||
// InterfaceElement.getVersionIntegers (via NodeDef)
|
||||
// TODO: This function can currently not be called, since we have a linker issue that messes up this function.
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0"?>
|
||||
<materialx version="1.38" colorspace="lin_rec709" xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||
<xi:include href="cycle.mtlx" />
|
||||
</materialx>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0"?>
|
||||
<materialx version="1.38" colorspace="lin_rec709" xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||
<xi:include href="../folder2/include3.mtlx" />
|
||||
</materialx>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0"?>
|
||||
<materialx version="1.38" colorspace="lin_rec709" xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||
<simple_srf name="sr_ps" type="surfaceshader">
|
||||
<input name="specColor" type="color3" value="0.05, 0.05, 0.05" />
|
||||
<input name="specRoughness" type="float" value="0.14" />
|
||||
</simple_srf>
|
||||
<surfacematerial name="paint_semigloss" type="material">
|
||||
<input name="surfaceshader" type="surfaceshader" nodename="sr_ps" />
|
||||
</surfacematerial>
|
||||
</materialx>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0"?>
|
||||
<materialx version="1.38" colorspace="lin_rec709" xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||
<xi:include href="folder2/include3.mtlx" />
|
||||
</materialx>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0"?>
|
||||
<materialx version="1.38" colorspace="lin_rec709" xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||
<xi:include href="include2.mtlx" />
|
||||
</materialx>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0"?>
|
||||
<materialx version="1.38" colorspace="lin_rec709" xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||
<xi:include href="include1.mtlx" />
|
||||
<xi:include href="folder/include2.mtlx" />
|
||||
</materialx>
|
||||
188
MaterialX/javascript/MaterialXTest/document.spec.js
Normal file
188
MaterialX/javascript/MaterialXTest/document.spec.js
Normal file
@@ -0,0 +1,188 @@
|
||||
import { expect } from 'chai';
|
||||
import Module from './_build/JsMaterialXCore.js';
|
||||
|
||||
describe('Document', () =>
|
||||
{
|
||||
let mx, doc;
|
||||
before(async () =>
|
||||
{
|
||||
mx = await Module();
|
||||
// Create a document.
|
||||
doc = mx.createDocument();
|
||||
});
|
||||
|
||||
function expectError(type, cb)
|
||||
{
|
||||
try
|
||||
{
|
||||
cb();
|
||||
throw new Error('Expected function to throw!');
|
||||
} catch (exceptionPtr)
|
||||
{
|
||||
const message = mx.getExceptionMessage(exceptionPtr);
|
||||
expect(message.indexOf(type) !== -1).to.be.true;
|
||||
}
|
||||
}
|
||||
|
||||
let nodeGraph;
|
||||
it('Build document', () =>
|
||||
{
|
||||
// Create a node graph with constant and image sources.
|
||||
nodeGraph = doc.addNodeGraph();
|
||||
expect(nodeGraph).to.exist;
|
||||
expectError('Child name is not unique: nodegraph1', () =>
|
||||
{
|
||||
doc.addNodeGraph(nodeGraph.getName());
|
||||
});
|
||||
const constant = nodeGraph.addNode('constant');
|
||||
const image = nodeGraph.addNode('image');
|
||||
|
||||
// Connect sources to outputs
|
||||
const output1 = nodeGraph.addOutput();
|
||||
const output2 = nodeGraph.addOutput();
|
||||
output1.setConnectedNode(constant);
|
||||
output2.setConnectedNode(image);
|
||||
expect(output1.getConnectedNode()).to.eql(constant);
|
||||
expect(output2.getConnectedNode()).to.eql(image);
|
||||
expect(output1.getUpstreamElement()).to.eql(constant);
|
||||
expect(output2.getUpstreamElement()).to.eql(image);
|
||||
|
||||
// Set constant node color
|
||||
const color = new mx.Color3(0.1, 0.2, 0.3);
|
||||
constant.setInputValueColor3('value', color);
|
||||
expect(constant.getInputValue('value').getData()).to.eql(color);
|
||||
|
||||
// Set image node file
|
||||
const file = 'image1.tif';
|
||||
image.setInputValueString('file', file, 'filename');
|
||||
expect(image.getInputValue('file').getData()).to.eql(file);
|
||||
|
||||
// Create a custom nodedef
|
||||
const nodeDef = doc.addNodeDef('nodeDef1', 'float', 'turbulence3d');
|
||||
nodeDef.setInputValueInteger('octaves', 3);
|
||||
nodeDef.setInputValueFloat('lacunarity', 2.0);
|
||||
nodeDef.setInputValueFloat('gain', 0.5);
|
||||
|
||||
// Reference the custom nodedef
|
||||
const custom = nodeGraph.addNode('turbulence3d', 'turbulence1', 'float');
|
||||
expect(custom.getInputValue('octaves').getData()).to.equal(3);
|
||||
custom.setInputValueInteger('octaves', 5);
|
||||
expect(custom.getInputValue('octaves').getData()).to.equal(5);
|
||||
|
||||
// Test scoped attributes
|
||||
nodeGraph.setFilePrefix('folder/');
|
||||
nodeGraph.setColorSpace('lin_rec709');
|
||||
expect(image.getInput('file').getResolvedValueString()).to.equal('folder/image1.tif');
|
||||
expect(constant.getActiveColorSpace()).to.equal('lin_rec709');
|
||||
|
||||
// Create a simple shader interface
|
||||
const simpleSrf = doc.addNodeDef('', 'surfaceshader', 'simpleSrf');
|
||||
simpleSrf.setInputValueColor3('diffColor', new mx.Color3(1.0, 1.0, 1.0));
|
||||
simpleSrf.setInputValueColor3('specColor', new mx.Color3(0.0, 0.0, 0.0));
|
||||
const roughness = simpleSrf.setInputValueFloat('roughness', 0.25);
|
||||
expect(roughness.getIsUniform()).to.be.false;
|
||||
roughness.setIsUniform(true);
|
||||
expect(roughness.getIsUniform()).to.be.true;
|
||||
|
||||
// Instantiate shader and material nodes
|
||||
const shaderNode = doc.addNodeInstance(simpleSrf);
|
||||
const materialNode = doc.addMaterialNode('', shaderNode);
|
||||
expect(materialNode.getUpstreamElement().equals(shaderNode)).to.be.true;
|
||||
|
||||
// Bind the diffuse color input to the constant color output
|
||||
shaderNode.setConnectedOutput('diffColor', output1);
|
||||
expect(shaderNode.getUpstreamElement().equals(constant)).to.be.true;
|
||||
|
||||
// Bind the roughness input to a value
|
||||
const instanceRoughness = shaderNode.setInputValueFloat('roughness', 0.5);
|
||||
expect(instanceRoughness.getValue().getData()).to.equal(0.5);
|
||||
expect(instanceRoughness.getDefaultValue().getData()).to.equal(0.25);
|
||||
|
||||
// Create a look for the material
|
||||
const look = doc.addLook();
|
||||
expect(doc.getLooks().length).to.equal(1);
|
||||
|
||||
// Bind the material to a geometry string
|
||||
let matAssign1 = look.addMaterialAssign('matAssign1', materialNode.getName());
|
||||
matAssign1 = look.getMaterialAssign('matAssign1');
|
||||
expect(matAssign1);
|
||||
matAssign1.setGeom('/robot1');
|
||||
expect(matAssign1.getReferencedMaterial().equals(materialNode)).to.be.true;
|
||||
expect(mx.getGeometryBindings(materialNode, '/robot1').length).to.equal(1);
|
||||
expect(mx.getGeometryBindings(materialNode, '/robot2').length).to.equal(0);
|
||||
|
||||
// Bind the material to a collection
|
||||
let matAssign2 = look.addMaterialAssign('matAssign2', materialNode.getName());
|
||||
matAssign2 = look.getMaterialAssign('matAssign1');
|
||||
expect(matAssign2);
|
||||
const collection = doc.addCollection();
|
||||
collection.setIncludeGeom('/robot2');
|
||||
collection.setExcludeGeom('/robot2/left_arm');
|
||||
matAssign2.setCollection(collection);
|
||||
expect(matAssign2.getReferencedMaterial().equals(materialNode)).to.be.true;
|
||||
expect(mx.getGeometryBindings(materialNode, '/robot2').length).to.equal(1);
|
||||
expect(mx.getGeometryBindings(materialNode, '/robot2/right_arm').length).to.equal(1);
|
||||
expect(mx.getGeometryBindings(materialNode, '/robot2/left_arm').length).to.equal(0);
|
||||
|
||||
const materialAssigns = look.getMaterialAssigns();
|
||||
expect(materialAssigns.length).to.equal(2);
|
||||
|
||||
// Create a property assignment
|
||||
const propertyAssign = look.addPropertyAssign();
|
||||
propertyAssign.setProperty('twosided');
|
||||
propertyAssign.setGeom('/robot1');
|
||||
propertyAssign.setValueBoolean(true);
|
||||
expect(propertyAssign.getProperty()).to.equal('twosided');
|
||||
expect(propertyAssign.getGeom()).to.equal('/robot1');
|
||||
expect(propertyAssign.getValue().getData()).to.equal(true);
|
||||
let propertyAssigns = look.getPropertyAssigns();
|
||||
expect(propertyAssigns.length).to.equal(1);
|
||||
|
||||
// Create a property set assignment
|
||||
const propertySet = doc.addPropertySet();
|
||||
propertySet.setPropertyValueBoolean('matte', false);
|
||||
expect(propertySet.getPropertyValue('matte').getData()).to.equal(false);
|
||||
const propertySetAssign = look.addPropertySetAssign();
|
||||
propertySetAssign.setPropertySet(propertySet);
|
||||
propertySetAssign.setGeom('/robot1');
|
||||
expect(propertySetAssign.getPropertySet().equals(propertySet)).to.be.true;
|
||||
expect(propertySetAssign.getGeom()).to.equal('/robot1');
|
||||
|
||||
// Create a variant set
|
||||
const variantSet = doc.addVariantSet();
|
||||
variantSet.addVariant('original');
|
||||
variantSet.addVariant('damaged');
|
||||
expect(variantSet.getVariants().length).to.equal(2);
|
||||
|
||||
// Validate the document
|
||||
expect(doc.validate()).to.be.true;
|
||||
|
||||
// Disconnect output from sources
|
||||
output1.setConnectedNode(null);
|
||||
output2.setConnectedNode(null);
|
||||
expect(output1.getConnectedNode()).to.equal(null);
|
||||
expect(output2.getConnectedNode()).to.equal(null);
|
||||
// Cleanup created wrappers
|
||||
propertySetAssign.delete();
|
||||
propertySet.delete();
|
||||
propertyAssign.delete();
|
||||
variantSet.delete();
|
||||
collection.delete();
|
||||
matAssign2.delete();
|
||||
matAssign1.delete();
|
||||
look.delete();
|
||||
instanceRoughness.delete();
|
||||
shaderNode.delete();
|
||||
materialNode.delete();
|
||||
simpleSrf.delete();
|
||||
output2.delete();
|
||||
output1.delete();
|
||||
custom.delete();
|
||||
color.delete();
|
||||
image.delete();
|
||||
constant.delete();
|
||||
nodeDef.delete();
|
||||
nodeGraph.delete();
|
||||
doc.delete();
|
||||
});
|
||||
});
|
||||
201
MaterialX/javascript/MaterialXTest/element.spec.js
Normal file
201
MaterialX/javascript/MaterialXTest/element.spec.js
Normal file
@@ -0,0 +1,201 @@
|
||||
import { expect } from 'chai';
|
||||
import Module from './_build/JsMaterialXCore.js';
|
||||
|
||||
describe('Element', () =>
|
||||
{
|
||||
let mx, doc, valueTypes;
|
||||
|
||||
const primitiveValueTypes = {
|
||||
Integer: 10,
|
||||
Boolean: true,
|
||||
String: 'test',
|
||||
Float: 15,
|
||||
IntegerArray: [1, 2, 3, 4, 5],
|
||||
FloatArray: [12, 14], // Not using actual floats to avoid precision problems
|
||||
StringArray: ['first', 'second'],
|
||||
BooleanArray: [true, true, false],
|
||||
}
|
||||
|
||||
before(async () =>
|
||||
{
|
||||
mx = await Module();
|
||||
doc = mx.createDocument();
|
||||
valueTypes = {
|
||||
Color3: new mx.Color3(1, 0, 0.5),
|
||||
Color4: new mx.Color4(0, 1, 0.5, 1),
|
||||
Vector2: new mx.Vector2(0, 1),
|
||||
Vector3: new mx.Vector3(0, 1, 2),
|
||||
Vector4: new mx.Vector4(0, 1, 2, 1),
|
||||
Matrix33: new mx.Matrix33(0, 1, 2, 3, 4, 5, 6, 7, 8),
|
||||
Matrix44: new mx.Matrix44(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15),
|
||||
};
|
||||
});
|
||||
|
||||
after(() =>
|
||||
{
|
||||
// Cleanup typed helper objects and document
|
||||
Object.values(valueTypes).forEach(v => v.delete());
|
||||
doc.delete();
|
||||
});
|
||||
|
||||
describe('value setters', () =>
|
||||
{
|
||||
const checkValue = (types, assertionCallback) =>
|
||||
{
|
||||
const elem = doc.addChildOfCategory('geomprop');
|
||||
Object.keys(types).forEach((typeName) =>
|
||||
{
|
||||
const setFn = `setValue${typeName}`;
|
||||
elem[setFn](types[typeName]);
|
||||
assertionCallback(elem.getValue().getData(), typeName);
|
||||
});
|
||||
elem.delete();
|
||||
};
|
||||
|
||||
it('should work with expected type', () =>
|
||||
{
|
||||
checkValue(valueTypes, (returnedValue, typeName) =>
|
||||
{
|
||||
expect(returnedValue).to.be.an.instanceof(mx[`${typeName}`]);
|
||||
expect(returnedValue.equals(valueTypes[typeName])).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with expected primitive type', () =>
|
||||
{
|
||||
checkValue(primitiveValueTypes, (returnedValue, typeName) =>
|
||||
{
|
||||
expect(returnedValue).to.eql(primitiveValueTypes[typeName]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail for incorrect type', () =>
|
||||
{
|
||||
const elem = doc.addChildOfCategory('geomprop');
|
||||
expect(() => elem.Matrix33(true)).to.throw();
|
||||
});
|
||||
});
|
||||
|
||||
describe('typed value setters', () =>
|
||||
{
|
||||
const checkTypes = (types, assertionCallback) =>
|
||||
{
|
||||
const elem = doc.addChildOfCategory('geomprop');
|
||||
Object.keys(types).forEach((typeName) =>
|
||||
{
|
||||
const setFn = `setTypedAttribute${typeName}`;
|
||||
const getFn = `getTypedAttribute${typeName}`;
|
||||
elem[setFn](typeName, types[typeName]);
|
||||
assertionCallback(elem[getFn](typeName), types[typeName]);
|
||||
});
|
||||
elem.delete();
|
||||
};
|
||||
|
||||
it('should work with expected custom type', () =>
|
||||
{
|
||||
checkTypes(valueTypes, (returnedValue, originalValue) =>
|
||||
{
|
||||
expect(returnedValue.equals(originalValue)).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with expected primitive type', () =>
|
||||
{
|
||||
checkTypes(primitiveValueTypes, (returnedValue, originalValue) =>
|
||||
{
|
||||
expect(returnedValue).to.eql(originalValue);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail for incorrect type', () =>
|
||||
{
|
||||
const elem = doc.addChildOfCategory('geomprop');
|
||||
expect(() => elem.setTypedAttributeColor3('wrongType', true)).to.throw();
|
||||
});
|
||||
});
|
||||
|
||||
it('factory invocation should match specialized functions', () =>
|
||||
{
|
||||
// List based in source/MaterialXCore/Element.cpp
|
||||
const elemtypeArr = [
|
||||
'Backdrop',
|
||||
'Collection',
|
||||
'GeomInfo',
|
||||
'MaterialAssign',
|
||||
'PropertySetAssign',
|
||||
'Visibility',
|
||||
'GeomPropDef',
|
||||
'Look',
|
||||
'LookGroup',
|
||||
'PropertySet',
|
||||
'TypeDef',
|
||||
'AttributeDef',
|
||||
'NodeGraph',
|
||||
'Implementation',
|
||||
'Node',
|
||||
'NodeDef',
|
||||
'Variant',
|
||||
'Member',
|
||||
'TargetDef',
|
||||
'GeomProp',
|
||||
'Input',
|
||||
'Output',
|
||||
'Property',
|
||||
'PropertyAssign',
|
||||
'Unit',
|
||||
'UnitDef',
|
||||
'UnitTypeDef',
|
||||
'VariantAssign',
|
||||
'VariantSet',
|
||||
];
|
||||
|
||||
elemtypeArr.forEach((typeName) =>
|
||||
{
|
||||
const specializedFn = `addChild${typeName}`;
|
||||
const factoryName = typeName.toLowerCase();
|
||||
const type = mx[typeName];
|
||||
expect(doc[specializedFn]()).to.be.an.instanceof(type);
|
||||
expect(doc.addChildOfCategory(factoryName)).to.be.an.instanceof(type);
|
||||
});
|
||||
|
||||
const specialElemType = {
|
||||
'MaterialX': mx.Document,
|
||||
'Comment': mx.CommentElement,
|
||||
'Generic': mx.GenericElement,
|
||||
};
|
||||
|
||||
Object.keys(specialElemType).forEach((typeName) =>
|
||||
{
|
||||
const specializedFn = `addChild${typeName}`;
|
||||
const factoryName = typeName.toLowerCase();
|
||||
expect(doc[specializedFn]()).to.be.an.instanceof(specialElemType[typeName]);
|
||||
expect(doc.addChildOfCategory(factoryName)).to.be.an.instanceof(specialElemType[typeName]);
|
||||
});
|
||||
// No doc.delete() here; cleaned up in after()
|
||||
});
|
||||
});
|
||||
|
||||
describe('Equivalence', () =>
|
||||
{
|
||||
let mx, doc, doc2
|
||||
|
||||
before(async () => {
|
||||
mx = await Module();
|
||||
doc = mx.createDocument();
|
||||
doc.addNodeGraph("graph");
|
||||
doc2 = mx.createDocument();
|
||||
doc2.addNodeGraph("graph1");
|
||||
});
|
||||
|
||||
it('Compare document equivalency', () =>
|
||||
{
|
||||
let options = new mx.ElementEquivalenceOptions();
|
||||
let differences = {};
|
||||
options.performValueComparisons = false;
|
||||
let result = doc.isEquivalent(doc2, options, differences);
|
||||
expect(result).to.be.false;
|
||||
expect(differences.message).to.not.be.empty;
|
||||
result = doc.isEquivalent(doc2, options, undefined);
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
});
|
||||
20
MaterialX/javascript/MaterialXTest/environ.spec.js
Normal file
20
MaterialX/javascript/MaterialXTest/environ.spec.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { expect } from 'chai';;
|
||||
import Module from './_build/JsMaterialXCore.js';
|
||||
|
||||
describe('Environ', () =>
|
||||
{
|
||||
let mx;
|
||||
before(async () =>
|
||||
{
|
||||
mx = await Module();
|
||||
});
|
||||
|
||||
it('Environment variables', () =>
|
||||
{
|
||||
expect(mx.getEnviron(mx.MATERIALX_SEARCH_PATH_ENV_VAR)).to.equal('');
|
||||
mx.setEnviron(mx.MATERIALX_SEARCH_PATH_ENV_VAR, 'test');
|
||||
expect(mx.getEnviron(mx.MATERIALX_SEARCH_PATH_ENV_VAR)).to.equal('test');
|
||||
mx.removeEnviron(mx.MATERIALX_SEARCH_PATH_ENV_VAR);
|
||||
expect(mx.getEnviron(mx.MATERIALX_SEARCH_PATH_ENV_VAR)).to.equal('');
|
||||
});
|
||||
});
|
||||
5670
MaterialX/javascript/MaterialXTest/package-lock.json
generated
Normal file
5670
MaterialX/javascript/MaterialXTest/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
MaterialX/javascript/MaterialXTest/package.json
Normal file
32
MaterialX/javascript/MaterialXTest/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "MaterialXTest",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"clean": "rimraf ./_build/",
|
||||
"copyBuild": "copyfiles -f ../build/bin/JsMaterialX* _build",
|
||||
"pretest": "npm run clean && npm run copyBuild",
|
||||
"test": "npm run mocha",
|
||||
"test:browser": "npm run karma -- --browsers ChromeHeadlessGL --singleRun true",
|
||||
"mocha": "mocha *.spec.js --require @babel/register --timeout 5000",
|
||||
"karma": "karma start browser/karma.conf.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.5",
|
||||
"@babel/preset-env": "^7.28.5",
|
||||
"@babel/register": "^7.28.3",
|
||||
"chai": "^4.5.0",
|
||||
"copyfiles": "^2.4.1",
|
||||
"karma": "^6.4.4",
|
||||
"karma-chai": "^0.1.0",
|
||||
"karma-chrome-launcher": "^3.2.0",
|
||||
"karma-mocha": "^2.0.1",
|
||||
"karma-mocha-reporter": "^2.2.5",
|
||||
"mocha": "^11.7.5",
|
||||
"rimraf": "^3.0.2"
|
||||
}
|
||||
}
|
||||
14
MaterialX/javascript/MaterialXTest/testHelpers.js
Normal file
14
MaterialX/javascript/MaterialXTest/testHelpers.js
Normal file
@@ -0,0 +1,14 @@
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
export function getMtlxStrings(fileNames, subPath)
|
||||
{
|
||||
const mtlxStrs = [];
|
||||
for (let i = 0; i < fileNames.length; i++)
|
||||
{
|
||||
const p = path.resolve(subPath, fileNames[parseInt(i, 10)]);
|
||||
const t = fs.readFileSync(p, 'utf8');
|
||||
mtlxStrs.push(t);
|
||||
}
|
||||
return mtlxStrs;
|
||||
}
|
||||
236
MaterialX/javascript/MaterialXTest/traversal.spec.js
Normal file
236
MaterialX/javascript/MaterialXTest/traversal.spec.js
Normal file
@@ -0,0 +1,236 @@
|
||||
import { expect } from 'chai';
|
||||
import Module from './_build/JsMaterialXCore.js';
|
||||
|
||||
describe('Traversal', () =>
|
||||
{
|
||||
let mx;
|
||||
before(async () =>
|
||||
{
|
||||
mx = await Module();
|
||||
});
|
||||
|
||||
it('Traverse Graph', () =>
|
||||
{
|
||||
// Create a document.
|
||||
const doc = mx.createDocument();
|
||||
// Create a node graph with the following structure:
|
||||
//
|
||||
// [image1] [constant] [image2]
|
||||
// \ / |
|
||||
// [multiply] [contrast] [noise3d]
|
||||
// \____________ | ____________/
|
||||
// [mix]
|
||||
// |
|
||||
// [output]
|
||||
//
|
||||
const nodeGraph = doc.addNodeGraph();
|
||||
const image1 = nodeGraph.addNode('image');
|
||||
const image2 = nodeGraph.addNode('image');
|
||||
const constant = nodeGraph.addNode('constant');
|
||||
const multiply = nodeGraph.addNode('multiply');
|
||||
const contrast = nodeGraph.addNode('contrast');
|
||||
const noise3d = nodeGraph.addNode('noise3d');
|
||||
const mix = nodeGraph.addNode('mix');
|
||||
const output = nodeGraph.addOutput();
|
||||
multiply.setConnectedNode('in1', image1);
|
||||
multiply.setConnectedNode('in2', constant);
|
||||
contrast.setConnectedNode('in', image2);
|
||||
mix.setConnectedNode('fg', multiply);
|
||||
mix.setConnectedNode('bg', contrast);
|
||||
mix.setConnectedNode('mask', noise3d);
|
||||
output.setConnectedNode(mix);
|
||||
|
||||
expect(doc.validate()).to.be.true;
|
||||
|
||||
// Traverse the document tree (implicit iterator).
|
||||
let nodeCount = 0;
|
||||
for (let elem of doc.traverseTree())
|
||||
{
|
||||
if (elem instanceof mx.Node)
|
||||
{
|
||||
nodeCount++;
|
||||
}
|
||||
elem.delete();
|
||||
}
|
||||
expect(nodeCount).to.equal(7);
|
||||
|
||||
// Traverse the document tree (explicit iterator)
|
||||
let treeIter = doc.traverseTree();
|
||||
nodeCount = 0;
|
||||
let maxElementDepth = 0;
|
||||
for (let elem of treeIter)
|
||||
{
|
||||
if (elem instanceof mx.Node)
|
||||
{
|
||||
nodeCount++;
|
||||
}
|
||||
maxElementDepth = Math.max(maxElementDepth, treeIter.getElementDepth());
|
||||
elem.delete();
|
||||
}
|
||||
expect(nodeCount).to.equal(7);
|
||||
expect(maxElementDepth).to.equal(3);
|
||||
|
||||
// Traverse the document tree (prune subtree).
|
||||
nodeCount = 0;
|
||||
treeIter = doc.traverseTree();
|
||||
for (let elem of treeIter)
|
||||
{
|
||||
if (elem instanceof mx.Node)
|
||||
{
|
||||
nodeCount++;
|
||||
}
|
||||
if (elem instanceof mx.NodeGraph)
|
||||
{
|
||||
treeIter.setPruneSubtree(true);
|
||||
}
|
||||
elem.delete();
|
||||
}
|
||||
expect(nodeCount).to.equal(0);
|
||||
|
||||
// Traverse upstream from the graph output (implicit iterator)
|
||||
nodeCount = 0;
|
||||
for (let edge of output.traverseGraph())
|
||||
{
|
||||
const upstreamElem = edge.getUpstreamElement();
|
||||
const connectingElem = edge.getConnectingElement();
|
||||
const downstreamElem = edge.getDownstreamElement();
|
||||
if (upstreamElem instanceof mx.Node)
|
||||
{
|
||||
nodeCount++;
|
||||
if (downstreamElem instanceof mx.Node)
|
||||
{
|
||||
expect(connectingElem instanceof mx.Input).to.be.true;
|
||||
}
|
||||
}
|
||||
if (upstreamElem) upstreamElem.delete();
|
||||
if (connectingElem) connectingElem.delete();
|
||||
if (downstreamElem) downstreamElem.delete();
|
||||
if (edge) edge.delete();
|
||||
}
|
||||
expect(nodeCount).to.equal(7);
|
||||
|
||||
// Traverse upstream from the graph output (explicit iterator)
|
||||
nodeCount = 0;
|
||||
maxElementDepth = 0;
|
||||
let maxNodeDepth = 0;
|
||||
let graphIter = output.traverseGraph();
|
||||
for (let edge of graphIter)
|
||||
{
|
||||
const upstreamElem = edge.getUpstreamElement();
|
||||
if (upstreamElem instanceof mx.Node)
|
||||
{
|
||||
nodeCount++;
|
||||
}
|
||||
maxElementDepth = Math.max(maxElementDepth, graphIter.getElementDepth());
|
||||
maxNodeDepth = Math.max(maxNodeDepth, graphIter.getNodeDepth());
|
||||
if (upstreamElem) upstreamElem.delete();
|
||||
if (edge) edge.delete();
|
||||
}
|
||||
expect(nodeCount).to.equal(7);
|
||||
expect(maxElementDepth).to.equal(3);
|
||||
expect(maxNodeDepth).to.equal(3);
|
||||
|
||||
// Traverse upstream from the graph output (prune subgraph)
|
||||
nodeCount = 0;
|
||||
graphIter = output.traverseGraph();
|
||||
for (let edge of graphIter)
|
||||
{
|
||||
const upstreamElem = edge.getUpstreamElement();
|
||||
expect(upstreamElem.getSelf()).to.be.an.instanceof(mx.Element);
|
||||
if (upstreamElem instanceof mx.Node)
|
||||
{
|
||||
nodeCount++;
|
||||
}
|
||||
if (upstreamElem.getCategory() === 'multiply')
|
||||
{
|
||||
graphIter.setPruneSubgraph(true);
|
||||
}
|
||||
if (upstreamElem) upstreamElem.delete();
|
||||
if (edge) edge.delete();
|
||||
}
|
||||
expect(nodeCount).to.equal(5);
|
||||
|
||||
// Create and detect a cycle
|
||||
multiply.setConnectedNode('in2', mix);
|
||||
expect(output.hasUpstreamCycle()).to.be.true;
|
||||
expect(doc.validate()).to.be.false;
|
||||
multiply.setConnectedNode('in2', constant);
|
||||
expect(output.hasUpstreamCycle()).to.be.false;
|
||||
expect(doc.validate()).to.be.true;
|
||||
|
||||
// Create and detect a loop
|
||||
contrast.setConnectedNode('in', contrast);
|
||||
expect(output.hasUpstreamCycle()).to.be.true;
|
||||
expect(doc.validate()).to.be.false;
|
||||
contrast.setConnectedNode('in', image2);
|
||||
expect(output.hasUpstreamCycle()).to.be.false;
|
||||
expect(doc.validate()).to.be.true;
|
||||
|
||||
// Cleanup wrappers
|
||||
output.delete();
|
||||
mix.delete();
|
||||
noise3d.delete();
|
||||
contrast.delete();
|
||||
multiply.delete();
|
||||
constant.delete();
|
||||
image2.delete();
|
||||
image1.delete();
|
||||
nodeGraph.delete();
|
||||
doc.delete();
|
||||
});
|
||||
|
||||
describe("Traverse inheritance", () =>
|
||||
{
|
||||
let nodeDefInheritanceLevel2, nodeDefInheritanceLevel1, nodeDefParent;
|
||||
let doc;
|
||||
beforeEach(() =>
|
||||
{
|
||||
doc = mx.createDocument();
|
||||
nodeDefParent = doc.addNodeDef();
|
||||
nodeDefParent.setName('BaseClass');
|
||||
nodeDefInheritanceLevel1 = doc.addNodeDef();
|
||||
nodeDefInheritanceLevel1.setName('InheritanceLevel1');
|
||||
nodeDefInheritanceLevel2 = doc.addNodeDef();
|
||||
nodeDefInheritanceLevel2.setName('InheritanceLevel2');
|
||||
nodeDefInheritanceLevel2.setInheritsFrom(nodeDefInheritanceLevel1);
|
||||
nodeDefInheritanceLevel1.setInheritsFrom(nodeDefParent);
|
||||
});
|
||||
afterEach(() =>
|
||||
{
|
||||
nodeDefInheritanceLevel2.delete();
|
||||
nodeDefInheritanceLevel1.delete();
|
||||
nodeDefParent.delete();
|
||||
doc.delete();
|
||||
});
|
||||
|
||||
it('for of loop', () =>
|
||||
{
|
||||
const inheritanceIterator = nodeDefInheritanceLevel2.traverseInheritance();
|
||||
let inheritanceChainLength = 0;
|
||||
for (const elem of inheritanceIterator)
|
||||
{
|
||||
if (elem instanceof mx.NodeDef)
|
||||
{
|
||||
inheritanceChainLength++;
|
||||
}
|
||||
}
|
||||
expect(inheritanceChainLength).to.equal(2);;
|
||||
});
|
||||
|
||||
it('while loop', () =>
|
||||
{
|
||||
const inheritanceIterator = nodeDefInheritanceLevel2.traverseInheritance();
|
||||
let inheritanceChainLength = 0;
|
||||
let elem = inheritanceIterator.next();
|
||||
while (!elem.done)
|
||||
{
|
||||
if (elem.value instanceof mx.NodeDef)
|
||||
{
|
||||
inheritanceChainLength++;
|
||||
}
|
||||
elem = inheritanceIterator.next();
|
||||
}
|
||||
expect(inheritanceChainLength).to.equal(2);;
|
||||
});
|
||||
});
|
||||
});
|
||||
178
MaterialX/javascript/MaterialXTest/types.spec.js
Normal file
178
MaterialX/javascript/MaterialXTest/types.spec.js
Normal file
@@ -0,0 +1,178 @@
|
||||
import { expect } from 'chai';;
|
||||
import Module from './_build/JsMaterialXCore.js';
|
||||
|
||||
describe('Types', () =>
|
||||
{
|
||||
let mx;
|
||||
before(async () =>
|
||||
{
|
||||
mx = await Module();
|
||||
});
|
||||
|
||||
it('Vectors', () =>
|
||||
{
|
||||
const v1 = new mx.Vector3(1, 2, 3);
|
||||
let v2 = new mx.Vector3(2, 4, 6);
|
||||
|
||||
// Indexing operators
|
||||
expect(v1.getItem(2)).to.equal(3);
|
||||
|
||||
v1.setItem(2, 4);
|
||||
expect(v1.getItem(2)).to.equal(4);
|
||||
v1.setItem(2, 3);
|
||||
// Component-wise operators
|
||||
let res = v2.add(v1);
|
||||
expect(res.equals(new mx.Vector3(3, 6, 9))).to.be.true;
|
||||
|
||||
res = v2.sub(v1);
|
||||
expect(res.equals(new mx.Vector3(1, 2, 3))).to.be.true;
|
||||
|
||||
res = v2.multiply(v1);
|
||||
expect(res.equals(new mx.Vector3(2, 8, 18))).to.be.true;
|
||||
|
||||
res = v2.divide(v1);
|
||||
expect(res.equals(new mx.Vector3(2, 2, 2))).to.be.true;
|
||||
|
||||
v2 = v2.add(v1);
|
||||
expect(v2.equals(new mx.Vector3(3, 6, 9))).to.be.true;
|
||||
|
||||
v2 = v2.sub(v1);
|
||||
expect(v2.equals(new mx.Vector3(2, 4, 6))).to.be.true;
|
||||
|
||||
v2 = v2.multiply(v1);
|
||||
expect(v2.equals(new mx.Vector3(2, 8, 18))).to.be.true;
|
||||
|
||||
v2 = v2.divide(v1);
|
||||
expect(v2.equals(new mx.Vector3(2, 4, 6))).to.be.true;
|
||||
|
||||
expect(v1.multiply(new mx.Vector3(2, 2, 2)).equals(v2)).to.be.true;
|
||||
expect(v2.divide(new mx.Vector3(2, 2, 2)).equals(v1)).to.be.true;
|
||||
|
||||
// Geometric methods
|
||||
let v3 = new mx.Vector4(4, 4, 4, 4);
|
||||
expect(v3.getMagnitude()).to.equal(8);
|
||||
expect(v3.getNormalized().getMagnitude()).to.equal(1);
|
||||
expect(v1.dot(v2)).to.equal(28);
|
||||
expect(v1.cross(v2).equals(new mx.Vector3())).to.be.true;
|
||||
|
||||
// Vector copy
|
||||
const v4 = v2.copy();
|
||||
expect(v4.equals(v2)).to.be.true;
|
||||
v4.setItem(0, v4.getItem(0) + 1);
|
||||
expect(v4.notEquals(v2)).to.be.true;
|
||||
});
|
||||
|
||||
function multiplyMatrix(matrix, val)
|
||||
{
|
||||
const clonedMatrix = matrix.copy();
|
||||
for (let i = 0; i < clonedMatrix.numRows(); ++i)
|
||||
{
|
||||
for (let k = 0; k < clonedMatrix.numColumns(); ++k)
|
||||
{
|
||||
const v = clonedMatrix.getItem(i, k);
|
||||
clonedMatrix.setItem(i, k, v * val);
|
||||
}
|
||||
}
|
||||
return clonedMatrix;
|
||||
}
|
||||
|
||||
function divideMatrix(matrix, val)
|
||||
{
|
||||
const clonedMatrix = matrix.copy();
|
||||
for (let i = 0; i < clonedMatrix.numRows(); ++i)
|
||||
{
|
||||
for (let k = 0; k < clonedMatrix.numColumns(); ++k)
|
||||
{
|
||||
const v = clonedMatrix.getItem(i, k);
|
||||
clonedMatrix.setItem(i, k, v / val);
|
||||
}
|
||||
}
|
||||
return clonedMatrix;
|
||||
}
|
||||
|
||||
it('Matrices', () =>
|
||||
{
|
||||
// Translation and scale
|
||||
const trans = mx.Matrix44.createTranslation(new mx.Vector3(1, 2, 3));
|
||||
const scale = mx.Matrix44.createScale(new mx.Vector3(2, 2, 2));
|
||||
expect(trans.equals(new mx.Matrix44(1, 0, 0, 0,
|
||||
0, 1, 0, 0,
|
||||
0, 0, 1, 0,
|
||||
1, 2, 3, 1)));
|
||||
expect(scale.equals(new mx.Matrix44(2, 0, 0, 0,
|
||||
0, 2, 0, 0,
|
||||
0, 0, 2, 0,
|
||||
0, 0, 0, 1)));
|
||||
|
||||
// Indexing operators
|
||||
expect(trans.getItem(3, 2)).to.equal(3);
|
||||
trans.setItem(3, 2, 4);
|
||||
expect(trans.getItem(3, 2)).to.equal(4);
|
||||
trans.setItem(3, 2, 3);
|
||||
|
||||
// Matrix methods
|
||||
expect(trans.getTranspose().equals(
|
||||
new mx.Matrix44(1, 0, 0, 1,
|
||||
0, 1, 0, 2,
|
||||
0, 0, 1, 3,
|
||||
0, 0, 0, 1)
|
||||
)).to.be.true;
|
||||
expect(scale.getTranspose().equals(scale)).to.be.true;
|
||||
expect(trans.getDeterminant()).to.equal(1);
|
||||
expect(scale.getDeterminant()).to.equal(8);
|
||||
expect(trans.getInverse().equals(
|
||||
mx.Matrix44.createTranslation(new mx.Vector3(-1, -2, -3)))).to.be.true;
|
||||
|
||||
// Matrix product
|
||||
const prod1 = trans.multiply(scale);
|
||||
const prod2 = scale.multiply(trans);
|
||||
const prod3 = multiplyMatrix(trans, 2);
|
||||
let prod4 = trans;
|
||||
prod4 = prod4.multiply(scale);
|
||||
expect(prod1.equals(new mx.Matrix44(2, 0, 0, 0,
|
||||
0, 2, 0, 0,
|
||||
0, 0, 2, 0,
|
||||
2, 4, 6, 1)));
|
||||
expect(prod2.equals(new mx.Matrix44(2, 0, 0, 0,
|
||||
0, 2, 0, 0,
|
||||
0, 0, 2, 0,
|
||||
1, 2, 3, 1)));
|
||||
expect(prod3.equals(new mx.Matrix44(2, 0, 0, 0,
|
||||
0, 2, 0, 0,
|
||||
0, 0, 2, 0,
|
||||
2, 4, 6, 2)));
|
||||
expect(prod4.equals(prod1));
|
||||
|
||||
// Matrix division
|
||||
const quot1 = prod1.divide(scale);
|
||||
const quot2 = prod2.divide(trans);
|
||||
const quot3 = divideMatrix(prod3, 2);
|
||||
let quot4 = quot1;
|
||||
quot4 = quot4.divide(trans);
|
||||
expect(quot1.equals(trans)).to.be.true;
|
||||
expect(quot2.equals(scale)).to.be.true;
|
||||
expect(quot3.equals(trans)).to.be.true;
|
||||
|
||||
// 2D rotation
|
||||
const _epsilon = 1e-4;
|
||||
const rot1 = mx.Matrix33.createRotation(Math.PI / 2);
|
||||
const rot2 = mx.Matrix33.createRotation(Math.PI);
|
||||
expect(rot1.multiply(rot1).isEquivalent(rot2, _epsilon));
|
||||
expect(rot2.isEquivalent(mx.Matrix33.createScale(new mx.Vector2(-1, -1)), _epsilon));
|
||||
expect(rot2.multiply(rot2).isEquivalent(mx.Matrix33.IDENTITY, _epsilon));
|
||||
|
||||
// 3D rotation
|
||||
const rotX = mx.Matrix44.createRotationX(Math.PI);
|
||||
const rotY = mx.Matrix44.createRotationY(Math.PI);
|
||||
const rotZ = mx.Matrix44.createRotationZ(Math.PI);
|
||||
expect(rotX.multiply(rotY).isEquivalent(mx.Matrix44.createScale(new mx.Vector3(-1, -1, 1)), _epsilon));
|
||||
expect(rotX.multiply(rotZ).isEquivalent(mx.Matrix44.createScale(new mx.Vector3(-1, 1, -1)), _epsilon));
|
||||
expect(rotY.multiply(rotZ).isEquivalent(mx.Matrix44.createScale(new mx.Vector3(1, -1, -1)), _epsilon));
|
||||
|
||||
// Matrix copy
|
||||
const trans2 = trans.copy();
|
||||
expect(trans2.equals(trans)).to.be.true;
|
||||
trans2.setItem(0, 0, trans2.getItem(0, 0) + 1);
|
||||
expect(trans2.notEquals(trans)).to.be.true;
|
||||
});
|
||||
});
|
||||
42
MaterialX/javascript/MaterialXTest/value.spec.js
Normal file
42
MaterialX/javascript/MaterialXTest/value.spec.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { expect } from 'chai';;
|
||||
import Module from './_build/JsMaterialXCore.js';
|
||||
|
||||
describe('Value', () =>
|
||||
{
|
||||
let mx;
|
||||
before(async () =>
|
||||
{
|
||||
mx = await Module();
|
||||
});
|
||||
|
||||
it('Create values of different types', () =>
|
||||
{
|
||||
const testValues = {
|
||||
integer: '1',
|
||||
boolean: 'true',
|
||||
float: '1.1',
|
||||
color3: '0.1, 0.2, 0.3',
|
||||
color4: '0.1, 0.2, 0.3, 0.4',
|
||||
vector2: '1.1, 2.1',
|
||||
vector3: '1.1, 2.1, 3.1',
|
||||
vector4: '1.1, 2.1, 3.1, 4.1',
|
||||
matrix33: '0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1',
|
||||
matrix44: '1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1',
|
||||
string: 'value',
|
||||
integerarray: '1, 2, 3',
|
||||
booleanarray: 'false, true, false',
|
||||
floatarray: '1.1, 2.1, 3.1',
|
||||
stringarray: "'one', 'two', 'three'",
|
||||
};
|
||||
|
||||
for (let type in testValues)
|
||||
{
|
||||
const value = testValues[String(type)];
|
||||
const newValue = mx.Value.createValueFromStrings(value, type);
|
||||
const typeString = newValue.getTypeString();
|
||||
const valueString = newValue.getValueString();
|
||||
expect(typeString).to.equal(type);
|
||||
expect(valueString).to.equal(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
304
MaterialX/javascript/MaterialXTest/xmlIo.spec.js
Normal file
304
MaterialX/javascript/MaterialXTest/xmlIo.spec.js
Normal file
@@ -0,0 +1,304 @@
|
||||
import Module from './_build/JsMaterialXCore.js';
|
||||
import { expect } from 'chai';
|
||||
import { getMtlxStrings } from './testHelpers';
|
||||
|
||||
const TIMEOUT = 60000;
|
||||
|
||||
describe('XmlIo', () =>
|
||||
{
|
||||
let mx;
|
||||
|
||||
// These should be relative to cwd
|
||||
const includeTestPath = 'data/includes';
|
||||
const libraryPath = '../../libraries/stdlib';
|
||||
const examplesPath = '../../resources/Materials/Examples';
|
||||
// TODO: Is there a better way to get these filenames than hardcoding them here?
|
||||
// The C++ tests load all files in the given directories. This would work in Node, but not in the browser.
|
||||
// Should we use a pre-test script that fetches the files and makes them available somehow?
|
||||
const libraryFilenames = ['stdlib_defs.mtlx', 'stdlib_ng.mtlx'];
|
||||
const exampleFilenames = [
|
||||
'StandardSurface/standard_surface_brass_tiled.mtlx',
|
||||
'StandardSurface/standard_surface_brick_procedural.mtlx',
|
||||
'StandardSurface/standard_surface_carpaint.mtlx',
|
||||
'StandardSurface/standard_surface_marble_solid.mtlx',
|
||||
'UsdPreviewSurface/usd_preview_surface_gold.mtlx',
|
||||
'UsdPreviewSurface/usd_preview_surface_plastic.mtlx',
|
||||
];
|
||||
|
||||
async function readStdLibrary(asString = false)
|
||||
{
|
||||
const libs = [];
|
||||
let iterable = libraryFilenames;
|
||||
if (asString)
|
||||
{
|
||||
const libraryMtlxStrings = getMtlxStrings(libraryFilenames, libraryPath);
|
||||
iterable = libraryMtlxStrings;
|
||||
}
|
||||
for (let file of iterable)
|
||||
{
|
||||
const lib = mx.createDocument();
|
||||
if (asString)
|
||||
{
|
||||
await mx.readFromXmlString(lib, file, libraryPath);
|
||||
} else
|
||||
{
|
||||
await mx.readFromXmlFile(lib, file, libraryPath);
|
||||
}
|
||||
libs.push(lib);
|
||||
};
|
||||
return libs;
|
||||
}
|
||||
|
||||
async function readAndValidateExamples(examples, libraries, readFunc, searchPath = undefined)
|
||||
{
|
||||
for (let file of examples)
|
||||
{
|
||||
const doc = mx.createDocument();
|
||||
await readFunc(doc, file, searchPath);
|
||||
// Import stdlib into the current document and validate it.
|
||||
for (let lib of libraries)
|
||||
{
|
||||
doc.importLibrary(lib);
|
||||
}
|
||||
expect(doc.validate()).to.be.true;
|
||||
|
||||
// Make sure the document does actually contain something.
|
||||
let valueElementCount = 0;
|
||||
const treeIter = doc.traverseTree();
|
||||
for (const elem of treeIter)
|
||||
{
|
||||
if (elem instanceof mx.ValueElement)
|
||||
{
|
||||
valueElementCount++;
|
||||
}
|
||||
// Release wrapper created by iterator
|
||||
elem.delete();
|
||||
}
|
||||
expect(valueElementCount).to.be.greaterThan(0);
|
||||
doc.delete();
|
||||
};
|
||||
}
|
||||
|
||||
before(async () =>
|
||||
{
|
||||
mx = await Module();
|
||||
});
|
||||
|
||||
it('Read XML from file', async () =>
|
||||
{
|
||||
// Read the standard library
|
||||
const libs = await readStdLibrary(false);
|
||||
|
||||
// Read and validate the example documents.
|
||||
await readAndValidateExamples(exampleFilenames, libs,
|
||||
async (document, file, sp) =>
|
||||
{
|
||||
await mx.readFromXmlFile(document, file, sp);
|
||||
}, examplesPath);
|
||||
|
||||
// Read the same document twice, and verify that duplicate elements
|
||||
// are skipped.
|
||||
const doc = mx.createDocument();
|
||||
const filename = 'StandardSurface/standard_surface_carpaint.mtlx';
|
||||
await mx.readFromXmlFile(doc, filename, examplesPath);
|
||||
const copy = doc.copy();
|
||||
await mx.readFromXmlFile(doc, filename, examplesPath);
|
||||
expect(doc.validate()).to.be.true;
|
||||
expect(copy.equals(doc)).to.be.true;
|
||||
copy.delete();
|
||||
doc.delete();
|
||||
libs.forEach(l => l.delete());
|
||||
}).timeout(TIMEOUT);
|
||||
|
||||
it('Read XML from string', async () =>
|
||||
{
|
||||
// Read the standard library
|
||||
const libs = await readStdLibrary(true);
|
||||
|
||||
// Read and validate each example document.
|
||||
const examplesStrings = getMtlxStrings(exampleFilenames, examplesPath);
|
||||
await readAndValidateExamples(examplesStrings, libs,
|
||||
async (document, file) =>
|
||||
{
|
||||
await mx.readFromXmlString(document, file);
|
||||
});
|
||||
|
||||
// Read the same document twice, and verify that duplicate elements
|
||||
// are skipped.
|
||||
const doc = mx.createDocument();
|
||||
const file = examplesStrings[exampleFilenames.indexOf('StandardSurface/standard_surface_carpaint.mtlx')];
|
||||
await mx.readFromXmlString(doc, file);
|
||||
const copy = doc.copy();
|
||||
await mx.readFromXmlString(doc, file);
|
||||
expect(doc.validate()).to.be.true;
|
||||
expect(copy.equals(doc)).to.be.true;
|
||||
copy.delete();
|
||||
doc.delete();
|
||||
libs.forEach(l => l.delete());
|
||||
}).timeout(TIMEOUT);
|
||||
|
||||
it('Read XML with recursive includes', async () =>
|
||||
{
|
||||
const doc = mx.createDocument();
|
||||
await mx.readFromXmlFile(doc, includeTestPath + '/root.mtlx');
|
||||
expect(doc.getChild('paint_semigloss')).to.exist;
|
||||
expect(doc.validate()).to.be.true;
|
||||
doc.delete();
|
||||
});
|
||||
|
||||
it('Locate XML includes via search path', async () =>
|
||||
{
|
||||
const searchPath = includeTestPath + ';' + includeTestPath + '/folder';
|
||||
const filename = 'non_relative_includes.mtlx';
|
||||
const doc = mx.createDocument();
|
||||
expect(async () => await mx.readFromXmlFile(doc, filename, includeTestPath)).to.throw;
|
||||
await mx.readFromXmlFile(doc, filename, searchPath);
|
||||
expect(doc.getChild('paint_semigloss')).to.exist;
|
||||
expect(doc.validate()).to.be.true;
|
||||
|
||||
const doc2 = mx.createDocument();
|
||||
const mtlxString = getMtlxStrings([filename], includeTestPath);
|
||||
expect(async () => await mx.readFromXmlString(doc2, mtlxString[0])).to.throw;
|
||||
await mx.readFromXmlString(doc2, mtlxString[0], searchPath);
|
||||
expect(doc2.getChild('paint_semigloss')).to.exist;
|
||||
expect(doc2.validate()).to.be.true;
|
||||
expect(doc2.equals(doc)).to.be.true;
|
||||
doc2.delete();
|
||||
doc.delete();
|
||||
});
|
||||
|
||||
it('Locate XML includes via environment variable', async () =>
|
||||
{
|
||||
const searchPath = includeTestPath + ';' + includeTestPath + '/folder';
|
||||
const filename = 'non_relative_includes.mtlx';
|
||||
|
||||
const doc = mx.createDocument();
|
||||
expect(async () => await mx.readFromXmlFile(doc, includeTestPath + '/' + filename)).to.throw;
|
||||
mx.setEnviron(mx.MATERIALX_SEARCH_PATH_ENV_VAR, searchPath);
|
||||
await mx.readFromXmlFile(doc, filename);
|
||||
mx.removeEnviron(mx.MATERIALX_SEARCH_PATH_ENV_VAR);
|
||||
expect(doc.getChild('paint_semigloss')).to.exist;
|
||||
expect(doc.validate()).to.be.true;
|
||||
|
||||
const doc2 = mx.createDocument();
|
||||
const mtlxString = getMtlxStrings([filename], includeTestPath);
|
||||
expect(async () => await mx.readFromXmlString(doc2, mtlxString[0])).to.throw;
|
||||
mx.setEnviron(mx.MATERIALX_SEARCH_PATH_ENV_VAR, searchPath);
|
||||
await mx.readFromXmlString(doc2, mtlxString[0]);
|
||||
mx.removeEnviron(mx.MATERIALX_SEARCH_PATH_ENV_VAR);
|
||||
expect(doc2.getChild('paint_semigloss')).to.exist;
|
||||
expect(doc2.validate()).to.be.true;
|
||||
expect(doc2.equals(doc)).to.be.true;
|
||||
doc2.delete();
|
||||
doc.delete();
|
||||
});
|
||||
|
||||
it('Locate XML includes via absolute search paths', async () =>
|
||||
{
|
||||
let absolutePath;
|
||||
if (typeof window === 'object')
|
||||
{
|
||||
// We're in the browser
|
||||
const cwd = window.location.origin + window.location.pathname;
|
||||
absolutePath = cwd + '/' + includeTestPath;
|
||||
} else if (typeof process === 'object')
|
||||
{
|
||||
// We're in Node
|
||||
const nodePath = require('path');
|
||||
absolutePath = nodePath.resolve(includeTestPath);
|
||||
}
|
||||
const doc = mx.createDocument();
|
||||
await mx.readFromXmlFile(doc, 'root.mtlx', absolutePath);
|
||||
doc.delete();
|
||||
});
|
||||
|
||||
it('Detect XML include cycles', async () =>
|
||||
{
|
||||
const doc = mx.createDocument();
|
||||
expect(async () => await mx.readFromXmlFile(doc, includeTestPath + '/cycle.mtlx')).to.throw;
|
||||
doc.delete();
|
||||
});
|
||||
|
||||
it('Disabling XML includes', async () =>
|
||||
{
|
||||
const doc = mx.createDocument();
|
||||
const readOptions = new mx.XmlReadOptions();
|
||||
readOptions.readXIncludes = false;
|
||||
expect(async () => await mx.readFromXmlFile(doc, includeTestPath + '/cycle.mtlx', readOptions)).to.not.throw;
|
||||
doc.delete();
|
||||
});
|
||||
|
||||
it('Write to XML string', async () =>
|
||||
{
|
||||
// Read all example documents and write them to an XML string
|
||||
const searchPath = libraryPath + ';' + examplesPath;
|
||||
for (let filename of exampleFilenames)
|
||||
{
|
||||
const doc = mx.createDocument();
|
||||
await mx.readFromXmlFile(doc, filename, searchPath);
|
||||
|
||||
// Serialize to XML.
|
||||
const writeOptions = new mx.XmlWriteOptions();
|
||||
writeOptions.writeXIncludeEnable = false;
|
||||
const xmlString = mx.writeToXmlString(doc, writeOptions);
|
||||
writeOptions.delete();
|
||||
|
||||
// Verify that the serialized document is identical.
|
||||
const writtenDoc = mx.createDocument();
|
||||
await mx.readFromXmlString(writtenDoc, xmlString);
|
||||
expect(writtenDoc).to.eql(doc);
|
||||
writtenDoc.delete();
|
||||
doc.delete();
|
||||
};
|
||||
});
|
||||
|
||||
it('Prepend include tag', () =>
|
||||
{
|
||||
const doc = mx.createDocument();
|
||||
const includePath = "SomePath";
|
||||
const writeOptions = new mx.XmlWriteOptions();
|
||||
mx.prependXInclude(doc, includePath);
|
||||
const xmlString = mx.writeToXmlString(doc, writeOptions);
|
||||
expect(xmlString).to.include(includePath);
|
||||
writeOptions.delete();
|
||||
doc.delete();
|
||||
});
|
||||
|
||||
// Node only, because we cannot read from a downloaded file in the browser
|
||||
it('Write XML to file', async () =>
|
||||
{
|
||||
const filename = '_build/testFile.mtlx';
|
||||
const includeRegex = /<xi:include href="(.*)"\s*\/>/g;
|
||||
const doc = mx.createDocument();
|
||||
await mx.readFromXmlFile(doc, 'root.mtlx', includeTestPath);
|
||||
|
||||
// Write using includes
|
||||
mx.writeToXmlFile(doc, filename);
|
||||
// Read written document and compare with the original
|
||||
const doc2 = mx.createDocument();
|
||||
await mx.readFromXmlFile(doc2, filename, includeTestPath);
|
||||
expect(doc2.equals(doc));
|
||||
// Read written file content and verify that includes are preserved
|
||||
let fileString = getMtlxStrings([filename], '')[0];
|
||||
let matches = Array.from(fileString.matchAll(includeRegex));
|
||||
expect(matches.length).to.be.greaterThan(0);
|
||||
|
||||
// Write inlining included content
|
||||
const writeOptions = new mx.XmlWriteOptions();
|
||||
writeOptions.writeXIncludeEnable = false;
|
||||
mx.writeToXmlFile(doc, filename, writeOptions);
|
||||
// Read written document and compare with the original
|
||||
const doc3 = mx.createDocument();
|
||||
await mx.readFromXmlFile(doc3, filename);
|
||||
expect(doc3.equals(doc));
|
||||
expect(doc.getChild('paint_semigloss')).to.exist;
|
||||
// Read written file content and verify that includes are inlined
|
||||
fileString = getMtlxStrings([filename], '')[0];
|
||||
matches = Array.from(fileString.matchAll(includeRegex));
|
||||
expect(matches.length).to.equal(0);
|
||||
doc3.delete();
|
||||
doc2.delete();
|
||||
doc.delete();
|
||||
writeOptions.delete();
|
||||
});
|
||||
});
|
||||
24
MaterialX/javascript/MaterialXView/example_materials.json
Normal file
24
MaterialX/javascript/MaterialXView/example_materials.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"materials": [
|
||||
{
|
||||
"name": "StandardSurface",
|
||||
"path": "../../resources/Materials/Examples/StandardSurface",
|
||||
"baseURL": "Materials/Examples/StandardSurface"
|
||||
},
|
||||
{
|
||||
"name": "UsdPreviewSurface",
|
||||
"path": "../../resources/Materials/Examples/UsdPreviewSurface",
|
||||
"baseURL": "Materials/Examples/UsdPreviewSurface"
|
||||
},
|
||||
{
|
||||
"name": "GltfPbr",
|
||||
"path": "../../resources/Materials/Examples/GltfPbr",
|
||||
"baseURL": "Materials/Examples/GltfPbr"
|
||||
},
|
||||
{
|
||||
"name": "OpenPbr",
|
||||
"path": "../../resources/Materials/Examples/OpenPbr",
|
||||
"baseURL": "Materials/Examples/OpenPbr"
|
||||
}
|
||||
]
|
||||
}
|
||||
57
MaterialX/javascript/MaterialXView/index.ejs
Normal file
57
MaterialX/javascript/MaterialXView/index.ejs
Normal file
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||
<title>MaterialX Web Viewer</title>
|
||||
<link rel="icon" type="image/x-icon" href="public/favicon.ico" />
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Arial
|
||||
}
|
||||
|
||||
/* Property editor item color */
|
||||
.peditoritem {
|
||||
background-color: #334444;
|
||||
}
|
||||
/* Property editor folder color */
|
||||
.peditorfolder {
|
||||
background-color: #333333;
|
||||
}
|
||||
|
||||
.peditor_material_assigned {
|
||||
background-color: #006cb8;
|
||||
}
|
||||
.peditor_material_assigned:hover {
|
||||
background-color: #32adff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0px; overflow: hidden;">
|
||||
<div id="container">
|
||||
<div style="color:white; position: absolute; top: 0em; margin: 1em">
|
||||
<label for="materials">Material:</label>
|
||||
<select name="materials" id="materials">
|
||||
<% materials.forEach(function(m){ %>
|
||||
<option value="<%-m.value%>">
|
||||
<%-m.name%>
|
||||
</option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div style="color:white; position: absolute; top: 1.5em; margin: 1em">
|
||||
<label for="geometry">Geometry:</label>
|
||||
<select name="geometry" id="geometry">
|
||||
<% geometry.forEach(function(m){ %>
|
||||
<option value="<%-m.value%>">
|
||||
<%-m.name%>
|
||||
</option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<canvas id="webglcanvas" tabindex="1" style="outline: none;"></canvas>
|
||||
</div>
|
||||
<script src="JsMaterialXGenShader.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
4509
MaterialX/javascript/MaterialXView/package-lock.json
generated
Normal file
4509
MaterialX/javascript/MaterialXView/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
MaterialX/javascript/MaterialXView/package.json
Normal file
23
MaterialX/javascript/MaterialXView/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "MaterialXView",
|
||||
"version": "1.0.0",
|
||||
"description": "MaterialX Web Viewer",
|
||||
"main": "source/index.js",
|
||||
"scripts": {
|
||||
"start": "webpack serve",
|
||||
"build": "webpack"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"lil-gui": "^0.19.2",
|
||||
"three": "^0.152.2",
|
||||
"webpack": "^5.103.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"html-webpack-plugin": "^5.6.5",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "^5.2.2"
|
||||
}
|
||||
}
|
||||
BIN
MaterialX/javascript/MaterialXView/public/favicon.ico
Normal file
BIN
MaterialX/javascript/MaterialXView/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 44C35.046 44 44 35.046 44 24C44 12.954 35.046 4 24 4C12.954 4 4 12.954 4 24C4 35.046 12.954 44 24 44Z" fill="#D8D8D8" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M24 44C17.372 44 12 35.046 12 24C12 12.954 17.372 4 24 4" fill="#D9D9D9"/>
|
||||
<path d="M24 44C17.372 44 12 35.046 12 24C12 12.954 17.372 4 24 4" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 547 B |
@@ -0,0 +1,8 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 44C35.046 44 44 35.046 44 24C44 12.954 35.046 4 24 4C12.954 4 4 12.954 4 24C4 35.046 12.954 44 24 44Z" fill="#46BBD0" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M24 44C17.372 44 12 35.046 12 24C12 12.954 17.372 4 24 4" fill="#D64274"/>
|
||||
<path d="M24 44C17.372 44 12 35.046 12 24C12 12.954 17.372 4 24 4" fill="#D64274"/>
|
||||
<path d="M24 44C17.372 44 12 35.046 12 24C12 12.954 17.372 4 24 4" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M24 44C17.372 44 12 35.046 12 24C12 12.954 17.372 4 24 4" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M24.9932 42.5C33.9864 42 42.5 34.7699 42.5 24C42.5 13.2302 34 5.5 24.4932 5.5C14.9932 6 21 14.5 20.5 24C20.5 34.7699 16 43 24.9932 42.5Z" fill="#D64274"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 943 B |
375
MaterialX/javascript/MaterialXView/source/dropHandling.js
Normal file
375
MaterialX/javascript/MaterialXView/source/dropHandling.js
Normal file
@@ -0,0 +1,375 @@
|
||||
import * as THREE from 'three';
|
||||
import * as fflate from 'three/examples/jsm/libs/fflate.module.js';
|
||||
|
||||
const debugFileHandling = false;
|
||||
let loadingCallback = null;
|
||||
let sceneLoadingCallback = null;
|
||||
|
||||
export function setLoadingCallback(cb)
|
||||
{
|
||||
loadingCallback = cb;
|
||||
}
|
||||
|
||||
export function setSceneLoadingCallback(cb)
|
||||
{
|
||||
sceneLoadingCallback = cb;
|
||||
}
|
||||
|
||||
export function dropHandler(ev)
|
||||
{
|
||||
if (debugFileHandling) console.log('File(s) dropped', ev.dataTransfer.items, ev.dataTransfer.files);
|
||||
|
||||
// Prevent default behavior (Prevent file from being opened)
|
||||
ev.preventDefault();
|
||||
|
||||
if (ev.dataTransfer.items)
|
||||
{
|
||||
const allEntries = [];
|
||||
|
||||
let haveGetAsEntry = false;
|
||||
if (ev.dataTransfer.items.length > 0)
|
||||
haveGetAsEntry =
|
||||
("getAsEntry" in ev.dataTransfer.items[0]) ||
|
||||
("webkitGetAsEntry" in ev.dataTransfer.items[0]);
|
||||
|
||||
// Useful for debugging file handling on platforms that don't support newer file system APIs
|
||||
// haveGetAsEntry = false;
|
||||
|
||||
if (haveGetAsEntry)
|
||||
{
|
||||
for (var i = 0; i < ev.dataTransfer.items.length; i++)
|
||||
{
|
||||
let item = ev.dataTransfer.items[i];
|
||||
let entry = ("getAsEntry" in item) ? item.getAsEntry() : item.webkitGetAsEntry();
|
||||
allEntries.push(entry);
|
||||
}
|
||||
handleFilesystemEntries(allEntries);
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < ev.dataTransfer.items.length; i++)
|
||||
{
|
||||
let item = ev.dataTransfer.items[i];
|
||||
|
||||
// API when there's no "getAsEntry" support
|
||||
console.log(item.kind, item);
|
||||
if (item.kind === 'file')
|
||||
{
|
||||
var file = item.getAsFile();
|
||||
testAndLoadFile(file);
|
||||
}
|
||||
// could also be a directory
|
||||
else if (item.kind === 'directory')
|
||||
{
|
||||
var dirReader = item.createReader();
|
||||
dirReader.readEntries(function (entries)
|
||||
{
|
||||
for (var i = 0; i < entries.length; i++)
|
||||
{
|
||||
console.log(entries[i].name);
|
||||
var entry = entries[i];
|
||||
if (entry.isFile)
|
||||
{
|
||||
entry.file(function (file)
|
||||
{
|
||||
testAndLoadFile(file);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else
|
||||
{
|
||||
for (var i = 0; i < ev.dataTransfer.files.length; i++)
|
||||
{
|
||||
let file = ev.dataTransfer.files[i];
|
||||
testAndLoadFile(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function dragOverHandler(ev)
|
||||
{
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
async function getBufferFromFile(fileEntry)
|
||||
{
|
||||
|
||||
if (fileEntry instanceof ArrayBuffer) return fileEntry;
|
||||
if (fileEntry instanceof String) return fileEntry;
|
||||
|
||||
const name = fileEntry.fullPath || fileEntry.name;
|
||||
const ext = name.split('.').pop();
|
||||
const readAsText = ext === 'mtlx';
|
||||
|
||||
if (debugFileHandling) console.log("reading ", fileEntry, "as text?", readAsText);
|
||||
|
||||
if (debugFileHandling) console.log("getBufferFromFile", fileEntry);
|
||||
const buffer = await new Promise((resolve, reject) =>
|
||||
{
|
||||
function readFile(file)
|
||||
{
|
||||
var reader = new FileReader();
|
||||
reader.onloadend = function (e)
|
||||
{
|
||||
if (debugFileHandling) console.log("loaded", "should be text?", readAsText, this.result);
|
||||
resolve(this.result);
|
||||
};
|
||||
|
||||
if (readAsText)
|
||||
reader.readAsText(file);
|
||||
else
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
|
||||
if ("file" in fileEntry)
|
||||
{
|
||||
fileEntry.file(function (file)
|
||||
{
|
||||
readFile(file);
|
||||
}, (e) =>
|
||||
{
|
||||
console.error("Error reading file ", e);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
readFile(fileEntry);
|
||||
}
|
||||
});
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async function handleFilesystemEntries(entries)
|
||||
{
|
||||
const allFiles = [];
|
||||
const fileIgnoreList = [
|
||||
'.gitignore',
|
||||
'README.md',
|
||||
'.DS_Store',
|
||||
]
|
||||
const dirIgnoreList = [
|
||||
'.git',
|
||||
'node_modules',
|
||||
]
|
||||
|
||||
let isGLB = false;
|
||||
let haveMtlx = false;
|
||||
for (let entry of entries)
|
||||
{
|
||||
if (debugFileHandling) console.log("file entry", entry)
|
||||
if (entry.isFile)
|
||||
{
|
||||
if (debugFileHandling)
|
||||
console.log("single file", entry);
|
||||
if (fileIgnoreList.includes(entry.name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
allFiles.push(entry);
|
||||
|
||||
if (entry.name.endsWith('glb'))
|
||||
{
|
||||
isGLB = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (entry.isDirectory)
|
||||
{
|
||||
if (dirIgnoreList.includes(entry.name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const files = await readDirectory(entry);
|
||||
if (debugFileHandling) console.log("all files", files);
|
||||
for (const file of files)
|
||||
{
|
||||
if (fileIgnoreList.includes(file.name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
allFiles.push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const imageLoader = new THREE.ImageLoader();
|
||||
|
||||
// unpack zip files first
|
||||
for (const fileEntry of allFiles)
|
||||
{
|
||||
// special case: zip archives
|
||||
if (fileEntry.fullPath.toLowerCase().endsWith('.zip'))
|
||||
{
|
||||
await new Promise(async (resolve, reject) =>
|
||||
{
|
||||
const arrayBuffer = await getBufferFromFile(fileEntry);
|
||||
|
||||
// use fflate to unpack them and add the files to the cache
|
||||
fflate.unzip(new Uint8Array(arrayBuffer), (error, unzipped) =>
|
||||
{
|
||||
// push these files into allFiles
|
||||
for (const [filePath, buffer] of Object.entries(unzipped))
|
||||
{
|
||||
|
||||
// mock FileEntry for easier usage downstream
|
||||
const blob = new Blob([buffer]);
|
||||
const newFileEntry = {
|
||||
fullPath: "/" + filePath,
|
||||
name: filePath.split('/').pop(),
|
||||
file: (callback) =>
|
||||
{
|
||||
callback(blob);
|
||||
},
|
||||
isFile: true,
|
||||
};
|
||||
allFiles.push(newFileEntry);
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// sort so mtlx files come first
|
||||
allFiles.sort((a, b) =>
|
||||
{
|
||||
if (a.name.endsWith('.mtlx') && !b.name.endsWith('.mtlx'))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (!a.name.endsWith('.mtlx') && b.name.endsWith('.mtlx'))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (isGLB)
|
||||
{
|
||||
console.log("Load GLB file", allFiles[0]);
|
||||
|
||||
const rootFile = allFiles[0];
|
||||
THREE.Cache.add(rootFile.fullPath, await getBufferFromFile(rootFile));
|
||||
|
||||
if (debugFileHandling) console.log("CACHE", THREE.Cache.files);
|
||||
|
||||
sceneLoadingCallback(rootFile);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!allFiles[0].name.endsWith('mtlx'))
|
||||
{
|
||||
console.log("No MaterialX files dropped. Skipping content.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (debugFileHandling)
|
||||
{
|
||||
console.log("- All files", allFiles);
|
||||
}
|
||||
|
||||
// put all files in three' Cache
|
||||
for (const fileEntry of allFiles)
|
||||
{
|
||||
|
||||
const allowedFileTypes = [
|
||||
'png', 'jpg', 'jpeg'
|
||||
];
|
||||
|
||||
const ext = fileEntry.fullPath.split('.').pop();
|
||||
if (!allowedFileTypes.includes(ext))
|
||||
{
|
||||
// console.log("skipping file", fileEntry.fullPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
const buffer = await getBufferFromFile(fileEntry);
|
||||
const img = await imageLoader.loadAsync(URL.createObjectURL(new Blob([buffer])));
|
||||
if (debugFileHandling) console.log("caching file", fileEntry.fullPath, img);
|
||||
THREE.Cache.add(fileEntry.fullPath, img);
|
||||
}
|
||||
|
||||
// TODO we could also allow dropping of multiple MaterialX files (or folders with them inside)
|
||||
// and seed the dropdown from that.
|
||||
// At that point, actually reading files and textures into memory should be deferred until they are actually used.
|
||||
if (allFiles.length > 0)
|
||||
{
|
||||
const rootFile = allFiles[0];
|
||||
THREE.Cache.add(rootFile.fullPath, await getBufferFromFile(rootFile));
|
||||
|
||||
if (debugFileHandling) console.log("CACHE", THREE.Cache.files);
|
||||
|
||||
loadingCallback(rootFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log('No files to add cache.')
|
||||
}
|
||||
}
|
||||
|
||||
async function readDirectory(directory)
|
||||
{
|
||||
let entries = [];
|
||||
let getEntries = async (directory) =>
|
||||
{
|
||||
let dirReader = directory.createReader();
|
||||
await new Promise((resolve, reject) =>
|
||||
{
|
||||
dirReader.readEntries(
|
||||
async (results) =>
|
||||
{
|
||||
if (results.length)
|
||||
{
|
||||
// entries = entries.concat(results);
|
||||
for (let entry of results)
|
||||
{
|
||||
if (entry.isDirectory)
|
||||
{
|
||||
await getEntries(entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
(error) =>
|
||||
{
|
||||
/* handle error — error is a FileError object */
|
||||
},
|
||||
)
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
await getEntries(directory);
|
||||
return entries;
|
||||
}
|
||||
|
||||
async function testAndLoadFile(file)
|
||||
{
|
||||
let ext = file.name.split('.').pop();
|
||||
if (debugFileHandling) console.log(file.name + ", " + file.size + ", " + ext);
|
||||
|
||||
const arrayBuffer = await getBufferFromFile(file);
|
||||
console.log(arrayBuffer)
|
||||
|
||||
// mock a fileEntry and pass through the same loading logic
|
||||
const newFileEntry = {
|
||||
fullPath: "/" + file.name,
|
||||
name: file.name.split('/').pop(),
|
||||
isFile: true,
|
||||
file: (callback) =>
|
||||
{
|
||||
callback(file);
|
||||
}
|
||||
};
|
||||
|
||||
handleFilesystemEntries([newFileEntry]);
|
||||
}
|
||||
352
MaterialX/javascript/MaterialXView/source/helper.js
Normal file
352
MaterialX/javascript/MaterialXView/source/helper.js
Normal file
@@ -0,0 +1,352 @@
|
||||
//
|
||||
// Copyright Contributors to the MaterialX Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
import * as THREE from 'three';
|
||||
|
||||
const IMAGE_PROPERTY_SEPARATOR = "_";
|
||||
const UADDRESS_MODE_SUFFIX = IMAGE_PROPERTY_SEPARATOR + "uaddressmode";
|
||||
const VADDRESS_MODE_SUFFIX = IMAGE_PROPERTY_SEPARATOR + "vaddressmode";
|
||||
const FILTER_TYPE_SUFFIX = IMAGE_PROPERTY_SEPARATOR + "filtertype";
|
||||
const IMAGE_PATH_SEPARATOR = "/";
|
||||
|
||||
/**
|
||||
* Initialized the environment texture as MaterialX expects it
|
||||
* @param {THREE.Texture} texture
|
||||
* @param {Object} capabilities
|
||||
* @returns {THREE.Texture}
|
||||
*/
|
||||
export function prepareEnvTexture(texture, capabilities)
|
||||
{
|
||||
let newTexture = new THREE.DataTexture(texture.image.data, texture.image.width, texture.image.height, texture.format, texture.type);
|
||||
newTexture.wrapS = THREE.RepeatWrapping;
|
||||
newTexture.anisotropy = capabilities.getMaxAnisotropy();
|
||||
newTexture.minFilter = THREE.LinearMipmapLinearFilter;
|
||||
newTexture.magFilter = THREE.LinearFilter;
|
||||
newTexture.generateMipmaps = true;
|
||||
newTexture.needsUpdate = true;
|
||||
|
||||
return newTexture;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Three uniform from MaterialX vector
|
||||
* @param {any} value
|
||||
* @param {any} dimension
|
||||
* @returns {THREE.Uniform}
|
||||
*/
|
||||
function fromVector(value, dimension)
|
||||
{
|
||||
let outValue;
|
||||
if (value)
|
||||
{
|
||||
outValue = value.data();
|
||||
}
|
||||
else
|
||||
{
|
||||
outValue = [];
|
||||
for (let i = 0; i < dimension; ++i)
|
||||
outValue.push(0.0);
|
||||
}
|
||||
|
||||
return outValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Three uniform from MaterialX matrix
|
||||
* @param {mx.matrix} matrix
|
||||
* @param {mx.matrix.size} dimension
|
||||
*/
|
||||
function fromMatrix(matrix, dimension)
|
||||
{
|
||||
let vec = [];
|
||||
if (matrix)
|
||||
{
|
||||
for (let i = 0; i < matrix.numRows(); ++i)
|
||||
{
|
||||
for (let k = 0; k < matrix.numColumns(); ++k)
|
||||
{
|
||||
vec.push(matrix.getItem(i, k));
|
||||
}
|
||||
}
|
||||
} else
|
||||
{
|
||||
for (let i = 0; i < dimension; ++i)
|
||||
vec.push(0.0);
|
||||
}
|
||||
|
||||
return vec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Three uniform from MaterialX value
|
||||
* @param {mx.Uniform.type} type
|
||||
* @param {mx.Uniform.value} value
|
||||
* @param {mx.Uniform.name} name
|
||||
* @param {mx.Uniforms} uniforms
|
||||
* @param {THREE.textureLoader} textureLoader
|
||||
*/
|
||||
function toThreeUniform(type, value, name, uniforms, textureLoader, searchPath, flipY)
|
||||
{
|
||||
let outValue = null;
|
||||
switch (type)
|
||||
{
|
||||
case 'float':
|
||||
case 'integer':
|
||||
case 'boolean':
|
||||
outValue = value;
|
||||
break;
|
||||
case 'vector2':
|
||||
outValue = fromVector(value, 2);
|
||||
break;
|
||||
case 'vector3':
|
||||
case 'color3':
|
||||
outValue = fromVector(value, 3);
|
||||
break;
|
||||
case 'vector4':
|
||||
case 'color4':
|
||||
outValue = fromVector(value, 4);
|
||||
break;
|
||||
case 'matrix33':
|
||||
outValue = fromMatrix(value, 9);
|
||||
break;
|
||||
case 'matrix44':
|
||||
outValue = fromMatrix(value, 16);
|
||||
break;
|
||||
case 'filename':
|
||||
if (value)
|
||||
{
|
||||
// Cache / reuse texture to avoid reload overhead.
|
||||
// Note: that data blobs and embedded data textures are not cached as they are transient data.
|
||||
let checkCache = true;
|
||||
let texturePath = searchPath + IMAGE_PATH_SEPARATOR + value;
|
||||
if (value.startsWith('blob:'))
|
||||
{
|
||||
texturePath = value;
|
||||
console.log('Load blob URL:', texturePath);
|
||||
checkCache = false;
|
||||
}
|
||||
else if (value.startsWith('http'))
|
||||
{
|
||||
texturePath = value;
|
||||
console.log('Load HTTP URL:', texturePath);
|
||||
}
|
||||
else if (value.startsWith('data:'))
|
||||
{
|
||||
texturePath = value;
|
||||
checkCache = false;
|
||||
console.log('Load data URL:', texturePath);
|
||||
}
|
||||
const cachedTexture = checkCache && THREE.Cache.get(texturePath);
|
||||
if (cachedTexture)
|
||||
{
|
||||
// Get texture from cache
|
||||
outValue = cachedTexture;
|
||||
console.log('Use cached texture: ', texturePath, outValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
outValue = textureLoader.load(
|
||||
texturePath,
|
||||
function (texture) {
|
||||
console.log('Load new texture: ' + texturePath, texture);
|
||||
outValue = texture;
|
||||
|
||||
// Add texture to ThreeJS cache
|
||||
if (checkCache)
|
||||
THREE.Cache.add(texturePath, texture);
|
||||
},
|
||||
undefined,
|
||||
function (error) {
|
||||
console.error('Error loading texture: ', error);
|
||||
});
|
||||
|
||||
// Set address & filtering mode
|
||||
if (outValue)
|
||||
setTextureParameters(outValue, name, uniforms, flipY);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'samplerCube':
|
||||
case 'string':
|
||||
break;
|
||||
default:
|
||||
console.log('Value type not supported: ' + type);
|
||||
outValue = null;
|
||||
}
|
||||
|
||||
return outValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Three wrapping mode
|
||||
* @param {mx.TextureFilter.wrap} mode
|
||||
* @returns {THREE.Wrapping}
|
||||
*/
|
||||
function getWrapping(mode)
|
||||
{
|
||||
let wrap;
|
||||
switch (mode)
|
||||
{
|
||||
case 1:
|
||||
wrap = THREE.ClampToEdgeWrapping;
|
||||
break;
|
||||
case 2:
|
||||
wrap = THREE.RepeatWrapping;
|
||||
break;
|
||||
case 3:
|
||||
wrap = THREE.MirroredRepeatWrapping;
|
||||
break;
|
||||
default:
|
||||
wrap = THREE.RepeatWrapping;
|
||||
break;
|
||||
}
|
||||
return wrap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Three minification filter
|
||||
* @param {mx.TextureFilter.minFilter} type
|
||||
* @param {mx.TextureFilter.generateMipmaps} generateMipmaps
|
||||
*/
|
||||
function getMinFilter(type, generateMipmaps)
|
||||
{
|
||||
const filterType = generateMipmaps ? THREE.LinearMipMapLinearFilter : THREE.LinearFilter;
|
||||
if (type === 0)
|
||||
{
|
||||
filterType = generateMipmaps ? THREE.NearestMipMapNearestFilter : THREE.NearestFilter;
|
||||
}
|
||||
return filterType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Three texture parameters
|
||||
* @param {THREE.Texture} texture
|
||||
* @param {mx.Uniform.name} name
|
||||
* @param {mx.Uniforms} uniforms
|
||||
* @param {mx.TextureFilter.generateMipmaps} generateMipmaps
|
||||
*/
|
||||
function setTextureParameters(texture, name, uniforms, flipY = true, generateMipmaps = true)
|
||||
{
|
||||
const idx = name.lastIndexOf(IMAGE_PROPERTY_SEPARATOR);
|
||||
const base = name.substring(0, idx) || name;
|
||||
|
||||
texture.generateMipmaps = generateMipmaps;
|
||||
texture.wrapS = THREE.RepeatWrapping;
|
||||
texture.wrapT = THREE.RepeatWrapping;
|
||||
texture.magFilter = THREE.LinearFilter;
|
||||
texture.flipY = flipY;
|
||||
|
||||
if (uniforms.find(base + UADDRESS_MODE_SUFFIX))
|
||||
{
|
||||
const uaddressmode = uniforms.find(base + UADDRESS_MODE_SUFFIX).getValue().getData();
|
||||
texture.wrapS = getWrapping(uaddressmode);
|
||||
}
|
||||
|
||||
if (uniforms.find(base + VADDRESS_MODE_SUFFIX))
|
||||
{
|
||||
const vaddressmode = uniforms.find(base + VADDRESS_MODE_SUFFIX).getValue().getData();
|
||||
texture.wrapT = getWrapping(vaddressmode);
|
||||
}
|
||||
|
||||
const filterType = uniforms.find(base + FILTER_TYPE_SUFFIX) ? uniforms.get(base + FILTER_TYPE_SUFFIX).value : -1;
|
||||
texture.minFilter = getMinFilter(filterType, generateMipmaps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the global light rotation matrix
|
||||
*/
|
||||
export function getLightRotation()
|
||||
{
|
||||
return new THREE.Matrix4().makeRotationY(Math.PI / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all lights nodes in a MaterialX document
|
||||
* @param {mx.Document} doc
|
||||
* @returns {Array.<mx.Node>}
|
||||
*/
|
||||
export function findLights(doc)
|
||||
{
|
||||
let lights = [];
|
||||
for (let node of doc.getNodes())
|
||||
{
|
||||
if (node.getType() === "lightshader")
|
||||
lights.push(node);
|
||||
}
|
||||
return lights;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register lights in shader generation context
|
||||
* @param {Object} mx MaterialX Module
|
||||
* @param {Array.<mx.Node>} lights Light nodes
|
||||
* @param {mx.GenContext} genContext Shader generation context
|
||||
* @returns {Array.<mx.Node>}
|
||||
*/
|
||||
export function registerLights(mx, lights, genContext)
|
||||
{
|
||||
mx.HwShaderGenerator.unbindLightShaders(genContext);
|
||||
|
||||
const lightTypesBound = {};
|
||||
const lightData = [];
|
||||
let lightId = 1;
|
||||
for (let light of lights)
|
||||
{
|
||||
let nodeDef = light.getNodeDef();
|
||||
let nodeName = nodeDef.getName();
|
||||
if (!lightTypesBound[nodeName])
|
||||
{
|
||||
lightTypesBound[nodeName] = lightId;
|
||||
mx.HwShaderGenerator.bindLightShader(nodeDef, lightId++, genContext);
|
||||
}
|
||||
|
||||
const lightDirection = light.getValueElement("direction").getValue().getData().data();
|
||||
const lightColor = light.getValueElement("color").getValue().getData().data();
|
||||
const lightIntensity = light.getValueElement("intensity").getValue().getData();
|
||||
|
||||
let rotatedLightDirection = new THREE.Vector3(...lightDirection)
|
||||
rotatedLightDirection.transformDirection(getLightRotation())
|
||||
|
||||
lightData.push({
|
||||
type: lightTypesBound[nodeName],
|
||||
direction: rotatedLightDirection,
|
||||
color: new THREE.Vector3(...lightColor),
|
||||
intensity: lightIntensity
|
||||
});
|
||||
}
|
||||
|
||||
// Make sure max light count is large enough
|
||||
genContext.getOptions().hwMaxActiveLightSources = Math.max(genContext.getOptions().hwMaxActiveLightSources, lights.length);
|
||||
|
||||
return lightData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get uniform values for a shader
|
||||
* @param {mx.shaderStage} shaderStage
|
||||
* @param {THREE.TextureLoader} textureLoader
|
||||
*/
|
||||
export function getUniformValues(shaderStage, textureLoader, searchPath, flipY)
|
||||
{
|
||||
let threeUniforms = {};
|
||||
|
||||
const uniformBlocks = Object.values(shaderStage.getUniformBlocks());
|
||||
uniformBlocks.forEach(uniforms =>
|
||||
{
|
||||
if (!uniforms.empty())
|
||||
{
|
||||
for (let i = 0; i < uniforms.size(); ++i)
|
||||
{
|
||||
const variable = uniforms.get(i);
|
||||
const value = variable.getValue()?.getData();
|
||||
const name = variable.getVariable();
|
||||
threeUniforms[name] = new THREE.Uniform(toThreeUniform(variable.getType().getName(), value, name, uniforms,
|
||||
textureLoader, searchPath, flipY));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return threeUniforms;
|
||||
}
|
||||
198
MaterialX/javascript/MaterialXView/source/index.js
Normal file
198
MaterialX/javascript/MaterialXView/source/index.js
Normal file
@@ -0,0 +1,198 @@
|
||||
//
|
||||
// Copyright Contributors to the MaterialX Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
import * as THREE from 'three';
|
||||
import { Viewer } from './viewer.js'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
import { dropHandler, dragOverHandler, setLoadingCallback, setSceneLoadingCallback } from './dropHandling.js';
|
||||
|
||||
let renderer, orbitControls;
|
||||
|
||||
// Turntable option. For now the step size is fixed.
|
||||
let turntableEnabled = false;
|
||||
let turntableSteps = 360;
|
||||
let turntableStep = 0;
|
||||
|
||||
let captureRequested = false;
|
||||
|
||||
// Get URL options. Fallback to defaults if not specified.
|
||||
let materialFilename = new URLSearchParams(document.location.search).get("file");
|
||||
if (!materialFilename)
|
||||
{
|
||||
materialFilename = 'Materials/Examples/StandardSurface/standard_surface_default.mtlx';
|
||||
}
|
||||
|
||||
let viewer = Viewer.create();
|
||||
init();
|
||||
viewer.getEditor().updateProperties(0.9);
|
||||
|
||||
// Capture the current frame and save an image file.
|
||||
function captureFrame()
|
||||
{
|
||||
let canvas = document.getElementById('webglcanvas');
|
||||
var url = canvas.toDataURL();
|
||||
var link = document.createElement('a');
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('target', '_blank');
|
||||
link.setAttribute('download', 'screenshot.png');
|
||||
link.click();
|
||||
}
|
||||
|
||||
function init()
|
||||
{
|
||||
let canvas = document.getElementById('webglcanvas');
|
||||
|
||||
// Handle material selection changes
|
||||
let materialsSelect = document.getElementById('materials');
|
||||
materialsSelect.value = materialFilename;
|
||||
materialsSelect.addEventListener('change', (e) =>
|
||||
{
|
||||
materialFilename = e.target.value;
|
||||
viewer.getEditor().initialize();
|
||||
viewer.getMaterial().loadMaterials(viewer, materialFilename);
|
||||
viewer.getEditor().updateProperties(0.9);
|
||||
});
|
||||
|
||||
// Handle geometry selection changes
|
||||
const scene = viewer.getScene();
|
||||
let geometrySelect = document.getElementById('geometry');
|
||||
geometrySelect.value = scene.getGeometryURL();
|
||||
geometrySelect.addEventListener('change', (e) =>
|
||||
{
|
||||
console.log('Change geometry to:', e.target.value);
|
||||
scene.setGeometryURL(e.target.value);
|
||||
scene.loadGeometry(viewer, orbitControls);
|
||||
});
|
||||
|
||||
// Set up scene
|
||||
scene.initialize();
|
||||
|
||||
// Set up renderer
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true, canvas });
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
renderer.debug.checkShaderErrors = false;
|
||||
|
||||
window.addEventListener('resize', onWindowResize);
|
||||
|
||||
// Set up controls
|
||||
orbitControls = new OrbitControls(scene.getCamera(), renderer.domElement);
|
||||
|
||||
// Add hotkey 'f' to capture the current frame and save an image file.
|
||||
// See check inside the render loop when a capture can be performed.
|
||||
document.addEventListener('keydown', (event) =>
|
||||
{
|
||||
if (event.key === 'f')
|
||||
{
|
||||
captureRequested = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize editor
|
||||
viewer.getEditor().initialize();
|
||||
|
||||
const hdrLoader = viewer.getHdrLoader();
|
||||
const fileLoader = viewer.getFileLoader();
|
||||
Promise.all([
|
||||
new Promise(resolve => hdrLoader.load('Lights/san_giuseppe_bridge_split.hdr', resolve)),
|
||||
new Promise(resolve => hdrLoader.load('Lights/irradiance/san_giuseppe_bridge_split.hdr', resolve)),
|
||||
new Promise(resolve => fileLoader.load('Lights/san_giuseppe_bridge_split.mtlx', resolve)),
|
||||
new Promise(function (resolve)
|
||||
{
|
||||
MaterialX().then((module) =>
|
||||
{
|
||||
resolve(module);
|
||||
});
|
||||
})
|
||||
]).then(async ([radianceTexture, irradianceTexture, lightRigXml, mxIn]) =>
|
||||
{
|
||||
// Initialize viewer + lighting
|
||||
await viewer.initialize(mxIn, renderer, radianceTexture, irradianceTexture, lightRigXml);
|
||||
|
||||
// Load geometry
|
||||
let scene = viewer.getScene();
|
||||
scene.loadGeometry(viewer, orbitControls);
|
||||
|
||||
// Load materials
|
||||
viewer.getMaterial().loadMaterials(viewer, materialFilename);
|
||||
|
||||
// Update assignments
|
||||
viewer.getMaterial().updateMaterialAssignments(viewer);
|
||||
|
||||
canvas.addEventListener("keydown", handleKeyEvents, true);
|
||||
|
||||
}).then(() =>
|
||||
{
|
||||
animate();
|
||||
}).catch(err =>
|
||||
{
|
||||
console.error(Number.isInteger(err) ? this.getMx().getExceptionMessage(err) : err);
|
||||
})
|
||||
|
||||
// allow dropping files and directories
|
||||
document.addEventListener('drop', dropHandler, false);
|
||||
document.addEventListener('dragover', dragOverHandler, false);
|
||||
|
||||
setLoadingCallback(file =>
|
||||
{
|
||||
materialFilename = file.fullPath || file.name;
|
||||
viewer.getEditor().initialize();
|
||||
viewer.getMaterial().loadMaterials(viewer, materialFilename);
|
||||
viewer.getEditor().updateProperties(0.9);
|
||||
});
|
||||
|
||||
setSceneLoadingCallback(file =>
|
||||
{
|
||||
let glbFileName = file.fullPath || file.name;
|
||||
console.log('Drop geometry to:', glbFileName);
|
||||
scene.setGeometryURL(glbFileName);
|
||||
scene.loadGeometry(viewer, orbitControls);
|
||||
});
|
||||
|
||||
// enable three.js Cache so that dropped files can reference each other
|
||||
THREE.Cache.enabled = true;
|
||||
}
|
||||
|
||||
function onWindowResize()
|
||||
{
|
||||
viewer.getScene().updateCamera();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
}
|
||||
|
||||
function animate()
|
||||
{
|
||||
requestAnimationFrame(animate);
|
||||
const scene = viewer.getScene();
|
||||
|
||||
if (turntableEnabled)
|
||||
{
|
||||
turntableStep = (turntableStep + 1) % 360;
|
||||
var turntableAngle = turntableStep * (360.0 / turntableSteps) / 180.0 * Math.PI;
|
||||
scene._scene.rotation.y = turntableAngle;
|
||||
}
|
||||
|
||||
scene.updateTimeUniforms();
|
||||
renderer.render(scene.getScene(), scene.getCamera());
|
||||
|
||||
if (captureRequested)
|
||||
{
|
||||
captureFrame();
|
||||
captureRequested = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyEvents(event)
|
||||
{
|
||||
const V_KEY = 86;
|
||||
const P_KEY = 80;
|
||||
|
||||
if (event.keyCode == V_KEY)
|
||||
{
|
||||
viewer.getScene().toggleBackgroundTexture();
|
||||
}
|
||||
else if (event.keyCode == P_KEY)
|
||||
{
|
||||
turntableEnabled = !turntableEnabled;
|
||||
}
|
||||
}
|
||||
1679
MaterialX/javascript/MaterialXView/source/viewer.js
Normal file
1679
MaterialX/javascript/MaterialXView/source/viewer.js
Normal file
File diff suppressed because it is too large
Load Diff
78
MaterialX/javascript/MaterialXView/webpack.config.js
Normal file
78
MaterialX/javascript/MaterialXView/webpack.config.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const CopyPlugin = require("copy-webpack-plugin");
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
|
||||
// Load material configuration from external JSON file
|
||||
const materialConfig = JSON.parse(fs.readFileSync('./example_materials.json', 'utf8'));
|
||||
|
||||
// Function to process materials from a given path
|
||||
function processMaterialPath(materialPath, baseURL) {
|
||||
const dirent = fs.readdirSync(materialPath).filter(
|
||||
function (file) { if (file.lastIndexOf(".mtlx") > -1) return file; }
|
||||
);
|
||||
return dirent.map((fileName) => ({
|
||||
name: fileName,
|
||||
value: `${baseURL}/${fileName}`
|
||||
}));
|
||||
}
|
||||
|
||||
// Generate materials array from configuration
|
||||
let materials = [];
|
||||
materialConfig.materials.forEach(materialType => {
|
||||
const materialFiles = processMaterialPath(materialType.path, materialType.baseURL);
|
||||
materials = materials.concat(materialFiles);
|
||||
});
|
||||
|
||||
const geometryFiles = "../../resources/Geometry";
|
||||
const geometryFilesURL = "Geometry";
|
||||
dirent = fs.readdirSync(geometryFiles).filter(
|
||||
function (file) { if (file.lastIndexOf(".glb") > -1) return file; }
|
||||
)
|
||||
let geometry = dirent
|
||||
.map((fileName) => ({ name: fileName, value: `${geometryFilesURL}/${fileName}` }));
|
||||
|
||||
module.exports = {
|
||||
entry: './source/index.js',
|
||||
output: {
|
||||
filename: 'main.js',
|
||||
path: path.resolve(__dirname, 'dist')
|
||||
},
|
||||
mode: "development",
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
templateParameters: {
|
||||
materials,
|
||||
geometry
|
||||
},
|
||||
template: 'index.ejs'
|
||||
}),
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
context: "../../resources/Images",
|
||||
from: "*.*",
|
||||
to: "Images",
|
||||
},
|
||||
{
|
||||
context: "../../resources/Geometry/",
|
||||
from: "*.glb",
|
||||
to: "Geometry",
|
||||
},
|
||||
{ from: "./public", to: 'public' },
|
||||
{ context: "../../resources/Lights", from: "*.*", to: "Lights" },
|
||||
{ context: "../../resources/Lights/irradiance", from: "*.*", to: "Lights/irradiance" },
|
||||
// Dynamically generate material copy patterns from configuration
|
||||
...materialConfig.materials.map(materialType => ({
|
||||
from: materialType.path,
|
||||
to: materialType.baseURL
|
||||
})),
|
||||
{ from: "../build/bin/JsMaterialXCore.wasm" },
|
||||
{ from: "../build/bin/JsMaterialXCore.js" },
|
||||
{ from: "../build/bin/JsMaterialXGenShader.wasm" },
|
||||
{ from: "../build/bin/JsMaterialXGenShader.js" },
|
||||
{ from: "../build/bin/JsMaterialXGenShader.data" },
|
||||
],
|
||||
}),
|
||||
]
|
||||
};
|
||||
307
MaterialX/javascript/README.md
Normal file
307
MaterialX/javascript/README.md
Normal file
@@ -0,0 +1,307 @@
|
||||
# MaterialX JavaScript
|
||||
|
||||
This folder contains tests and examples that leverage the JavaScript bindings for the MaterialX library. The bindings are generated using the emscripten SDK.
|
||||
|
||||
## Generating the Bindings
|
||||
|
||||
### Prerequisites
|
||||
|
||||
The emscripten SDK is required to generate the JavaScript bindings. There are several ways of using the SDK on different platforms. In general, we recommend to install the SDK directly, following the instructions below. Alternative options are explained [below](#alternative-options-to-use-the-emscripten-sdk).
|
||||
|
||||
To install the SDK directly, follow the instructions of the [emscripten SDK installation Guide](https://emscripten.org/docs/getting_started/downloads.html#installation-instructions-using-the-emsdk-recommended). Make sure to install prerequisites depending on your platform and read usage hints first (e.g. differences between Unix / Windows scripts).
|
||||
|
||||
The recommended version is `4.0.8`. Do not automatically update to the latest version to avoid build issues.
|
||||
|
||||
Note that following the instructions will set some environment variables that are required to use the SDK. These variables are only set temporarily for the current terminal, though. Setting the environment variables in other terminals can be achieved by running
|
||||
```sh
|
||||
source ./emsdk_env.sh
|
||||
```
|
||||
inside of the `emsdk` folder (check the documentation for the Windows equivalent). In case of the MaterialX project, it is not required to have these environment variables set. You can also use a CMake build flag instead, as described in the [build instructions](#build-steps) below.
|
||||
|
||||
Emscripten installs its own Node.js runtime. We recommend using the Node.js bundled with your active emsdk to avoid package/runtime mismatches.
|
||||
|
||||
Setting the environment variables permanently is also possible, either by adding a `--permanent` flag to the `activate` command, or by sourcing the `emsdk_env` script every time a shell is launched, e.g. by adding the `source` call to `~/.bash_profile` or an equivalent file. Note however, that the environment variables set by the emscripten SDK might override existing system settings, like the default Python, Java or NodeJs version, so setting them permanently might not be desired on all systems.
|
||||
|
||||
### Alternative options to use the emscripten SDK
|
||||
If installing the emscripten SDK directly isn't desired, e.g. because you prefer to keep development environments cleanly separated, it can be provided in different ways.
|
||||
|
||||
On Windows, using WSL2 (e.g. with an Ubuntu image) is a viable alternative to isolate the build environment from your main system. Simply set up the build environment in that Linux container.
|
||||
|
||||
Another alternative is to use a [Docker](https://docs.docker.com/) image. With Docker installed, use the [emscripten Docker image](https://hub.docker.com/r/emscripten/emsdk) as described in the [Docker build instructions](#docker) below.
|
||||
|
||||
### Build Steps
|
||||
Run the following commands in the root folder of this repository.
|
||||
|
||||
Create a build folder in the javascript folder below the *root* of the repository and navigate to that folder:
|
||||
```sh
|
||||
mkdir ./javascript/build
|
||||
cd ./javascript/build
|
||||
```
|
||||
|
||||
If you are using the emsdk directly on Windows, note that the emscripten SDK doesn't work with Microsoft's Visual Studio build tools. You need to use an alternative CMake generator like [MinGW](http://mingw-w64.org/doku.php) Makefiles or [Ninja](https://ninja-build.org/). We recommend to use Ninja (unless you already have MinGW installed), since it's pretty lightweight and a pure build system, instead of a full compiler suite. Download Ninja for Windows and unzip the ninja.exe file to some suitable directory in your path (use the command "echo $PATH" or similar to view your PATH variable).
|
||||
|
||||
Generate the build files with CMake. When building the JavaScript bindings, you can optionally specify the emsdk path with the `MATERIALX_EMSDK_PATH` option. This option can be omitted if the `emsdk/emsdk_env.sh` script was run beforehand.
|
||||
```sh
|
||||
cmake .. -DMATERIALX_BUILD_JS=ON -DMATERIALX_BUILD_RENDER=OFF
|
||||
-DMATERIALX_BUILD_TESTS=OFF
|
||||
-DMATERIALX_BUILD_GEN_OSL=OFF
|
||||
-DMATERIALX_BUILD_GEN_MDL=OFF
|
||||
-DMATERIALX_EMSDK_PATH=</path to emsdk>
|
||||
```
|
||||
On Windows, remember to set the CMake generator via the `-G` flag , e.g. `-G "Ninja"`.
|
||||
|
||||
To build the project, run
|
||||
```sh
|
||||
cmake --build . --target install -j8
|
||||
```
|
||||
Change the value of the `-j` option to the number of threads you want to build with.
|
||||
#### Docker
|
||||
In order to build using the Docker image, execute the following command in a terminal (in the root of this repository, after creating the build folder), using the CMake commands introduced above.
|
||||
```sh
|
||||
docker run --rm -v {path_to_MaterialX_repo}:/src emscripten/emsdk sh -c "cd build && <cmake generator command> && <cmake build command>"
|
||||
```
|
||||
|
||||
For follow-up builds (i.e. after changing the source code), remove the `<cmake generator command>` step from the above call.
|
||||
|
||||
### Output
|
||||
After building the project the `JsMaterialXCore.wasm`, `JsMaterialXCore.js`, `JsMaterialXGenShader.wasm`, `JsMaterialXGenShader.js` and `JsMaterialXGenShader.data` files can be found in the global install directory of this project.
|
||||
|
||||
## Testing
|
||||
JavaScript unit tests are located in the `MaterialXTest` folder and use the `.spec.js` suffix. A sample browser is located in the `MaterialXView` folder which allows preview of some of provided sample MaterialX materials.
|
||||
|
||||
### Unit Tests
|
||||
These tests require `node.js`, which is shipped with the emscripten environment. Make sure to `source` the `emsdk/emsdk_env.sh` script before running the steps described below, if you don't have NodeJs installed on your system already (running the command is not required otherwise).
|
||||
|
||||
1. From the `MaterialXTest` directory, install the npm packages.
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Run the tests from the MaterialXTest directory.
|
||||
```sh
|
||||
npm run test # Core library tests
|
||||
npm run test:browser # Code generation library test
|
||||
```
|
||||
|
||||
### Sample Viewer
|
||||
|
||||
1. From the `MaterialXView` directory, install the npm packages and build the viewer.
|
||||
```sh
|
||||
npm install
|
||||
call npm run build
|
||||
```
|
||||
|
||||
2. Start a local host and open in a browser. In this example `http-server` is being used but any utility which can open a local host can be used.
|
||||
```sh
|
||||
call npm install http-server -g
|
||||
call http-server . -p 8000
|
||||
# Open browser
|
||||
```
|
||||
## Sample build scripts
|
||||
Note that a sample build script is provided in
|
||||
`javascript/build_javascript_win.bat` with a corresponding script to clean the build area in
|
||||
`javascript/clean_javascript_win.bat`. Modify the Emscripten SDK and MaterialX build locations as needed.
|
||||
|
||||
Additionally the github actions workflow YAML file (`.github/workflows/main.yml`) can be examined as well.
|
||||
|
||||
## Using the Bindings
|
||||
### Consuming the Module
|
||||
The JavaScript bindings come in two different flavors. `JsMaterialXCore.js` contains all bindings for the MaterialXCore and MaterialXFormat packages, allowing to load, traverse, modify and write MaterialX documents. `JsMaterialXGenShader.js` contains the Core and Format bindings, as well as the bindings required to generate WebGL-compatible shader code from a MaterialX document. Since this involves shipping additional data, we recommend to the `JsMaterialXCore.js` if shader generation is not required.
|
||||
|
||||
The bindings can be consumed via a script tag in the browser:
|
||||
```html
|
||||
<script src="./JsMaterialXCore.js" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
MaterialX().then((mx) => {
|
||||
mx.createDocument();
|
||||
...
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
In NodeJs, simply `require` the MaterialX module like this:
|
||||
```javascript
|
||||
const MaterialX = require('./JsMaterialXCore.js');
|
||||
|
||||
MaterialX().then(mx => {
|
||||
mx.createDocument();
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
### JavaScript API
|
||||
In general, the JavaScript API is the same as the C++ API. Sometimes, it doesn't make sense to bind certain functions or even classes, for example when there is already an alternative in native JS. Other special cases are explained in the following sections.
|
||||
|
||||
#### Data Type Conversions
|
||||
Data types are automatically converted from C++ types to the corresponding JavaScript types, to provide a more natural interface on the JavaScript side. For example, a string that is returned from a C++ function as an `std::string` won't have the C++ string interface, but will be a JavaScript string instead. While this is usually straight-forward, it has some implications when it comes to containers.
|
||||
|
||||
C++ vectors will be converted to JS arrays (and vice versa). Other C++ containers/collections are converted to either JS arrays or objects as well. While this provides a more natural interface on the JS side, it comes with the side-effect that modifications to such containers on the JS side will not be reflected in C++. For example, pushing an element to a JS array that was returned in place of a C++ vector will not update the vector in C++. Elements within the array can be modified and their updates will be reflected in C++, though (this does only apply to class types, not primitives or strings, of course).
|
||||
|
||||
#### Template Functions
|
||||
Functions that handle generic types in C++ via templates are mapped to JavaScript by creating multiple bindings, one for each type. For example, the `InterfaceElement::setInputValue` function is mapped as `setInputValueString`, `setInputValueBoolean`, `setInputValueInteger` and so on.
|
||||
|
||||
#### Iterators
|
||||
MaterialX comes with a number of iterators (e.g. `TreeIterator`, `GraphIterator`). These iterators implement the iterable (and iterator) protocol in JS, and can therefore be used in `for ... of` loops.
|
||||
|
||||
#### Memory Management (Embind)
|
||||
Objects that originate from C++ (e.g. `Document`, `Node`, `Shader`, `GenContext`) are backed by C++ instances. When you're done with them, explicitly call `.delete()` to release the underlying C++ object. This is especially important for objects returned as smart pointers in C++ (such as elements yielded by iterators or getters), otherwise you may see warnings like "Embind found a leaked C++ instance".
|
||||
|
||||
Examples:
|
||||
|
||||
```javascript
|
||||
// Documents and elements
|
||||
const doc = mx.createDocument();
|
||||
const node = doc.addNode('image');
|
||||
// ... use node ...
|
||||
node.delete();
|
||||
doc.delete();
|
||||
|
||||
// Iterator-yielded elements
|
||||
for (const elem of doc.traverseTree()) {
|
||||
// ... use elem ...
|
||||
elem.delete();
|
||||
}
|
||||
|
||||
// Edges in graph traversal
|
||||
for (const edge of output.traverseGraph()) {
|
||||
const up = edge.getUpstreamElement();
|
||||
const conn = edge.getConnectingElement();
|
||||
const down = edge.getDownstreamElement();
|
||||
// ... use them ...
|
||||
up.delete();
|
||||
conn.delete();
|
||||
down.delete();
|
||||
edge.delete();
|
||||
}
|
||||
```
|
||||
|
||||
#### Exception Handling
|
||||
When a C++ function throws an exception, the JS binding throws as well. Depending on the Emscripten version, the caught value may be either a numeric pointer to the C++ exception (legacy) or an exception object (Error-like). Use `getExceptionMessage` on the caught value in both cases:
|
||||
|
||||
```javascript
|
||||
const doc = mx.createDocument();
|
||||
doc.addNode('category', 'node1');
|
||||
|
||||
try {
|
||||
doc.addNode('category', 'node1');
|
||||
} catch (err) {
|
||||
// Works with both a numeric pointer and an exception object
|
||||
console.log(mx.getExceptionMessage(err)); // 'Child name is not unique: node1'
|
||||
}
|
||||
```
|
||||
|
||||
#### Loading MaterialX files
|
||||
The bindings expose the `readFromXmlString` and `readFromXmlFile` functions. Their usage is similar to C++ and should work in browsers and NodeJs. Note that these functions are asynchronous in JavaScript, so you need to either `await` them or place depending code in a `.then()` block.
|
||||
|
||||
By default, the functions will resolve referenced (i.e. included) documents. This can be disabled by setting the `readOptions.readXIncludes` to `false`:
|
||||
```javascript
|
||||
const readOptions = new mx.XmlReadOptions();
|
||||
readOptions.readXIncludes = false;
|
||||
await readFromXmlFile(doc, filename, searchPath, readOptions); // will only read the top-level file, no includes
|
||||
```
|
||||
Note that the `readXIncludesFunction` option that exists on the C++ read options is not supported in JavaScript.
|
||||
|
||||
The `searchPath` is a semicolon-separated list of absolute or relative paths. Relative paths will be evaluated with regards to the current working directory. In case of using absolute search paths in web browsers (i.e. urls), note that urls like `mydomain.com/path` or `localhost/path` might be considered relative paths. To ensure they're used as absolute paths, make them fully formed urls, i.e. have a protocol prefix like `https://`.
|
||||
|
||||
#### Writing MaterialX files
|
||||
Documents can be written to strings via the `writeToXmlString` method, and to files with the `writeToXmlFile` method. In NodeJs, the latter will write the file to the path provided to the method (relative paths will be evaluated with respect to the current working directory). In the browser, the written file will be downloaded automatically, so only the file name matters.
|
||||
|
||||
Note that the `XmlWriteOptions.elementPredicate` option is not supported in JavaScript.
|
||||
|
||||
### Using the EsslShaderGenerator JavaScript bindings
|
||||
#### Setup
|
||||
Make sure to consume `JsMaterialXGenShader.js` instead of `JsMaterialXCore.js` as described [here](#consuming-the-module). Additionally, ensure that the app serves `JsMaterialXGenShader.data` and `JsMaterialXGenShader.wasm`. The `.data` file includes the prepackaged library files containing the shader snippets and MaterialX node definitions/implementations required for generating the shader code.
|
||||
|
||||
#### Generating Essl Shader Code & Compiling with WebGL
|
||||
To generate WebGL 2 compatible shader code a generator context and an instance of the `EsslShaderGenerator` class is required.
|
||||
```javascript
|
||||
const gen = mx.EsslShaderGenerator.create();
|
||||
const genContext = new mx.GenContext(gen);
|
||||
```
|
||||
The standard libraries need to be loaded and imported into the document. This step is required as the standard libraries contain all the definitions and snippets needed for assembly of the shader code.
|
||||
```javascript
|
||||
const stdlib = mx.loadStandardLibraries(genContext);
|
||||
const doc = mx.createDocument();
|
||||
doc.importLibrary(stdlib);
|
||||
```
|
||||
Now it is either time to load a document from a file as outline [here](#loading-materialx-files) or create one using the API.
|
||||
Generating the code consists of finding a renderable element (the javascript binding returns the first renderable element) and calling the `generate` method from the `EsslShaderGenerator` class. The shader code for the vertex/pixel shader may be requested from the resulting `Shader` instance.
|
||||
```javascript
|
||||
const elem = mx.findRenderableElement(doc);
|
||||
const shader = gen.generate(elem.getNamePath(), elem, genContext);
|
||||
const fShader = shader.getSourceCode("pixel");
|
||||
const vShader = shader.getSourceCode("vertex");
|
||||
// Cleanup when done
|
||||
shader.delete();
|
||||
stdlib.delete();
|
||||
genContext.delete();
|
||||
gen.delete();
|
||||
```
|
||||
Shader generation options may be changed by getting the options from the context and altering its properties. Changes to these options must occur after the standard libraries have been loaded as the call to `mx.loadStandardLibraries(genContext)` sets the options to some defaults.
|
||||
```javascript
|
||||
genContext.getOptions().fileTextureVerticalFlip = false;
|
||||
```
|
||||
Compilation using a WebGL 2 context (`gl`) is straight forward.
|
||||
```javascript
|
||||
const glVertexShader = gl.createShader(gl.VERTEX_SHADER);
|
||||
gl.shaderSource(glVertexShader, vShader);
|
||||
gl.compileShader(glVertexShader);
|
||||
const glPixelShader = gl.createShader(gl.FRAGMENT_SHADER);
|
||||
gl.shaderSource(glPixelShader, fShader);
|
||||
gl.compileShader(glPixelShader);
|
||||
```
|
||||
However, any rendering framework that supports custom shaders should do. In the [Web Viewer sample app](./MaterialXView/src/index.js) we use the [RawShaderMaterial](https://threejs.org/docs/index.html?q=RawSh#api/en/materials/RawShaderMaterial) class from [three.js](https://threejs.org/).
|
||||
|
||||
#### Getting the shader uniforms
|
||||
The uniform values can be obtained from the shader as a JSON, either for the vertex or the pixel shader.
|
||||
```javascript
|
||||
shader.getUniformValues("vertex");
|
||||
shader.getUniformValues("pixel")
|
||||
```
|
||||
Each entry corresponds to a uniform name and the value is an object which contains the type as specified in the generators Syntax class and the stringified value. Some of the commonly used uniform names in the generated shader are listed [here](../../documents/DeveloperGuide/ShaderGeneration.md#162-variable-naming-convention).
|
||||
An example that parses the JSON and feeds the uniform data to a three.js based application can be found in the [Web Viewer Sample App](./MaterialXView/src/index.js).
|
||||
|
||||
## Maintaining the Bindings
|
||||
This section provides some background on binding creation for contributors. In general, we recommend to look at existing bindings for examples.
|
||||
|
||||
### What to Bind?
|
||||
In general, we aim for 100% coverage of the MaterialX API, at least for the Core and Format packages. However, there are functions and even classes where creating bindings wouldn't make much sense. The `splitString` utility function is such an example, because the JavaScript string class does already have a `split` method. The `FilePath` and `FileSearchPath` classes of the Format package are simply represented as strings on the JavaScript side, even though they provide complex APIs in C++. This is because most of their APIs do not apply to browsers, since they are specific to file system operations. In NodeJs, they would present a competing implementation of the core `fs` module, and therefore be redundant (even though they might be convenient in some cases).
|
||||
|
||||
The examples above illustrate that it does not always make sense to create bindings, if there is no easy way to map them to both browsers and NodeJs, or if there is already an alternative in native JS. The overhead, both in maintenance and bundle size, wouldn't pay off.
|
||||
|
||||
### Emscripten's optional_override
|
||||
Emscripten's `optional_override` allows to provide custom binding implementations in-place and enables function overloading by parameter count, which is otherwise not supported in JavaScript. Contributors need to be careful when using it, though, since there is a small pitfall.
|
||||
|
||||
If a function binding has multiple overloads defined via `optional_override` to support optional parameters, this binding must only be defined once on the base class (i.e. the class that defines the function initially). Virtual functions that are overridden in deriving classes must not be bound again when creating bindings for these derived classes. Doing so can lead to the wrong function (i.e. base class' vs derived class' implementation) being called at runtime.
|
||||
|
||||
### Optional Parameters
|
||||
Many C++ functions have optional parameters. Unfortunately, emscripten does not automatically deal with optional parameters. Binding these functions the 'normal' way will require users to provide all parameters in JavaScript, including optional ones. We provide helper macros to cicumvent this issue. Different flavors of the `BIND_*_FUNC` macros defined in `Helpers.h` can be used to conveniently bind functions with optional parameters. See uses of these macros in the existing bindings for examples.
|
||||
|
||||
NOTE: Since these macros use `optional_override` internally, the restrictions explained above go for them as well. Only define bindings for virtual functions once on the base class with these macros.
|
||||
|
||||
### Template Functions
|
||||
Generic functions that deal with multiple types cannot be bound directly to JavaScript. Instead, we create multiple bindings, one per type. The binding name follows the pattern `functionName<Type>`. For convenience, we usually provide a custom macro that takes the type and constructs the corresponding binding. See the existing bindings for examples.
|
||||
|
||||
### Array <-> Vector conversion
|
||||
As explained in the user documentation, types are automatically converted between C++ and JavaScript. While there are multiple examples for custom marshalling of types (e.g. `std::pair<int, int>` to array, or `FileSearchPath` to string), the most common use case is the conversion of C++ vectors to JS arrays, and vice versa. This conversion can automatically be achieved by including the `VectorHelper.h` header in each binding file that covers functions which either accept or return vectors in C++.
|
||||
|
||||
### Custom JavaScript Code
|
||||
Some bindings cannot be direct mappings to a C++ function. In particular when operations are asynchronous in JavaScript (e.g. loading files), it's easier to provide custom JavaScript implementations for the affected functions. This is how `readFromXmlString` and `readFromXmlFile` are implemented, for example. Such JavaScript code can be provided using the post-JS feature of emscripten. There should be one `post.js` file per MaterialX module, if that module requires any custom JS code. Note that these files need to be added to `CMakeLists.txt` in the `JsMaterialX` source folder. We recommend to provide custom code that depends on the WebAssembly module like this:
|
||||
```javascript
|
||||
onModuleReady(function () {
|
||||
<your code here>
|
||||
});
|
||||
```
|
||||
This will register your code after the module has been initialized. The wasm module will be available as `Module`.
|
||||
Since the module itself is ES5 code, we recommend to write custom code in ES5 as well, even though ES6 should work as well in most cases.
|
||||
|
||||
In order to avoid conflicting definitions in post.js files, we recommend to wrap custom code in an [IIFE](https://developer.mozilla.org/en-US/docs/Glossary/IIFE) (Immediately Invoked Function Expression).
|
||||
|
||||
### Testing strategy
|
||||
Testing every binding doesn't seem desirable, since most of them will directly map to the C++ implementation, which should already be tested in the C++ tests. Instead, we only test common workflows (e.g. iterating/parsing a document), bindings with custom implementations, and our custom binding mechanisms. The latter involves custom marshalling, e.g. of the vector <-> array conversion, or support for optional parameters. Additionally, all features that might behave different on the web, compared to desktop, should be tested as well.
|
||||
|
||||
The C++ and [Python binding tests](../../python/MaterialXTest/main.py) follow a different approach than the JS unit tests, by testing larger workflows instead of single features. In order to cover at least as much functionality in JS as in Python, the tests have been ported to JS. However, JS tests are organized in the same file structure as the bindings, so these workflow tests have been added to the file where they fit in best (e.g. the `Traverse Graph` test is in `traversal.spec.js`). This is equivalent to how tests are organized in C++.
|
||||
|
||||
## CI
|
||||
Emscripten builds and test runs are specified in `.github/workflows/build_wasm.yml`.
|
||||
|
||||
30
MaterialX/javascript/build_javascript_win.bat
Normal file
30
MaterialX/javascript/build_javascript_win.bat
Normal file
@@ -0,0 +1,30 @@
|
||||
@rem This script builds MaterialX JavaScript on Windows. The final command starts a local server, allowing you to
|
||||
@rem run the MaterialX Web Viewer locally by entering 'http://localhost:8080' in the search bar of your browser.
|
||||
@echo --------------------- Setup Emscripten ---------------------
|
||||
@echo on
|
||||
@rem Edit the following paths to match your local locations for the Emscripten and MaterialX projects.
|
||||
set EMSDK_LOCATION=C:/GitHub/emsdk
|
||||
set MATERIALX_LOCATION=C:/GitHub/MaterialX
|
||||
call %EMSDK_LOCATION%/emsdk.bat install 4.0.8
|
||||
call %EMSDK_LOCATION%/emsdk.bat activate 4.0.8
|
||||
if NOT ["%errorlevel%"]==["0"] pause
|
||||
@echo --------------------- Build MaterialX With JavaScript ---------------------
|
||||
@echo on
|
||||
cd %MATERIALX_LOCATION%
|
||||
cmake -S . -B javascript/build -DMATERIALX_BUILD_JS=ON -DMATERIALX_EMSDK_PATH=%EMSDK_LOCATION% -G Ninja
|
||||
cmake --build javascript/build --target install --config RelWithDebInfo --parallel 2
|
||||
if NOT ["%errorlevel%"]==["0"] pause
|
||||
@echo --------------------- Run JavaScript Tests ---------------------
|
||||
@echo on
|
||||
cd javascript/MaterialXTest
|
||||
call npm install
|
||||
call npm run test
|
||||
call npm run test:browser
|
||||
if NOT ["%errorlevel%"]==["0"] pause
|
||||
@echo --------------------- Run Interactive Viewer ---------------------
|
||||
@echo on
|
||||
cd ../MaterialXView
|
||||
call npm install
|
||||
call npm run build
|
||||
call npm run start
|
||||
if NOT ["%errorlevel%"]==["0"] pause
|
||||
5
MaterialX/javascript/clean_javascript_win.bat
Normal file
5
MaterialX/javascript/clean_javascript_win.bat
Normal file
@@ -0,0 +1,5 @@
|
||||
rmdir build /s /q
|
||||
rmdir MaterialXTest\_build /s /q
|
||||
rmdir MaterialXTest\node_modules /s /q
|
||||
rmdir MaterialXView\dist /s /q
|
||||
rmdir MaterialXView\node_modules /s /q
|
||||
6
MaterialX/javascript/package-lock.json
generated
Normal file
6
MaterialX/javascript/package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "javascript",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
Reference in New Issue
Block a user