diff --git a/CMakeLists.txt b/CMakeLists.txt index 63ae3ab53c..c3bff685c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,7 @@ option(MATERIALX_BUILD_DOCS "Create HTML documentation using Doxygen. Requires t option(MATERIALX_BUILD_GEN_GLSL "Build the GLSL shader generator back-end." ON) option(MATERIALX_BUILD_GEN_OSL "Build the OSL shader generator back-end." ON) +option(MATERIALX_BUILD_GEN_OSL_NODES "Build the OSL nodes shader generator back-end." ON) option(MATERIALX_BUILD_GEN_MDL "Build the MDL shader generator back-end." ON) option(MATERIALX_BUILD_GEN_MSL "Build the MSL shader generator back-end." ON) option(MATERIALX_BUILD_RENDER "Build the MaterialX Render modules." ON) @@ -84,6 +85,7 @@ if (CMAKE_SYSTEM_NAME MATCHES "iOS" OR CMAKE_SYSTEM_NAME MATCHES "tvOS" OR CMAKE set(MATERIALX_BUILD_GRAPH_EDITOR OFF) set(MATERIALX_BUILD_GEN_GLSL OFF) set(MATERIALX_BUILD_GEN_OSL OFF) + set(MATERIALX_BUILD_GEN_OSL_NODES OFF) set(MATERIALX_BUILD_GEN_MDL OFF) set(MATERIALX_BUILD_TESTS OFF) endif() @@ -103,6 +105,7 @@ if (MATERIALX_BUILD_APPLE_FRAMEWORK) endif() if (MATERIALX_BUILD_JS) + set(MATERIALX_BUILD_GEN_OSL_NODES OFF) set(MATERIALX_BUILD_RENDER OFF) set(MATERIALX_BUILD_TESTS OFF) endif() @@ -136,6 +139,12 @@ if(SKBUILD) set(MATERIALX_PYTHON_FOLDER_NAME "MaterialX") endif() +if (MATERIALX_BUILD_GEN_OSL_NODES) + set(MATERIALX_BUILD_GEN_OSL ON) + set(MATERIALX_BUILD_RENDER ON) + set(MATERIALX_BUILD_RENDER_PLATFORMS ON) +endif() + # Helpers for MDL validation if (MATERIALX_BUILD_GEN_MDL) set(MATERIALX_MDLC_EXECUTABLE "" CACHE FILEPATH "Full path to the mdlc binary.") @@ -162,6 +171,7 @@ set(MATERIALX_LIBNAME_SUFFIX "" CACHE STRING "Specify a suffix to all libraries mark_as_advanced(MATERIALX_BUILD_DOCS) mark_as_advanced(MATERIALX_BUILD_GEN_GLSL) mark_as_advanced(MATERIALX_BUILD_GEN_OSL) +mark_as_advanced(MATERIALX_BUILD_GEN_OSL_NODES) mark_as_advanced(MATERIALX_BUILD_GEN_MDL) mark_as_advanced(MATERIALX_BUILD_GEN_MSL) mark_as_advanced(MATERIALX_BUILD_RENDER) @@ -215,7 +225,7 @@ endif() # Allow the OSL CMake package to provide binary locations for render tests. # This will not override explicitly provided oslc, testrender, and include paths. -if(MATERIALX_BUILD_RENDER AND MATERIALX_BUILD_GEN_OSL AND MATERIALX_BUILD_TESTS) +if((MATERIALX_BUILD_RENDER AND MATERIALX_BUILD_GEN_OSL AND MATERIALX_BUILD_TESTS) OR MATERIALX_BUILD_GEN_OSL_NODES) find_package(OSL QUIET) if(OSL_FOUND) if(NOT MATERIALX_OSL_BINARY_OSLC) @@ -449,7 +459,7 @@ add_subdirectory(source/MaterialXFormat) # Add shader generation subdirectories add_subdirectory(source/MaterialXGenShader) -if(MATERIALX_BUILD_GEN_GLSL OR MATERIALX_BUILD_GEN_OSL OR MATERIALX_BUILD_GEN_MDL OR MATERIALX_BUILD_GEN_MSL) +if(MATERIALX_BUILD_GEN_GLSL OR MATERIALX_BUILD_GEN_OSL OR MATERIALX_BUILD_GEN_OSL_NODES OR MATERIALX_BUILD_GEN_MDL OR MATERIALX_BUILD_GEN_MSL) if (MATERIALX_BUILD_GEN_GLSL) add_definitions(-DMATERIALX_BUILD_GEN_GLSL) add_subdirectory(source/MaterialXGenGlsl) @@ -458,6 +468,10 @@ if(MATERIALX_BUILD_GEN_GLSL OR MATERIALX_BUILD_GEN_OSL OR MATERIALX_BUILD_GEN_MD add_definitions(-DMATERIALX_BUILD_GEN_OSL) add_subdirectory(source/MaterialXGenOsl) endif() + if (MATERIALX_BUILD_GEN_OSL_NODES) + add_definitions(-DMATERIALX_BUILD_GEN_OSL_NODES) + add_subdirectory(source/MaterialXGenOslNodes) + endif() if (MATERIALX_BUILD_GEN_MDL) add_definitions(-DMATERIALX_BUILD_GEN_MDL) add_subdirectory(source/MaterialXGenMdl) diff --git a/source/MaterialXGenOslNodes/CMakeLists.txt b/source/MaterialXGenOslNodes/CMakeLists.txt new file mode 100644 index 0000000000..0999bdda16 --- /dev/null +++ b/source/MaterialXGenOslNodes/CMakeLists.txt @@ -0,0 +1,28 @@ +file(GLOB GenNodes_SRC "${CMAKE_CURRENT_SOURCE_DIR}/LibsToOso.cpp") + +set(MATERIALX_LIBRARIES + MaterialXCore + MaterialXFormat + MaterialXGenShader + MaterialXGenOsl + MaterialXRenderOsl) + +add_executable(MaterialXGenOslNodes_LibsToOso ${GenNodes_SRC}) + +target_link_libraries( + MaterialXGenOslNodes_LibsToOso + ${MATERIALX_LIBRARIES}) + +set_target_properties( + MaterialXGenOslNodes_LibsToOso PROPERTIES + INSTALL_RPATH "${MATERIALX_UP_ONE_RPATH}") + +# TODO: We likely want to install that file elsewhere and not under `bin`, +# if at all, as we maybe want to keep this executable available at build time only. +install(TARGETS MaterialXGenOslNodes_LibsToOso + EXPORT MaterialX + RUNTIME DESTINATION ${MATERIALX_INSTALL_BIN_PATH}) +if(MSVC) + install(FILES $ + DESTINATION ${MATERIALX_INSTALL_BIN_PATH} OPTIONAL) +endif() diff --git a/source/MaterialXGenOslNodes/LibsToOso.cpp b/source/MaterialXGenOslNodes/LibsToOso.cpp new file mode 100644 index 0000000000..ce542a8b0d --- /dev/null +++ b/source/MaterialXGenOslNodes/LibsToOso.cpp @@ -0,0 +1,331 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#include +#include + +#include +#include + +#include +#include + +#include + +#include +#include + +#include + +namespace mx = MaterialX; + +const std::string options = + " Options: \n" + " --oslOutputPath [DIRPATH] TODO\n" + " --osoOutputPath [DIRPATH] TODO\n" + " --oslCompilerPath [FILEPATH] TODO\n" + " --oslIncludePath [DIRPATH] TODO\n" + " --libraries [STRING] TODO\n" + " --removeNdPrefix [BOOLEAN] TODO\n" + " --prefix [STRING] TODO\n" + " --help Display the complete list of command-line options\n"; + +template void parseToken(std::string token, std::string type, T& res) +{ + if (token.empty()) + return; + + mx::ValuePtr value = mx::Value::createValueFromStrings(token, type); + + if (!value) + { + std::cout << "Unable to parse token " << token << " as type " << type << std::endl; + + return; + } + + res = value->asA(); +} + +int main(int argc, char* const argv[]) +{ + std::vector tokens; + + // Gather the provided arguments. + for (int i = 1; i < argc; i++) + { + tokens.emplace_back(argv[i]); + } + + std::string argOslOutputPath; + std::string argOsoOutputPath; + std::string argOslCompilerPath; + std::string argOslIncludePath; + std::string argLibraries; + bool argRemoveNdPrefix = false; + std::string argPrefix; + + // Loop over the provided arguments, and store their associated values. + for (size_t i = 0; i < tokens.size(); i++) + { + const std::string& token = tokens[i]; + const std::string& nextToken = i + 1 < tokens.size() ? tokens[i + 1] : mx::EMPTY_STRING; + + if (token == "--oslOutputPath") + argOslOutputPath = nextToken; + else if (token == "--osoOutputPath") + argOsoOutputPath = nextToken; + else if (token == "--oslCompilerPath") + argOslCompilerPath = nextToken; + else if (token == "--oslIncludePath") + argOslIncludePath = nextToken; + else if (token == "--libraries") + argLibraries = nextToken; + else if (token == "--removeNdPrefix") + parseToken(nextToken, "boolean", argRemoveNdPrefix); + else if (token == "--prefix") + argPrefix = nextToken; + else if (token == "--help") + { + std::cout << "MaterialXGenOslNodes - LibsToOso version " << mx::getVersionString() << std::endl; + std::cout << options << std::endl; + + return 0; + } + else + { + std::cout << "Unrecognized command-line option: " << token << std::endl; + std::cout << "Launch the graph editor with '--help' for a complete list of supported " + "options." + << std::endl; + + continue; + } + + if (nextToken.empty()) + std::cout << "Expected another token following command-line option: " << token << std::endl; + else + i++; + } + + // TODO: Debug prints, to be removed. + std::cout << "MaterialXGenOslNodes - LibsToOso" << std::endl; + std::cout << "\toslOutputPath: " << argOslOutputPath << std::endl; + std::cout << "\tosoOutputPath: " << argOsoOutputPath << std::endl; + std::cout << "\toslCompilerPath: " << argOslCompilerPath << std::endl; + std::cout << "\toslIncludePath: " << argOslIncludePath << std::endl; + std::cout << "\tlibraries: " << argLibraries << std::endl; + std::cout << "\tremoveNdPrefix: " << argRemoveNdPrefix << std::endl; + std::cout << "\tprefix: " << argPrefix << std::endl; + + // Ensure we have a valid OSL output path. + mx::FilePath oslOutputPath(argOslOutputPath); + + if (!oslOutputPath.exists() || !oslOutputPath.isDirectory()) + { + oslOutputPath.createDirectory(); + + if (!oslOutputPath.exists() || !oslOutputPath.isDirectory()) + { + std::cerr << "Failed to find and/or create the provided OSL output " + "path: " + << oslOutputPath.asString() << std::endl; + + return 1; + } + } + + // If provided, ensure we have a valid OSO output path, otherwise use the OSL output path. + mx::FilePath osoOutputPath(argOsoOutputPath); + + if (osoOutputPath.isEmpty()) + { + osoOutputPath = oslOutputPath; + } + else + { + if (!osoOutputPath.exists() || !osoOutputPath.isDirectory()) + { + osoOutputPath.createDirectory(); + + if (!osoOutputPath.exists() || !osoOutputPath.isDirectory()) + { + std::cerr << "Failed to find and/or create the provided OSO output " + "path: " + << osoOutputPath.asString() << std::endl; + + return 1; + } + } + } + + // Ensure we have a valid path to the OSL compiler. + mx::FilePath oslCompilerPath(argOslCompilerPath); + + if (!oslCompilerPath.exists()) + { + std::cerr << "The provided path to the OSL compiler is not valid: " << oslCompilerPath.asString() << std::endl; + + return 1; + } + + // Ensure we have a valid path to the OSL includes. + mx::FilePath oslIncludePath(argOslIncludePath); + + if (!oslIncludePath.exists() || !oslIncludePath.isDirectory()) + { + std::cerr << "The provided path to the OSL includes is not valid: " << oslIncludePath.asString() << std::endl; + + return 1; + } + + // Create the libraries search path and document. + mx::FileSearchPath librariesSearchPath = mx::getDefaultDataSearchPath(); + mx::DocumentPtr librariesDoc = mx::createDocument(); + + // If a list of comma separated libraries was provided, load them individually into our document. + if (!argLibraries.empty()) + { + // TODO: Should we check that we actually split something based on the separator, just to be sure? + const mx::StringVec& librariesVec = mx::splitString(argLibraries, ","); + mx::FilePathVec librariesPaths{ "libraries/targets" }; + + for (const std::string& library : librariesVec) + librariesPaths.emplace_back("libraries/" + library); + + loadLibraries(librariesPaths, librariesSearchPath, librariesDoc); + } + // Otherwise, simply load all the available libraries. + else + loadLibraries({ "libraries" }, librariesSearchPath, librariesDoc); + + // Create and setup the `OslRenderer` that will be used to both generate the `.osl` files as well as compile + // them to `.oso` files. + mx::OslRendererPtr oslRenderer = mx::OslRenderer::create(); + oslRenderer->setOslCompilerExecutable(oslCompilerPath); + oslRenderer->setOslOutputFilePath(osoOutputPath); + + // Build the list of include paths that will be passed to the `OslRenderer`. + mx::FileSearchPath oslRendererIncludePaths; + + // Add the provided OSL include path. + oslRendererIncludePaths.append(oslIncludePath); + // Add the MaterialX's OSL include path. + oslRendererIncludePaths.append(librariesSearchPath.find("libraries/stdlib/genosl/include")); + + oslRenderer->setOslIncludePath(oslRendererIncludePaths); + + // Create the OSL shader generator. + mx::ShaderGeneratorPtr oslShaderGen = mx::OslShaderGenerator::create(); + + // Register types from the libraries on the OSL shader generator. + oslShaderGen->registerTypeDefs(librariesDoc); + + // Setup the context of the OSL shader generator. + mx::GenContext context(oslShaderGen); + context.registerSourceCodeSearchPath(librariesSearchPath); + // TODO: It might be good to find a way to not hardcode these options, especially the texture flip. + context.getOptions().addUpstreamDependencies = false; + context.getOptions().fileTextureVerticalFlip = true; + + // We'll use this boolean to return an error code is one of the `NodeDef` failed to codegen/compile. + bool hasFailed = false; + + // We create and use a dedicated `NodeGraph` to avoid `NodeDef` names collision. + mx::NodeGraphPtr librariesDocGraph = librariesDoc->addNodeGraph("librariesDocGraph"); + + // Loop over all the `NodeDef` gathered in our documents from the provided libraries. + for (const mx::NodeDefPtr& nodeDef : librariesDoc->getNodeDefs()) + { + std::string nodeName = nodeDef->getName(); + + // Remove the "ND_" prefix from a valid `NodeDef` name. + if (argRemoveNdPrefix) + { + if (nodeName.size() > 3 && nodeName.substr(0, 3) == "ND_") + nodeName = nodeName.substr(3); + + // Add a prefix to the shader's name, both in the filename as well as inside the shader itself. + if (!argPrefix.empty()) + nodeName = argPrefix + "_" + nodeName; + } + + // Determine whether or not there's a valid implementation of the current `NodeDef` for the type associated + // to our OSL shader generator, i.e. OSL, and if not, skip it. + mx::InterfaceElementPtr nodeImpl = nodeDef->getImplementation(oslShaderGen->getTarget()); + + if (!nodeImpl) + { + std::cout << "The following `NodeDef` does not provide a valid OSL implementation, " + "and will be skipped: " + << nodeDef->getName() << std::endl; + + continue; + } + + // TODO: Check for the existence/validity of the `Node`? + mx::NodePtr node = librariesDocGraph->addNodeInstance(nodeDef, nodeName); + + const std::string& oslFileName = nodeName + ".osl"; + const std::string& oslFilePath = (oslOutputPath / oslFileName).asString(); + std::ofstream oslFile; + + // Codegen the `Node` to an `.osl` file. + try + { + // Codegen the `Node` to OSL. + mx::ShaderPtr oslShader = oslShaderGen->generate(node->getName(), node, context); + + // TODO: Check that we have a valid/opened file descriptor before doing anything with it? + oslFile.open(oslFilePath); + // Dump the content of the codegen'd `NodeDef` to our `.osl` file. + oslFile << oslShader->getSourceCode(); + oslFile.close(); + } + // Catch any codegen/compilation related exceptions. + catch (mx::ExceptionShaderGenError& exc) + { + std::cerr << "Encountered a shader codegen related exception for the " + "following node: " + << nodeDef->getName() << std::endl; + std::cerr << exc.what() << std::endl; + + hasFailed = true; + } + + // Compile the codegen'd `.osl` file. + try + { + // Compile the `.osl` file to a `.oso` file next to it. + oslRenderer->compileOSL(oslFilePath); + } + // Catch any codegen/compilation related exceptions. + catch (mx::ExceptionRenderError& exc) + { + std::cerr << "Encountered a shader compilation related exception for the " + "following node: " + << nodeDef->getName() << std::endl; + std::cerr << exc.what() << std::endl; + + // Dump details about the exception in the log file. + for (const std::string& error : exc.errorLog()) + std::cerr << error << std::endl; + + hasFailed = true; + } + } + + // If something went wrong, return an appropriate error code. + if (hasFailed) + { + std::cerr << "Failed to codegen and compile all the OSL shaders associated to the provided MaterialX " + "libraries, see the log file for more details." + << std::endl; + + return 1; + } + + return 0; +}