Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create material from textures #1746

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
c811404
Initial setup
Michaelredaa Oct 29, 2023
de15a9c
Add support for multiple configration files
Michaelredaa Nov 6, 2023
cf55632
Add shader model
Michaelredaa Nov 6, 2023
675acb3
Change files structure
Michaelredaa Nov 6, 2023
2886327
Add best guess for shader model inputs instead of JSON
Michaelredaa Feb 3, 2024
61a9f1e
Update command help
Michaelredaa Feb 3, 2024
c3ea1e5
Merge branch 'AcademySoftwareFoundation:main' into create-material-fr…
Michaelredaa Feb 3, 2024
e2bcaa9
Merge branch 'main' into create-material-from-textures
Michaelredaa Feb 3, 2024
1610f6e
Merge branch 'AcademySoftwareFoundation:main' into create-material-fr…
Michaelredaa Feb 3, 2024
fa81c89
Merge branch 'create-material-from-textures' of https://github.com/Ci…
Michaelredaa Feb 3, 2024
7da41ba
Make creatematerial in one file
Michaelredaa Feb 4, 2024
e5073d6
Handel base_color pattern
Michaelredaa Feb 5, 2024
d0b024c
Modify createMtlxDoc to take list of files
Michaelredaa Feb 7, 2024
5bba221
Add initial unit test
Michaelredaa Feb 7, 2024
01dc95a
Add tests_creatematerial.py
Michaelredaa Feb 7, 2024
890b437
Adjust import of creatematerial
Michaelredaa Feb 7, 2024
6ef75ba
Adjust import of creatematerial
Michaelredaa Feb 7, 2024
f4a93db
Remove unsupported return type from findBestMatch
Michaelredaa Feb 7, 2024
886ed0b
Sort UDIMs before validation in test_udimFile
Michaelredaa Feb 7, 2024
c02c365
Fix relative paths
Michaelredaa Feb 7, 2024
646c0e4
Add log to test_listTextures
Michaelredaa Feb 8, 2024
a543338
Add log to test_listTextures
Michaelredaa Feb 8, 2024
5c9cdc8
Add log to test_listTextures
Michaelredaa Feb 8, 2024
793e36f
Add log to test_listTextures
Michaelredaa Feb 8, 2024
d50f621
Fix the paths issue that happen in github tests
Michaelredaa Feb 9, 2024
2e7c08a
Merge branch 'main' into create-material-from-textures
jstone-lucasfilm Feb 9, 2024
6fd270a
Add texture prefix arg
Michaelredaa Feb 11, 2024
d85a4e0
Fix relative mtlx file path
Michaelredaa Feb 11, 2024
ed9035d
Add creatematerial command and mxvalidate to validate the output
Michaelredaa Feb 11, 2024
d1f17cb
Merge branch 'create-material-from-textures' of https://github.com/Ci…
Michaelredaa Feb 11, 2024
d4e7456
Set default value of texturePrefix in listTextures function
Michaelredaa Feb 11, 2024
38f4e2a
Focus on new functional tests
jstone-lucasfilm Feb 11, 2024
e380de6
Focus on new functional tests
jstone-lucasfilm Feb 11, 2024
c067477
Update header and doc string
jstone-lucasfilm Feb 11, 2024
ce26970
Use standard mx alias
jstone-lucasfilm Feb 11, 2024
aa3cc2b
Merge branch 'main' into create-material-from-textures
jstone-lucasfilm Feb 12, 2024
b3a0da7
Use print for user feedback
jstone-lucasfilm Feb 12, 2024
a4e88a5
Remove apostrophe
jstone-lucasfilm Feb 12, 2024
c99fc8a
Harmonize on "shading model"
jstone-lucasfilm Feb 12, 2024
593aef8
Align constant values with codebase conventions
jstone-lucasfilm Feb 12, 2024
2e745a4
Remove unused method
jstone-lucasfilm Feb 12, 2024
a85f2e2
Clarify function signatures and doc strings
jstone-lucasfilm Feb 12, 2024
84d1cc7
Minor fixes
jstone-lucasfilm Feb 12, 2024
b536642
Separate document creation and writing
jstone-lucasfilm Feb 12, 2024
74d2635
Default to displaying the generated document
jstone-lucasfilm Feb 12, 2024
9adb17e
Test document display for chessboard
jstone-lucasfilm Feb 12, 2024
18dab79
Add a second test example
jstone-lucasfilm Feb 12, 2024
da98bb1
Print additional details
jstone-lucasfilm Feb 13, 2024
46d0257
Improve special handling for normal maps
jstone-lucasfilm Feb 13, 2024
7b21d20
Clarify shading model rules
jstone-lucasfilm Feb 13, 2024
3f9c225
Clarify inheritance rules
jstone-lucasfilm Feb 13, 2024
f19413e
More robust logic for shader nodedef
jstone-lucasfilm Feb 13, 2024
58aa3a7
Simplify document construction
jstone-lucasfilm Feb 13, 2024
9ac5868
Clarify image category selection
jstone-lucasfilm Feb 13, 2024
5f776b7
Clarify UdimFilePath class
jstone-lucasfilm Feb 13, 2024
8d43f85
Remove unneeded call
jstone-lucasfilm Feb 13, 2024
c6b3216
Additional fixes and simplifications
jstone-lucasfilm Feb 13, 2024
e9e103a
Merge branch 'AcademySoftwareFoundation:main' into create-material-fr…
Michaelredaa Feb 14, 2024
e4e1fc7
Handle roughness to take specular_roughness as a default
Michaelredaa Feb 14, 2024
62821fb
Merge branch 'AcademySoftwareFoundation:main' into create-material-fr…
Michaelredaa Mar 24, 2024
5369b21
Merge branch 'dev_1.39' into create-material-from-textures
Michaelredaa Mar 24, 2024
68b14a7
Merge branch 'dev_1.39' into create-material-from-textures
jstone-lucasfilm Mar 29, 2024
17f7ef2
Merge branch 'dev_1.39' into create-material-from-textures
jstone-lucasfilm Apr 22, 2024
c573ee9
Merge branch 'dev_1.39' into create-material-from-textures
jstone-lucasfilm Apr 30, 2024
c8fb1ec
Clarify input alias logic
jstone-lucasfilm Apr 30, 2024
168c86d
Clarify comment
jstone-lucasfilm Apr 30, 2024
488df0c
Assume relative paths for simplicity
jstone-lucasfilm Apr 30, 2024
d1c47b7
Remove extra comment
jstone-lucasfilm Apr 30, 2024
b0e87fe
Use isColorType for clarity
jstone-lucasfilm Apr 30, 2024
32e64af
Remove extra newline
jstone-lucasfilm Apr 30, 2024
b3109ef
Remove platform-specific formatting
jstone-lucasfilm Apr 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ jobs:
run: |
python MaterialXTest/main.py
python MaterialXTest/genshader.py
python Scripts/creatematerial.py ../resources/Materials/Examples/StandardSurface/chess_set --texturePrefix chessboard --shadingModel standard_surface
python Scripts/creatematerial.py ../resources/Materials/Examples/GltfPbr/boombox --shadingModel gltf_pbr
python Scripts/mxformat.py ../resources/Materials/TestSuite/stdlib/upgrade --yes --upgrade
python Scripts/mxvalidate.py ../resources/Materials/Examples/StandardSurface/standard_surface_marble_solid.mtlx --stdlib --verbose
python Scripts/mxdoc.py --docType md ../libraries/pbrlib/pbrlib_defs.mtlx
Expand Down
268 changes: 268 additions & 0 deletions python/Scripts/creatematerial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
#!/usr/bin/env python
'''
Construct a MaterialX file from the textures in the given folder, using the standard data libraries
to build a mapping from texture filenames to shader inputs.

By default the standard_surface shading model is assumed, with the --shadingModel option used to
select any other shading model in the data libraries.
'''

