397 lines
16 KiB
C++
397 lines
16 KiB
C++
//
|
|
// Copyright Contributors to the MaterialX Project
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
|
|
#include <MaterialXTest/External/Catch/catch.hpp>
|
|
#include <MaterialXTest/MaterialXGenMdl/GenMdl.h>
|
|
|
|
#include <MaterialXCore/Document.h>
|
|
|
|
#include <MaterialXFormat/File.h>
|
|
#include <MaterialXFormat/Util.h>
|
|
|
|
#include <MaterialXGenMdl/MdlShaderGenerator.h>
|
|
#include <MaterialXGenMdl/MdlSyntax.h>
|
|
|
|
#include <MaterialXGenShader/DefaultColorManagementSystem.h>
|
|
#include <MaterialXGenShader/GenContext.h>
|
|
#include <MaterialXGenShader/Util.h>
|
|
|
|
|
|
namespace mx = MaterialX;
|
|
|
|
TEST_CASE("GenShader: MDL Syntax", "[genmdl]")
|
|
{
|
|
mx::TypeSystemPtr ts = mx::TypeSystem::create();
|
|
mx::SyntaxPtr syntax = mx::MdlSyntax::create(ts);
|
|
|
|
REQUIRE(syntax->getTypeName(mx::Type::FLOAT) == "float");
|
|
REQUIRE(syntax->getTypeName(mx::Type::COLOR3) == "color");
|
|
REQUIRE(syntax->getTypeName(mx::Type::VECTOR3) == "float3");
|
|
REQUIRE(syntax->getTypeName(mx::Type::FLOATARRAY) == "float");
|
|
REQUIRE(syntax->getTypeName(mx::Type::INTEGERARRAY) == "int");
|
|
REQUIRE(mx::Type::FLOATARRAY.isArray());
|
|
REQUIRE(mx::Type::INTEGERARRAY.isArray());
|
|
|
|
REQUIRE(syntax->getTypeName(mx::Type::BSDF) == "material");
|
|
REQUIRE(syntax->getOutputTypeName(mx::Type::BSDF) == "material");
|
|
|
|
// Set fixed precision with one digit
|
|
mx::ScopedFloatFormatting format(mx::Value::FloatFormatFixed, 1);
|
|
|
|
std::string value;
|
|
value = syntax->getDefaultValue(mx::Type::FLOAT);
|
|
REQUIRE(value == "0.0");
|
|
value = syntax->getDefaultValue(mx::Type::COLOR3);
|
|
REQUIRE(value == "color(0.0)");
|
|
value = syntax->getDefaultValue(mx::Type::COLOR3, true);
|
|
REQUIRE(value == "color(0.0)");
|
|
value = syntax->getDefaultValue(mx::Type::COLOR4);
|
|
REQUIRE(value == "mk_color4(0.0)");
|
|
value = syntax->getDefaultValue(mx::Type::COLOR4, true);
|
|
REQUIRE(value == "mk_color4(0.0)");
|
|
value = syntax->getDefaultValue(mx::Type::FLOATARRAY, true);
|
|
REQUIRE(value.empty());
|
|
value = syntax->getDefaultValue(mx::Type::INTEGERARRAY, true);
|
|
REQUIRE(value.empty());
|
|
|
|
mx::ValuePtr floatValue = mx::Value::createValue<float>(42.0f);
|
|
value = syntax->getValue(mx::Type::FLOAT, *floatValue);
|
|
REQUIRE(value == "42.0");
|
|
value = syntax->getValue(mx::Type::FLOAT, *floatValue, true);
|
|
REQUIRE(value == "42.0");
|
|
|
|
mx::ValuePtr color3Value = mx::Value::createValue<mx::Color3>(mx::Color3(1.0f, 2.0f, 3.0f));
|
|
value = syntax->getValue(mx::Type::COLOR3, *color3Value);
|
|
REQUIRE(value == "color(1.0, 2.0, 3.0)");
|
|
value = syntax->getValue(mx::Type::COLOR3, *color3Value, true);
|
|
REQUIRE(value == "color(1.0, 2.0, 3.0)");
|
|
|
|
mx::ValuePtr color4Value = mx::Value::createValue<mx::Color4>(mx::Color4(1.0f, 2.0f, 3.0f, 4.0f));
|
|
value = syntax->getValue(mx::Type::COLOR4, *color4Value);
|
|
REQUIRE(value == "mk_color4(1.0, 2.0, 3.0, 4.0)");
|
|
value = syntax->getValue(mx::Type::COLOR4, *color4Value, true);
|
|
REQUIRE(value == "mk_color4(1.0, 2.0, 3.0, 4.0)");
|
|
|
|
std::vector<float> floatArray = { 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f };
|
|
mx::ValuePtr floatArrayValue = mx::Value::createValue<std::vector<float>>(floatArray);
|
|
value = syntax->getValue(mx::Type::FLOATARRAY, *floatArrayValue);
|
|
REQUIRE(value == "float[](0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7)");
|
|
|
|
std::vector<int> intArray = { 1, 2, 3, 4, 5, 6, 7 };
|
|
mx::ValuePtr intArrayValue = mx::Value::createValue<std::vector<int>>(intArray);
|
|
value = syntax->getValue(mx::Type::INTEGERARRAY, *intArrayValue);
|
|
REQUIRE(value == "int[](1, 2, 3, 4, 5, 6, 7)");
|
|
}
|
|
|
|
|
|
TEST_CASE("GenShader: MDL Implementation Check", "[genmdl]")
|
|
{
|
|
mx::GenContext context(mx::MdlShaderGenerator::create());
|
|
|
|
mx::StringSet generatorSkipNodeTypes;
|
|
generatorSkipNodeTypes.insert("light");
|
|
|
|
mx::StringSet generatorSkipNodeDefs;
|
|
|
|
GenShaderUtil::checkImplementations(context, generatorSkipNodeTypes, generatorSkipNodeDefs);
|
|
}
|
|
|
|
|
|
class MdlStringResolver : public mx::StringResolver
|
|
{
|
|
public:
|
|
/// Create a new string resolver.
|
|
static MdlStringResolverPtr create()
|
|
{
|
|
return MdlStringResolverPtr(new MdlStringResolver());
|
|
}
|
|
~MdlStringResolver() = default;
|
|
|
|
void initialize(
|
|
mx::DocumentPtr document,
|
|
std::ofstream* logFile,
|
|
std::initializer_list<mx::FilePath> additionalSearchpaths)
|
|
{
|
|
mx::FileSearchPath searchPath = mx::getDefaultDataSearchPath();
|
|
mx::FilePath rootPath = searchPath.isEmpty() ? mx::FilePath() : searchPath[0];
|
|
mx::FilePath coreModulePath = rootPath / std::string(MATERIALX_INSTALL_MDL_MODULE_PATH) / "mdl";
|
|
mx::FilePath coreModulePath2 = coreModulePath / mx::FilePath("materialx");
|
|
|
|
// use the source search paths as base
|
|
mx::FileSearchPath paths = mx::getSourceSearchPath(document);
|
|
paths.append(mx::FilePath(document->getSourceUri()).getParentPath());
|
|
|
|
// paths specified by the build system
|
|
paths.append(mx::FilePath(MATERIALX_MDL_IMPL_MODULE_PATH));
|
|
mx::StringVec extraModulePaths = mx::splitString(MATERIALX_MDL_MODULE_PATHS, ",");
|
|
for (const std::string& extraPath : extraModulePaths)
|
|
{
|
|
paths.append(mx::FilePath(extraPath));
|
|
}
|
|
|
|
// add additional search paths for the tests
|
|
paths.append(rootPath);
|
|
paths.append(coreModulePath);
|
|
paths.append(coreModulePath2);
|
|
for (const auto& addSp : additionalSearchpaths)
|
|
{
|
|
paths.append(addSp);
|
|
}
|
|
|
|
_mdl_searchPaths.clear();
|
|
for (const auto& path : paths)
|
|
{
|
|
// normalize all search paths, as we need this later in `resolve`
|
|
auto normalizedPath = path.getNormalized();
|
|
if (normalizedPath.exists())
|
|
_mdl_searchPaths.append(normalizedPath);
|
|
}
|
|
|
|
_logFile = logFile;
|
|
}
|
|
|
|
std::string resolve(const std::string& str, const std::string&) const override
|
|
{
|
|
mx::FilePath normalizedPath = mx::FilePath(str).getNormalized();
|
|
|
|
// in case the path is absolute we need to find a proper search path to put the file in
|
|
if (normalizedPath.isAbsolute())
|
|
{
|
|
// find the highest priority search path that is a prefix of the resource path
|
|
for (const auto& sp : _mdl_searchPaths)
|
|
{
|
|
if (sp.size() > normalizedPath.size())
|
|
continue;
|
|
|
|
bool isParent = true;
|
|
for (size_t i = 0; i < sp.size(); ++i)
|
|
{
|
|
if (sp[i] != normalizedPath[i])
|
|
{
|
|
isParent = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!isParent)
|
|
continue;
|
|
|
|
// found a search path that is a prefix of the resource
|
|
std::string resource_path = normalizedPath.asString().substr(sp.asString().size());
|
|
if (resource_path[0] != '/')
|
|
resource_path = "/" + resource_path;
|
|
return resource_path;
|
|
}
|
|
}
|
|
|
|
*_logFile << "MaterialX resource can not be accessed through an MDL search path. "
|
|
<< "Dropping the resource from the Material. Resource Path: "
|
|
<< normalizedPath.asString().c_str() << std::endl;
|
|
|
|
// drop the resource by returning the empty string.
|
|
// alternatively, the resource could be copied into an MDL search path,
|
|
// maybe even only temporary.
|
|
return "";
|
|
}
|
|
|
|
const mx::FileSearchPath& getMdlSearchPaths() const { return _mdl_searchPaths; }
|
|
|
|
private:
|
|
// list of MDL search paths from which we can locate resources.
|
|
mx::FileSearchPath _mdl_searchPaths;
|
|
|
|
// log file of the tester
|
|
std::ofstream* _logFile;
|
|
};
|
|
|
|
void MdlShaderGeneratorTester::preprocessDocument(mx::DocumentPtr doc)
|
|
{
|
|
if (!_mdlCustomResolver)
|
|
_mdlCustomResolver = MdlStringResolver::create();
|
|
|
|
_mdlCustomResolver->initialize(doc, &_logFile, { _searchPath.asString() });
|
|
mx::flattenFilenames(doc, _mdlCustomResolver->getMdlSearchPaths(), _mdlCustomResolver);
|
|
}
|
|
|
|
void MdlShaderGeneratorTester::compileSource(const std::vector<mx::FilePath>& sourceCodePaths)
|
|
{
|
|
if (sourceCodePaths.empty() || sourceCodePaths[0].isEmpty())
|
|
return;
|
|
|
|
mx::FilePath moduleToTestPath = sourceCodePaths[0].getParentPath();
|
|
mx::FilePath module = sourceCodePaths[0];
|
|
std::string moduleToTest = module[module.size()-1];
|
|
moduleToTest = moduleToTest.substr(0, moduleToTest.size() - sourceCodePaths[0].getExtension().length() - 1);
|
|
|
|
std::string renderExec(MATERIALX_MDL_RENDER_EXECUTABLE);
|
|
std::string mdlcExec(MATERIALX_MDLC_EXECUTABLE);
|
|
if (!mdlcExec.empty()) // always run compiler
|
|
{
|
|
std::string mdlcCommand = mdlcExec;
|
|
|
|
// use the same paths as the resolver
|
|
for (const auto& sp : _mdlCustomResolver->getMdlSearchPaths())
|
|
{
|
|
mdlcCommand += " -p \"" + sp.asString() + "\"";
|
|
}
|
|
|
|
// additionally the generated module needs to found in a search path too
|
|
mdlcCommand += " -p \"" + moduleToTestPath.asString() + "\"";
|
|
mdlcCommand += " -p \"" + moduleToTestPath.getParentPath().asString() + "\"";
|
|
|
|
mdlcCommand += " -W \"181=off\" -W \"183=off\" -W \"225=off\"";
|
|
mdlcCommand += " " + moduleToTest;
|
|
mx::FilePath errorFile = moduleToTestPath / (moduleToTest + ".mdl_compile_errors.txt");
|
|
mdlcCommand += " > " + errorFile.asString() + " 2>&1";
|
|
|
|
int returnValue = std::system(mdlcCommand.c_str());
|
|
std::ifstream errorStream(errorFile);
|
|
mx::StringVec result;
|
|
std::string line;
|
|
bool writeErrorCode = false;
|
|
while (std::getline(errorStream, line))
|
|
{
|
|
if (!writeErrorCode)
|
|
{
|
|
_logFile << mdlcCommand << std::endl;
|
|
_logFile << "\tReturn code: " << std::to_string(returnValue) << std::endl;
|
|
writeErrorCode = true;
|
|
}
|
|
if (line.find(": Warning ") != std::string::npos)
|
|
{
|
|
_logFile << "\tWarning: " << line << std::endl;
|
|
}
|
|
else
|
|
{
|
|
_logFile << "\tError: " << line << std::endl;
|
|
}
|
|
}
|
|
|
|
CHECK(returnValue == 0);
|
|
}
|
|
if (!renderExec.empty()) // render if renderer is available
|
|
{
|
|
std::string renderCommand = renderExec;
|
|
|
|
// use the same paths as the resolver
|
|
for (const auto& sp : _mdlCustomResolver->getMdlSearchPaths())
|
|
{
|
|
renderCommand += " --mdl_path \"" + sp.asString() + "\"";
|
|
}
|
|
|
|
// additionally the generated module needs to found in a search path too
|
|
renderCommand += " --mdl_path \"" + moduleToTestPath.asString() + "\"";
|
|
renderCommand += " --mdl_path \"" + moduleToTestPath.getParentPath().asString() + "\"";
|
|
|
|
mx::FileSearchPath searchPath = mx::getDefaultDataSearchPath();
|
|
mx::FilePath rootPath = searchPath.isEmpty() ? mx::FilePath() : searchPath[0];
|
|
// set environment
|
|
std::string iblFile = (rootPath / "resources/lights/san_giuseppe_bridge.hdr").asString();
|
|
renderCommand += " --hdr \"" + iblFile + "\" --hdr_rotate 90";
|
|
// set scene
|
|
renderCommand += " --uv_scale 0.5 1.0 --uv_offset 0.0 0.0 --uv_repeat";
|
|
renderCommand += " --uv_flip"; // this will flip the v coordinate of the vertices, which flips all the
|
|
// UV operations. In contrast, the fileTextureVerticalFlip option will
|
|
// only flip the image access nodes.
|
|
renderCommand += " --camera 0 0 3 0 0 0 --fov 45";
|
|
|
|
// set the material
|
|
// compute the MDL module name as fully qualified name wrt to the "rootPath/resources" as MDL search path
|
|
std::string mdlModuleName = "::resources::";
|
|
for (size_t s = rootPath.size() + 1; s < moduleToTestPath.size(); ++s)
|
|
{
|
|
mdlModuleName += moduleToTestPath[s] + "::";
|
|
}
|
|
mdlModuleName += moduleToTest;
|
|
renderCommand += " --mat " + mdlModuleName + "::*";
|
|
|
|
// This must be a render args option. Rest are consistent between dxr and cuda example renderers.
|
|
std::string renderArgs(MATERIALX_MDL_RENDER_ARGUMENTS);
|
|
if (renderArgs.empty())
|
|
{
|
|
// Assume MDL example DXR is being used and set reasonable arguments automatically
|
|
renderCommand += " --nogui --res 512 512 --iterations 1024 --max_path_length 3 --noaux --no_firefly_clamp";
|
|
renderCommand += " --background 0.073239 0.073239 0.083535";
|
|
}
|
|
else
|
|
{
|
|
renderCommand += " " + renderArgs;
|
|
}
|
|
|
|
std::string extension("_mdl.png");
|
|
#if defined(MATERIALX_BUILD_OIIO)
|
|
extension = "_mdl.exr";
|
|
#endif
|
|
// drop the `.genmdl` in order to have filenames supported by the image comparison
|
|
std::string imageFilename = moduleToTest.substr(0, moduleToTest.size() - 7);
|
|
|
|
mx::FilePath outputImageName = moduleToTestPath / (imageFilename + extension);
|
|
|
|
renderCommand += " -o " + outputImageName.asString();
|
|
mx::FilePath logFile = moduleToTestPath / (moduleToTest + ".mdl_render_log.txt");
|
|
renderCommand += " --log_file " + logFile.asString();
|
|
mx::FilePath errorLogFile = moduleToTestPath / (moduleToTest + ".mdl_render_errors.txt");
|
|
int returnValue = std::system(renderCommand.c_str());
|
|
std::ifstream logStream(errorLogFile);
|
|
mx::StringVec result;
|
|
std::string line;
|
|
bool writeLogCode = false;
|
|
while (std::getline(logStream, line))
|
|
{
|
|
if (!writeLogCode)
|
|
{
|
|
_logFile << renderCommand << std::endl;
|
|
_logFile << "\tReturn code: " << std::to_string(returnValue) << std::endl;
|
|
writeLogCode = true;
|
|
}
|
|
_logFile << "\tLog: " << line << std::endl;
|
|
}
|
|
|
|
CHECK(returnValue == 0);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("GenShader: MDL Shader Generation", "[genmdl]")
|
|
{
|
|
mx::FileSearchPath searchPath = mx::getDefaultDataSearchPath();
|
|
|
|
mx::FilePathVec testRootPaths;
|
|
testRootPaths.push_back(searchPath.find("resources/Materials/TestSuite"));
|
|
testRootPaths.push_back(searchPath.find("resources/Materials/Examples/StandardSurface"));
|
|
testRootPaths.push_back(searchPath.find("resources/Materials/Examples/UsdPreviewSurface"));
|
|
|
|
const mx::FilePath logPath("genmdl_mdl_generate_test.txt");
|
|
|
|
// Write shaders and try to compile only if mdlc exe specified.
|
|
std::string mdlcExec(MATERIALX_MDLC_EXECUTABLE);
|
|
bool writeShadersToDisk = !mdlcExec.empty();
|
|
MdlShaderGeneratorTester tester(mx::MdlShaderGenerator::create(), testRootPaths, searchPath, logPath, writeShadersToDisk);
|
|
tester.addSkipLibraryFiles();
|
|
|
|
mx::GenOptions genOptions;
|
|
genOptions.targetColorSpaceOverride = "lin_rec709";
|
|
|
|
// Flipping the texture lookups for the test renderer only.
|
|
// This is because OSL testrender does not allow to change the UV layout of their sphere (yet) and the MaterialX test suite
|
|
// adopts the OSL behavior in order to produce comparable results. This means that raw texture coordinates, or procedurals
|
|
// that use the texture coordinates, do not match what might be expected when reading the MaterialX spec:
|
|
// "[...] the image is mapped onto the geometry based on geometry UV coordinates, with the lower-left corner of an image
|
|
// mapping to the (0,0) UV coordinate [...]"
|
|
// This means for MDL: here, and only here in the test suite, we flip the UV coordinates of mesh using the `--uv_flip` option
|
|
// of the renderer, and to correct the image orientation, we apply `fileTextureVerticalFlip`.
|
|
// In regular MDL integrations this is not needed because MDL and MaterialX define the texture space equally with the origin
|
|
// at the bottom left.
|
|
genOptions.fileTextureVerticalFlip = true;
|
|
|
|
mx::FilePath optionsFilePath = searchPath.find("resources/Materials/TestSuite/_options.mtlx");
|
|
|
|
// Specify the MDL target version to be the latest which is also the default.
|
|
mx::GenMdlOptionsPtr genMdlOptions = std::make_shared<mx::GenMdlOptions>();
|
|
genMdlOptions->targetVersion = mx::GenMdlOptions::MdlVersion::MDL_LATEST;
|
|
tester.addUserData(mx::GenMdlOptions::GEN_CONTEXT_USER_DATA_KEY, genMdlOptions);
|
|
|
|
tester.validate(genOptions, optionsFilePath);
|
|
}
|