Files
UnrealEngine/Engine/Source/ThirdParty/MaterialX/MaterialX-1.38.10/python/MaterialXTest/main.py
2025-05-18 13:04:45 +08:00

499 lines
20 KiB
Python

#!/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)
# 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 product
prod1 = trans * scale
prod2 = scale * trans
prod3 = trans * 2
prod4 = trans
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
quot4 /= trans
self.assertTrue(quot1 == trans)
self.assertTrue(quot2 == scale)
self.assertTrue(quot3 == trans)
self.assertTrue(quot4 == mx.Matrix44.IDENTITY)
# 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))
# Matrix copy
trans2 = trans.copy()
self.assertTrue(trans2 == trans)
trans2[0, 0] += 1;
self.assertTrue(trans2 != trans)
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()