import os
import re
import argparse
from difflib import SequenceMatcher

import MaterialX as mx

UDIM_TOKEN = '.<UDIM>.'
UDIM_REGEX = r'\.\d+\.'
TEXTURE_EXTENSIONS = [ "exr", "png", "jpg", "jpeg", "tif", "hdr" ]
INPUT_ALIASES = { "roughness": "specular_roughness" }

class UdimFilePath(mx.FilePath):

def __init__(self, pathString):
super().__init__(pathString)

self._isUdim = False
self._udimFiles = []
self._udimRegex = re.compile(UDIM_REGEX)

textureDir = self.getParentPath()
textureName = self.getBaseName()
textureExtension = self.getExtension()

if not self._udimRegex.search(textureName):
self._udimFiles = [self]
return

self._isUdim = True
fullNamePattern = self._udimRegex.sub(self._udimRegex.pattern.replace('\\', '\\\\'),
textureName)

udimFiles = filter(
lambda f: re.search(fullNamePattern, f.asString()),
textureDir.getFilesInDirectory(textureExtension)
)
self._udimFiles = [textureDir / f for f in udimFiles]

def __str__(self):
return self.asPattern()

def asPattern(self):
if not self._isUdim:
return self.asString()

textureDir = self.getParentPath()
textureName = self.getBaseName()

