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

Handle locale for MaterialX inputs #1178

Merged
merged 14 commits into from
May 5, 2021
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)
263 changes: 263 additions & 0 deletions source/MaterialXContrib/Utilities/Scripts/mxnodedefconvert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
#!/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 convert.")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick. "converter" instead of "convert" ?

parser.add_argument(dest="inputFilename",
help="Filename of the input document.")
parser.add_argument("--node", dest="nodedef", type=str,
default="ND_standard_surface_surfaceshader",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe leave the default to be empty string instead ?

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:
exportNodeDef(nodedef)
print("%d NodeDef%s found.\nNode '%s' exported to %s(.json/.hpp)"
% (len(nodedefs), pl(nodedefs), opts.nodedef, nodedef.getNodeString()))


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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove commented out code.

return elem
return None


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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we catch exceptions here in case the files cannot be written.

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()
nodefInterface["members"] = asJsonArray(elem)
with open(filename, 'w', encoding='utf-8') as f:
json.dump(nodefInterface, f, indent=4)


def asJsonArray(nodedef):
memberlist = []
for inp in nodedef.getActiveInputs():
memberlist.append((_getType(inp.getType()),
inp.getName(),
str(inp.getValue())))
return memberlist


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():

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in chat, would you want to export outputs as well ?
In which case would it be better to get "value elements" instead of just inputs. ?

#create decl
decl = getVarDeclaration(inp)

#emit variable decl
if decl is None:
variable_def = ' {typename} {inputname};\n' \
.format(typename=_getType(inp.getType()),
inputname=inp.getName())
else:
variable_def = ' {typename} {inputname} = {declaration};\n' \
.format(typename=_getType(inp.getType()),
inputname=inp.getName(),
declaration=decl)
variable_defs += variable_def
nodename_definition = ' std::string _nodename_ = "{nodename}";\n'.format(
nodename=elem.getNodeString())
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
77 changes: 77 additions & 0 deletions source/MaterialXTest/MaterialXFormat/XmlIo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,80 @@ TEST_CASE("Export Document", "[xmlio]")
REQUIRE(exportedDoc->getLookGroups().size() == 0);
REQUIRE(exportedDoc->getLooks().size() == 1);
}

TEST_CASE("Load locale content", "[xmlio_locale]")
{
/// Test locale region
/// The character used as the thousands separator.
/// The character used as the decimal separator.

/// In the United States, this character is a comma(, ).
/// In Germany, it is a period(.).
/// Thus one thousandand twenty - five is displayed as 1, 025 in the United States and 1.025 in Germany.In Sweden, the thousands separator is a space.
/// mx:Vector3(1,1.5,2.0) should be interpreted as float[3] = [1.0f, 1.5f, 2.0f]

try {
//Set locale to de
std::locale deLocale("de_DE");
std::locale::global(deLocale);
}
catch (const std::runtime_error& e) {
WARN("Unable to change locale " << e.what());
return;
}

mx::FilePath libraryPath("libraries/stdlib");
mx::FilePath testPath("resources/Materials/TestSuite/locale");
mx::FileSearchPath searchPath = libraryPath.asString() +
mx::PATH_LIST_SEPARATOR +
testPath.asString();

// Read the standard library.
std::vector<mx::DocumentPtr> libs;
for (const mx::FilePath& filename : libraryPath.getFilesInDirectory(mx::MTLX_EXTENSION))
{
mx::DocumentPtr lib = mx::createDocument();
mx::readFromXmlFile(lib, filename, searchPath);
libs.push_back(lib);
}

// Read and validate each example document.
for (const mx::FilePath& filename : testPath.getFilesInDirectory(mx::MTLX_EXTENSION))
{
mx::DocumentPtr doc = mx::createDocument();
mx::readFromXmlFile(doc, filename, searchPath);
for (mx::DocumentPtr lib : libs)
{
doc->importLibrary(lib);
}
std::string message;

bool docValid = doc->validate(&message);
if (!docValid)
{
WARN("[" + filename.asString() + "] " + message);
}
REQUIRE(docValid);

// Traverse the document tree
int valueElementCount = 0;
int uiattributeCount = 0;
for (mx::ElementPtr elem : doc->traverseTree())
{

if (elem->isA<mx::ValueElement>())
{

valueElementCount++;

if (elem->hasAttribute("uiname"))
{
REQUIRE(!elem->getAttribute("uiname").empty());
uiattributeCount++;
}
}
}
REQUIRE(valueElementCount > 0);
REQUIRE(uiattributeCount > 0);
}
}