This commit is contained in:
2026-01-06 13:25:49 +00:00
parent 5d495d731b
commit 4e15e08b7f
1395 changed files with 295666 additions and 323 deletions

View File

@@ -0,0 +1,126 @@
#!/usr/bin/env python
'''
Unit tests for shader generation in MaterialX Python.
'''
import os, unittest
import MaterialX as mx
import MaterialX.PyMaterialXGenShader as mx_gen_shader
import MaterialX.PyMaterialXGenOsl as mx_gen_osl
class TestGenShader(unittest.TestCase):
def test_ShaderInterface(self):
doc = mx.createDocument()
searchPath = mx.getDefaultDataSearchPath()
mx.loadLibraries(mx.getDefaultDataLibraryFolders(), searchPath, doc)
exampleName = u"shader_interface"
# Create a nodedef taking three color3 and producing another color3
nodeDef = doc.addNodeDef("ND_foo", "color3", "foo")
fooInputA = nodeDef.addInput("a", "color3")
fooInputB = nodeDef.addInput("b", "color3")
fooOutput = nodeDef.getOutput("out")
fooInputA.setValue(mx.Color3(1.0, 1.0, 0.0))
fooInputB.setValue(mx.Color3(0.8, 0.1, 0.1))
# Create an implementation graph for the nodedef performing
# a multiplication of the three colors.
nodeGraph = doc.addNodeGraph("IMP_foo")
nodeGraph.setAttribute("nodedef", nodeDef.getName())
output = nodeGraph.addOutput(fooOutput.getName(), "color3")
mult1 = nodeGraph.addNode("multiply", "mult1", "color3")
in1 = mult1.addInput("in1", "color3")
in1.setInterfaceName(fooInputA.getName())
in2 = mult1.addInput("in2", "color3")
in2.setInterfaceName(fooInputB.getName())
output.setConnectedNode(mult1)
doc.addNode("foo", "foo1", "color3")
output = doc.addOutput("foo_test", "color3");
output.setNodeName("foo1");
output.setAttribute("output", "o");
# Test for target
targetDefs = doc.getTargetDefs()
self.assertTrue(len(targetDefs))
shadergen = mx_gen_osl.OslShaderGenerator.create()
target = shadergen.getTarget()
foundTarget = next((
t for t in targetDefs
if t.getName() == target), None)
self.assertTrue(foundTarget)
context = mx_gen_shader.GenContext(shadergen)
context.registerSourceCodeSearchPath(searchPath)
shadergen.registerTypeDefs(doc);
# Test generator with complete mode
context.getOptions().shaderInterfaceType = mx_gen_shader.ShaderInterfaceType.SHADER_INTERFACE_COMPLETE;
shader = shadergen.generate(exampleName, output, context);
self.assertTrue(shader)
self.assertTrue(len(shader.getSourceCode(mx_gen_shader.PIXEL_STAGE)) > 0)
ps = shader.getStage(mx_gen_shader.PIXEL_STAGE);
uniforms = ps.getUniformBlock(mx_gen_osl.OSL_UNIFORMS)
self.assertTrue(uniforms.size() == 2)
outputs = ps.getOutputBlock(mx_gen_osl.OSL_OUTPUTS)
self.assertTrue(outputs.size() == 1)
self.assertTrue(outputs[0].getName() == output.getName())
file = open(shader.getName() + "_complete.osl", "w+")
file.write(shader.getSourceCode(mx_gen_shader.PIXEL_STAGE))
file.close()
os.remove(shader.getName() + "_complete.osl");
# Test generator with reduced mode
context.getOptions().shaderInterfaceType = mx_gen_shader.ShaderInterfaceType.SHADER_INTERFACE_REDUCED;
shader = shadergen.generate(exampleName, output, context);
self.assertTrue(shader)
self.assertTrue(len(shader.getSourceCode(mx_gen_shader.PIXEL_STAGE)) > 0)
ps = shader.getStage(mx_gen_shader.PIXEL_STAGE);
uniforms = ps.getUniformBlock(mx_gen_osl.OSL_UNIFORMS)
self.assertTrue(uniforms.size() == 0)
outputs = ps.getOutputBlock(mx_gen_osl.OSL_OUTPUTS)
self.assertTrue(outputs.size() == 1)
self.assertTrue(outputs[0].getName() == output.getName())
file = open(shader.getName() + "_reduced.osl", "w+")
file.write(shader.getSourceCode(mx_gen_shader.PIXEL_STAGE))
file.close()
os.remove(shader.getName() + "_reduced.osl");
# Define a custom attribute
customAttribute = doc.addAttributeDef("AD_attribute_node_name");
self.assertIsNotNone(customAttribute)
customAttribute.setType("string");
customAttribute.setAttrName("node_name");
customAttribute.setExportable(True);
# Define a nodedef referencing the custom attribute.
stdSurfNodeDef = doc.getNodeDef("ND_standard_surface_surfaceshader");
self.assertIsNotNone(stdSurfNodeDef)
stdSurfNodeDef.setAttribute("node_name", "Standard_Surface_Number_1");
self.assertTrue(stdSurfNodeDef.getAttribute("node_name") == "Standard_Surface_Number_1")
stdSurf1 = doc.addNodeInstance(stdSurfNodeDef, "standardSurface1");
self.assertIsNotNone(stdSurf1)
# Register shader metadata
shadergen.registerShaderMetadata(doc, context);
# Generate and test that attribute is in the code
context.getOptions().shaderInterfaceType = mx_gen_shader.ShaderInterfaceType.SHADER_INTERFACE_COMPLETE;
shader = shadergen.generate(stdSurf1.getName(), stdSurf1, context);
self.assertIsNotNone(shader)
code = shader.getSourceCode(mx_gen_shader.PIXEL_STAGE)
self.assertTrue('Standard_Surface_Number_1' in code)
self.assertTrue('node_name' in code)
print()
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,517 @@
#!/usr/bin/env python
'''
Unit tests for MaterialX Python.
'''
import math, os, unittest
import MaterialX as mx
#--------------------------------------------------------------------------------
_testValues = (1,
True,
1.0,
mx.Color3(0.1, 0.2, 0.3),
mx.Color4(0.1, 0.2, 0.3, 0.4),
mx.Vector2(1.0, 2.0),
mx.Vector3(1.0, 2.0, 3.0),
mx.Vector4(1.0, 2.0, 3.0, 4.0),
mx.Matrix33(0.0),
mx.Matrix44(1.0),
'value',
[1, 2, 3],
[False, True, False],
[1.0, 2.0, 3.0],
['one', 'two', 'three'])
_fileDir = os.path.dirname(os.path.abspath(__file__))
_libraryDir = os.path.join(_fileDir, '../../libraries/stdlib/')
_exampleDir = os.path.join(_fileDir, '../../resources/Materials/Examples/')
_searchPath = _libraryDir + mx.PATH_LIST_SEPARATOR + _exampleDir
_libraryFilenames = ('stdlib_defs.mtlx',
'stdlib_ng.mtlx')
_exampleFilenames = ('StandardSurface/standard_surface_brass_tiled.mtlx',
'StandardSurface/standard_surface_brick_procedural.mtlx',
'StandardSurface/standard_surface_carpaint.mtlx',
'StandardSurface/standard_surface_marble_solid.mtlx',
'StandardSurface/standard_surface_look_brass_tiled.mtlx',
'UsdPreviewSurface/usd_preview_surface_gold.mtlx',
'UsdPreviewSurface/usd_preview_surface_plastic.mtlx')
_epsilon = 1e-4
#--------------------------------------------------------------------------------
class TestMaterialX(unittest.TestCase):
def test_Globals(self):
self.assertTrue(mx.__version__ == mx.getVersionString())
def test_DataTypes(self):
for value in _testValues:
valueString = mx.getValueString(value)
typeString = mx.getTypeString(value)
newValue = mx.createValueFromStrings(valueString, typeString)
self.assertTrue(newValue == value)
self.assertTrue(mx.getTypeString(newValue) == typeString)
def test_Vectors(self):
v1 = mx.Vector3(1, 2, 3)
v2 = mx.Vector3(2, 4, 6)
# Indexing operators
self.assertTrue(v1[2] == 3)
v1[2] = 4
self.assertTrue(v1[2] == 4)
v1[2] = 3
# Component-wise operators
self.assertTrue(v2 + v1 == mx.Vector3(3, 6, 9))
self.assertTrue(v2 - v1 == mx.Vector3(1, 2, 3))
self.assertTrue(v2 * v1 == mx.Vector3(2, 8, 18))
self.assertTrue(v2 / v1 == mx.Vector3(2, 2, 2))
v2 += v1
self.assertTrue(v2 == mx.Vector3(3, 6, 9))
v2 -= v1
self.assertTrue(v2 == mx.Vector3(2, 4, 6))
v2 *= v1
self.assertTrue(v2 == mx.Vector3(2, 8, 18))
v2 /= v1
self.assertTrue(v2 == mx.Vector3(2, 4, 6))
self.assertTrue(v1 * 2 == v2)
self.assertTrue(v2 / 2 == v1)
# Unary operation
self.assertTrue(-v1 == mx.Vector3(-1, -2, -3))
v1 *= -1
self.assertTrue(+v1 == mx.Vector3(-1, -2, -3))
v1 *= -1
# Geometric methods
v3 = mx.Vector4(4)
self.assertTrue(v3.getMagnitude() == 8)
self.assertTrue(v3.getNormalized().getMagnitude() == 1)
self.assertTrue(v1.dot(v2) == 28)
self.assertTrue(v1.cross(v2) == mx.Vector3())
# Vector copy
v4 = v2.copy()
self.assertTrue(v4 == v2)
v4[0] += 1;
self.assertTrue(v4 != v2)
def test_Matrices(self):
# Translation and scale
trans = mx.Matrix44.createTranslation(mx.Vector3(1, 2, 3))
scale = mx.Matrix44.createScale(mx.Vector3(2))
self.assertTrue(trans == mx.Matrix44(1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
1, 2, 3, 1))
self.assertTrue(scale == mx.Matrix44(2, 0, 0, 0,
0, 2, 0, 0,
0, 0, 2, 0,
0, 0, 0, 1))
# Indexing operators
self.assertTrue(trans[3, 2] == 3)
trans[3, 2] = 4
self.assertTrue(trans[3, 2] == 4)
trans[3, 2] = 3
# Matrix methods
self.assertTrue(trans.getTranspose() == mx.Matrix44(1, 0, 0, 1,
0, 1, 0, 2,
0, 0, 1, 3,
0, 0, 0, 1))
self.assertTrue(scale.getTranspose() == scale)
self.assertTrue(trans.getDeterminant() == 1)
self.assertTrue(scale.getDeterminant() == 8)
self.assertTrue(trans.getInverse() ==
mx.Matrix44.createTranslation(mx.Vector3(-1, -2, -3)))
# Matrix copy
trans2 = trans.copy()
self.assertTrue(trans2 == trans)
trans2[0, 0] += 1;
self.assertTrue(trans2 != trans)
# Matrix product
prod1 = trans * scale
prod2 = scale * trans
prod3 = trans * 2
prod4 = trans.copy()
prod4 *= scale
self.assertTrue(prod1 == mx.Matrix44(2, 0, 0, 0,
0, 2, 0, 0,
0, 0, 2, 0,
2, 4, 6, 1))
self.assertTrue(prod2 == mx.Matrix44(2, 0, 0, 0,
0, 2, 0, 0,
0, 0, 2, 0,
1, 2, 3, 1))
self.assertTrue(prod3 == mx.Matrix44(2, 0, 0, 0,
0, 2, 0, 0,
0, 0, 2, 0,
2, 4, 6, 2))
self.assertTrue(prod4 == prod1)
# Matrix division
quot1 = prod1 / scale
quot2 = prod2 / trans
quot3 = prod3 / 2
quot4 = quot1.copy()
quot4 /= trans
self.assertTrue(quot1 == trans)
self.assertTrue(quot2 == scale)
self.assertTrue(quot3 == trans)
self.assertTrue(quot4 == mx.Matrix44.IDENTITY)
# Unary operation
self.assertTrue(-trans == mx.Matrix44(-1, 0, 0, 0,
0, -1, 0, 0,
0, 0, -1, 0,
-1, -2, -3, -1))
trans *= -1
self.assertTrue(+trans == mx.Matrix44(-1, 0, 0, 0,
0, -1, 0, 0,
0, 0, -1, 0,
-1, -2, -3, -1))
trans *= -1
# 2D rotation
rot1 = mx.Matrix33.createRotation(math.pi / 2)
rot2 = mx.Matrix33.createRotation(math.pi)
self.assertTrue((rot1 * rot1).isEquivalent(rot2, _epsilon))
self.assertTrue(rot2.isEquivalent(
mx.Matrix33.createScale(mx.Vector2(-1)), _epsilon))
self.assertTrue((rot2 * rot2).isEquivalent(mx.Matrix33.IDENTITY, _epsilon))
# 3D rotation
rotX = mx.Matrix44.createRotationX(math.pi)
rotY = mx.Matrix44.createRotationY(math.pi)
rotZ = mx.Matrix44.createRotationZ(math.pi)
self.assertTrue((rotX * rotY).isEquivalent(
mx.Matrix44.createScale(mx.Vector3(-1, -1, 1)), _epsilon))
self.assertTrue((rotX * rotZ).isEquivalent(
mx.Matrix44.createScale(mx.Vector3(-1, 1, -1)), _epsilon))
self.assertTrue((rotY * rotZ).isEquivalent(
mx.Matrix44.createScale(mx.Vector3(1, -1, -1)), _epsilon))
def test_BuildDocument(self):
# Create a document.
doc = mx.createDocument()
# Create a node graph with constant and image sources.
nodeGraph = doc.addNodeGraph()
self.assertTrue(nodeGraph)
self.assertRaises(LookupError, doc.addNodeGraph, nodeGraph.getName())
constant = nodeGraph.addNode('constant')
image = nodeGraph.addNode('image')
# Connect sources to outputs.
output1 = nodeGraph.addOutput()
output2 = nodeGraph.addOutput()
output1.setConnectedNode(constant)
output2.setConnectedNode(image)
self.assertTrue(output1.getConnectedNode() == constant)
self.assertTrue(output2.getConnectedNode() == image)
self.assertTrue(output1.getUpstreamElement() == constant)
self.assertTrue(output2.getUpstreamElement() == image)
# Set constant node color.
color = mx.Color3(0.1, 0.2, 0.3)
constant.setInputValue('value', color)
self.assertTrue(constant.getInputValue('value') == color)
# Set image node file.
file = 'image1.tif'
image.setInputValue('file', file, 'filename')
self.assertTrue(image.getInputValue('file') == file)
# Create a custom nodedef.
nodeDef = doc.addNodeDef('nodeDef1', 'float', 'turbulence3d')
nodeDef.setInputValue('octaves', 3)
nodeDef.setInputValue('lacunarity', 2.0)
nodeDef.setInputValue('gain', 0.5)
# Reference the custom nodedef.
custom = nodeGraph.addNode('turbulence3d', 'turbulence1', 'float')
self.assertTrue(custom.getInputValue('octaves') == 3)
custom.setInputValue('octaves', 5)
self.assertTrue(custom.getInputValue('octaves') == 5)
# Test scoped attributes.
nodeGraph.setFilePrefix('folder/')
nodeGraph.setColorSpace('lin_rec709')
self.assertTrue(image.getInput('file').getResolvedValueString() == 'folder/image1.tif')
self.assertTrue(constant.getActiveColorSpace() == 'lin_rec709')
# Create a simple shader interface.
simpleSrf = doc.addNodeDef('', 'surfaceshader', 'simpleSrf')
simpleSrf.setInputValue('diffColor', mx.Color3(1.0))
simpleSrf.setInputValue('specColor', mx.Color3(0.0))
roughness = simpleSrf.setInputValue('roughness', 0.25)
self.assertTrue(roughness.getIsUniform() == False)
roughness.setIsUniform(True);
self.assertTrue(roughness.getIsUniform() == True)
# Instantiate shader and material nodes.
shaderNode = doc.addNodeInstance(simpleSrf)
materialNode = doc.addMaterialNode('', shaderNode)
# Bind the diffuse color input to the constant color output.
shaderNode.setConnectedOutput('diffColor', output1)
self.assertTrue(shaderNode.getUpstreamElement() == constant)
# Bind the roughness input to a value.
instanceRoughness = shaderNode.setInputValue('roughness', 0.5)
self.assertTrue(instanceRoughness.getValue() == 0.5)
self.assertTrue(instanceRoughness.getDefaultValue() == 0.25)
# Create a look for the material.
look = doc.addLook()
self.assertTrue(len(doc.getLooks()) == 1)
# Bind the material to a geometry string.
matAssign1 = look.addMaterialAssign("matAssign1", materialNode.getName())
matAssign1.setGeom("/robot1")
self.assertTrue(matAssign1.getReferencedMaterial() == materialNode)
self.assertTrue(len(mx.getGeometryBindings(materialNode, "/robot1")) == 1)
self.assertTrue(len(mx.getGeometryBindings(materialNode, "/robot2")) == 0)
# Bind the material to a collection.
matAssign2 = look.addMaterialAssign("matAssign2", materialNode.getName())
collection = doc.addCollection()
collection.setIncludeGeom("/robot2")
collection.setExcludeGeom("/robot2/left_arm")
matAssign2.setCollection(collection)
self.assertTrue(len(mx.getGeometryBindings(materialNode, "/robot2")) == 1)
self.assertTrue(len(mx.getGeometryBindings(materialNode, "/robot2/right_arm")) == 1)
self.assertTrue(len(mx.getGeometryBindings(materialNode, "/robot2/left_arm")) == 0)
# Create a property assignment.
propertyAssign = look.addPropertyAssign()
propertyAssign.setProperty("twosided")
propertyAssign.setGeom("/robot1")
propertyAssign.setValue(True)
self.assertTrue(propertyAssign.getProperty() == "twosided")
self.assertTrue(propertyAssign.getGeom() == "/robot1")
self.assertTrue(propertyAssign.getValue() == True)
# Create a property set assignment.
propertySet = doc.addPropertySet()
propertySet.setPropertyValue('matte', False)
self.assertTrue(propertySet.getPropertyValue('matte') == False)
propertySetAssign = look.addPropertySetAssign()
propertySetAssign.setPropertySet(propertySet)
propertySetAssign.setGeom('/robot1')
self.assertTrue(propertySetAssign.getPropertySet() == propertySet)
self.assertTrue(propertySetAssign.getGeom() == '/robot1')
# Create a variant set.
variantSet = doc.addVariantSet()
variantSet.addVariant("original")
variantSet.addVariant("damaged")
self.assertTrue(len(variantSet.getVariants()) == 2)
# Validate the document.
valid, message = doc.validate()
self.assertTrue(valid, 'Document returned validation warnings: ' + message)
# Disconnect outputs from sources.
output1.setConnectedNode(None)
output2.setConnectedNode(None)
self.assertTrue(output1.getConnectedNode() == None)
self.assertTrue(output2.getConnectedNode() == None)
def test_TraverseGraph(self):
# Create a document.
doc = mx.createDocument()
# Create a node graph with the following structure:
#
# [image1] [constant] [image2]
# \ / |
# [multiply] [contrast] [noise3d]
# \____________ | ____________/
# [mix]
# |
# [output]
#
nodeGraph = doc.addNodeGraph()
image1 = nodeGraph.addNode('image')
image2 = nodeGraph.addNode('image')
constant = nodeGraph.addNode('constant')
multiply = nodeGraph.addNode('multiply')
contrast = nodeGraph.addNode('contrast')
noise3d = nodeGraph.addNode('noise3d')
mix = nodeGraph.addNode('mix')
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)
# Validate the document.
valid, message = doc.validate()
self.assertTrue(valid, 'Document returned validation warnings: ' + message)
# Traverse the document tree (implicit iterator).
nodeCount = 0
for elem in doc.traverseTree():
if elem.isA(mx.Node):
nodeCount += 1
self.assertTrue(nodeCount == 7)
# Traverse the document tree (explicit iterator).
nodeCount = 0
maxElementDepth = 0
treeIter = doc.traverseTree()
for elem in treeIter:
if elem.isA(mx.Node):
nodeCount += 1
maxElementDepth = max(maxElementDepth, treeIter.getElementDepth())
self.assertTrue(nodeCount == 7)
self.assertTrue(maxElementDepth == 3)
# Traverse the document tree (prune subtree).
nodeCount = 0
treeIter = doc.traverseTree()
for elem in treeIter:
if elem.isA(mx.Node):
nodeCount += 1
if elem.isA(mx.NodeGraph):
treeIter.setPruneSubtree(True)
self.assertTrue(nodeCount == 0)
# Traverse upstream from the graph output (implicit iterator).
nodeCount = 0
for edge in output.traverseGraph():
upstreamElem = edge.getUpstreamElement()
connectingElem = edge.getConnectingElement()
downstreamElem = edge.getDownstreamElement()
if upstreamElem.isA(mx.Node):
nodeCount += 1
if downstreamElem.isA(mx.Node):
self.assertTrue(connectingElem.isA(mx.Input))
self.assertTrue(nodeCount == 7)
# Traverse upstream from the graph output (explicit iterator).
nodeCount = 0
maxElementDepth = 0
maxNodeDepth = 0
graphIter = output.traverseGraph()
for edge in graphIter:
upstreamElem = edge.getUpstreamElement()
connectingElem = edge.getConnectingElement()
downstreamElem = edge.getDownstreamElement()
if upstreamElem.isA(mx.Node):
nodeCount += 1
maxElementDepth = max(maxElementDepth, graphIter.getElementDepth())
maxNodeDepth = max(maxNodeDepth, graphIter.getNodeDepth())
self.assertTrue(nodeCount == 7)
self.assertTrue(maxElementDepth == 3)
self.assertTrue(maxNodeDepth == 3)
# Traverse upstream from the graph output (prune subgraph).
nodeCount = 0
graphIter = output.traverseGraph()
for edge in graphIter:
upstreamElem = edge.getUpstreamElement()
connectingElem = edge.getConnectingElement()
downstreamElem = edge.getDownstreamElement()
if upstreamElem.isA(mx.Node):
nodeCount += 1
if upstreamElem.getCategory() == 'multiply':
graphIter.setPruneSubgraph(True)
self.assertTrue(nodeCount == 5)
# Create and detect a cycle.
multiply.setConnectedNode('in2', mix)
self.assertTrue(output.hasUpstreamCycle())
self.assertFalse(doc.validate()[0])
multiply.setConnectedNode('in2', constant)
self.assertFalse(output.hasUpstreamCycle())
self.assertTrue(doc.validate()[0])
# Create and detect a loop.
contrast.setConnectedNode('in', contrast)
self.assertTrue(output.hasUpstreamCycle())
self.assertFalse(doc.validate()[0])
contrast.setConnectedNode('in', image2)
self.assertFalse(output.hasUpstreamCycle())
self.assertTrue(doc.validate()[0])
def test_Xmlio(self):
# Read the standard library.
libs = []
for filename in _libraryFilenames:
lib = mx.createDocument()
mx.readFromXmlFile(lib, filename, _searchPath)
libs.append(lib)
# Declare write predicate for write filter test
def skipLibraryElement(elem):
return not elem.hasSourceUri()
# Read and validate each example document.
for filename in _exampleFilenames:
doc = mx.createDocument()
mx.readFromXmlFile(doc, filename, _searchPath)
valid, message = doc.validate()
self.assertTrue(valid, filename + ' returned validation warnings: ' + message)
# Copy the document.
copiedDoc = doc.copy()
self.assertTrue(copiedDoc == doc)
copiedDoc.addLook()
self.assertTrue(copiedDoc != doc)
# Traverse the document tree.
valueElementCount = 0
for elem in doc.traverseTree():
if elem.isA(mx.ValueElement):
valueElementCount += 1
self.assertTrue(valueElementCount > 0)
# Serialize to XML.
writeOptions = mx.XmlWriteOptions()
writeOptions.writeXIncludeEnable = False
xmlString = mx.writeToXmlString(doc, writeOptions)
# Verify that the serialized document is identical.
writtenDoc = mx.createDocument()
mx.readFromXmlString(writtenDoc, xmlString)
self.assertTrue(writtenDoc == doc)
# Combine document with the standard library.
doc2 = doc.copy()
for lib in libs:
doc2.importLibrary(lib)
self.assertTrue(doc2.validate()[0])
# Write without definitions
writeOptions.writeXIncludeEnable = False
writeOptions.elementPredicate = skipLibraryElement
result = mx.writeToXmlString(doc2, writeOptions)
doc3 = mx.createDocument()
mx.readFromXmlString(doc3, result)
self.assertTrue(len(doc3.getNodeDefs()) == 0)
# Read the same document twice, and verify that duplicate elements
# are skipped.
doc = mx.createDocument()
filename = 'StandardSurface/standard_surface_carpaint.mtlx'
mx.readFromXmlFile(doc, filename, _searchPath)
mx.readFromXmlFile(doc, filename, _searchPath)
self.assertTrue(doc.validate()[0])
#--------------------------------------------------------------------------------
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,2 @@
@echo off
python tests_to_html.py -i1 ../../build %* -d