pattern = textureDir / mx.FilePath(
self._udimRegex.sub(UDIM_TOKEN, textureName))
return pattern.asString()

def isUdim(self):
return self._isUdim

def getUdimFiles(self):
return self._udimFiles

def getUdimNumbers(self):
def _extractUdimNumber(_file):
pattern = self._udimRegex.search(_file.getBaseName())
if pattern:
return re.search(r"\d+", pattern.group()).group()

return list(map(_extractUdimNumber, self._udimFiles))

def getNameWithoutExtension(self):
if self._isUdim:
name = self._udimRegex.split(self.getBaseName())[0]
else:
name = self.getBaseName().rsplit('.', 1)[0]

return re.sub(r'[^\w\s]+', '_', name)

def listTextures(textureDir, texturePrefix=None):
'''
Return a list of texture filenames matching known extensions.
'''

texturePrefix = texturePrefix or ""
allTextures = []
for ext in TEXTURE_EXTENSIONS:
textures = [textureDir / f for f in textureDir.getFilesInDirectory(ext)
if f.asString().lower().startswith(texturePrefix.lower())]
while textures:
textureFile = UdimFilePath(textures[0].asString())
allTextures.append(textureFile)
for udimFile in textureFile.getUdimFiles():
textures.remove(udimFile)
return allTextures

def findBestMatch(textureName, shadingModel):
'''
Given a texture name and shading model, return the shader input that is the closest match.
'''

parts = textureName.rsplit("_")

baseTexName = parts[-1]
if baseTexName.lower() == 'color':
baseTexName = ''.join(parts[-2:])
if baseTexName in INPUT_ALIASES:
baseTexName = INPUT_ALIASES.get(baseTexName.lower())

shaderInputs = shadingModel.getActiveInputs()
ratios = []
for shaderInput in shaderInputs:
inputName = shaderInput.getName()
inputName = re.sub(r'[^a-zA-Z0-9\s]', '', inputName).lower()
baseTexName = re.sub(r'[^a-zA-Z0-9\s]', '', baseTexName).lower()

sequenceScore = SequenceMatcher(None, inputName, baseTexName).ratio()
ratios.append(sequenceScore * 100)

highscore = max(ratios)
if highscore < 50:
return None

idx = ratios.index(highscore)
return shaderInputs[idx]

def buildDocument(textureFiles, mtlxFile, shadingModel, colorspace, useTiledImage):
'''
Build a MaterialX document from the given textures and shading model.
'''

# Find the default library nodedef, if any, for the requested shading model.
stdlib = mx.createDocument()
mx.loadLibraries(mx.getDefaultDataLibraryFolders(), mx.getDefaultDataSearchPath(), stdlib)
matchingNodeDefs = stdlib.getMatchingNodeDefs(shadingModel)
if not matchingNodeDefs:
print('Shading model', shadingModel, 'not found in the MaterialX data libraries')
return None
shadingModelNodeDef = matchingNodeDefs[0]
for nodeDef in matchingNodeDefs:
if nodeDef.getAttribute('isdefaultversion') == 'true':
shadingModelNodeDef = nodeDef

# Create content document.
doc = mx.createDocument()
materialName = mx.createValidName(mtlxFile.getBaseName().rsplit('.', 1)[0])
nodeGraph = doc.addNodeGraph('NG_' + materialName)
shaderNode = doc.addNode(shadingModel, 'SR_' + materialName, 'surfaceshader')
doc.addMaterialNode('M_' + materialName, shaderNode)

