diff --git a/resources/Materials/TestSuite/locale/numericformat.mtlx b/resources/Materials/TestSuite/locale/numericformat.mtlx
new file mode 100644
index 0000000000..6e9738b8f6
--- /dev/null
+++ b/resources/Materials/TestSuite/locale/numericformat.mtlx
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/Materials/TestSuite/locale/utf8.mtlx b/resources/Materials/TestSuite/locale/utf8.mtlx
new file mode 100644
index 0000000000..9dca21491c
--- /dev/null
+++ b/resources/Materials/TestSuite/locale/utf8.mtlx
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/source/MaterialXContrib/Utilities/CMakeLists.txt b/source/MaterialXContrib/Utilities/CMakeLists.txt
new file mode 100644
index 0000000000..f8a604dff5
--- /dev/null
+++ b/source/MaterialXContrib/Utilities/CMakeLists.txt
@@ -0,0 +1 @@
+install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/Scripts/" DESTINATION "${CMAKE_INSTALL_PREFIX}/python" MESSAGE_NEVER)
\ No newline at end of file
diff --git a/source/MaterialXContrib/Utilities/Scripts/mxnodedefconvert.py b/source/MaterialXContrib/Utilities/Scripts/mxnodedefconvert.py
new file mode 100644
index 0000000000..32b08da4b9
--- /dev/null
+++ b/source/MaterialXContrib/Utilities/Scripts/mxnodedefconvert.py
@@ -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', None],
+ 'floatarray': ['std::vector', None],
+ 'color3array': ['std::vector', None],
+ 'color4array': ['std::vector', None],
+ 'vector2array': ['std::vector', None],
+ 'vector3array': ['std::vector', None],
+ 'vector4array': ['std::vector', None],
+ 'stringarray': ['std::vector', 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()
diff --git a/source/MaterialXCore/Value.cpp b/source/MaterialXCore/Value.cpp
index f6adfa89b0..fe199bc958 100644
--- a/source/MaterialXCore/Value.cpp
+++ b/source/MaterialXCore/Value.cpp
@@ -31,6 +31,7 @@ template using enable_if_std_vector_t =
template 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);
@@ -94,7 +95,7 @@ template void stringToData(const string& str, enable_if_std_vector_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(
diff --git a/source/MaterialXTest/MaterialXFormat/XmlIo.cpp b/source/MaterialXTest/MaterialXFormat/XmlIo.cpp
index 8410bf0fb9..e34f42b3e6 100644
--- a/source/MaterialXTest/MaterialXFormat/XmlIo.cpp
+++ b/source/MaterialXTest/MaterialXFormat/XmlIo.cpp
@@ -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 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())
+ {
+
+ valueElementCount++;
+
+ if (elem->hasAttribute("uiname"))
+ {
+ REQUIRE(!elem->getAttribute("uiname").empty());
+ uiattributeCount++;
+ }
+ }
+ }
+ REQUIRE(valueElementCount > 0);
+ REQUIRE(uiattributeCount > 0);
+ }
+}