View File

@@ -0,0 +1,230 @@
#!/usr/bin/python
import sys
import os
import datetime
import argparse
try:
# Install pillow via pip to enable image differencing and statistics.
from PIL import Image, ImageChops, ImageStat
DIFF_ENABLED = True
except Exception:
DIFF_ENABLED = False
def computeDiff(image1Path, image2Path, imageDiffPath):
try:
if os.path.exists(imageDiffPath):
os.remove(imageDiffPath)
if not os.path.exists(image1Path):
print ("Image diff input missing: " + image1Path)
return
if not os.path.exists(image2Path):
print ("Image diff input missing: " + image2Path)
return
image1 = Image.open(image1Path).convert('RGB')
image2 = Image.open(image2Path).convert('RGB')
diff = ImageChops.difference(image1, image2)
diff.save(imageDiffPath)
diffStat = ImageStat.Stat(diff)
return sum(diffStat.rms) / (3.0 * 255.0)
except Exception:
if os.path.exists(imageDiffPath):
os.remove(imageDiffPath)
print ("Failed to create image diff between: " + image1Path + ", " + image2Path)
def main(args=None):
parser = argparse.ArgumentParser()
parser.add_argument('-i1', '--inputdir1', dest='inputdir1', action='store', help='Input directory', default=".")
parser.add_argument('-i2', '--inputdir2', dest='inputdir2', action='store', help='Second input directory', default="")
parser.add_argument('-i3', '--inputdir3', dest='inputdir3', action='store', help='Third input directory', default="")
parser.add_argument('-o', '--outputfile', dest='outputfile', action='store', help='Output file name', default="tests.html")
parser.add_argument('-d', '--diff', dest='CREATE_DIFF', action='store_true', help='Perform image diff', default=False)
parser.add_argument('-t', '--timestamp', dest='ENABLE_TIMESTAMPS', action='store_true', help='Write image timestamps', default=False)
parser.add_argument('-w', '--imagewidth', type=int, dest='imagewidth', action='store', help='Set image display width', default=256)
parser.add_argument('-ht', '--imageheight', type=int, dest='imageheight', action='store', help='Set image display height', default=256)
parser.add_argument('-cp', '--cellpadding', type=int, dest='cellpadding', action='store', help='Set table cell padding', default=0)
parser.add_argument('-tb', '--tableborder', type=int, dest='tableborder', action='store', help='Table border width. 0 means no border', default=3)
parser.add_argument('-l1', '--lang1', dest='lang1', action='store', help='First target language for comparison. Default is glsl', default="glsl")
parser.add_argument('-l2', '--lang2', dest='lang2', action='store', help='Second target language for comparison. Default is osl', default="osl")
parser.add_argument('-l3', '--lang3', dest='lang3', action='store', help='Third target language for comparison. Default is empty', default="")
parser.add_argument('-e', '--error', dest='error', action='store', help='Filter out results with RMS less than this. Negative means all results are kept.', default=-1, type=float)
args = parser.parse_args(args)
fh = open(args.outputfile,"w+")
fh.write("<html>\n")
fh.write("<style>\n")
fh.write("td {")
fh.write(" padding: " + str(args.cellpadding) + ";")
fh.write(" border: " + str(args.tableborder) + "px solid black;")
fh.write("}")
fh.write("table, tbody, th, .td_image {")
fh.write(" border-collapse: collapse;")
fh.write(" padding: 0;")
fh.write(" margin: 0;")
fh.write("}")
fh.write("</style>")
fh.write("<body>\n")
if args.inputdir1 == ".":
args.inputdir1 = os.getcwd()
if args.inputdir2 == ".":
args.inputdir2 = os.getcwd()
elif args.inputdir2 == "":
args.inputdir2 = args.inputdir1
if args.inputdir3 == ".":
args.inputdir3 = os.getcwd()
elif args.inputdir3 == "":
args.inputdir3 = args.inputdir1
useThirdLang = args.lang3
if useThirdLang:
fh.write("<h3>" + args.lang1 + " (in: " + args.inputdir1 + ") vs "+ args.lang2 + " (in: " + args.inputdir2 + ") vs "+ args.lang3 + " (in: " + args.inputdir3 + ")</h3>\n")
else:
fh.write("<h3>" + args.lang1 + " (in: " + args.inputdir1 + ") vs "+ args.lang2 + " (in: " + args.inputdir2 + ")</h3>\n")
if not DIFF_ENABLED and args.CREATE_DIFF:
print("--diff argument ignored. Diff utility not installed.")
# Remove potential trailing path separators
if args.inputdir1[-1:] == '/' or args.inputdir1[-1:] == '\\':
args.inputdir1 = args.inputdir1[:-1]
if args.inputdir2[-1:] == '/' or args.inputdir2[-1:] == '\\':
args.inputdir2 = args.inputdir2[:-1]
if args.inputdir3[-1:] == '/' or args.inputdir3[-1:] == '\\':
args.inputdir3 = args.inputdir3[:-1]
# Get all source files
langFiles1 = []
langPaths1 = []
for subdir, _, files in os.walk(args.inputdir1):
for curFile in files:
if curFile.endswith(args.lang1 + ".png"):
langFiles1.append(curFile)
langPaths1.append(subdir)
# Get all destination files, matching source files
langFiles2 = []
langPaths2 = []
langFiles3 = []
langPaths3 = []
preFixLen: int = len(args.inputdir1) + 1 # including the path separator
postFix: str = args.lang1 + ".png"
for file1, path1 in zip(langFiles1, langPaths1):
# Allow for just one language to be shown if source and dest are the same.
# Otherwise add in equivalent name with dest language replacement if
# pointing to the same directory
if args.inputdir1 != args.inputdir2 or args.lang1 != args.lang2:
file2 = file1[:-len(postFix)] + args.lang2 + ".png"
path2 = os.path.join(args.inputdir2, path1[len(args.inputdir1)+1:])
else:
file2 = ""
path2 = None
langFiles2.append(file2)
langPaths2.append(path2)
if useThirdLang:
file3 = file1[:-len(postFix)] + args.lang3 + ".png"
path3 = os.path.join(args.inputdir2, path1[len(args.inputdir1)+1:])
else:
file3 = ""
path3 = None
langFiles3.append(file3)
langPaths3.append(path3)
if langFiles1:
curPath = ""
for file1, file2, file3, path1, path2, path3 in zip(langFiles1, langFiles2, langFiles3, langPaths1, langPaths2, langPaths3):
fullPath1 = os.path.join(path1, file1) if file1 else None
fullPath2 = os.path.join(path2, file2) if file2 else None
fullPath3 = os.path.join(path3, file3) if file3 else None
diffPath1 = diffPath2 = diffPath3 = None
diffRms1 = diffRms2 = diffRms3 = None
if file1 and file2 and DIFF_ENABLED and args.CREATE_DIFF:
diffPath1 = fullPath1[0:-8] + "_" + args.lang1 + "-1_vs_" + args.lang2 + "-2_diff.png"
diffRms1 = computeDiff(fullPath1, fullPath2, diffPath1)
if useThirdLang and file1 and file3 and DIFF_ENABLED and args.CREATE_DIFF:
diffPath2 = fullPath1[0:-8] + "_" + args.lang1 + "-1_vs_" + args.lang3 + "-3_diff.png"
diffRms2 = computeDiff(fullPath1, fullPath3, diffPath2)
diffPath3 = fullPath1[0:-8] + "_" + args.lang2 + "-2_vs_" + args.lang3 + "-3_diff.png"
diffRms3 = computeDiff(fullPath2, fullPath3, diffPath3)
if args.error >= 0:
ok1 = (not diffPath1) or (not diffRms1) or (diffRms1 and diffRms1 <= args.error)
ok2 = (not diffPath2) or (not diffRms2) or (diffRms2 and diffRms2 <= args.error)
ok3 = (not diffPath3) or (not diffRms3) or (diffRms3 and diffRms3 <= args.error)
if ok1 and ok2 and ok3:
continue
if curPath != path1:
if curPath != "":
fh.write("</table>\n")
fh.write("<p>" + os.path.normpath(path1) + ":</p>\n")
fh.write("<table>\n")
curPath = path1
def prependFileUri(filepath: str) -> str:
if os.path.isabs(filepath):
return 'file:///' + filepath
else:
return filepath
fh.write("<tr>\n")
if fullPath1:
fh.write("<td class='td_image'><img src='" + prependFileUri(fullPath1) + "' height='" + str(args.imageheight) + "' width='" + str(args.imagewidth) + "' loading='lazy' style='background-color:black;'/></td>\n")
if fullPath2:
fh.write("<td class='td_image'><img src='" + prependFileUri(fullPath2) + "' height='" + str(args.imageheight) + "' width='" + str(args.imagewidth) + "' loading='lazy' style='background-color:black;'/></td>\n")
if fullPath3:
fh.write("<td class='td_image'><img src='" + prependFileUri(fullPath3) + "' height='" + str(args.imageheight) + "' width='" + str(args.imagewidth) + "' loading='lazy' style='background-color:black;'/></td>\n")
if diffPath1:
fh.write("<td class='td_image'><img src='" + prependFileUri(diffPath1) + "' height='" + str(args.imageheight) + "' width='" + str(args.imagewidth) + "' loading='lazy' style='background-color:black;'/></td>\n")
if diffPath2:
fh.write("<td class='td_image'><img src='" + prependFileUri(diffPath2) + "' height='" + str(args.imageheight) + "' width='" + str(args.imagewidth) + "' loading='lazy' style='background-color:black;'/></td>\n")
if diffPath3:
fh.write("<td class='td_image'><img src='" + prependFileUri(diffPath3) + "' height='" + str(args.imageheight) + "' width='" + str(args.imagewidth) + "' loading='lazy' style='background-color:black;'/></td>\n")
fh.write("</tr>\n")
fh.write("<tr>\n")
if fullPath1:
fh.write("<td align='center'>" + file1)
if args.ENABLE_TIMESTAMPS and os.path.isfile(fullPath1):
fh.write("<br>(" + str(datetime.datetime.fromtimestamp(os.path.getmtime(fullPath1))) + ")")
fh.write("</td>\n")
if fullPath2:
fh.write("<td align='center'>" + file2)
if args.ENABLE_TIMESTAMPS and os.path.isfile(fullPath2):
fh.write("<br>(" + str(datetime.datetime.fromtimestamp(os.path.getmtime(fullPath2))) + ")")
fh.write("</td>\n")
if fullPath3:
fh.write("<td align='center'>" + file3)
if args.ENABLE_TIMESTAMPS and os.path.isfile(fullPath3):
fh.write("<br>(" + str(datetime.datetime.fromtimestamp(os.path.getmtime(fullPath3))) + ")")
fh.write("</td>\n")
if diffPath1:
rms = " (RMS " + "%.5f" % diffRms1 + ")" if diffRms1 else ""
fh.write("<td align='center'>" + args.lang1.upper() + " vs. " + args.lang2.upper() + rms + "</td>\n")
if diffPath2:
rms = " (RMS " + "%.5f" % diffRms2 + ")" if diffRms2 else ""
fh.write("<td align='center'>" + args.lang1.upper() + " vs. " + args.lang3.upper() + rms + "</td>\n")
if diffPath3:
rms = " (RMS " + "%.5f" % diffRms3 + ")" if diffRms3 else ""
fh.write("<td align='center'>" + args.lang2.upper() + " vs. " + args.lang3.upper() + rms + "</td>\n")
fh.write("</tr>\n")
fh.write("</table>\n")
fh.write("</body>\n")
fh.write("</html>\n")
if __name__ == "__main__":
main(sys.argv[1:])