This book will examine how to set up for rendering with MaterialX. It is not about how to write a renderer.
The topics covered include:
renderable
items.roots
and handling "transparency".This book requires a MaterialX build or release which contains the following fixes for code generation. PR which is part of the 1.38.9 release.
Some example results are shown below to show: lit vs unlit, texture resource usage. (more to come)
Execution Note: The notebook can cause some loss to the current context for the renderer resulting in bad state. If this occurs then the notebook can be restarted, or the Python file can be run from the command line. In general, a renderer would not inject Python queries and Markdown code intermixed with rendering as is the case for this book.
import MaterialX as mx
stdlib = mx.createDocument()
searchPath = mx.getDefaultDataSearchPath()
#searchPath.append(os.path.dirname(inputFilename))
libraryFolders = mx.getDefaultDataLibraryFolders()
try:
libFiles = mx.loadLibraries(libraryFolders, searchPath, stdlib)
except mx.Exception as err:
print('Failed to load standard library definitions: "', err, '"')
if libFiles:
doc = mx.createDocument()
doc.importLibrary(stdlib)
print('Loaded %s standard library definitions' % len(doc.getNodeDefs()))
Loaded 750 standard library definitions
For code generation MaterialxGenShader
and any per target generation modules are loaded.
In this example we load in the GLSL code generator. Note that we use the mtlxutils
utility
logic found in mxshadergen
to handle code generation so that the GLSL generation module
is not directly used.
For rendering an example GLSL renderer (PyMaterialXRenderGlsl) is loaded. This is used for MaterialX Viewer and Node Editor
as well as render test suite unit testing. This makes use of the base rendering module (MaterialXRender
) which provides access to utilities such as geometry and image loaders as well as higher level utilities such as texture baking.
Some additional utilities are added for display (IPython
) and module discovery (inspect
)
import MaterialX.PyMaterialXGenShader as mx_gen_shader
import MaterialX.PyMaterialXGenGlsl as mx_gen_glsl
import MaterialX.PyMaterialXRender as mx_render
import MaterialX.PyMaterialXRenderGlsl as mx_render_glsl
from mtlxutils import mxshadergen
import inspect, sys
from IPython.display import display_markdown
A class called GlslRenderer
is added to encapsulate the logic required to set up the GLSL example renderer,
set up resource handlers and a source code generator, create executable shader programs, and run the render pipeline.
A "TODO" has been added as a comment for C++ apis which are missing Python API wrappers.
The main methods of interest are:
initialize()
which calls the GLSL example renderer to initialize a device and framebuffer. Image and geometry handlers are also initialized.
initializeImageHandler
which initialize image handlers and
loaders such as the built in STB image loader. If built the Open Image IO (OIIO)
loader (OiioImageLoader) can also be instantiated and used. Note that as this is a hardware renderer a specific GLSL handler is instantiated which allows for hardware texture resource management. The handler used by the renderer is set using setImageHandler()
.
initializeGeometryHandler
which uses a geometry handler to setup geometry loaders. By default an 'obj' file loader is created.A GLTF loader is available in C++, but at time of writing has no Python wrapper. The loader will be used to
load in geometry for rendering. Note that the loaders will automatically create tangent and bitangents. This
is important to note as all shading models (except for unlit surface
) require these geometric streams.
loadGeometry()
calls into actual geometry loaders to load geometry files.
initializeLights
which is used to set up a light handler which handles setting up directional lights specified in a MaterialX file as well as set up indirect lighting by specifying environment lighting files. These files are loaded in using the ImageLoader.acquireImage()
interface.
setupGenerator
which sets up the shader code generator for the desired target (genglsl
). Note that the
shader generation utility keeps GenContext for reuse. It is important to "register" where source code stored in files can be found by calling registerSourceCodeSearchPath() on the context. Generally this would be to the root of where the definition libraries are found but could also be elsewhere. The interface appends additional paths to search.
generateShader
sets up some basic generation options such as whether to use lighting or not and a hint if the shader is transparent. The utility methods: elementRequiresShading() and isTransparentSurface() perform this introspection respectively. They are both found as utilities within the MaterialXGenShader
module.Note that an application integration should provide the following additional information for the renderer's working color space as as well as the geometry scene real-world units. This is because only an integration can provide this information.
See the reference Glossary for units and color space transform information.
createProgram
is used to create a GLSL program from a Shader which is created via code generation. This is just one example of source code usage.
render
is used to render a frame. As the example renderer's pipline is a limited one used for unit testing, it
will perform all of the required program setup and input bindings based on inspecting the program itself, making used of the specified image, geometry and light handlers to (in this case) set up hardware resources for binding.
Thought not strictly necessary, it is useful to check for available unit and color management support.
buildUnitDict()
will scan for available unit types and unit identifiers. For example distance
units are supported with unit identifiers such as meter
, inch
, and foot
conversions being supported.buildColorTransformDict()
will scan for available colorspace
transforms. Note that only transforms to a (target) linear color space (lin_rec709
) is currently supported.def buildUnitDict(doc):
'''
Sample code to examine unit types and unit name information
'''
unitdict = {}
for ud in doc.getUnitDefs():
unittype = ud.getAttribute('unittype')
unitinfo = {}
for unit in ud.getChildren():
unitinfo[unit.getName()] = unit.getAttribute('scale')
unitdict[unittype] = unitinfo
return unitdict
def buildColorTransformDict(doc):
colordict = {}
targetdict = {}
for cmnode in doc.getNodeDefs():
if cmnode.getNodeGroup() == 'colortransform':
name = cmnode.getName()
name = name.removeprefix('ND_')
namesplit = name.split('_to_')
type = 'color3'
if 'color4' in namesplit[1]:
continue
else:
namesplit[1] = namesplit[1].removesuffix('_color3')
sourceSpace = namesplit[0]
targetSpace = namesplit[1]
if sourceSpace in colordict:
sourceItem = colordict[sourceSpace]
sourceItem.append(targetSpace)
else:
colordict[sourceSpace] = [targetSpace]
if targetSpace in targetdict:
taregetItem = targetdict[targetSpace]
taregetItem.append(sourceSpace)
else:
targetdict[targetSpace] = [sourceSpace]
return colordict, targetdict
# Build unit dictionary
unitdict = buildUnitDict(doc)
for unittype in unitdict:
print('Unit Type: %s' % unittype)
units = unitdict[unittype]
for unit in units:
print(' Unit: %s. Scale Factor: %s' % (unit, units[unit]))
print('')
# Build colorspace dictionary
stdict, tsdict = buildColorTransformDict(doc)
print('Supported Source to Target Transforms:')
for sourceSpace in stdict:
print(' %s --> %s supported' % (sourceSpace, ', '.join(stdict[sourceSpace])))
print('Supported Target From Source Transforms:')
for targetSpace in tsdict:
print(' %s <-- %s supported' % (targetSpace, ', '.join(tsdict[targetSpace])))
Unit Type: distance Unit: nanometer. Scale Factor: 0.000000001 Unit: micron. Scale Factor: 0.000001 Unit: millimeter. Scale Factor: 0.001 Unit: centimeter. Scale Factor: 0.01 Unit: inch. Scale Factor: 0.0254 Unit: foot. Scale Factor: 0.3048 Unit: yard. Scale Factor: 0.9144 Unit: meter. Scale Factor: 1.0 Unit: kilometer. Scale Factor: 1000.0 Unit: mile. Scale Factor: 1609.344 Unit Type: angle Unit: degree. Scale Factor: 1.0 Unit: radian. Scale Factor: 57.295779513 Supported Source to Target Transforms: g18_rec709 --> lin_rec709 supported g22_rec709 --> lin_rec709 supported rec709_display --> lin_rec709 supported acescg --> lin_rec709 supported g22_ap1 --> lin_rec709 supported srgb_texture --> lin_rec709 supported lin_adobergb --> lin_rec709 supported adobergb --> lin_rec709 supported srgb_displayp3 --> lin_rec709 supported lin_displayp3 --> lin_rec709 supported Supported Target From Source Transforms: lin_rec709 <-- g18_rec709, g22_rec709, rec709_display, acescg, g22_ap1, srgb_texture, lin_adobergb, adobergb, srgb_displayp3, lin_displayp3 supported
class GlslRenderer():
'''
Wrapper for GLSL sample renderer.
Handles setup of image, geometry and light handlers as well as GLSL code and
program generation.
Calls into sample renderer to render and capture images as desired.
'''
def __init__(self):
# Renderer
self.renderSize = [512, 512]
self.renderer = None
# Code Generator
self.mxgen = None
self.activeShader = None
self.activeShaderErrors = ''
self.sourceCode = {}
# Image Handling
self.capturedImage = None
self.haveOIIOImageHandler = False
mxrenderMembers = inspect.getmembers(sys.modules['MaterialX.PyMaterialXRender'])
for className, classObject in mxrenderMembers:
if className == 'OiioImageLoader' and inspect.isclass(classObject):
self.haveOIIOImageHandler = True
break
# Geometry loading
self.haveCGLTFLoader = False
# Note: TODO: Test for existence of GLTF loader in Python module. This does not exist in a release currently.
for className, classObject in mxrenderMembers:
if className == 'CgltfLoader' and inspect.isclass(classObject):
self.haveCGLTFLoader = True
break
# Light setup
self.lightHandler = None
# Units dictionary
self.unitDict = None
# Colorspace dictionaries
self.sourceColorDict = None
self.targetColorDict = None
def getRenderer(self):
return self.renderer
def getDefaultRenderSize(self):
return self.renderSize
def getCodeGenerator(self):
return self.mxgen
def getActiveShader(self):
return self.activeShader
def getActiveShaderErrors(self):
return self.activeShaderErrors
def getSourceCode(self):
return self.sourceCode
def haveGLTFLoader(self):
return self.haveCGLTFLoader
def haveOIIOLoader(self):
return self.haveOIIOImageHandler
def getLightHandler(self):
return self.lightHandler
def initialize(self, w=0, h=0, bufferFormat=mx_render.BaseType.UINT8):
'''
Setup sample renderer with a given frame buffer size.
Initialize image and geometry handlers.
'''
if w == 0 and h == 0:
w = self.renderSize[0]
h = self.renderSize[1]
if w < 4:
w = 4
if h < 4:
h = 4
self.renderer = mx_render_glsl.GlslRenderer.create(w, h, bufferFormat)
if self.renderer:
self.renderer.initialize()
self.initializeImageHandler()
self.initializeGeometryHandler()
def resize(self, w, h):
'''
Resize frame buffer.
Clears any cached captured image.
'''
if not self.renderer:
return False
self.renderer.setSize(w, h)
self.capturedImage = None
def initializeImageHandler(self):
'''
Initialize image handler.
'''
if self.renderer.getImageHandler():
return
# TODO: Missing fom the Python API for createImageHandler()
#imageHandler = renderer.createImageHandler()
imageLoader = mx_render.StbImageLoader.create()
imageHandler = mx_render_glsl.GLTextureHandler.create(imageLoader)
# Add OIIO handler if it exists
if self.haveOIIOImageHandler:
imageHandler.addLoader(mx_render.OIIOHandler.create())
if imageHandler:
imageSearchPath = mx.FileSearchPath()
imageSearchPath.append(mx.FilePath('./data'))
imageHandler.setSearchPath(imageSearchPath)
self.renderer.setImageHandler(imageHandler)
def initializeGeometryHandler(self):
# renderer has a geometry handler created by
# default so not need to call: mx_render.GeometryHandler.create()
geometryHandler = self.renderer.getGeometryHandler()
# TODO: Currently missing gltf loader from Python API
if self.haveCGLTFLoader:
gltfLoader = mx_render.CgltfLoader.create()
geometryHandler.addLoader(gltfLoader)
def loadGeometry(self, fileName):
geometryHandler = self.renderer.getGeometryHandler()
if geometryHandler:
texcoordVerticalFlip = True
if not geometryHandler.hasGeometry(fileName):
geometryHandler.loadGeometry(fileName, texcoordVerticalFlip)
def getGeometyHandler(self):
return self.renderer.getGeometryHandler()
def initializeLights(self, doc, enableDirectLighting, radianceIBLPath, irradianceIBLPath, enableReferenceQuality):
if self.lightHandler:
return
# Ensure image handler is initialized
self.initializeImageHandler()
# Create a light handler
self.lightHandler = mx_render.LightHandler.create()
# Scan for lights
if enableDirectLighting:
lights = []
self.lightHandler.findLights(doc, lights)
mxcontext = self.mxgen.getContext()
self.lightHandler.registerLights(doc, lights, mxcontext)
# Set the list of lights on the with the generator
self.lightHandler.setLightSources(lights)
# Load environment lights.
imageHandler = self.renderer.getImageHandler()
envRadiance = imageHandler.acquireImage(radianceIBLPath)
envIrradiance = imageHandler.acquireImage(irradianceIBLPath)
# Apply light settings for render tests.
self.lightHandler.setEnvRadianceMap(envRadiance)
self.lightHandler.setEnvIrradianceMap(envIrradiance)
self.lightHandler.setEnvSampleCount(4096 if enableReferenceQuality else 1024)
# TODO: Python API missing
#self.lightHandler.setRefractionTwoSided(True)
def captureImage(self):
'''
Capture the framebuffer contents to an image
'''
self.capturedImage = self.renderer.captureImage(self.capturedImage)
def clearCaptureImage(self):
'''
Clear out any captured image
'''
self.captureImage = None
def saveCapture(self, filePath, verticalFlip=True):
'''
Save captured image to a file.
Vertical flip image as needed.
'''
if not self.capturedImage:
self.captureImage()
imageHandler = self.renderer.getImageHandler()
if imageHandler:
imageHandler.saveImage(filePath, self.capturedImage, verticalFlip)
def getImageHandler(self):
return self.renderer.getImageHandler()
def getCapturedImage(self):
return self.capturedImage
def setupGenerator(self, doc, stdlib, searchPath):
'''
Setup code generation. Returns the generator instantiated.
Note: It is important to set up the source code path so that
file implementations can be found.
'''
self.mxgen = mxshadergen.MtlxShaderGen(stdlib)
self.mxgen.setup()
# Check generator and generator options
mxgenerator = None
mxcontext = self.mxgen.setGeneratorForTarget('genglsl')
if mxcontext:
mxgenerator = mxcontext.getShaderGenerator()
# Set source code path
self.mxgen.registerSourceCodeSearchPath(searchPath)
return mxgenerator
def findRenderableElements(self, doc):
# Generate shader for a given node
self.nodes = self.mxgen.findRenderableElements(doc)
return self.nodes
def buildUnitDict(self, doc):
'''
Create real-world units dictionary for target unit checking
'''
if self.unitDict:
return
self.unitDict = {}
for ud in doc.getUnitDefs():
unittype = ud.getAttribute('unittype')
unitinfo = {}
for unit in ud.getChildren():
unitinfo[unit.getName()] = unit.getAttribute('scale')
self.unitDict[unittype] = unitinfo
return self.unitDict
def buildColorTransformDict(self,doc):
'''
Build a pair of dictionaries to test for supported colorspace transforms.
One is from source color space to target, and the other is to a target from source.
'''
if self.sourceColorDict:
return
colordict = {}
targetdict = {}
for cmnode in doc.getNodeDefs():
if cmnode.getNodeGroup() == 'colortransform':
name = cmnode.getName()
name = name.removeprefix('ND_')
namesplit = name.split('_to_')
type = 'color3'
if 'color4' in namesplit[1]:
continue
else:
namesplit[1] = namesplit[1].removesuffix('_color3')
sourceSpace = namesplit[0]
targetSpace = namesplit[1]
if sourceSpace in colordict:
sourceItem = colordict[sourceSpace]
sourceItem.append(targetSpace)
else:
colordict[sourceSpace] = [targetSpace]
if targetSpace in targetdict:
taregetItem = targetdict[targetSpace]
taregetItem.append(sourceSpace)
else:
targetdict[targetSpace] = [sourceSpace]
self.sourceColorDict = colordict
self.targetColorDict = targetdict
return colordict, targetdict
def getColorTransformDict(self):
return self.sourceColorDict, self.targetColorDict
def generateShader(self, node, targetColorSpaceOverride='lin_rec709', targetDistanceUnit='meter'):
'''
Generate new GLSL shader.
- Inspects node to check if it requires lighting and / or is transparent.
- Sets target colorspace and real-world units
- Generates code and caches it
- Caches the "active" Shader node
'''
self.activeShader = None
if not node:
return None
# Set up generation options.
# Detect requirement for shading and transparency.
mxcontext = self.mxgen.getContext()
mxoptions = mxcontext.getOptions()
mxgenerator = mxcontext.getShaderGenerator()
if not mx_gen_shader.elementRequiresShading(node):
mxoptions.hwMaxActiveLightSources = 0
else:
mxoptions.hwMaxActiveLightSources = 0
mxoptions.hwTransparency = mx_gen_shader.isTransparentSurface(node, mxgenerator.getTarget())
# Check support of units and working color space
doc = node.getDocument()
if doc:
self.buildUnitDict(doc)
units = self.unitDict['distance']
if targetDistanceUnit not in units:
targetDistanceUnit = 'meter'
sdict, tdict = self.buildColorTransformDict(doc)
if tdict:
if targetColorSpaceOverride not in tdict:
targetColorSpaceOverride = 'lin_rec709'
else:
targetDistanceUnit = 'meter'
targetColorSpaceOverride = 'lin_rec709'
mxoptions.targetDistanceUnit = targetDistanceUnit
mxoptions.targetColorSpaceOverride = targetColorSpaceOverride
self.activeShader, self.activeShaderErrors = self.mxgen.generateShader(node)
if self.activeShader:
self.sourceCode[mx_gen_shader.VERTEX_STAGE] = self.activeShader.getSourceCode(mx_gen_shader.VERTEX_STAGE)
self.sourceCode[mx_gen_shader.PIXEL_STAGE] = self.activeShader.getSourceCode(mx_gen_shader.PIXEL_STAGE)
return self.activeShader
def createProgram(self):
'''
Create a GLSL program from the active shader node and validates it's inputs.
Note: A light handler **must** be set to for validation to work properly.
'''
if not self.activeShader:
return False
self.renderer.setLightHandler(self.lightHandler)
self.renderer.createProgram(self.activeShader)
#self.renderer.validateInputs()
program = self.renderer.getProgram()
if program:
return True
else:
return False
def getProgram(self):
if self.renderer:
return self.renderer.getProgram()
def render(self):
'''
Render a frame.
- Note: LookupError's are returned if any failure occurs.
- Status and and any errors are returned.
'''
if not self.renderer:
return False, 'No renderer'
# Render
try:
self.renderer.render()
except LookupError as err:
return False, err
return True, ''
This utility class can now be used for rendering with specified output frame parameters.
glslRenderer = GlslRenderer()
renderSize = glslRenderer.getDefaultRenderSize()
glslRenderer.initialize(renderSize[0], renderSize[1], mx_render.BaseType.UINT8)
print('Initialized renderer')
print('- Have OIIO loader support: %s' % glslRenderer.haveOIIOLoader())
print('- Have GLTF loader support: %s' % glslRenderer.haveGLTFLoader())
# This is not exposed
#clearColor = mx.Color3(1.0, 1.0, 1.0)
#glslRenderer.setScreenColor(clearColor)
Initialized renderer - Have OIIO loader support: False - Have GLTF loader support: True
In the sample code we set up to:
geometryHandler = glslRenderer.getGeometyHandler()
if geometryHandler:
print('- Initialized geometry loader:')
desiredGeometry = 'sphere'
geometryFile = './data/sphere.obj'
if desiredGeometry == 'shaderball':
if glslRenderer.haveGLTFLoader():
geometryFile = './data/shaderball.glb'
glslRenderer.loadGeometry(geometryFile)
for mesh in geometryHandler.getMeshes():
print(' - Loaded Mesh: "%s"' % mesh.getName())
- Initialized geometry loader: - Loaded Mesh: ".\data\sphere.obj"
inputFilename = './data/standard_surface_marble_solid.mtlx'
inputFilename = './data/unlit_marble_solid.mtlx'
inputFilename = './data/unlit_image.mtlx'
try:
mx.readFromXmlFile(doc, inputFilename)
valid, msg = doc.validate()
if not valid:
raise mx.Exception('Document is invalid')
print('Read in valid file "'"%s"'" for rendering.' % inputFilename)
except mx.ExceptionFileMissing as err:
print('File %s could not be loaded: "' % inputFilename, err, '"')
except mx.Exception as err:
print('File %s fail to load properly: "' % inputFilename, err, '"')
Read in valid file "./data/unlit_image.mtlx" for rendering.
the lighting is required. This could be in the working document or a separately loaded in document. Here only indirect lighting is setup.
glslRenderer.initializeLights(None, False,
'./data/lights/san_giuseppe_bridge.hdr',
'./data/lights/irradiance/san_giuseppe_bridge.hdr',
False)
lightHandler = glslRenderer.getLightHandler()
if lightHandler:
print('Setup lighting:')
radMap = lightHandler.getEnvRadianceMap()
irradMap = lightHandler.getEnvIrradianceMap()
print('- Loaded radiance map: %d x %d' % (radMap.getWidth(), radMap.getHeight()))
print('- Loaded irradiance map: %d x %d' % (irradMap.getWidth(), irradMap.getHeight()))
Setup lighting: - Loaded radiance map: 2048 x 1024 - Loaded irradiance map: 256 x 128
GLSL
. This requires a working document to initializebased on the working document and a definition document (which may be the same). Additionally
a source code search path needs to specified. The default libraries
path is used as the
search path. If source code resides elsewhere then the search path can be extended as needed
but at a minimum the libraries
path must be included to use the standard definition library.
sourceCodeSearchPath = searchPath
glslRenderer.setupGenerator(doc, stdlib, sourceCodeSearchPath)
context = glslRenderer.getCodeGenerator().getContext()
if context:
generator = context.getShaderGenerator()
if generator:
print('- Iniitialize generator for target: %s.\n - Source path: %s' %
(generator.getTarget(), sourceCodeSearchPath.asString()))
- Iniitialize generator for target: genglsl. - Source path: c:\Users\home\AppData\Local\Programs\Python\Python310\lib\site-packages\MaterialX
Shader
# Set up additional options for generation
context = glslRenderer.getCodeGenerator().getContext()
genOptions = context.getOptions()
genOptions.emitColorTransforms = True # This is True by default
genOptions.fileTextureVerticalFlip = True
# TODO: This and a number of other options are not been exposed in the Python API
#genOptions.addUpstreamDependencies = True
# Find a renderable and generate the shader for it
nodes = glslRenderer.findRenderableElements(doc)
shader = None
printSource = True
# Set up overrides for color space and units. Color space may come from the document,
# but units are a property of the application.
targetColorSpaceOverride = 'lin_rec709'
docColorSpace = doc.getColorSpace()
targetDistanceUnit = 'centimeter'
if nodes:
shader = glslRenderer.generateShader(nodes[0], targetColorSpaceOverride, targetDistanceUnit)
if shader:
print('Generate shader for node: "%s"\n- Is Transparent: %s. V-Flip textures: %d.\n- Emit Color Xforms: %d. Default input colorspace: "%s".\n- Target Color space: "%s". Scene Units: "%s"' %
(nodes[0].getNamePath(),
genOptions.hwTransparency,
genOptions.fileTextureVerticalFlip,
genOptions.emitColorTransforms,
docColorSpace,
genOptions.targetColorSpaceOverride,
genOptions.targetDistanceUnit))
Generate shader for node: "unlit_surfacematerial" - Is Transparent: False. V-Flip textures: 1. - Emit Color Xforms: 1. Default input colorspace: "lin_rec709". - Target Color space: "lin_rec709". Scene Units: "centimeter"
pixel
shader -- and thus one stage.defaults code generators only emit a vertex
and pixel
stage.
getVariable()
interfacefrom the original value stored on a node Input. For example tokens may be resolved on geometric attribute and filenames.
It makes use of the flattenFilenames() utility before performing additional resolves
for Omniverse
compatibility
getPath()
, and thencalling Document.getDescendent()
with the path as the interface argument. An Input
will be returned if found.
getPortPath()
shows this logic.In the sample function debugStages()
, each stage is iterated over. For each stage the list of uniform blocks is extracted.
Then for each block the list of shader ports is printed out. Note that "private" vertex stage uniforms involve things like model / view transforms, there are private and pixel stage uniforms as well as "light data" uniforms for environment map binding.
def getPortPath(inputPath, doc):
'''
Find any upstream interface input which maps to a given path
'''
if not inputPath:
return inputPath, None
input = doc.getDescendant(inputPath)
if input:
# Redirect to interface input if it exists.
# TODO: This should be done during shader generation !
interfaceInput = input.getInterfaceInput()
if interfaceInput:
input = interfaceInput
return input.getNamePath(), interfaceInput
return inputPath, None
def debugStages(shader, doc, filter='Public'):
'''
Scan through each stage of a shader and get the uniform blocks for each stage.
For each block, print out list of assocaited ports.
'''
if not shader:
return
for i in range(0, shader.numStages()):
stage = shader.getStage(i)
if stage:
print('Stage name: "%s"' % stage.getName())
print('-' * 30)
if stage.getName():
for blockName in stage.getUniformBlocks():
block = stage.getUniformBlock(blockName)
if filter:
if filter not in block.getName():
continue
print('- Block: ', block.getName())
for shaderPort in block:
variable = shaderPort.getVariable()
value = shaderPort.getValue().getValueString() if shaderPort.getValue() else '<NONE>'
origPath = shaderPort.getPath()
path, interfaceInput = getPortPath(shaderPort.getPath(), doc)
if not path:
path = '<NONE>'
else:
if path != origPath:
path = origPath + ' --> ' + path
type = shaderPort.getType().getName()
print(' - Variable: %s. Value: (%s). Type: %s, Path: "%s"' % (variable, value, type, path))
unit = shaderPort.getUnit()
if interfaceInput:
colorspace = interfaceInput.getColorSpace()
else:
colorspace = shaderPort.getColorSpace()
if unit or colorspace:
print(' - Unit:%s, ColorSpace:%s' % (unit,colorspace))
if shader:
# Examine public uniforms first
debugStages(shader, doc, 'Public')
Stage name: "vertex" ------------------------------ - Block: PublicUniforms Stage name: "pixel" ------------------------------ - Block: PublicUniforms - Variable: displacementshader1. Value: (<NONE>). Type: displacementshader, Path: "<NONE>" - Variable: texcoord_vector2_index. Value: (0). Type: integer, Path: "nodegraph1/texcoord_vector2/index" - Variable: image_color3_file. Value: (checker.png). Type: filename, Path: "nodegraph1/filename_port" - Variable: image_color3_layer. Value: (<NONE>). Type: string, Path: "nodegraph1/image_color3/layer" - Variable: image_color3_default. Value: (0.574572, 0.0112386, 0.0112386). Type: color3, Path: "nodegraph1/image_color3/default" - Variable: image_color3_uaddressmode. Value: (2). Type: integer, Path: "nodegraph1/image_color3/uaddressmode" - Variable: image_color3_vaddressmode. Value: (2). Type: integer, Path: "nodegraph1/image_color3/vaddressmode" - Variable: image_color3_filtertype. Value: (1). Type: integer, Path: "nodegraph1/image_color3/filtertype" - Variable: image_color3_framerange. Value: (<NONE>). Type: string, Path: "nodegraph1/image_color3/framerange" - Variable: image_color3_frameoffset. Value: (0). Type: integer, Path: "nodegraph1/image_color3/frameoffset" - Variable: image_color3_frameendaction. Value: (0). Type: integer, Path: "nodegraph1/image_color3/frameendaction" - Variable: image_color3_uv_scale. Value: (1, 1). Type: vector2, Path: "<NONE>" - Variable: image_color3_uv_offset. Value: (0, 0). Type: vector2, Path: "<NONE>" - Variable: multiply_color3_in1. Value: (0.0225967, 0.33904, 0.440098). Type: color3, Path: "nodegraph1/color3_port" - Variable: surface_unlit_emission. Value: (1). Type: float, Path: "surface_unlit/emission" - Variable: surface_unlit_transmission. Value: (0). Type: float, Path: "surface_unlit/transmission" - Variable: surface_unlit_transmission_color. Value: (1, 1, 1). Type: color3, Path: "surface_unlit/transmission_color" - Variable: surface_unlit_opacity. Value: (1). Type: float, Path: "surface_unlit/opacity"
In the output, you will note that:
multiply_color3_in1
corresponds to an input: nodegraph1/multiply_color3/in1
maps to the interface input nodegraph1/color3_port
.image_color3_file
corresponds to an interior input: nodegraph1/image_color3/file
is maps to the interface input nodegraph1/filename_port
.Then updating the interface ports, the appropriate shader uniform needs ot be used.
The file image input 'nodegraph1/filename_port' is an interface input which has a colorspace
transform specified.
MaterialXRender has the utility createUIPropertyGroups()
which performs parsing on a block to build
UI for the MaterialX Viewer and Graph Editor.
It goes through the interface mapping step as well as extracting desired information from the MaterialX Inputs
and ShaderPort
inputs.
The uniform information can be compared against the produced source code. In the sample code below we scan the source for "uniforms" and prints them out.
if printSource:
sourceCode = glslRenderer.getSourceCode()
for stage in sourceCode:
print('-' * 80)
print('- "%s" Stage Code:' % stage)
lines = sourceCode[stage].split('\n')
for l in lines:
if l.startswith('uniform'):
print(' ', l)
-------------------------------------------------------------------------------- - "vertex" Stage Code: uniform mat4 u_worldMatrix = mat4(1.0); uniform mat4 u_viewProjectionMatrix = mat4(1.0); -------------------------------------------------------------------------------- - "pixel" Stage Code: uniform mat4 u_envMatrix = mat4(-1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, -1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000); uniform sampler2D u_envRadiance; uniform int u_envRadianceMips = 1; uniform int u_envRadianceSamples = 16; uniform sampler2D u_envIrradiance; uniform bool u_refractionTwoSided = false; uniform displacementshader displacementshader1; uniform int texcoord_vector2_index = 0; uniform sampler2D image_color3_file; uniform int image_color3_layer = 0; uniform vec3 image_color3_default = vec3(0.574572, 0.011239, 0.011239); uniform int image_color3_uaddressmode = 2; uniform int image_color3_vaddressmode = 2; uniform int image_color3_filtertype = 1; uniform int image_color3_framerange = 0; uniform int image_color3_frameoffset = 0; uniform int image_color3_frameendaction = 0; uniform vec2 image_color3_uv_scale = vec2(1.000000, 1.000000); uniform vec2 image_color3_uv_offset = vec2(0.000000, 0.000000); uniform vec3 multiply_color3_in1 = vec3(0.022597, 0.339040, 0.440098); uniform float surface_unlit_emission = 1.000000; uniform float surface_unlit_transmission = 0.000000; uniform vec3 surface_unlit_transmission_color = vec3(1.000000, 1.000000, 1.000000); uniform float surface_unlit_opacity = 1.000000;
createdProgram = False
if shader:
print('Generated shader for node: %s' % nodes[0].getNamePath())
createdProgram = glslRenderer.createProgram()
printAttribs = False
if createdProgram:
print('Create renderer program from shader')
program = glslRenderer.getProgram()
if program:
if printAttribs:
attribs = program.getAttributesList()
print('%d geometry attribs in program' % len(attribs))
for attrib in attribs:
print('- attribute: %s' % attrib)
input = attribs[attrib]
uniforms = program.getUniformsList()
print('%d uniforms' % len(uniforms))
for uniform in uniforms:
print('- Uniform:', uniform)
port = uniforms[uniform]
print(' - Port type:', port.gltype)
Generated shader for node: unlit_surfacematerial Create renderer program from shader
runRender = True
if createdProgram and runRender:
rendered, renderErrors = glslRenderer.render()
if not rendered:
print('Failed to render, Errors:', renderErrors)
else:
print('Rendered frame.')
glslRenderer.captureImage()
capturedImage = glslRenderer.getCapturedImage()
if capturedImage:
flipImage = True
fileName = mx.FilePath(inputFilename)
fileName.removeExtension()
fileName.addExtension('png')
glslRenderer.saveCapture(fileName, flipImage)
imageMD = '### %s\n<img src="%s" style="border:5px outset silver">' % (fileName.asString(), fileName.asString())
display_markdown(imageMD, raw=True)
Rendered frame.
This section goes over binding of scalars and images.
Note that currently we don't do the actual process of binding but just find the shader uniform, load in an image and then find the target shader variable to update.
imagesToBind = []
variablesToBind = []
nodePathsToBind = []
# Loading in images
imageHandler = glslRenderer.getImageHandler()
# Get the program
program = glslRenderer.getProgram()
# Scan for input filenames, create the image and bind it to the program
stage = shader.getStage('pixel') if shader else None
if stage:
block = stage.getUniformBlock('PublicUniforms')
for shaderPort in block:
value = shaderPort.getValue()
if not value:
continue
type = shaderPort.getType().getName()
if type != 'filename':
continue
variable = shaderPort.getVariable()
value = shaderPort.getValue().getValueString() if shaderPort.getValue() else '<NONE>'
origPath = shaderPort.getPath()
path, interfaceInput = getPortPath(shaderPort.getPath(), doc)
unit = shaderPort.getUnit()
if interfaceInput:
colorspace = interfaceInput.getColorSpace()
else:
colorspace = shaderPort.getColorSpace()
imagesToBind.append(value)
variablesToBind.append(variable)
nodePathsToBind.append(path)
newImage = imageHandler.acquireImage(value)
if newImage:
print('- Loaded image: "%s". Size: %d x %d. Channel count: %d' %
(value, newImage.getWidth(), newImage.getHeight(), newImage.getChannelCount()))
print(' - Base Type:', newImage.getBaseType(), '. Base Stride:', newImage.getBaseStride() )
if colorspace:
print(' - Source color space: %s' % colorspace)
elif unit:
print(' - Source unit: %s' % unit)
# Find the appropriate port on the program
if program:
uniforms = program.getUniformsList()
if variable in uniforms:
print('- Bind to program / shader port:', variable)
- Loaded image: "checker.png". Size: 1920 x 1920. Channel count: 3 - Base Type: BaseType.UINT8 . Base Stride: 1 - Bind to program / shader port: image_color3_file
In earlier versions of MaterialX there was a "dirty/notification" system which could be hooked into when a document changed. As this no longer exists, it is up the integration to keep track of relevant changes.
Value changes can require rebinding of resources such as geometry and images as well as scalar values.
Topological changes can occur due to:
Vulkan
creates uniform blocks)For this it would be very useful if there was a way to specify a hint that a value change means a topological change.
Value changes only require rebinding to an existing shader while topological changes require a shader to be rebuilt.