# Iterate over texture files.
imageNodeCategory = 'tiledimage' if useTiledImage else 'image'
udimNumbers = set()
for textureFile in textureFiles:
textureName = textureFile.getNameWithoutExtension()
shaderInput = findBestMatch(textureName, shadingModelNodeDef)

if not shaderInput:
print('Skipping', textureFile.getBaseName(), 'which does not match any', shadingModel, 'input')
continue

inputName = shaderInput.getName()
inputType = shaderInput.getType()

# Skip inputs that have already been created, e.g. in multi-UDIM materials.
if shaderNode.getInput(inputName) or nodeGraph.getChild(textureName):
continue

mtlInput = shaderNode.addInput(inputName)
textureName = nodeGraph.createValidChildName(textureName)
imageNode = nodeGraph.addNode(imageNodeCategory, textureName, inputType)

# Set color space.
if shaderInput.isColorType():
imageNode.setColorSpace(colorspace)

# Set file path.
filePathString = os.path.relpath(textureFile.asPattern(), mtlxFile.getParentPath().asString())
imageNode.setInputValue('file', filePathString, 'filename')

# Apply special cases for normal maps.
inputNode = imageNode
connNode = imageNode
inBetweenNodes = []
if inputName.endswith('normal') and shadingModel == 'standard_surface':
inBetweenNodes = ["normalmap"]
for inNodeName in inBetweenNodes:
connNode = nodeGraph.addNode(inNodeName, textureName + '_' + inNodeName, inputType)
connNode.setConnectedNode('in', inputNode)
inputNode = connNode

# Create output.
outputNode = nodeGraph.addOutput(textureName + '_output', inputType)
outputNode.setConnectedNode(connNode)
mtlInput.setConnectedOutput(outputNode)
mtlInput.setType(inputType)

if textureFile.isUdim():
udimNumbers.update(set(textureFile.getUdimNumbers()))

# Create udim set
if udimNumbers:
geomInfoName = doc.createValidChildName('GI_' + materialName)
geomInfo = doc.addGeomInfo(geomInfoName)
geomInfo.setGeomPropValue('udimset', list(udimNumbers), "stringarray")

# Return the new document
return doc

def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--outputFilename', dest='outputFilename', type=str, help='Filename of the output MaterialX document.')
parser.add_argument('--shadingModel', dest='shadingModel', type=str, default="standard_surface", help='The shading model used in analyzing input textures.')
parser.add_argument('--colorSpace', dest='colorSpace', type=str, help='The colorspace in which input textures should be interpreted, defaulting to srgb_texture.')
parser.add_argument('--texturePrefix', dest='texturePrefix', type=str, help='Filter input textures by the given prefix.')
parser.add_argument('--tiledImage', dest='tiledImage', action="store_true", help='Request tiledimage nodes instead of image nodes.')
parser.add_argument(dest='inputDirectory', nargs='?', help='Input folder that will be scanned for textures, defaulting to the current working directory.')

options = parser.parse_args()

texturePath = mx.FilePath.getCurrentPath()
if options.inputDirectory:
texturePath = mx.FilePath(options.inputDirectory)
if not texturePath.isDirectory():
print('Input folder not found:', texturePath)
return

mtlxFile = texturePath / mx.FilePath('material.mtlx')
if options.outputFilename:
mtlxFile = mx.FilePath(options.outputFilename)

textureFiles = listTextures(texturePath, texturePrefix=options.texturePrefix)
if not textureFiles:
print('No matching textures found in input folder.')
return

# Get shading model and color space.
shadingModel = 'standard_surface'
colorspace = 'srgb_texture'
if options.shadingModel:
shadingModel = options.shadingModel
if options.colorSpace:
colorspace = options.colorSpace
print('Analyzing textures in the', texturePath.asString(), 'folder for the', shadingModel, 'shading model.')

# Create the MaterialX document.
doc = buildDocument(textureFiles, mtlxFile, shadingModel, colorspace, options.tiledImage)
if not doc:
return

if options.outputFilename:
# Write the document to disk.
if not mtlxFile.getParentPath().exists():
mtlxFile.getParentPath().createDirectory()
mx.writeToXmlFile(doc, mtlxFile.asString())
print('Wrote MaterialX document to disk:', mtlxFile.asString())
else:
# Print the document to the standard output.
print('Generated MaterialX document:')
print(mx.writeToXmlString(doc))

if __name__ == '__main__':
main()