Skip to content

Commit

Permalink
Handle locale for MaterialX inputs (#1178)
Browse files Browse the repository at this point in the history
For MaterialX let's assume we have a xml with `<input name="diffColor" type="vector3" value="1,1.5,2.0"/>`
mx:Vector3(1,1.5,2.0) should be interpreted as float[3] = [1.0f, 1.5f, 2.0f]. But based on the locale settings, 1,1.5 will be parsed as a value. By setting the locale on std::stringstream we ensure that input values are correctly read 
Some background reference around this https://stdcxx.apache.org/doc/stdlibug/24-3.html
https://docs.microsoft.com/en-us/globalization/locale/number-formatting
Partially addresses Issue #1183

Other changes:
Basic utility to generate a json and hpp export from MaterialX nodedef
e.g. given a node def e.g. ND_standard_surface_surfaceshader will
     generate a standard_surface.json and standard_surface.hpp
The hpp/json can be used for simple reflection instead of parsing mtlx libraries
  • Loading branch information
ashwinbhat authored May 5, 2021
1 parent 2cf58a1 commit b302c4b
Show file tree
Hide file tree
Showing 6 changed files with 379 additions and 1 deletion.
12 changes: 12 additions & 0 deletions resources/Materials/TestSuite/locale/numericformat.mtlx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<materialx version="1.38" cms="ocio" colorspace="lin_rec709">
<nodedef name="ND_simple_srf_surface" node="simple_srf">
<input name="diffColor" type="color3" value="0.18,0.18, 0.18"/>
<input name="specColor" type="color3" value="0.05, 0.05,0.05" />
<input name="normal" type="vector3" value="1,0.5,1" />
<input name="specRoughness" type="float" value="0.25" />
<input name="intensity" type="integer" value="-1" />
<output name="out" type="surfaceshader" />
</nodedef>
<implementation name="IM_simple_srf" nodedef="ND_simple_srf_surface" />
</materialx>
12 changes: 12 additions & 0 deletions resources/Materials/TestSuite/locale/utf8.mtlx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<materialx version="1.38" cms="ocio" colorspace="lin_rec709">
<nodedef name="ND_simple_srf_surface" node="simple_srf">
<input name="diffColor" type="color3" value="0.18,0.18, 0.18" uiname="びまん性"/>
<input name="specColor" type="color3" value="0.05, 0.05,0.05" uiname="spéculaire"/>
<input name="normal" type="vector3" value="1,0.5,1" />
<input name="specRoughness" type="float" value="0.25" uiname="表面粗さ"/>
<input name="intensity" type="integer" value="-1" />
<output name="out" type="surfaceshader" />
</nodedef>
<implementation name="IM_simple_srf" nodedef="ND_simple_srf_surface" />
</materialx>
1 change: 1 addition & 0 deletions source/MaterialXContrib/Utilities/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/Scripts/" DESTINATION "${CMAKE_INSTALL_PREFIX}/python" MESSAGE_NEVER)
275 changes: 275 additions & 0 deletions source/MaterialXContrib/Utilities/Scripts/mxnodedefconvert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
#!/usr/bin/env python
"""
Utility to generate json and hpp from MaterialX nodedef
Given a node def e.g. ND_standard_surface_surfaceshader will
generate a standard_surface.json and standard_surface.hpp
The hpp/json can be used for simple reflection instead
of parsing mtlx libraries
"""

import sys
import os
import argparse
import json
import hashlib
import MaterialX as mx

INPUTFILEHASH = 0
mx_stdTypes = {
'color3': ['MaterialX::Color3', mx.Color3(1, 1, 1)],
'color4': ['MaterialX::Color4', mx.Color4(1, 1, 1, 1)],
'vector4': ['MaterialX::Vector4', mx.Vector4(1, 1, 1, 1)],
'vector3': ['MaterialX::Vector3', mx.Vector3(1, 1, 1)],
'vector2': ['MaterialX::Vector2', mx.Vector2(1, 1)],
'matrix33': ['MaterialX::Matrix33', None],
'matrix44': ['MaterialX::Matrix44', None],
'integerarray': ['std::vector<int>', None],
'floatarray': ['std::vector<float>', None],
'color3array': ['std::vector<MaterialX::Color3>', None],
'color4array': ['std::vector<MaterialX::Color4>', None],
'vector2array': ['std::vector<MaterialX::Vector2>', None],
'vector3array': ['std::vector<MaterialX::Vector3>', None],
'vector4array': ['std::vector<MaterialX::Vector4>', None],
'stringarray': ['std::vector<std::string>', None],
'boolean': ['bool', False],
'integer': ['int', 0],
'file': ['std::string', ""],
'filename': ['std::string', ""],
'string': ['std::string', ""],
'float': ['float', 0],

#TODO: create custom structs (fixme)
'lightshader': ['lightshader', None],
'volumeshader': ['volumeshader', None],
'displacementshader': ['displacementshader', None],
'surfaceshader': ['surfaceshader', None],
'BSDF': ['BSDF', None],
'EDF': ['EDF', None],
'VDF': ['VDF', None],
}

def _getType(mxType):
return mx_stdTypes[mxType][0]

def _getDefault(mxType):
return mx_stdTypes[mxType][1]

# Compute gitHash
def _computeGitHash(mtlxfile):
with open(mtlxfile, 'r') as afile:
buf = afile.read().encode()
hasher = hashlib.sha1()
hasher.update(b"blob %u\0" % len(buf))
hasher.update(buf)
return hasher.hexdigest()

def main():
parser = argparse.ArgumentParser(
description="MaterialX nodedef to json/hpp converter.")
parser.add_argument(dest="inputFilename",
help="Filename of the input document.")
parser.add_argument("--node", dest="nodedef", type=str,
help="Node to export")
parser.add_argument("--stdlib", dest="stdlib", action="store_true",
help="Import standard MaterialX libraries into the document.")
opts = parser.parse_args()

doc = mx.createDocument()
try:
mx.readFromXmlFile(doc, opts.inputFilename)
# Git hash for tracking source document
global INPUTFILEHASH
INPUTFILEHASH = _computeGitHash(opts.inputFilename)

except mx.ExceptionFileMissing as err:
print(err)
sys.exit(0)

if opts.stdlib:
stdlib = mx.createDocument()
filePath = os.path.dirname(os.path.abspath(__file__))
searchPath = mx.FileSearchPath(os.path.join(filePath, '..', '..'))
searchPath.append(os.path.dirname(opts.inputFilename))
libraryFolders = ["libraries"]
mx.loadLibraries(libraryFolders, searchPath, stdlib)
doc.importLibrary(stdlib)

(valid, message) = doc.validate()
if valid:
print("%s is a valid MaterialX document in v%s" %
(opts.inputFilename, mx.getVersionString()))
else:
print("%s is not a valid MaterialX document in v%s" %
(opts.inputFilename, mx.getVersionString()))
print(message)

nodedefs = doc.getNodeDefs()
nodedef = findNodeDef(nodedefs, opts.nodedef)

print("Document Version: {}.{:02d}".format(*doc.getVersionIntegers()))
if nodedef is None:
print("Nodedef %s not found" % (opts.nodedef))
else:
try:
exportNodeDef(nodedef)
print("%d NodeDef%s found.\nNode '%s' exported to %s(.json/.hpp)"
% (len(nodedefs), pl(nodedefs), opts.nodedef, nodedef.getNodeString()))
except Exception as e:
print(e)
sys.exit(0)

def findNodeDef(elemlist, nodedefname):
if len(elemlist) == 0:
return None
for elem in elemlist:
if elem.isA(mx.NodeDef) and elem.getName() == nodedefname:
return elem
return None

def exportNodeDef(elem):
if elem.isA(mx.NodeDef):
jsonfilename = elem.getNodeString()+'.json'
hppfilename = elem.getNodeString()+'.hpp'
export_json(elem, jsonfilename)
export_hpp(elem, hppfilename)

def export_json(elem, filename):
nodefInterface = {}
nodefInterface["Nodedef"] = elem.getName()
nodefInterface["SHA1"] = INPUTFILEHASH
nodefInterface["MaterialX"] = mx.getVersionString()
nodefInterface["name"] = elem.getNodeString()
asJsonArray(nodefInterface, elem)
with open(filename, 'w', encoding='utf-8') as f:
json.dump(nodefInterface, f, indent=4)

def asJsonArray(nodefInterface, nodedef):
inputs = []
outputs = []
for inp in nodedef.getActiveInputs():
inputs.append((_getType(inp.getType()),
inp.getName(),
str(inp.getValue())))
nodefInterface["inputs"] = inputs
for output in nodedef.getActiveOutputs():
outputs.append((_getType(output.getType()),
output.getName(),
str(output.getValue())))
nodefInterface["outputs"] = outputs

def export_hpp(elem, filename):
# write to file
preamble = "/*\nGenerated using MaterialX nodedef \
\n{nodename}\nSHA1:{filehash}\nVersion:{version}\n*/\n"\
.format(nodename=elem, filehash=INPUTFILEHASH, version=mx.getVersionString())
variable_defs = ""
for inp in elem.getActiveInputs():
#create decl
decl = getVarDeclaration(inp)
#emit variable decl
if decl is None:
variable_def = ' {typename} {name};\n' \
.format(typename=_getType(inp.getType()),
name=inp.getName())
else:
variable_def = ' {typename} {name} = {declaration};\n' \
.format(typename=_getType(inp.getType()),
name=inp.getName(),
declaration=decl)
variable_defs += variable_def
for output in elem.getActiveOutputs():
#create decl
decl = getVarDeclaration(output)
#emit output
if decl is None:
variable_def = ' {typename}* {name};\n' \
.format(typename=_getType(output.getType()),
name=output.getName())
else:
variable_def = ' {typename} {name} = {declaration};\n' \
.format(typename=_getType(output.getType()),
name=output.getName(),
declaration=decl)
variable_defs += variable_def
nodename_definition = ' std::string _nodename_ = "{nodename}";\n'.format(
nodename=elem.getNodeString())
# create struct definition
struct_definition = """struct {structname} {{\n{variabledefs}{nodeiddef}}};""" \
.format(structname=elem.getName(),
variabledefs=variable_defs,
nodeiddef=nodename_definition)

with open(filename, 'w', encoding='utf-8') as f:
f.write(preamble)
f.write(struct_definition)
f.close()


def getVarDeclaration(inputVar):

inputValue = inputVar.getValue()
typeName = _getType(inputVar.getType())
if isinstance(inputValue, (mx.Color3, mx.Vector3)):
val = '{typename}({v0}f, {v1}f, {v2}f)'.format(typename=typeName,
v0=round(
inputValue[0], 5),
v1=round(
inputValue[1], 5),
v2=round(inputValue[2], 5))
return val
if isinstance(inputValue, (mx.Color4, mx.Vector4)):
val = '{typename}({v0}f, {v1}f, {v2}f, {v3}f)'.format(typename=typeName,
v0=round(
inputValue[0], 5),
v1=round(
inputValue[1], 5),
v2=round(
inputValue[2], 5),
v3=round(inputValue[3], 5))
return val
if isinstance(inputValue, float):
val = '{0}f'.format(round(inputValue, 5))
return val
if isinstance(inputValue, bool):
val = '{0}'.format('true' if inputValue is True else 'false')
return val
if isinstance(inputValue, int):
val = '{0}'.format(inputValue)
return val

# use input type if value is not defined and set default
defaultValue = _getDefault(inputVar.getType())
if inputValue is None:
if inputVar.getType() in ['vector2']:
val = '{typename}({v0}f, {v1}f)'.format(typename=typeName,
v0=defaultValue[0],
v1=defaultValue[1])
return val
if inputVar.getType() in ['vector3', 'color3']:
val = '{typename}({v0}f, {v1}f, {v2}f)'.format(typename=typeName,
v0=defaultValue[0],
v1=defaultValue[1],
v2=defaultValue[2])
return val
if inputVar.getType() in ['vector4', 'color4']:
val = '{typename}({v0}f, {v1}f, {v2}f, {v3}f)'.format(typename=typeName,
v0=defaultValue[0],
v1=defaultValue[1],
v2=defaultValue[2],
v3=defaultValue[3])
return val
else:
print("unhandled: " + typeName)
return None


def pl(elem):
if len(elem) == 1:
return ""
else:
return "s"


if __name__ == '__main__':
main()
3 changes: 2 additions & 1 deletion source/MaterialXCore/Value.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ template <class T> using enable_if_std_vector_t =
template <class T> void stringToData(const string& str, T& data)
{
std::stringstream ss(str);
ss.imbue(std::locale::classic());
if (!(ss >> data))
{
throw ExceptionTypeError("Type mismatch in generic stringToData: " + str);
Expand Down Expand Up @@ -94,7 +95,7 @@ template <class T> void stringToData(const string& str, enable_if_std_vector_t<T
template <class T> void dataToString(const T& data, string& str)
{
std::stringstream ss;

ss.imbue(std::locale::classic());
// Set float format and precision for the stream
const Value::FloatFormat fmt = Value::getFloatFormat();
ss.setf(std::ios_base::fmtflags(
Expand Down
Loading

0 comments on commit b302c4b

Please sign in to comment.