From b1b5d0a2e87c4c89d55b28b1a5511ed2a106b8c0 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Thu, 2 Jul 2026 17:32:02 -0700 Subject: [PATCH 01/19] Add native WGSL (genwgsl) shader generator Introduce a native WebGPU/WGSL shader generator as a new "genwgsl" target, replacing the old GLSL-rewriting WgslShaderGenerator in MaterialXGenGlsl. - MaterialXGenWgsl is HwShaderGenerator based generator that emits complete standalone WGSL so that there is no transpiling required. - MATERIALX_BUILD_GEN_WGSL option to enable this project - Add js and python binding. The main addition is a helper python script that uses naga to convert glsl data library to wgsl data library. The script is not a general purpose converter. It run in few phases 1. wraps a glsl fragment into a shader and uses naga to generate wgsl. 2. the generated wgsl is cleaned up to keep doc comments, and maintian similar code style as glsl 3. Add a banner to alert future authors to not edit the wgsl directly. Note: only stright forward simple conversion is done. there are cases with glsl overloads in math and lib that have hand written wgsl counterparts. see source\MaterialXGenWgsl\tools\glsl_to_wgsl.py and associated readme.md --- CMakeLists.txt | 10 +- source/JsMaterialX/CMakeLists.txt | 11 +- .../JsWgslShaderGenerator.cpp | 4 +- .../WgslResourceBindingContext.cpp | 105 --- .../WgslResourceBindingContext.h | 39 - .../MaterialXGenGlsl/WgslShaderGenerator.cpp | 67 -- source/MaterialXGenGlsl/WgslShaderGenerator.h | 52 -- source/MaterialXGenGlsl/WgslSyntax.cpp | 192 ----- source/MaterialXGenGlsl/WgslSyntax.h | 27 - source/MaterialXGenWgsl/CMakeLists.txt | 102 +++ source/MaterialXGenWgsl/Export.h | 22 + .../Nodes/WgslCompoundNode.cpp | 150 ++++ .../MaterialXGenWgsl/Nodes/WgslCompoundNode.h | 28 + .../MaterialXGenWgsl/Nodes/WgslLightNodes.cpp | 128 +++ .../MaterialXGenWgsl/Nodes/WgslLightNodes.h | 47 ++ .../Nodes/WgslMaterialNode.cpp | 54 ++ .../MaterialXGenWgsl/Nodes/WgslMaterialNode.h | 31 + .../Nodes/WgslSourceCodeNode.cpp | 110 +++ .../Nodes/WgslSourceCodeNode.h | 32 + .../Nodes/WgslSurfaceNode.cpp | 218 +++++ .../MaterialXGenWgsl/Nodes/WgslSurfaceNode.h | 26 + source/MaterialXGenWgsl/README.md | 50 ++ .../WgslResourceBindingContext.cpp | 129 +++ .../WgslResourceBindingContext.h | 53 ++ .../MaterialXGenWgsl/WgslShaderGenerator.cpp | 772 ++++++++++++++++++ source/MaterialXGenWgsl/WgslShaderGenerator.h | 140 ++++ source/MaterialXGenWgsl/WgslSyntax.cpp | 398 +++++++++ source/MaterialXGenWgsl/WgslSyntax.h | 69 ++ source/MaterialXGenWgsl/tools/README.md | 112 +++ source/MaterialXGenWgsl/tools/glsl_to_wgsl.py | 631 ++++++++++++++ source/MaterialXTest/CMakeLists.txt | 6 +- .../MaterialXGenGlsl/GenGlsl.cpp | 16 +- .../MaterialXGenWgsl/CMakeLists.txt | 9 + .../MaterialXGenWgsl/GenWgsl.cpp | 88 ++ .../PyGlslShaderGenerator.cpp | 16 - .../PyMaterialXGenGlsl/PyModule.cpp | 2 - 36 files changed, 3425 insertions(+), 521 deletions(-) delete mode 100644 source/MaterialXGenGlsl/WgslResourceBindingContext.cpp delete mode 100644 source/MaterialXGenGlsl/WgslResourceBindingContext.h delete mode 100644 source/MaterialXGenGlsl/WgslShaderGenerator.cpp delete mode 100644 source/MaterialXGenGlsl/WgslShaderGenerator.h delete mode 100644 source/MaterialXGenGlsl/WgslSyntax.cpp delete mode 100644 source/MaterialXGenGlsl/WgslSyntax.h create mode 100644 source/MaterialXGenWgsl/CMakeLists.txt create mode 100644 source/MaterialXGenWgsl/Export.h create mode 100644 source/MaterialXGenWgsl/Nodes/WgslCompoundNode.cpp create mode 100644 source/MaterialXGenWgsl/Nodes/WgslCompoundNode.h create mode 100644 source/MaterialXGenWgsl/Nodes/WgslLightNodes.cpp create mode 100644 source/MaterialXGenWgsl/Nodes/WgslLightNodes.h create mode 100644 source/MaterialXGenWgsl/Nodes/WgslMaterialNode.cpp create mode 100644 source/MaterialXGenWgsl/Nodes/WgslMaterialNode.h create mode 100644 source/MaterialXGenWgsl/Nodes/WgslSourceCodeNode.cpp create mode 100644 source/MaterialXGenWgsl/Nodes/WgslSourceCodeNode.h create mode 100644 source/MaterialXGenWgsl/Nodes/WgslSurfaceNode.cpp create mode 100644 source/MaterialXGenWgsl/Nodes/WgslSurfaceNode.h create mode 100644 source/MaterialXGenWgsl/README.md create mode 100644 source/MaterialXGenWgsl/WgslResourceBindingContext.cpp create mode 100644 source/MaterialXGenWgsl/WgslResourceBindingContext.h create mode 100644 source/MaterialXGenWgsl/WgslShaderGenerator.cpp create mode 100644 source/MaterialXGenWgsl/WgslShaderGenerator.h create mode 100644 source/MaterialXGenWgsl/WgslSyntax.cpp create mode 100644 source/MaterialXGenWgsl/WgslSyntax.h create mode 100644 source/MaterialXGenWgsl/tools/README.md create mode 100644 source/MaterialXGenWgsl/tools/glsl_to_wgsl.py create mode 100644 source/MaterialXTest/MaterialXGenWgsl/CMakeLists.txt create mode 100644 source/MaterialXTest/MaterialXGenWgsl/GenWgsl.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index acb1afbc67..a55bd667d6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,6 +43,7 @@ option(MATERIALX_BUILD_GEN_OSL "Build the OSL shader generator back-ends." 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_GEN_SLANG "Build the Slang shader generator back-end." ON) +option(MATERIALX_BUILD_GEN_WGSL "Build the WGSL (WebGPU) shader generator back-end." ON) option(MATERIALX_BUILD_RENDER "Build the MaterialX Render modules." ON) option(MATERIALX_BUILD_RENDER_PLATFORMS "Build platform-specific render modules for each shader generator." ON) option(MATERIALX_BUILD_OIIO "Build OpenImageIO support for MaterialXRender." OFF) @@ -176,6 +177,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_SLANG) +mark_as_advanced(MATERIALX_BUILD_GEN_WGSL) mark_as_advanced(MATERIALX_BUILD_GEN_OSL) mark_as_advanced(MATERIALX_BUILD_GEN_MDL) mark_as_advanced(MATERIALX_BUILD_GEN_MSL) @@ -527,8 +529,8 @@ endif() # 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 OR MATERIALX_BUILD_GEN_SLANG) - if(MATERIALX_BUILD_GEN_GLSL OR MATERIALX_BUILD_GEN_MSL OR MATERIALX_BUILD_GEN_SLANG) +if(MATERIALX_BUILD_GEN_GLSL OR MATERIALX_BUILD_GEN_OSL OR MATERIALX_BUILD_GEN_MDL OR MATERIALX_BUILD_GEN_MSL OR MATERIALX_BUILD_GEN_SLANG OR MATERIALX_BUILD_GEN_WGSL) + if(MATERIALX_BUILD_GEN_GLSL OR MATERIALX_BUILD_GEN_MSL OR MATERIALX_BUILD_GEN_SLANG OR MATERIALX_BUILD_GEN_WGSL) add_subdirectory(source/MaterialXGenHw) endif() if (MATERIALX_BUILD_GEN_GLSL) @@ -539,6 +541,10 @@ if(MATERIALX_BUILD_GEN_GLSL OR MATERIALX_BUILD_GEN_OSL OR MATERIALX_BUILD_GEN_MD add_definitions(-DMATERIALX_BUILD_GEN_SLANG) add_subdirectory(source/MaterialXGenSlang) endif() + if (MATERIALX_BUILD_GEN_WGSL) + add_definitions(-DMATERIALX_BUILD_GEN_WGSL) + add_subdirectory(source/MaterialXGenWgsl) + endif() if (MATERIALX_BUILD_GEN_OSL) add_definitions(-DMATERIALX_BUILD_GEN_OSL) add_subdirectory(source/MaterialXGenOsl) diff --git a/source/JsMaterialX/CMakeLists.txt b/source/JsMaterialX/CMakeLists.txt index 60ecd9048a..091db4b311 100644 --- a/source/JsMaterialX/CMakeLists.txt +++ b/source/JsMaterialX/CMakeLists.txt @@ -102,8 +102,12 @@ add_executable(JsMaterialXGenShader MaterialXLib.cpp ${GENESSL_DEPS}) if (MATERIALX_BUILD_GEN_GLSL) - message("JS: Building JsMaterialXGenShader with GLSL, VK, WGSL support") - target_sources(JsMaterialXGenShader PRIVATE ${GENGLSL_DEPS} ${GENVK_DEPS} ${GENWGSL_DEPS}) + message("JS: Building JsMaterialXGenShader with GLSL, VK support") + target_sources(JsMaterialXGenShader PRIVATE ${GENGLSL_DEPS} ${GENVK_DEPS}) +endif() +if (MATERIALX_BUILD_GEN_WGSL) + message("JS: Building JsMaterialXGenShader with WGSL support") + target_sources(JsMaterialXGenShader PRIVATE ${GENWGSL_DEPS}) endif() if (MATERIALX_BUILD_GEN_OSL) message("JS: Building JsMaterialXGenShader with OSL support") @@ -163,6 +167,9 @@ endif() if (MATERIALX_BUILD_GEN_SLANG) target_link_libraries(JsMaterialXGenShader PUBLIC MaterialXGenSlang) endif() +if (MATERIALX_BUILD_GEN_WGSL) + target_link_libraries(JsMaterialXGenShader PUBLIC MaterialXGenWgsl) +endif() # Install the JavaScript output install(TARGETS JsMaterialXCore DESTINATION "JavaScript/MaterialX") diff --git a/source/JsMaterialX/JsMaterialXGenWgsl/JsWgslShaderGenerator.cpp b/source/JsMaterialX/JsMaterialXGenWgsl/JsWgslShaderGenerator.cpp index b0c05d3228..34cff9b5be 100644 --- a/source/JsMaterialX/JsMaterialXGenWgsl/JsWgslShaderGenerator.cpp +++ b/source/JsMaterialX/JsMaterialXGenWgsl/JsWgslShaderGenerator.cpp @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // -#include +#include #include #include @@ -22,6 +22,6 @@ namespace EMSCRIPTEN_BINDINGS(WgslShaderGenerator) { - ems::class_>("WgslShaderGenerator") + ems::class_>("WgslShaderGenerator") .class_function("create", &WgslShaderGenerator_create); } diff --git a/source/MaterialXGenGlsl/WgslResourceBindingContext.cpp b/source/MaterialXGenGlsl/WgslResourceBindingContext.cpp deleted file mode 100644 index b9ceb270a4..0000000000 --- a/source/MaterialXGenGlsl/WgslResourceBindingContext.cpp +++ /dev/null @@ -1,105 +0,0 @@ -// -// Copyright Contributors to the MaterialX Project -// SPDX-License-Identifier: Apache-2.0 -// - -#include - -#include -#include - -MATERIALX_NAMESPACE_BEGIN - -// -// WgslResourceBindingContext methods -// - -WgslResourceBindingContext::WgslResourceBindingContext(size_t uniformBindingLocation) : - VkResourceBindingContext(uniformBindingLocation) -{ -} - -// Copied from VkResourceBindingContext::emitResourceBindings(). -// Modified the Type::FILENAME uniform codegen. -void WgslResourceBindingContext::emitResourceBindings(GenContext& context, const VariableBlock& uniforms, ShaderStage& stage) -{ - const ShaderGenerator& generator = context.getShaderGenerator(); - const Syntax& syntax = generator.getSyntax(); - - // First, emit all value uniforms in a block with single layout binding - bool hasValueUniforms = false; - for (auto uniform : uniforms.getVariableOrder()) - { - if (uniform->getType() != Type::FILENAME) - { - hasValueUniforms = true; - break; - } - } - if (hasValueUniforms) - { - generator.emitLine("layout (std140, binding=" + std::to_string(_hwUniformBindLocation++) + ") " + - syntax.getUniformQualifier() + " " + uniforms.getName() + "_" + stage.getName(), - stage, false); - generator.emitScopeBegin(stage); - for (auto uniform : uniforms.getVariableOrder()) - { - if (uniform->getType() != Type::FILENAME) - { - if ( uniform->getType() == Type::BOOLEAN ) - { - // Cannot have boolean uniforms in WGSL - std::cerr << "Warning: WGSL does not allow boolean types to be stored in uniform or storage address spaces." << std::endl; - - // Set uniform type to integer - uniform->setType( Type::INTEGER ); - - // Write declaration as normal - generator.emitLineBegin(stage); - generator.emitVariableDeclaration(uniform, EMPTY_STRING, context, stage, false); - generator.emitString(Syntax::SEMICOLON, stage); - generator.emitLineEnd(stage, false); - - // Add macro to treat any follow usages of this variable as a boolean - // eg. u_myUniformBool -> bool(u_myUniformBool) - generator.emitString("#define " + uniform->getVariable() + " bool(" + uniform->getVariable() + ")", stage); - generator.emitLineBreak(stage); - } - else - { - generator.emitLineBegin(stage); - generator.emitVariableDeclaration(uniform, EMPTY_STRING, context, stage, false); - generator.emitString(Syntax::SEMICOLON, stage); - generator.emitLineEnd(stage, false); - } - - } - } - generator.emitScopeEnd(stage, true); - } - - // Second, emit all sampler uniforms as separate uniforms with separate layout bindings - for (auto uniform : uniforms.getVariableOrder()) - { - if (uniform->getType() == Type::FILENAME) - { - // Bind separately as texture2D + sampler - // - // NOTE: the *_texture and *_sampler binding names method below expect that - // variables from HwShaderGenerator.cpp (HW::ENV_RADIANCE_SPLIT and HW::ENV_IRRADIANCE_SPLIT) - // use the same naming convention as here. - // - generator.emitString("layout (binding=" + std::to_string(_hwUniformBindLocation++) + ") " + syntax.getUniformQualifier() + " ", stage); - generator.emitString(string("texture2D ")+uniform->getVariable()+"_texture", stage); - generator.emitLineEnd(stage, true); - - generator.emitString("layout (binding=" + std::to_string(_hwUniformBindLocation++) + ") " + syntax.getUniformQualifier() + " ", stage); - generator.emitString(string("sampler ")+uniform->getVariable()+"_sampler", stage); - generator.emitLineEnd(stage, true); - } - } - - generator.emitLineBreak(stage); -} - -MATERIALX_NAMESPACE_END diff --git a/source/MaterialXGenGlsl/WgslResourceBindingContext.h b/source/MaterialXGenGlsl/WgslResourceBindingContext.h deleted file mode 100644 index 7b39b65661..0000000000 --- a/source/MaterialXGenGlsl/WgslResourceBindingContext.h +++ /dev/null @@ -1,39 +0,0 @@ -// -// Copyright Contributors to the MaterialX Project -// SPDX-License-Identifier: Apache-2.0 -// - -#ifndef MATERIALX_WGSLRESOURCEBINDING_H -#define MATERIALX_WGSLRESOURCEBINDING_H - -/// @file -/// Vulkan GLSL resource binding context for WGSL - -#include - -#include - -MATERIALX_NAMESPACE_BEGIN - -/// Shared pointer to a WgslResourceBindingContext -using WgslResourceBindingContextPtr = shared_ptr; - -/// @class WgslResourceBindingContext -/// Class representing a resource binding for Vulkan Glsl shader resources. -class MX_GENGLSL_API WgslResourceBindingContext : public VkResourceBindingContext -{ - public: - WgslResourceBindingContext(size_t uniformBindingLocation); - - static WgslResourceBindingContextPtr create(size_t uniformBindingLocation = 0) - { - return std::make_shared(uniformBindingLocation); - } - - // Emit uniforms with binding information - void emitResourceBindings(GenContext& context, const VariableBlock& uniforms, ShaderStage& stage) override; -}; - -MATERIALX_NAMESPACE_END - -#endif diff --git a/source/MaterialXGenGlsl/WgslShaderGenerator.cpp b/source/MaterialXGenGlsl/WgslShaderGenerator.cpp deleted file mode 100644 index d39a2498f4..0000000000 --- a/source/MaterialXGenGlsl/WgslShaderGenerator.cpp +++ /dev/null @@ -1,67 +0,0 @@ -// -// Copyright Contributors to the MaterialX Project -// SPDX-License-Identifier: Apache-2.0 -// - -#include -#include - -#include - -MATERIALX_NAMESPACE_BEGIN - -const string WgslShaderGenerator::LIGHTDATA_TYPEVAR_STRING = "light_type"; - -WgslShaderGenerator::WgslShaderGenerator(TypeSystemPtr typeSystem) : - VkShaderGenerator(typeSystem) -{ - _syntax = WgslSyntax::create(typeSystem); - - // Set binding context to handle resource binding layouts - _resourceBindingCtx = std::make_shared(0); - - // For functions described in ::emitSpecularEnvironment() - // override map value from HwShaderGenerator - _tokenSubstitutions[HW::T_ENV_RADIANCE] = HW::ENV_RADIANCE_SPLIT; - _tokenSubstitutions[HW::T_ENV_RADIANCE_SAMPLER2D] = HW::ENV_RADIANCE_SAMPLER2D_SPLIT; - _tokenSubstitutions[HW::T_ENV_IRRADIANCE] = HW::ENV_IRRADIANCE_SPLIT; - _tokenSubstitutions[HW::T_ENV_IRRADIANCE_SAMPLER2D] = HW::ENV_IRRADIANCE_SAMPLER2D_SPLIT; - _tokenSubstitutions[HW::T_TEX_SAMPLER_SAMPLER2D] = HW::TEX_SAMPLER_SAMPLER2D_SPLIT; - _tokenSubstitutions[HW::T_TEX_SAMPLER_SIGNATURE] = HW::TEX_SAMPLER_SIGNATURE_SPLIT; -} - -void WgslShaderGenerator::emitDirectives(GenContext& context, ShaderStage& stage) const -{ - VkShaderGenerator::emitDirectives(context, stage); - // Add additional directives and #define statements here - // Example: emitLine("#define HW_SEPARATE_SAMPLERS", stage, false); - emitLineBreak(stage); -} - -// Called by CompoundNode::emitFunctionDefinition() -void WgslShaderGenerator::emitFunctionDefinitionParameter(const ShaderPort* shaderPort, bool isOutput, GenContext& context, ShaderStage& stage) const -{ - if (shaderPort->getType() == Type::FILENAME) - { - emitString("texture2D " + shaderPort->getVariable() + "_texture, sampler "+shaderPort->getVariable() + "_sampler", stage); - } - else - { - VkShaderGenerator::emitFunctionDefinitionParameter(shaderPort, isOutput, context, stage); - } -} - -// Called by SourceCodeNode::emitFunctionCall() -void WgslShaderGenerator::emitInput(const ShaderInput* input, GenContext& context, ShaderStage& stage) const -{ - if (input->getType() == Type::FILENAME) - { - emitString(getUpstreamResult(input, context)+"_texture, "+getUpstreamResult(input, context)+"_sampler", stage); - } - else - { - VkShaderGenerator::emitInput(input, context, stage); - } -} - -MATERIALX_NAMESPACE_END diff --git a/source/MaterialXGenGlsl/WgslShaderGenerator.h b/source/MaterialXGenGlsl/WgslShaderGenerator.h deleted file mode 100644 index af9141f440..0000000000 --- a/source/MaterialXGenGlsl/WgslShaderGenerator.h +++ /dev/null @@ -1,52 +0,0 @@ -// -// Copyright Contributors to the MaterialX Project -// SPDX-License-Identifier: Apache-2.0 -// - -#ifndef MATERIALX_WGSLSHADERGENERATOR_H -#define MATERIALX_WGSLSHADERGENERATOR_H - -/// @file -/// Vulkan GLSL shader generator flavor for WGSL - -#include -#include -#include - -MATERIALX_NAMESPACE_BEGIN - -using WgslShaderGeneratorPtr = shared_ptr; - -/// @class WgslShaderGenerator -/// WGSL Flavor of Vulkan GLSL shader generator -class MX_GENGLSL_API WgslShaderGenerator : public VkShaderGenerator -{ - public: - /// Constructor. - WgslShaderGenerator(TypeSystemPtr typeSystem); - - /// Creator function. - /// If a TypeSystem is not provided it will be created internally. - /// Optionally pass in an externally created TypeSystem here, - /// if you want to keep type descriptions alive after the lifetime - /// of the shader generator. - static ShaderGeneratorPtr create(TypeSystemPtr typeSystem = nullptr) - { - return std::make_shared(typeSystem ? typeSystem : TypeSystem::create()); - } - - void emitDirectives(GenContext& context, ShaderStage& stage) const override; - - const string& getLightDataTypevarString() const override { return LIGHTDATA_TYPEVAR_STRING; } - - void emitFunctionDefinitionParameter(const ShaderPort* shaderPort, bool isOutput, GenContext& context, ShaderStage& stage) const override; - - void emitInput(const ShaderInput* input, GenContext& context, ShaderStage& stage) const override; - - protected: - static const string LIGHTDATA_TYPEVAR_STRING; -}; - -MATERIALX_NAMESPACE_END - -#endif diff --git a/source/MaterialXGenGlsl/WgslSyntax.cpp b/source/MaterialXGenGlsl/WgslSyntax.cpp deleted file mode 100644 index d5d58dc014..0000000000 --- a/source/MaterialXGenGlsl/WgslSyntax.cpp +++ /dev/null @@ -1,192 +0,0 @@ -// -// Copyright Contributors to the MaterialX Project -// SPDX-License-Identifier: Apache-2.0 -// - -#include - -MATERIALX_NAMESPACE_BEGIN - -WgslSyntax::WgslSyntax(TypeSystemPtr typeSystem) : VkSyntax(typeSystem) -{ - // Add in WGSL specific keywords - registerReservedWords( { - // Keywords (https://www.w3.org/TR/WGSL/#keyword-summary) - "alias", - "break", - "case", - "const", - "const_assert", - "continue", - "continuing", - "default", - "diagnostic", - "discard", - "else", - "enable", - "false", - "fn", - "for", - "if", - "let", - "loop", - "override", - "requires", - "return", - "struct", - "switch", - "true", - "var", - "while", - // Reserved Words (https://www.w3.org/TR/WGSL/#reserved-words) - "NULL", - "Self", - "abstract", - "active", - "alignas", - "alignof", - "as", - "asm", - "asm_fragment", - "async", - "attribute", - "auto", - "await", - "become", - "cast", - "catch", - "class", - "co_await", - "co_return", - "co_yield", - "coherent", - "column_major", - "common", - "compile", - "compile_fragment", - "concept", - "const_cast", - "consteval", - "constexpr", - "constinit", - "crate", - "debugger", - "decltype", - "delete", - "demote", - "demote_to_helper", - "do", - "dynamic_cast", - "enum", - "explicit", - "export", - "extends", - "extern", - "external", - "fallthrough", - "filter", - "final", - "finally", - "friend", - "from", - "fxgroup", - "get", - "goto", - "groupshared", - "highp", - "impl", - "implements", - "import", - "inline", - "instanceof", - "interface", - "layout", - "lowp", - "macro", - "macro_rules", - "match", - "mediump", - "meta", - "mod", - "module", - "move", - "mut", - "mutable", - "namespace", - "new", - "nil", - "noexcept", - "noinline", - "nointerpolation", - "non_coherent", - "noncoherent", - "noperspective", - "null", - "nullptr", - "of", - "operator", - "package", - "packoffset", - "partition", - "pass", - "patch", - "pixelfragment", - "precise", - "precision", - "premerge", - "priv", - "protected", - "pub", - "public", - "readonly", - "ref", - "regardless", - "register", - "reinterpret_cast", - "require", - "resource", - "restrict", - "self", - "set", - "shared", - "sizeof", - "smooth", - "snorm", - "static", - "static_assert", - "static_cast", - "std", - "subroutine", - "super", - "target", - "template", - "this", - "thread_local", - "throw", - "trait", - "try", - "type", - "typedef", - "typeid", - "typename", - "typeof", - "union", - "unless", - "unorm", - "unsafe", - "unsized", - "use", - "using", - "varying", - "virtual", - "volatile", - "wgsl", - "where", - "with", - "writeonly", - "yield" - } ); -} - - -MATERIALX_NAMESPACE_END diff --git a/source/MaterialXGenGlsl/WgslSyntax.h b/source/MaterialXGenGlsl/WgslSyntax.h deleted file mode 100644 index 0e16f221bd..0000000000 --- a/source/MaterialXGenGlsl/WgslSyntax.h +++ /dev/null @@ -1,27 +0,0 @@ -// -// Copyright Contributors to the MaterialX Project -// SPDX-License-Identifier: Apache-2.0 -// - -#ifndef MATERIALX_WGSLSYNTAX_H -#define MATERIALX_WGSLSYNTAX_H - -/// @file -/// Vulkan GLSL syntax class for WGSL - -#include - -MATERIALX_NAMESPACE_BEGIN - -/// Syntax class for Wgsl GLSL -class MX_GENGLSL_API WgslSyntax : public VkSyntax -{ - public: - WgslSyntax(TypeSystemPtr typeSystem); - - static SyntaxPtr create(TypeSystemPtr typeSystem) { return std::make_shared(typeSystem); } -}; - -MATERIALX_NAMESPACE_END - -#endif diff --git a/source/MaterialXGenWgsl/CMakeLists.txt b/source/MaterialXGenWgsl/CMakeLists.txt new file mode 100644 index 0000000000..32d7a39158 --- /dev/null +++ b/source/MaterialXGenWgsl/CMakeLists.txt @@ -0,0 +1,102 @@ +file(GLOB_RECURSE materialx_source "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") +file(GLOB_RECURSE materialx_headers "${CMAKE_CURRENT_SOURCE_DIR}/*.h*") + +mx_add_library(MaterialXGenWgsl + SOURCE_FILES + ${materialx_source} + HEADER_FILES + ${materialx_headers} + MTLX_MODULES + MaterialXGenHw + MaterialXCore + EXPORT_DEFINE + MATERIALX_GENWGSL_EXPORTS) + +# Optional build-time regeneration of the genwgsl shader-node library from genglsl. This runs the +# offline transpiler (tools/glsl_to_wgsl.py) over libraries/ into the build tree purely for +# validation: transpile or naga errors -- and any previously-generable node that regresses -- make +# the build fail. The committed genwgsl .wgsl files remain the source of truth (the output goes to +# the build dir, not the source tree). Requires a Python interpreter and the `naga` CLI. +if(MATERIALX_GENERATE_WGSL_LIBRARY) + if(MATERIALX_PYTHON_EXECUTABLE) + set(_wgsl_python "${MATERIALX_PYTHON_EXECUTABLE}") + else() + find_package(Python3 COMPONENTS Interpreter) + set(_wgsl_python "${Python3_EXECUTABLE}") + endif() + + # Locate the naga CLI. Precedence: an explicit -D/cache value, then the NAGA env var, then a + # search of PATH and the standard cargo bin directories. `cargo install naga-cli` lands naga in + # ~/.cargo/bin (or $CARGO_HOME/bin), which is frequently NOT on PATH in CI / IDE / GUI shells -- + # so a developer who installed naga still hits "not found" without these hints. + if(NOT MATERIALX_NAGA_EXECUTABLE AND DEFINED ENV{NAGA}) + set(MATERIALX_NAGA_EXECUTABLE "$ENV{NAGA}" CACHE FILEPATH + "Path to the naga CLI used to transpile/validate the genwgsl library." FORCE) + endif() + find_program(MATERIALX_NAGA_EXECUTABLE NAMES naga naga.exe + HINTS + "$ENV{CARGO_HOME}/bin" # custom cargo home (all platforms) + "$ENV{HOME}/.cargo/bin" # default on Linux / macOS + "$ENV{USERPROFILE}/.cargo/bin" # default on Windows + DOC "Path to the naga CLI used to transpile/validate the genwgsl library.") + + # If naga is still not found, obtain it via cargo. cargo is located EMSDK-style (mirrors + # MATERIALX_EMSDK_PATH): an explicit MATERIALX_CARGO_PATH, then $CARGO_HOME, then the standard + # cargo bin dirs, then PATH. cargo is assumed to already be installed -- we do NOT bootstrap + # Rust. When found, naga is built into the build tree at *build* time (not configure, so + # configure stays fast) and cached there across rebuilds. + set(_wgsl_install_naga OFF) + if(NOT MATERIALX_NAGA_EXECUTABLE) + find_program(MATERIALX_CARGO_EXECUTABLE NAMES cargo cargo.exe + HINTS + "${MATERIALX_CARGO_PATH}/bin" # explicit -DMATERIALX_CARGO_PATH + "$ENV{CARGO_HOME}/bin" # custom cargo home + "$ENV{HOME}/.cargo/bin" # default on Linux / macOS + "$ENV{USERPROFILE}/.cargo/bin" # default on Windows + DOC "Path to the Rust cargo executable used to install the naga CLI.") + if(MATERIALX_CARGO_EXECUTABLE) + if(CMAKE_HOST_WIN32) + set(_naga_exe "naga.exe") + else() + set(_naga_exe "naga") + endif() + set(_naga_root "${CMAKE_BINARY_DIR}/naga") + # Predicted install location -- the file may not exist yet; a plain set() (not + # find_program) so the generation target can reference it before it is built. + set(MATERIALX_NAGA_EXECUTABLE "${_naga_root}/bin/${_naga_exe}") + set(_wgsl_install_naga ON) + endif() + endif() + + if(_wgsl_python AND MATERIALX_NAGA_EXECUTABLE) + # Build-time, one-time cargo install of naga-cli (only when resolved to a cargo location). + # The custom-command OUTPUT is the naga binary, so it re-runs only when that file is missing. + if(_wgsl_install_naga) + add_custom_command(OUTPUT "${MATERIALX_NAGA_EXECUTABLE}" + COMMAND "${MATERIALX_CARGO_EXECUTABLE}" install naga-cli --root "${_naga_root}" + COMMENT "Installing naga-cli via cargo into ${_naga_root} (one-time; compiles from source)" + VERBATIM) + add_custom_target(MaterialXInstallNaga DEPENDS "${MATERIALX_NAGA_EXECUTABLE}") + endif() + + add_custom_target(MaterialXGenWgslLibrary ALL + COMMAND ${CMAKE_COMMAND} -E env "NAGA=${MATERIALX_NAGA_EXECUTABLE}" + "${_wgsl_python}" "${CMAKE_CURRENT_SOURCE_DIR}/tools/glsl_to_wgsl.py" + --libraries "${PROJECT_SOURCE_DIR}/libraries" + --out "${CMAKE_BINARY_DIR}/genwgsl_generated" + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + COMMENT "Transpiling genglsl shader-node library to genwgsl (validation)" + VERBATIM) + if(_wgsl_install_naga) + add_dependencies(MaterialXGenWgslLibrary MaterialXInstallNaga) + endif() + else() + message(WARNING + "MATERIALX_GENERATE_WGSL_LIBRARY is ON but a prerequisite was not found " + "(Python='${_wgsl_python}', naga='${MATERIALX_NAGA_EXECUTABLE}', " + "cargo='${MATERIALX_CARGO_EXECUTABLE}'). Install naga (`cargo install naga-cli`) and point " + "-DMATERIALX_NAGA_EXECUTABLE / the NAGA env var at it, or install cargo and set " + "-DMATERIALX_CARGO_PATH to its home so naga can be built into the tree. " + "Skipping genwgsl library generation.") + endif() +endif() diff --git a/source/MaterialXGenWgsl/Export.h b/source/MaterialXGenWgsl/Export.h new file mode 100644 index 0000000000..212a958ca7 --- /dev/null +++ b/source/MaterialXGenWgsl/Export.h @@ -0,0 +1,22 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#ifndef MATERIALX_GENWGSL_EXPORT_H +#define MATERIALX_GENWGSL_EXPORT_H + +#include + +/// @file +/// Macros for declaring imported and exported symbols. + +#if defined(MATERIALX_GENWGSL_EXPORTS) + #define MX_GENWGSL_API MATERIALX_SYMBOL_EXPORT + #define MX_GENWGSL_EXTERN_TEMPLATE(...) MATERIALX_EXPORT_EXTERN_TEMPLATE(__VA_ARGS__) +#else + #define MX_GENWGSL_API MATERIALX_SYMBOL_IMPORT + #define MX_GENWGSL_EXTERN_TEMPLATE(...) MATERIALX_IMPORT_EXTERN_TEMPLATE(__VA_ARGS__) +#endif + +#endif diff --git a/source/MaterialXGenWgsl/Nodes/WgslCompoundNode.cpp b/source/MaterialXGenWgsl/Nodes/WgslCompoundNode.cpp new file mode 100644 index 0000000000..c46326c70e --- /dev/null +++ b/source/MaterialXGenWgsl/Nodes/WgslCompoundNode.cpp @@ -0,0 +1,150 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include +#include +#include + +MATERIALX_NAMESPACE_BEGIN + +ShaderNodeImplPtr WgslCompoundNode::create() +{ + return std::make_shared(); +} + +void WgslCompoundNode::emitFunctionDefinition(const ShaderNode& node, GenContext& context, ShaderStage& stage) const +{ + DEFINE_SHADER_STAGE(stage, Stage::PIXEL) + { + const ShaderGenerator& shadergen = context.getShaderGenerator(); + + // Emit function definitions for all child nodes. + shadergen.emitFunctionDefinitions(*_rootGraph, context, stage); + + shadergen.emitLineBegin(stage); + shadergen.emitString("fn " + _functionName + "(", stage); + + // Closure data parameter (emits a trailing ", " when present). + shadergen.emitClosureDataParameter(node, context, stage); + + // Thread vertex data into closure/surface-shader functions. + const bool needsVertexData = nodeOutputIsClosure(node); + if (needsVertexData) + { + const VariableBlock& vertexData = stage.getInputBlock(HW::VERTEX_DATA); + const string vdTypeName = vertexData.empty() ? string("VertexData") : vertexData.getName(); + shadergen.emitString(vertexData.getInstance() + ": " + vdTypeName + ", ", stage); + } + + string delim; + for (ShaderGraphInputSocket* inputSocket : _rootGraph->getInputSockets()) + { + shadergen.emitString(delim, stage); + shadergen.emitFunctionDefinitionParameter(inputSocket, false, context, stage); + delim = ", "; + } + + // Output parameters become ptr so the caller passes &outVar. + for (ShaderGraphOutputSocket* outputSocket : _rootGraph->getOutputSockets()) + { + const string typeName = shadergen.getSyntax().getTypeName(outputSocket->getType()); + shadergen.emitString(delim + outputSocket->getVariable() + ": ptr", stage); + delim = ", "; + } + + shadergen.emitString(")", stage); + shadergen.emitLineEnd(stage, false); + + shadergen.emitFunctionBodyBegin(*_rootGraph, context, stage); + + if (nodeOutputIsClosure(node)) + { + shadergen.emitFunctionCalls(*_rootGraph, context, stage, ShaderNode::Classification::TEXTURE); + for (ShaderGraphOutputSocket* outputSocket : _rootGraph->getOutputSockets()) + { + if (outputSocket->getConnection()) + { + const ShaderNode* upstream = outputSocket->getConnection()->getNode(); + if (upstream->getParent() == _rootGraph.get() && + (upstream->hasClassification(ShaderNode::Classification::CLOSURE) || + upstream->hasClassification(ShaderNode::Classification::SHADER) || + upstream->hasClassification(ShaderNode::Classification::MATERIAL))) + { + shadergen.emitFunctionCall(*upstream, context, stage); + } + } + } + } + else + { + shadergen.emitFunctionCalls(*_rootGraph, context, stage); + } + + // Write results through the output pointers. + for (ShaderGraphOutputSocket* outputSocket : _rootGraph->getOutputSockets()) + { + const string result = shadergen.getUpstreamResult(outputSocket, context); + shadergen.emitLine("(*" + outputSocket->getVariable() + ") = " + result, stage); + } + + shadergen.emitFunctionBodyEnd(*_rootGraph, context, stage); + } +} + +void WgslCompoundNode::emitFunctionCall(const ShaderNode& node, GenContext& context, ShaderStage& stage) const +{ + const ShaderGenerator& shadergen = context.getShaderGenerator(); + + DEFINE_SHADER_STAGE(stage, Stage::VERTEX) + { + shadergen.emitFunctionCalls(*_rootGraph, context, stage); + } + + DEFINE_SHADER_STAGE(stage, Stage::PIXEL) + { + if (nodeOutputIsClosure(node)) + { + shadergen.emitDependentFunctionCalls(node, context, stage, ShaderNode::Classification::CLOSURE); + } + + // Declare the output variables. + emitOutputVariables(node, context, stage); + + shadergen.emitLineBegin(stage); + shadergen.emitString(_functionName + "(", stage); + + // Closure data argument (emits trailing ", " when present). + shadergen.emitClosureDataArg(node, context, stage); + + // Pass the vertex data instance into closure/surface-shader functions. + if (nodeOutputIsClosure(node)) + { + const VariableBlock& vertexData = stage.getInputBlock(HW::VERTEX_DATA); + shadergen.emitString(vertexData.getInstance() + ", ", stage); + } + + string delim; + for (ShaderInput* input : node.getInputs()) + { + shadergen.emitString(delim, stage); + shadergen.emitInput(input, context, stage); + delim = ", "; + } + for (size_t i = 0; i < node.numOutputs(); ++i) + { + shadergen.emitString(delim, stage); + shadergen.emitOutput(node.getOutput(i), false, false, context, stage); + delim = ", "; + } + + shadergen.emitString(")", stage); + shadergen.emitLineEnd(stage); + } +} + +MATERIALX_NAMESPACE_END diff --git a/source/MaterialXGenWgsl/Nodes/WgslCompoundNode.h b/source/MaterialXGenWgsl/Nodes/WgslCompoundNode.h new file mode 100644 index 0000000000..72f5f79a30 --- /dev/null +++ b/source/MaterialXGenWgsl/Nodes/WgslCompoundNode.h @@ -0,0 +1,28 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#ifndef MATERIALX_WGSLCOMPOUNDNODE_H +#define MATERIALX_WGSLCOMPOUNDNODE_H + +#include + +#include + +MATERIALX_NAMESPACE_BEGIN + +/// Compound node that emits a WGSL function ("fn name(param: type, ...)"). +/// For closure/surface-shader compound nodes a `vd: VertexData` parameter is threaded so the +/// surface node body can access vertex data (normalWorld, positionWorld, ...). +class MX_GENWGSL_API WgslCompoundNode : public CompoundNode +{ + public: + static ShaderNodeImplPtr create(); + void emitFunctionDefinition(const ShaderNode& node, GenContext& context, ShaderStage& stage) const override; + void emitFunctionCall(const ShaderNode& node, GenContext& context, ShaderStage& stage) const override; +}; + +MATERIALX_NAMESPACE_END + +#endif diff --git a/source/MaterialXGenWgsl/Nodes/WgslLightNodes.cpp b/source/MaterialXGenWgsl/Nodes/WgslLightNodes.cpp new file mode 100644 index 0000000000..0573fc7183 --- /dev/null +++ b/source/MaterialXGenWgsl/Nodes/WgslLightNodes.cpp @@ -0,0 +1,128 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include +#include +#include +#include + +MATERIALX_NAMESPACE_BEGIN + +// ============== WgslLightSamplerNode ============== + +namespace +{ +const string WGSL_SAMPLE_LIGHTS_FUNC_SIGNATURE = "fn sampleLightSource(light: LightData, position: vec3f, result: ptr)"; +const string WGSL_NUM_LIGHTS_FUNC_SIGNATURE = "fn numActiveLightSources() -> i32"; +} + +WgslLightSamplerNode::WgslLightSamplerNode() +{ + _hash = std::hash{}(WGSL_SAMPLE_LIGHTS_FUNC_SIGNATURE); +} + +ShaderNodeImplPtr WgslLightSamplerNode::create() +{ + return std::make_shared(); +} + +void WgslLightSamplerNode::emitFunctionDefinition(const ShaderNode& node, GenContext& context, ShaderStage& stage) const +{ + DEFINE_SHADER_STAGE(stage, Stage::PIXEL) + { + const ShaderGenerator& shadergen = context.getShaderGenerator(); + const Syntax& syntax = shadergen.getSyntax(); + const string vec3_zero = syntax.getValue(Type::VECTOR3, HW::VEC3_ZERO); + + shadergen.emitLine(WGSL_SAMPLE_LIGHTS_FUNC_SIGNATURE, stage, false); + shadergen.emitFunctionBodyBegin(node, context, stage); + shadergen.emitLine("(*result).intensity = " + vec3_zero, stage); + shadergen.emitLine("(*result).direction = " + vec3_zero, stage); + + HwLightShadersPtr lightShaders = context.getUserData(HW::USER_DATA_LIGHT_SHADERS); + if (lightShaders) + { + string ifstatement = "if "; + for (const auto& it : lightShaders->get()) + { + shadergen.emitLine(ifstatement + "(light." + shadergen.getLightDataTypevarString() + " == " + std::to_string(it.first) + ")", stage, false); + shadergen.emitScopeBegin(stage); + shadergen.emitFunctionCall(*it.second, context, stage); + shadergen.emitScopeEnd(stage); + ifstatement = "else if "; + } + } + + shadergen.emitFunctionBodyEnd(node, context, stage); + } +} + +// ============== WgslLightCompoundNode ============== + +ShaderNodeImplPtr WgslLightCompoundNode::create() +{ + return std::make_shared(); +} + +void WgslLightCompoundNode::emitFunctionDefinition(const ShaderNode& /*node*/, GenContext& context, ShaderStage& stage) const +{ + DEFINE_SHADER_STAGE(stage, Stage::PIXEL) + { + const HwShaderGenerator& shadergen = static_cast(context.getShaderGenerator()); + const Syntax& syntax = shadergen.getSyntax(); + const string vec3_zero = syntax.getValue(Type::VECTOR3, HW::VEC3_ZERO); + + shadergen.emitFunctionDefinitions(*_rootGraph, context, stage); + + shadergen.emitLine("fn " + _functionName + "(light: LightData, position: vec3f, result: ptr)", stage, false); + + shadergen.emitFunctionBodyBegin(*_rootGraph, context, stage); + + shadergen.emitFunctionCalls(*_rootGraph, context, stage, ShaderNode::Classification::TEXTURE); + + shadergen.emitLine("var closureData: ClosureData = makeClosureData(CLOSURE_TYPE_EMISSION, " + vec3_zero + ", -L, light.direction, " + vec3_zero + ", 0.0)", stage); + shadergen.emitFunctionCalls(*_rootGraph, context, stage, ShaderNode::Classification::SHADER | ShaderNode::Classification::LIGHT); + + shadergen.emitFunctionBodyEnd(*_rootGraph, context, stage); + } +} + +void WgslLightCompoundNode::emitFunctionCall(const ShaderNode&, GenContext& context, ShaderStage& stage) const +{ + DEFINE_SHADER_STAGE(stage, Stage::PIXEL) + { + const ShaderGenerator& shadergen = context.getShaderGenerator(); + shadergen.emitLine(_functionName + "(light, position, result)", stage); + } +} + +// ============== WgslNumLightsNode ============== + +WgslNumLightsNode::WgslNumLightsNode() +{ + _hash = std::hash{}(WGSL_NUM_LIGHTS_FUNC_SIGNATURE); +} + +ShaderNodeImplPtr WgslNumLightsNode::create() +{ + return std::make_shared(); +} + +void WgslNumLightsNode::emitFunctionDefinition(const ShaderNode& node, GenContext& context, ShaderStage& stage) const +{ + DEFINE_SHADER_STAGE(stage, Stage::PIXEL) + { + const ShaderGenerator& shadergen = context.getShaderGenerator(); + shadergen.emitLine(WGSL_NUM_LIGHTS_FUNC_SIGNATURE, stage, false); + shadergen.emitFunctionBodyBegin(node, context, stage); + shadergen.emitLine("return min(" + HW::T_NUM_ACTIVE_LIGHT_SOURCES + ", " + HW::LIGHT_DATA_MAX_LIGHT_SOURCES + ")", stage); + shadergen.emitFunctionBodyEnd(node, context, stage); + } +} + +MATERIALX_NAMESPACE_END diff --git a/source/MaterialXGenWgsl/Nodes/WgslLightNodes.h b/source/MaterialXGenWgsl/Nodes/WgslLightNodes.h new file mode 100644 index 0000000000..5f2ee5e67c --- /dev/null +++ b/source/MaterialXGenWgsl/Nodes/WgslLightNodes.h @@ -0,0 +1,47 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#ifndef MATERIALX_WGSLLIGHTNODES_H +#define MATERIALX_WGSLLIGHTNODES_H + +#include + +#include +#include +#include + +MATERIALX_NAMESPACE_BEGIN + +/// Light sampler node emitting the WGSL signature: +/// fn sampleLightSource(light: LightData, position: vec3f, result: ptr). +class MX_GENWGSL_API WgslLightSamplerNode : public HwLightSamplerNode +{ + public: + WgslLightSamplerNode(); + static ShaderNodeImplPtr create(); + void emitFunctionDefinition(const ShaderNode& node, GenContext& context, ShaderStage& stage) const override; +}; + +/// Light compound node emitting a WGSL light function definition and call. +class MX_GENWGSL_API WgslLightCompoundNode : public HwLightCompoundNode +{ + public: + static ShaderNodeImplPtr create(); + void emitFunctionDefinition(const ShaderNode& node, GenContext& context, ShaderStage& stage) const override; + void emitFunctionCall(const ShaderNode& node, GenContext& context, ShaderStage& stage) const override; +}; + +/// Num-lights node emitting fn numActiveLightSources() -> i32. +class MX_GENWGSL_API WgslNumLightsNode : public HwNumLightsNode +{ + public: + WgslNumLightsNode(); + static ShaderNodeImplPtr create(); + void emitFunctionDefinition(const ShaderNode& node, GenContext& context, ShaderStage& stage) const override; +}; + +MATERIALX_NAMESPACE_END + +#endif diff --git a/source/MaterialXGenWgsl/Nodes/WgslMaterialNode.cpp b/source/MaterialXGenWgsl/Nodes/WgslMaterialNode.cpp new file mode 100644 index 0000000000..f088ddfdfb --- /dev/null +++ b/source/MaterialXGenWgsl/Nodes/WgslMaterialNode.cpp @@ -0,0 +1,54 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include +#include +#include + +MATERIALX_NAMESPACE_BEGIN + +ShaderNodeImplPtr WgslMaterialNode::create() +{ + return std::make_shared(); +} + +void WgslMaterialNode::emitFunctionCall(const ShaderNode& _node, GenContext& context, ShaderStage& stage) const +{ + DEFINE_SHADER_STAGE(stage, Stage::PIXEL) + { + ShaderNode& node = const_cast(_node); + ShaderInput* surfaceshaderInput = node.getInput(ShaderNode::SURFACESHADER); + + if (!surfaceshaderInput->getConnection()) + { + // Just declare the output variable with a default value. + emitOutputVariables(node, context, stage); + return; + } + + const ShaderGenerator& shadergen = context.getShaderGenerator(); + const Syntax& syntax = shadergen.getSyntax(); + + // Emit the function call for upstream surface shader. + const ShaderNode* surfaceshaderNode = surfaceshaderInput->getConnection()->getNode(); + if (surfaceshaderNode->isAGraph()) + { + emitOutputVariables(node, context, stage); + return; + } + shadergen.emitFunctionCall(*surfaceshaderNode, context, stage); + + // Assign this result to the material output variable (WGSL declaration order). + const ShaderOutput* output = node.getOutput(); + shadergen.emitLine("var " + output->getVariable() + ": " + syntax.getTypeName(output->getType()) + + " = " + surfaceshaderInput->getConnection()->getVariable(), + stage); + } +} + +MATERIALX_NAMESPACE_END diff --git a/source/MaterialXGenWgsl/Nodes/WgslMaterialNode.h b/source/MaterialXGenWgsl/Nodes/WgslMaterialNode.h new file mode 100644 index 0000000000..fc84ded797 --- /dev/null +++ b/source/MaterialXGenWgsl/Nodes/WgslMaterialNode.h @@ -0,0 +1,31 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#ifndef MATERIALX_WGSLMATERIALNODE_H +#define MATERIALX_WGSLMATERIALNODE_H + +#include + +#include + +MATERIALX_NAMESPACE_BEGIN + +/// @class WgslMaterialNode +/// WGSL material node implementation. +/// +/// Identical to MaterialNode except that the material output variable is declared +/// in WGSL order ("var : = ") instead of the C/GLSL order +/// (" = ") hardcoded in the shared MaterialNode. +class MX_GENWGSL_API WgslMaterialNode : public MaterialNode +{ + public: + static ShaderNodeImplPtr create(); + + void emitFunctionCall(const ShaderNode& node, GenContext& context, ShaderStage& stage) const override; +}; + +MATERIALX_NAMESPACE_END + +#endif diff --git a/source/MaterialXGenWgsl/Nodes/WgslSourceCodeNode.cpp b/source/MaterialXGenWgsl/Nodes/WgslSourceCodeNode.cpp new file mode 100644 index 0000000000..fa887dca37 --- /dev/null +++ b/source/MaterialXGenWgsl/Nodes/WgslSourceCodeNode.cpp @@ -0,0 +1,110 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include +#include +#include +#include + +MATERIALX_NAMESPACE_BEGIN + +namespace +{ + +// Matches the inline-expression markers used by the shared SourceCodeNode. +const string INLINE_VARIABLE_PREFIX("{{"); +const string INLINE_VARIABLE_SUFFIX("}}"); + +} // anonymous namespace + +ShaderNodeImplPtr WgslSourceCodeNode::create() +{ + return std::make_shared(); +} + +void WgslSourceCodeNode::emitFunctionCall(const ShaderNode& node, GenContext& context, ShaderStage& stage) const +{ + // Only the inlined path needs WGSL-specific handling for its constant + // temporaries; the ordinary function-call path defers to the base class, + // whose argument emission already routes through the WGSL generator's + // emitInput / emitOutput overrides. + if (!_inlined) + { + SourceCodeNode::emitFunctionCall(node, context, stage); + return; + } + + DEFINE_SHADER_STAGE(stage, Stage::PIXEL) + { + const ShaderGenerator& shadergen = context.getShaderGenerator(); + + if (nodeOutputIsClosure(node)) + { + // Emit calls for any closure dependencies upstream from this nodedef. + shadergen.emitDependentFunctionCalls(node, context, stage, ShaderNode::Classification::CLOSURE); + } + + size_t pos = 0; + size_t i = _functionSource.find(INLINE_VARIABLE_PREFIX); + StringSet variableNames; + StringVec code; + while (i != string::npos) + { + code.push_back(_functionSource.substr(pos, i - pos)); + + size_t j = _functionSource.find(INLINE_VARIABLE_SUFFIX, i + 2); + if (j == string::npos) + { + throw ExceptionShaderGenError("Malformed inline expression in implementation for node " + node.getName()); + } + + const string variable = _functionSource.substr(i + 2, j - i - 2); + const ShaderInput* input = node.getInput(variable); + if (!input) + { + throw ExceptionShaderGenError("Could not find an input named '" + variable + + "' on node '" + node.getName() + "'"); + } + + if (input->getConnection()) + { + code.push_back(shadergen.getUpstreamResult(input, context)); + } + else + { + string variableName = node.getName() + "_" + input->getName() + "_tmp"; + if (!variableNames.count(variableName)) + { + // Emit the constant temporary in WGSL declaration order via the + // generator's emitVariableDeclaration override ("const name: type = value"). + ShaderPort v(nullptr, input->getType(), variableName, input->getValue()); + shadergen.emitLineBegin(stage); + shadergen.emitVariableDeclaration(&v, shadergen.getSyntax().getConstantQualifier(), context, stage); + shadergen.emitLineEnd(stage); + variableNames.insert(variableName); + } + code.push_back(variableName); + } + + pos = j + 2; + i = _functionSource.find(INLINE_VARIABLE_PREFIX, pos); + } + code.push_back(_functionSource.substr(pos)); + + shadergen.emitLineBegin(stage); + shadergen.emitOutput(node.getOutput(), true, false, context, stage); + shadergen.emitString(" = ", stage); + for (const string& c : code) + { + shadergen.emitString(c, stage); + } + shadergen.emitLineEnd(stage); + } +} + +MATERIALX_NAMESPACE_END diff --git a/source/MaterialXGenWgsl/Nodes/WgslSourceCodeNode.h b/source/MaterialXGenWgsl/Nodes/WgslSourceCodeNode.h new file mode 100644 index 0000000000..fb46e7e9c0 --- /dev/null +++ b/source/MaterialXGenWgsl/Nodes/WgslSourceCodeNode.h @@ -0,0 +1,32 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#ifndef MATERIALX_WGSLSOURCECODENODE_H +#define MATERIALX_WGSLSOURCECODENODE_H + +#include + +#include + +MATERIALX_NAMESPACE_BEGIN + +/// @class WgslSourceCodeNode +/// WGSL source-code node implementation. +/// +/// Identical to SourceCodeNode except that the local constant temporaries emitted +/// for unconnected inputs of an inlined node use WGSL declaration order +/// ("const : = ") instead of the C/GLSL order +/// ("const = ") hardcoded in the shared SourceCodeNode. +class MX_GENWGSL_API WgslSourceCodeNode : public SourceCodeNode +{ + public: + static ShaderNodeImplPtr create(); + + void emitFunctionCall(const ShaderNode& node, GenContext& context, ShaderStage& stage) const override; +}; + +MATERIALX_NAMESPACE_END + +#endif diff --git a/source/MaterialXGenWgsl/Nodes/WgslSurfaceNode.cpp b/source/MaterialXGenWgsl/Nodes/WgslSurfaceNode.cpp new file mode 100644 index 0000000000..fe7019a220 --- /dev/null +++ b/source/MaterialXGenWgsl/Nodes/WgslSurfaceNode.cpp @@ -0,0 +1,218 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include +#include +#include + +MATERIALX_NAMESPACE_BEGIN + +ShaderNodeImplPtr WgslSurfaceNode::create() +{ + return std::make_shared(); +} + +void WgslSurfaceNode::emitFunctionCall(const ShaderNode& node, GenContext& context, ShaderStage& stage) const +{ + const HwShaderGenerator& shadergen = static_cast(context.getShaderGenerator()); + const Syntax& syntax = shadergen.getSyntax(); + + const string& vec3 = syntax.getTypeName(Type::VECTOR3); + const string vec3_zero = syntax.getValue(Type::VECTOR3, HW::VEC3_ZERO); + const string vec3_one = syntax.getValue(Type::VECTOR3, HW::VEC3_ONE); + + DEFINE_SHADER_STAGE(stage, Stage::VERTEX) + { + VariableBlock& vertexData = stage.getOutputBlock(HW::VERTEX_DATA); + const string prefix = shadergen.getVertexDataPrefix(vertexData); + ShaderPort* position = vertexData[HW::T_POSITION_WORLD]; + if (!position->isEmitted()) + { + position->setEmitted(); + shadergen.emitLine(prefix + position->getVariable() + " = hPositionWorld.xyz", stage); + } + ShaderPort* normal = vertexData[HW::T_NORMAL_WORLD]; + if (!normal->isEmitted()) + { + normal->setEmitted(); + shadergen.emitLine(prefix + normal->getVariable() + " = normalize((" + HW::T_WORLD_INVERSE_TRANSPOSE_MATRIX + " * " + syntax.getTypeName(Type::VECTOR4) + "(" + HW::T_IN_NORMAL + ", 0.0)).xyz)", stage); + } + if (context.getOptions().hwAmbientOcclusion) + { + ShaderPort* texcoord = vertexData[HW::T_TEXCOORD + "_0"]; + if (!texcoord->isEmitted()) + { + texcoord->setEmitted(); + shadergen.emitLine(prefix + texcoord->getVariable() + " = " + HW::T_IN_TEXCOORD + "_0", stage); + } + } + } + + DEFINE_SHADER_STAGE(stage, Stage::PIXEL) + { + VariableBlock& vertexData = stage.getInputBlock(HW::VERTEX_DATA); + const string prefix = shadergen.getVertexDataPrefix(vertexData); + + const ShaderOutput* output = node.getOutput(); + shadergen.emitLineBegin(stage); + shadergen.emitOutput(output, true, true, context, stage); + shadergen.emitLineEnd(stage); + + shadergen.emitScopeBegin(stage); + + shadergen.emitLine("var N: " + vec3 + " = normalize(" + prefix + HW::T_NORMAL_WORLD + ")", stage); + shadergen.emitLine("var V: " + vec3 + " = normalize(" + HW::T_VIEW_POSITION + " - " + prefix + HW::T_POSITION_WORLD + ")", stage); + shadergen.emitLine("var P: " + vec3 + " = " + prefix + HW::T_POSITION_WORLD, stage); + shadergen.emitLine("var L: " + vec3 + " = " + vec3_zero, stage); + shadergen.emitLine("var occlusion: f32 = 1.0", stage); + shadergen.emitLineBreak(stage); + + const string outColor = output->getVariable() + ".color"; + const string outTransparency = output->getVariable() + ".transparency"; + + const ShaderInput* bsdfInput = node.getInput("bsdf"); + if (const ShaderNode* bsdf = bsdfInput->getConnectedSibling()) + { + shadergen.emitLineBegin(stage); + shadergen.emitString("var surfaceOpacity: f32 = ", stage); + shadergen.emitInput(node.getInput("opacity"), context, stage); + shadergen.emitLineEnd(stage); + shadergen.emitLineBreak(stage); + + emitLightLoop(node, context, stage, outColor); + + shadergen.emitComment("Ambient occlusion", stage); + shadergen.emitLine("occlusion = 1.0", stage); + shadergen.emitLineBreak(stage); + + shadergen.emitComment("Add environment contribution", stage); + shadergen.emitScopeBegin(stage); + + if (bsdf->hasClassification(ShaderNode::Classification::BSDF_R)) + { + shadergen.emitLine("var closureData: ClosureData = makeClosureData(CLOSURE_TYPE_INDIRECT, L, V, N, P, occlusion)", stage); + shadergen.emitFunctionCall(*bsdf, context, stage); + } + else + { + shadergen.emitLineBegin(stage); + shadergen.emitOutput(bsdf->getOutput(), true, true, context, stage); + shadergen.emitLineEnd(stage); + } + + shadergen.emitLineBreak(stage); + shadergen.emitLine(outColor + " += occlusion * " + bsdf->getOutput()->getVariable() + ".response", stage); + shadergen.emitScopeEnd(stage); + shadergen.emitLineBreak(stage); + } + + const ShaderInput* edfInput = node.getInput("edf"); + if (const ShaderNode* edf = edfInput->getConnectedSibling()) + { + shadergen.emitComment("Add surface emission", stage); + shadergen.emitScopeBegin(stage); + + if (edf->hasClassification(ShaderNode::Classification::EDF)) + { + shadergen.emitLine("var closureData: ClosureData = makeClosureData(CLOSURE_TYPE_EMISSION, L, V, N, P, occlusion)", stage); + shadergen.emitFunctionCall(*edf, context, stage); + } + else + { + shadergen.emitLineBegin(stage); + shadergen.emitOutput(edf->getOutput(), true, true, context, stage); + shadergen.emitLineEnd(stage); + } + + shadergen.emitLine(outColor + " += " + edf->getOutput()->getVariable(), stage); + shadergen.emitScopeEnd(stage); + shadergen.emitLineBreak(stage); + } + + if (const ShaderNode* bsdf = bsdfInput->getConnectedSibling()) + { + shadergen.emitComment("Calculate the BSDF transmission for viewing direction", stage); + if (bsdf->hasClassification(ShaderNode::Classification::BSDF_T) || bsdf->hasClassification(ShaderNode::Classification::VDF)) + { + shadergen.emitLine("var closureData: ClosureData = makeClosureData(CLOSURE_TYPE_TRANSMISSION, L, V, N, P, occlusion)", stage); + shadergen.emitFunctionCall(*bsdf, context, stage); + } + else + { + shadergen.emitLineBegin(stage); + shadergen.emitOutput(bsdf->getOutput(), true, true, context, stage); + shadergen.emitLineEnd(stage); + } + + if (context.getOptions().hwTransmissionRenderMethod == TRANSMISSION_REFRACTION) + { + shadergen.emitLine(outColor + " += " + bsdf->getOutput()->getVariable() + ".response", stage); + } + else + { + shadergen.emitLine(outTransparency + " += " + bsdf->getOutput()->getVariable() + ".response", stage); + } + + shadergen.emitLineBreak(stage); + shadergen.emitComment("Compute and apply surface opacity", stage); + shadergen.emitScopeBegin(stage); + shadergen.emitLine(outColor + " *= surfaceOpacity", stage); + shadergen.emitLine(outTransparency + " = mix(" + vec3_one + ", " + outTransparency + ", surfaceOpacity)", stage); + shadergen.emitScopeEnd(stage); + } + + shadergen.emitScopeEnd(stage); + shadergen.emitLineBreak(stage); + } +} + +void WgslSurfaceNode::emitLightLoop(const ShaderNode& node, GenContext& context, ShaderStage& stage, const string& outColor) const +{ + if (context.getOptions().hwMaxActiveLightSources <= 0) + return; + + const HwShaderGenerator& shadergen = static_cast(context.getShaderGenerator()); + const VariableBlock& vertexData = stage.getInputBlock(HW::VERTEX_DATA); + const string prefix = shadergen.getVertexDataPrefix(vertexData); + + const ShaderInput* bsdfInput = node.getInput("bsdf"); + const ShaderNode* bsdf = bsdfInput->getConnectedSibling(); + + shadergen.emitComment("Light loop", stage); + shadergen.emitLine("var numLights: i32 = numActiveLightSources()", stage); + shadergen.emitLine("var lightShader: lightshader", stage); + shadergen.emitLine("for (var activeLightIndex: i32 = 0; activeLightIndex < numLights; activeLightIndex += 1)", stage, false); + + shadergen.emitScopeBegin(stage); + + shadergen.emitLine("sampleLightSource(" + HW::T_LIGHT_DATA_INSTANCE + "[activeLightIndex], " + prefix + HW::T_POSITION_WORLD + ", &lightShader)", stage); + shadergen.emitLine("L = lightShader.direction", stage); + shadergen.emitLineBreak(stage); + + shadergen.emitComment("Calculate the BSDF response for this light source", stage); + if (bsdf->hasClassification(ShaderNode::Classification::BSDF_R)) + { + shadergen.emitLine("var closureData: ClosureData = makeClosureData(CLOSURE_TYPE_REFLECTION, L, V, N, P, occlusion)", stage); + shadergen.emitFunctionCall(*bsdf, context, stage); + } + else + { + shadergen.emitLineBegin(stage); + shadergen.emitOutput(bsdf->getOutput(), true, true, context, stage); + shadergen.emitLineEnd(stage); + } + + shadergen.emitLineBreak(stage); + shadergen.emitComment("Accumulate the light's contribution", stage); + shadergen.emitLine(outColor + " += lightShader.intensity * " + bsdf->getOutput()->getVariable() + ".response", stage); + + shadergen.emitScopeEnd(stage); + shadergen.emitLineBreak(stage); +} + +MATERIALX_NAMESPACE_END diff --git a/source/MaterialXGenWgsl/Nodes/WgslSurfaceNode.h b/source/MaterialXGenWgsl/Nodes/WgslSurfaceNode.h new file mode 100644 index 0000000000..fcf1495ee0 --- /dev/null +++ b/source/MaterialXGenWgsl/Nodes/WgslSurfaceNode.h @@ -0,0 +1,26 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#ifndef MATERIALX_WGSLSURFACENODE_H +#define MATERIALX_WGSLSURFACENODE_H + +#include + +#include + +MATERIALX_NAMESPACE_BEGIN + +/// Surface node that emits native WGSL (var name: type = value, for (var i: i32 = ...; ...)). +class MX_GENWGSL_API WgslSurfaceNode : public HwSurfaceNode +{ + public: + static ShaderNodeImplPtr create(); + void emitFunctionCall(const ShaderNode& node, GenContext& context, ShaderStage& stage) const override; + void emitLightLoop(const ShaderNode& node, GenContext& context, ShaderStage& stage, const string& outColor) const override; +}; + +MATERIALX_NAMESPACE_END + +#endif diff --git a/source/MaterialXGenWgsl/README.md b/source/MaterialXGenWgsl/README.md new file mode 100644 index 0000000000..779edb8961 --- /dev/null +++ b/source/MaterialXGenWgsl/README.md @@ -0,0 +1,50 @@ +# MaterialXGenWgsl + +A native [WGSL](https://www.w3.org/TR/WGSL/) (WebGPU Shading Language) shader generator back-end for +MaterialX, registered under the `genwgsl` target. `WgslShaderGenerator` derives directly from +`HwShaderGenerator` (not the GLSL hierarchy) and emits standalone WGSL vertex + fragment shaders, +mirroring the structure of the `MaterialXGenMsl` / `MaterialXGenSlang` back-ends. It is gated behind +the `MATERIALX_BUILD_GEN_WGSL` CMake option (ON by default). + +## What makes this back-end unusual + +Every other shader-gen back-end ships a fully hand-written node library. `genwgsl` does **not** — +its node library is a *hybrid*, and that is the main thing to understand before editing it: + +* **Most node `.wgsl` files are machine-generated** from their `genglsl` originals by the offline + transpiler in [`tools/`](tools/README.md). They are committed (the generator does not run at + build time by default) but carry a `// Generated from … do not edit` banner to distinguish them + from the hand-written files alongside — do not hand-edit them; regenerate. +* **A small number of nodes are hand-written** and intentionally *not* generated, because WGSL has + no function overloading and a few `genwgsl/lib/` helpers were hand-adapted away from their GLSL + signatures. These are listed as `EXPECTED_FALLBACK` in `tools/glsl_to_wgsl.py` (currently the + `conductor` / `dielectric` / `generalized_schlick` BSDFs, `subsurface`, and `chiang_hair`). +* **Texture / image and light nodes are hand-written** too — the transpiler skips them (naga's GLSL + front-end has no sampler support, and light shaders use the dynamically generated `LightData`). +* **Core `lib/` math and closure helpers are hand-maintained** (out of scope for the transpiler). + +The library lives in `libraries/{stdlib,pbrlib,lights}/genwgsl/` with the target defined in +`libraries/targets/genwgsl.mtlx`; node implementations are wired up via `*_genwgsl_impl.mtlx`. + +## Keeping the generated library in sync with genglsl + +The generated `.wgsl` files can drift as `genglsl` improves. The transpiler regenerates them and a +diff surfaces the drift — see [`tools/README.md`](tools/README.md) for the tool, its overload +mapping table, and its lib-arity self-validation. + +For CI / local validation, configure with `-DMATERIALX_GENERATE_WGSL_LIBRARY=ON` (requires Python +and the [`naga`](https://github.com/gfx-rs/wgpu/tree/trunk/naga) CLI). This adds a +`MaterialXGenWgslLibrary` build target that re-transpiles the library into the build tree on every +build and **fails the build** if a node that should generate cleanly does not (a regression or a new +naga validation error), while tolerating the known `EXPECTED_FALLBACK` nodes. The committed `.wgsl` +files remain the source of truth; the build-tree output is for validation only. + +## Layout + +| Path | Contents | +| --- | --- | +| `WgslShaderGenerator.{h,cpp}` | The `genwgsl` shader generator (vertex + fragment WGSL). | +| `WgslSyntax.{h,cpp}` | WGSL type names and syntactic rules. | +| `WgslResourceBindingContext.{h,cpp}` | `@group`/`@binding` uniform, split texture/sampler, and structured light-data bindings. | +| `Nodes/` | C++ node implementations that emit WGSL dynamically. | +| `tools/` | The offline `genglsl`→`genwgsl` library transpiler (see its README). | diff --git a/source/MaterialXGenWgsl/WgslResourceBindingContext.cpp b/source/MaterialXGenWgsl/WgslResourceBindingContext.cpp new file mode 100644 index 0000000000..1c5c5faab0 --- /dev/null +++ b/source/MaterialXGenWgsl/WgslResourceBindingContext.cpp @@ -0,0 +1,129 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include +#include +#include + +MATERIALX_NAMESPACE_BEGIN + +WgslResourceBindingContext::WgslResourceBindingContext(size_t group) : + _group(group), + _binding(0) +{ +} + +void WgslResourceBindingContext::initialize() +{ + _binding = 0; +} + +void WgslResourceBindingContext::emitDirectives(GenContext&, ShaderStage&) +{ + // WGSL has no preprocessor directives for resource binding. +} + +void WgslResourceBindingContext::emitResourceBindings(GenContext& context, const VariableBlock& uniforms, ShaderStage& stage) +{ + const ShaderGenerator& generator = context.getShaderGenerator(); + const Syntax& syntax = generator.getSyntax(); + const string groupStr = std::to_string(_group); + + for (const ShaderPort* uniform : uniforms.getVariableOrder()) + { + const TypeDesc type = uniform->getType(); + + // Closure / shader types are internal MaterialX types, not CPU-supplied uniforms. + if (type.isClosure()) + { + continue; + } + + const string& name = uniform->getVariable(); + + if (type == Type::FILENAME) + { + // File textures split into a texture + sampler, each with its own binding. + generator.emitLine("@group(" + groupStr + ") @binding(" + std::to_string(_binding++) + + ") var " + name + "_texture: texture_2d", + stage); + generator.emitLine("@group(" + groupStr + ") @binding(" + std::to_string(_binding++) + + ") var " + name + "_sampler: sampler", + stage); + continue; + } + + // Value uniform: emit individually so node code references it by its plain name. + // WGSL: bool is not host-shareable in the uniform address space, so use u32. + string typeName = syntax.getTypeName(type); + if (type == Type::BOOLEAN) + { + typeName = "u32"; + } + if (type.isArray() && uniform->getValue()) + { + // array form. + typeName = "array<" + typeName + ", " + std::to_string(uniform->getValue()->asA>().size()) + ">"; + } + generator.emitLine("@group(" + groupStr + ") @binding(" + std::to_string(_binding++) + + ") var " + name + ": " + typeName, + stage); + } + generator.emitLineBreak(stage); +} + +void WgslResourceBindingContext::emitStructuredResourceBindings(GenContext& context, const VariableBlock& uniforms, + ShaderStage& stage, const string& structInstanceName, + const string& arraySuffix) +{ + const ShaderGenerator& generator = context.getShaderGenerator(); + const Syntax& syntax = generator.getSyntax(); + const string groupStr = std::to_string(_group); + + // Emit the element struct definition. + generator.emitLine("struct " + uniforms.getName() + " ", stage, false); + generator.emitScopeBegin(stage); + const auto& order = uniforms.getVariableOrder(); + for (size_t i = 0; i < order.size(); ++i) + { + const ShaderPort* port = order[i]; + string typeName = syntax.getTypeName(port->getType()); + if (port->getType() == Type::BOOLEAN) + { + typeName = "u32"; + } + if (port->getType().isArray() && port->getValue()) + { + typeName = "array<" + typeName + ", " + std::to_string(port->getValue()->asA>().size()) + ">"; + } + const string comma = (i + 1 < order.size()) ? "," : ""; + generator.emitLine(" " + port->getVariable() + ": " + typeName + comma, stage, false); + } + generator.emitScopeEnd(stage, false, false); + generator.emitLineBreak(stage); + + // Convert a GLSL-style "[N]" suffix into a WGSL array store type. + string storeType = uniforms.getName(); + if (!arraySuffix.empty() && arraySuffix.front() == '[' && arraySuffix.back() == ']') + { + const string count = arraySuffix.substr(1, arraySuffix.size() - 2); + storeType = "array<" + uniforms.getName() + ", " + count + ">"; + } + + // Bind the structured array (e.g. the light-data array) as read-only storage + // rather than uniform: the WGSL uniform address space requires array element + // strides to be a multiple of 16, which a minimal element struct (e.g. a single + // i32 light_type) does not satisfy. The storage address space relaxes this and + // is the idiomatic choice for variable-length light arrays. + generator.emitLine("@group(" + groupStr + ") @binding(" + std::to_string(_binding++) + + ") var " + structInstanceName + ": " + storeType, + stage); + generator.emitLineBreak(stage); +} + +MATERIALX_NAMESPACE_END diff --git a/source/MaterialXGenWgsl/WgslResourceBindingContext.h b/source/MaterialXGenWgsl/WgslResourceBindingContext.h new file mode 100644 index 0000000000..131a4a500f --- /dev/null +++ b/source/MaterialXGenWgsl/WgslResourceBindingContext.h @@ -0,0 +1,53 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#ifndef MATERIALX_WGSLRESOURCEBINDINGCONTEXT_H +#define MATERIALX_WGSLRESOURCEBINDINGCONTEXT_H + +/// @file +/// WGSL resource binding context for standalone shaders. + +#include + +#include + +MATERIALX_NAMESPACE_BEGIN + +using WgslResourceBindingContextPtr = shared_ptr; + +/// @class WgslResourceBindingContext +/// Emits standard WGSL resource bindings for a complete standalone shader. +/// +/// All resources are placed in a single bind group with a monotonically increasing +/// binding index: +/// - Value uniforms are emitted individually as `@group(g) @binding(n) var name: type;` +/// so node code can reference them by their plain variable names (no struct prefix). +/// - File textures are split into a `texture_2d` + `sampler` pair, each with its own binding. +/// - Light data is emitted as a uniform array of a `LightData` struct. +class MX_GENWGSL_API WgslResourceBindingContext : public HwResourceBindingContext +{ + public: + explicit WgslResourceBindingContext(size_t group); + + static WgslResourceBindingContextPtr create(size_t group = 0) + { + return std::make_shared(group); + } + + void initialize() override; + void emitDirectives(GenContext& context, ShaderStage& stage) override; + void emitResourceBindings(GenContext& context, const VariableBlock& uniforms, ShaderStage& stage) override; + void emitStructuredResourceBindings(GenContext& context, const VariableBlock& uniforms, + ShaderStage& stage, const string& structInstanceName, + const string& arraySuffix) override; + + protected: + size_t _group; + size_t _binding; +}; + +MATERIALX_NAMESPACE_END + +#endif diff --git a/source/MaterialXGenWgsl/WgslShaderGenerator.cpp b/source/MaterialXGenWgsl/WgslShaderGenerator.cpp new file mode 100644 index 0000000000..5f9f6fa05c --- /dev/null +++ b/source/MaterialXGenWgsl/WgslShaderGenerator.cpp @@ -0,0 +1,772 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +MATERIALX_NAMESPACE_BEGIN + +const string WgslShaderGenerator::TARGET = "genwgsl"; +const string WgslShaderGenerator::VERSION = "1.0"; +const string WgslShaderGenerator::LIGHTDATA_TYPEVAR_STRING = "light_type"; + +namespace +{ + +// Name of the @builtin(position) member of the vertex-data struct. +const string WGSL_CLIP_POSITION = "clipPosition"; + +// User data to track emitted WGSL function names and prevent duplicate definitions. +class WgslEmittedFunctions : public GenUserData +{ + public: + std::set names; +}; +const string WGSL_EMITTED_FUNCTIONS = "WGSL_EMITTED_FUNCTIONS"; + +// Scan WGSL source for "fn NAME(" and collect the function names. +void trackEmittedFunctions(const string& source, std::set& names) +{ + size_t pos = 0; + while (pos < source.size()) + { + size_t fnPos = source.find("fn ", pos); + if (fnPos == string::npos) + break; + size_t nameStart = fnPos + 3; + size_t nameEnd = source.find_first_of("( \t\n", nameStart); + if (nameEnd != string::npos && nameEnd > nameStart) + { + names.insert(source.substr(nameStart, nameEnd - nameStart)); + } + pos = (nameEnd != string::npos) ? nameEnd : fnPos + 3; + } +} + +// Wrap a non-vec4 value in a vec4f for the final pixel output. +void toVec4Wgsl(const TypeDesc& type, string& variable) +{ + if (type.isFloat3()) + variable = "vec4f(" + variable + ", 1.0)"; + else if (type.isFloat2()) + variable = "vec4f(" + variable + ", 0.0, 1.0)"; + else if (type == Type::FLOAT || type == Type::INTEGER) + variable = "vec4f(" + variable + ", " + variable + ", " + variable + ", 1.0)"; + else if (type == Type::BSDF || type == Type::EDF) + variable = "vec4f(" + variable + ", 1.0)"; + else + variable = "vec4f(0.0, 0.0, 0.0, 1.0)"; +} + +} // anonymous namespace + +WgslShaderGenerator::WgslShaderGenerator(TypeSystemPtr typeSystem) : + HwShaderGenerator(typeSystem, WgslSyntax::create(typeSystem)) +{ + registerImplementations(TARGET); + + // WGSL splits each FILENAME uniform into a separate texture and sampler (see + // WgslResourceBindingContext), so the environment lookups take the texture and + // sampler as two arguments. Override the default combined-sampler tokens to the + // split "_texture" / "_sampler" forms emitted by the binding context. + // ($envRadianceSampler / $envIrradianceSampler are WGSL-specific companion tokens + // used by the genwgsl environment libraries; they have no HwConstants entry.) + _tokenSubstitutions[HW::T_ENV_RADIANCE] = HW::ENV_RADIANCE + "_texture"; + _tokenSubstitutions["$envRadianceSampler"] = HW::ENV_RADIANCE + "_sampler"; + _tokenSubstitutions[HW::T_ENV_IRRADIANCE] = HW::ENV_IRRADIANCE + "_texture"; + _tokenSubstitutions["$envIrradianceSampler"] = HW::ENV_IRRADIANCE + "_sampler"; + + _lightSamplingNodes.push_back(ShaderNode::create(nullptr, "numActiveLightSources", WgslNumLightsNode::create())); + _lightSamplingNodes.push_back(ShaderNode::create(nullptr, "sampleLightSource", WgslLightSamplerNode::create())); +} + +void WgslShaderGenerator::registerImplementations(const string& target) +{ + StringVec elementNames; + + registerImplementation("IM_position_vector3_" + target, HwPositionNode::create); + registerImplementation("IM_normal_vector3_" + target, HwNormalNode::create); + registerImplementation("IM_tangent_vector3_" + target, HwTangentNode::create); + registerImplementation("IM_bitangent_vector3_" + target, HwBitangentNode::create); + registerImplementation("IM_texcoord_vector2_" + target, HwTexCoordNode::create); + registerImplementation("IM_texcoord_vector3_" + target, HwTexCoordNode::create); + registerImplementation("IM_geomcolor_float_" + target, HwGeomColorNode::create); + registerImplementation("IM_geomcolor_color3_" + target, HwGeomColorNode::create); + registerImplementation("IM_geomcolor_color4_" + target, HwGeomColorNode::create); + + elementNames = { + "IM_geompropvalue_integer_" + target, + "IM_geompropvalue_float_" + target, + "IM_geompropvalue_color3_" + target, + "IM_geompropvalue_color4_" + target, + "IM_geompropvalue_vector2_" + target, + "IM_geompropvalue_vector3_" + target, + "IM_geompropvalue_vector4_" + target, + }; + registerImplementation(elementNames, HwGeomPropValueNode::create); + registerImplementation("IM_geompropvalue_boolean_" + target, HwGeomPropValueNodeAsUniform::create); + registerImplementation("IM_geompropvalue_string_" + target, HwGeomPropValueNodeAsUniform::create); + registerImplementation("IM_geompropvalue_filename_" + target, HwGeomPropValueNodeAsUniform::create); + + registerImplementation("IM_frame_float_" + target, HwFrameNode::create); + registerImplementation("IM_time_float_" + target, HwTimeNode::create); + registerImplementation("IM_viewdirection_vector3_" + target, HwViewDirectionNode::create); + + registerImplementation("IM_surface_" + target, WgslSurfaceNode::create); + registerImplementation("IM_light_" + target, HwLightNode::create); + registerImplementation("IM_point_light_" + target, HwLightShaderNode::create); + registerImplementation("IM_directional_light_" + target, HwLightShaderNode::create); + registerImplementation("IM_spot_light_" + target, HwLightShaderNode::create); + + registerImplementation("IM_transformpoint_vector3_" + target, HwTransformPointNode::create); + registerImplementation("IM_transformvector_vector3_" + target, HwTransformVectorNode::create); + registerImplementation("IM_transformnormal_vector3_" + target, HwTransformNormalNode::create); + + elementNames = { + "IM_image_float_" + target, + "IM_image_color3_" + target, + "IM_image_color4_" + target, + "IM_image_vector2_" + target, + "IM_image_vector3_" + target, + "IM_image_vector4_" + target, + }; + registerImplementation(elementNames, HwImageNode::create); + registerImplementation("IM_surfacematerial_" + target, WgslMaterialNode::create); + + registerImplementation("IM_dot_lightshader_" + target, HwLightShaderNode::create); +} + +ShaderNodeImplPtr WgslShaderGenerator::createShaderNodeImplForNodeGraph(const NodeGraph& nodegraph) const +{ + vector outputs = nodegraph.getActiveOutputs(); + if (outputs.empty()) + { + throw ExceptionShaderGenError("NodeGraph '" + nodegraph.getName() + "' has no outputs defined"); + } + + const TypeDesc outputType = _typeSystem->getType(outputs[0]->getType()); + if (outputType == Type::LIGHTSHADER) + { + return WgslLightCompoundNode::create(); + } + return WgslCompoundNode::create(); +} + +ShaderNodeImplPtr WgslShaderGenerator::createShaderNodeImplForImplementation(const Implementation&) const +{ + return WgslSourceCodeNode::create(); +} + +HwResourceBindingContextPtr WgslShaderGenerator::getResourceBindingContext(GenContext& context) const +{ + return context.getUserData(HW::USER_DATA_BINDING_CONTEXT); +} + +ShaderPtr WgslShaderGenerator::generate(const string& name, ElementPtr element, GenContext& context) const +{ + // Provide a default standalone resource binding context if none was supplied. + if (!getResourceBindingContext(context)) + { + auto bindingCtx = WgslResourceBindingContext::create(0); + context.pushUserData(HW::USER_DATA_BINDING_CONTEXT, std::static_pointer_cast(bindingCtx)); + } + if (HwResourceBindingContextPtr binding = getResourceBindingContext(context)) + { + binding->initialize(); + } + + ShaderPtr shader = createShader(name, element, context); + ScopedFloatFormatting fmt(Value::FloatFormatFixed); + + auto setDataSemantics = [](VariableBlock& vertexData) + { + for (size_t i = 0; i < vertexData.size(); ++i) + { + ShaderPort* port = vertexData[i]; + if (port->getSemantic().empty()) + port->setSemantic(port->getName()); + } + }; + + // Vertex stage. + ShaderStage& vs = shader->getStage(Stage::VERTEX); + setDataSemantics(vs.getOutputBlock(HW::VERTEX_DATA)); + emitVertexStage(shader->getGraph(), context, vs); + replaceTokens(_tokenSubstitutions, vs); + + // Pixel stage. + ShaderStage& ps = shader->getStage(Stage::PIXEL); + setDataSemantics(ps.getInputBlock(HW::VERTEX_DATA)); + emitPixelStage(shader->getGraph(), context, ps); + replaceTokens(_tokenSubstitutions, ps); + + return shader; +} + +string WgslShaderGenerator::getVertexDataPrefix(const VariableBlock& vertexData) const +{ + return vertexData.getInstance() + "."; +} + +bool WgslShaderGenerator::requiresLighting(const ShaderGraph& graph) const +{ + const bool isBsdf = graph.hasClassification(ShaderNode::Classification::BSDF); + const bool isLitSurfaceShader = graph.hasClassification(ShaderNode::Classification::SHADER) && + graph.hasClassification(ShaderNode::Classification::SURFACE) && + !graph.hasClassification(ShaderNode::Classification::UNLIT); + return isBsdf || isLitSurfaceShader; +} + +void WgslShaderGenerator::emitDirectives(GenContext&, ShaderStage&) const +{ + // WGSL has no #version or preprocessor directives. +} + +void WgslShaderGenerator::emitConstants(GenContext& context, ShaderStage& stage) const +{ + emitLine("const M_PI: f32 = 3.1415926535897932", stage); + emitLine("const M_FLOAT_EPS: f32 = 1e-6", stage); + emitLine("const CLOSURE_TYPE_DEFAULT: i32 = 0", stage); + emitLine("const CLOSURE_TYPE_REFLECTION: i32 = 1", stage); + emitLine("const CLOSURE_TYPE_TRANSMISSION: i32 = 2", stage); + emitLine("const CLOSURE_TYPE_INDIRECT: i32 = 3", stage); + emitLine("const CLOSURE_TYPE_EMISSION: i32 = 4", stage); + + const VariableBlock& constants = stage.getConstantBlock(); + for (size_t i = 0; i < constants.size(); ++i) + { + emitLineBegin(stage); + emitVariableDeclaration(constants[i], _syntax->getConstantQualifier(), context, stage); + emitLineEnd(stage); + } + emitLineBreak(stage); +} + +void WgslShaderGenerator::emitTypeDefinitions(GenContext& context, ShaderStage& stage) const +{ + // surfaceshader must be defined before mx_closure_type.wgsl (material = surfaceshader alias). + emitLine("struct surfaceshader {", stage, false); + emitLine(" color: vec3f,", stage, false); + emitLine(" transparency: vec3f,", stage, false); + emitLine("}", stage, false); + emitLineBreak(stage); + emitLine("struct displacementshader {", stage, false); + emitLine(" offset: vec3f,", stage, false); + emitLine(" scale: f32,", stage, false); + emitLine("}", stage, false); + emitLineBreak(stage); + emitLine("struct lightshader {", stage, false); + emitLine(" intensity: vec3f,", stage, false); + emitLine(" direction: vec3f,", stage, false); + emitLine("}", stage, false); + emitLineBreak(stage); + + // ClosureData, BSDF, VDF, EDF, material, FresnelData, makeClosureData. + emitLibraryInclude("pbrlib/genwgsl/lib/mx_closure_type.wgsl", context, stage); + emitLineBreak(stage); +} + +void WgslShaderGenerator::emitUniforms(GenContext& context, ShaderStage& stage) const +{ + HwResourceBindingContextPtr binding = getResourceBindingContext(context); + if (!binding) + return; + + for (const auto& it : stage.getUniformBlocks()) + { + const VariableBlock& uniforms = *it.second; + if (uniforms.empty() || uniforms.getName() == HW::LIGHT_DATA) + continue; + binding->emitResourceBindings(context, uniforms, stage); + } +} + +void WgslShaderGenerator::emitLightData(GenContext& context, ShaderStage& stage) const +{ + const VariableBlock& lightData = stage.getUniformBlock(HW::LIGHT_DATA); + if (lightData.empty()) + return; + + HwResourceBindingContextPtr binding = getResourceBindingContext(context); + if (!binding) + return; + + const string structArraySuffix = "[" + HW::LIGHT_DATA_MAX_LIGHT_SOURCES + "]"; + binding->emitStructuredResourceBindings(context, lightData, stage, lightData.getInstance(), structArraySuffix); +} + +void WgslShaderGenerator::emitSpecularEnvironment(GenContext& context, ShaderStage& stage) const +{ + const int specularMethod = context.getOptions().hwSpecularEnvironmentMethod; + if (specularMethod == SPECULAR_ENVIRONMENT_FIS) + { + emitLibraryInclude("pbrlib/genwgsl/lib/mx_environment_fis.wgsl", context, stage); + } + else if (specularMethod == SPECULAR_ENVIRONMENT_NONE) + { + emitLibraryInclude("pbrlib/genwgsl/lib/mx_environment_none.wgsl", context, stage); + } + // SPECULAR_ENVIRONMENT_PREFILTER is not supported for standalone WGSL shaders. + emitLineBreak(stage); +} + +void WgslShaderGenerator::emitTransmissionRender(GenContext& context, ShaderStage& stage) const +{ + const int transmissionMethod = context.getOptions().hwTransmissionRenderMethod; + if (transmissionMethod == TRANSMISSION_REFRACTION) + { + emitLibraryInclude("pbrlib/genwgsl/lib/mx_transmission_refract.wgsl", context, stage); + } + else if (transmissionMethod == TRANSMISSION_OPACITY) + { + emitLibraryInclude("pbrlib/genwgsl/lib/mx_transmission_opacity.wgsl", context, stage); + } + emitLineBreak(stage); +} + +void WgslShaderGenerator::emitLightFunctionDefinitions(const ShaderGraph& graph, GenContext& context, ShaderStage& stage) const +{ + DEFINE_SHADER_STAGE(stage, Stage::PIXEL) + { + if (requiresLighting(graph) && context.getOptions().hwMaxActiveLightSources > 0) + { + if (graph.hasClassification(ShaderNode::Classification::SHADER | ShaderNode::Classification::SURFACE)) + { + HwLightShadersPtr lightShaders = context.getUserData(HW::USER_DATA_LIGHT_SHADERS); + if (lightShaders) + { + for (const auto& it : lightShaders->get()) + emitFunctionDefinition(*it.second, context, stage); + } + for (const auto& it : _lightSamplingNodes) + emitFunctionDefinition(*it, context, stage); + } + } + } +} + +void WgslShaderGenerator::emitInputs(GenContext& /*context*/, ShaderStage& stage) const +{ + DEFINE_SHADER_STAGE(stage, Stage::VERTEX) + { + const VariableBlock& vertexInputs = stage.getInputBlock(HW::VERTEX_INPUTS); + if (!vertexInputs.empty()) + { + emitLine("struct " + vertexInputs.getName() + " ", stage, false); + emitScopeBegin(stage); + for (size_t i = 0; i < vertexInputs.size(); ++i) + { + const ShaderPort* p = vertexInputs[i]; + emitLine("@location(" + std::to_string(i) + ") " + p->getVariable() + ": " + + _syntax->getTypeName(p->getType()) + ",", + stage, false); + } + emitScopeEnd(stage, false, false); + emitLineBreak(stage); + } + } + + DEFINE_SHADER_STAGE(stage, Stage::PIXEL) + { + const VariableBlock& vertexData = stage.getInputBlock(HW::VERTEX_DATA); + emitLine("struct " + vertexData.getName() + " ", stage, false); + emitScopeBegin(stage); + emitLine("@builtin(position) " + WGSL_CLIP_POSITION + ": vec4f,", stage, false); + size_t loc = 0; + for (size_t i = 0; i < vertexData.size(); ++i) + { + const ShaderPort* p = vertexData[i]; + string attr = "@location(" + std::to_string(loc++) + ")"; + if (p->getType() == Type::INTEGER) + attr += " " + WgslSyntax::FLAT_QUALIFIER; + emitLine(attr + " " + p->getVariable() + ": " + _syntax->getTypeName(p->getType()) + ",", stage, false); + } + emitScopeEnd(stage, false, false); + emitLineBreak(stage); + } +} + +void WgslShaderGenerator::emitOutputs(GenContext& context, ShaderStage& stage) const +{ + DEFINE_SHADER_STAGE(stage, Stage::VERTEX) + { + const VariableBlock& vertexData = stage.getOutputBlock(HW::VERTEX_DATA); + emitLine("struct " + vertexData.getName() + " ", stage, false); + emitScopeBegin(stage); + emitLine("@builtin(position) " + WGSL_CLIP_POSITION + ": vec4f,", stage, false); + size_t loc = 0; + for (size_t i = 0; i < vertexData.size(); ++i) + { + const ShaderPort* p = vertexData[i]; + string attr = "@location(" + std::to_string(loc++) + ")"; + if (p->getType() == Type::INTEGER) + attr += " " + WgslSyntax::FLAT_QUALIFIER; + emitLine(attr + " " + p->getVariable() + ": " + _syntax->getTypeName(p->getType()) + ",", stage, false); + } + emitScopeEnd(stage, false, false); + emitLineBreak(stage); + } + + DEFINE_SHADER_STAGE(stage, Stage::PIXEL) + { + // Declare the pixel output variables as locals within fragmentMain. + const VariableBlock& outputs = stage.getOutputBlock(HW::PIXEL_OUTPUTS); + for (size_t i = 0; i < outputs.size(); ++i) + { + emitLineBegin(stage); + emitVariableDeclaration(outputs[i], EMPTY_STRING, context, stage, false); + emitLineEnd(stage); + } + emitLineBreak(stage); + } +} + +void WgslShaderGenerator::emitVertexStage(const ShaderGraph& graph, GenContext& context, ShaderStage& stage) const +{ + emitDirectives(context, stage); + emitConstants(context, stage); + emitUniforms(context, stage); + emitInputs(context, stage); + emitOutputs(context, stage); + + emitLibraryInclude("stdlib/genwgsl/lib/mx_math.wgsl", context, stage); + emitLineBreak(stage); + + emitFunctionDefinitions(graph, context, stage); + + const VariableBlock& vertexInputs = stage.getInputBlock(HW::VERTEX_INPUTS); + const VariableBlock& vertexData = stage.getOutputBlock(HW::VERTEX_DATA); + const string prefix = getVertexDataPrefix(vertexData); + + setFunctionName("vertexMain", stage); + emitLine("@vertex", stage, false); + emitLine("fn vertexMain(vsIn: " + vertexInputs.getName() + ") -> " + vertexData.getName() + " ", stage, false); + emitFunctionBodyBegin(graph, context, stage); + + // Alias the vertex inputs as locals so token-substituted node code resolves. + emitComment("Vertex input variables", stage); + for (size_t i = 0; i < vertexInputs.size(); ++i) + { + emitLine("var " + vertexInputs[i]->getVariable() + " = vsIn." + vertexInputs[i]->getVariable(), stage); + } + + emitLine("var hPositionWorld: vec4f = " + HW::T_WORLD_MATRIX + " * vec4f(" + HW::T_IN_POSITION + ", 1.0)", stage); + emitLine("var " + vertexData.getInstance() + ": " + vertexData.getName(), stage); + emitLine(prefix + WGSL_CLIP_POSITION + " = " + HW::T_VIEW_PROJECTION_MATRIX + " * hPositionWorld", stage); + + for (const ShaderNode* node : graph.getNodes()) + { + emitFunctionCall(*node, context, stage); + } + + emitLine("return " + vertexData.getInstance(), stage); + emitFunctionBodyEnd(graph, context, stage); +} + +void WgslShaderGenerator::emitPixelStage(const ShaderGraph& graph, GenContext& context, ShaderStage& stage) const +{ + emitDirectives(context, stage); + + // Type definitions (surfaceshader, displacementshader, lightshader, closure types). + emitTypeDefinitions(context, stage); + + // Constants. + emitConstants(context, stage); + + // Uniforms (material + private), excluding the light data block. + emitUniforms(context, stage); + + // Shared vertex-data input struct. + emitInputs(context, stage); + + // Common math helpers. + emitLibraryInclude("stdlib/genwgsl/lib/mx_math.wgsl", context, stage); + emitLineBreak(stage); + + const bool lighting = requiresLighting(graph); + const bool emitLightUniforms = lighting && context.getOptions().hwMaxActiveLightSources > 0; + + if (emitLightUniforms) + { + const unsigned int maxLights = std::max(1u, context.getOptions().hwMaxActiveLightSources); + emitLine("const " + HW::LIGHT_DATA_MAX_LIGHT_SOURCES + ": i32 = " + std::to_string(maxLights), stage); + emitLineBreak(stage); + } + + if (lighting) + { + emitSpecularEnvironment(context, stage); + emitTransmissionRender(context, stage); + } + + if (emitLightUniforms) + { + emitLightData(context, stage); + } + + _tokenSubstitutions[ShaderGenerator::T_FILE_TRANSFORM_UV] = + context.getOptions().fileTextureVerticalFlip ? "mx_transform_uv_vflip.wgsl" : "mx_transform_uv.wgsl"; + + emitLightFunctionDefinitions(graph, context, stage); + emitFunctionDefinitions(graph, context, stage); + + const ShaderGraphOutputSocket* outputSocket = graph.getOutputSocket(); + const VariableBlock& vertexData = stage.getInputBlock(HW::VERTEX_DATA); + + setFunctionName("fragmentMain", stage); + emitLine("@fragment", stage, false); + // The fragment entry takes only the interpolated vertex data. (Double-sided shading via + // @builtin(front_facing) is not currently emitted; a single struct parameter also keeps the + // entry consumable as a Three.js wgslFn by the TSL bridge in the web viewer.) + emitLine("fn fragmentMain(" + vertexData.getInstance() + ": " + vertexData.getName() + + ") -> @location(0) vec4f ", + stage, false); + emitFunctionBodyBegin(graph, context, stage); + + // Declare the pixel output local variable(s). + emitOutputs(context, stage); + + if (graph.hasClassification(ShaderNode::Classification::CLOSURE) && + !graph.hasClassification(ShaderNode::Classification::SHADER)) + { + // A bare closure with no surface: output black. + emitLine(outputSocket->getVariable() + " = vec4f(0.0, 0.0, 0.0, 1.0)", stage); + } + else + { + if (graph.hasClassification(ShaderNode::Classification::SHADER | ShaderNode::Classification::SURFACE)) + { + emitFunctionCalls(graph, context, stage, ShaderNode::Classification::TEXTURE); + for (ShaderGraphOutputSocket* socket : graph.getOutputSockets()) + { + if (socket->getConnection()) + { + const ShaderNode* upstream = socket->getConnection()->getNode(); + if (upstream->getParent() == &graph && + (upstream->hasClassification(ShaderNode::Classification::CLOSURE) || + upstream->hasClassification(ShaderNode::Classification::SHADER))) + { + emitFunctionCall(*upstream, context, stage); + } + } + } + } + else + { + emitFunctionCalls(graph, context, stage); + } + + const ShaderOutput* outputConnection = outputSocket->getConnection(); + if (outputConnection) + { + if (graph.hasClassification(ShaderNode::Classification::SURFACE)) + { + string outColor = outputConnection->getVariable() + ".color"; + const string outTransparency = outputConnection->getVariable() + ".transparency"; + if (context.getOptions().hwSrgbEncodeOutput) + { + outColor = "mx_srgb_encode(" + outColor + ")"; + } + if (context.getOptions().hwTransparency) + { + emitLine("var outAlpha: f32 = clamp(1.0 - dot(" + outTransparency + ", vec3f(0.3333, 0.3333, 0.3333)), 0.0, 1.0)", stage); + emitLine(outputSocket->getVariable() + " = vec4f(" + outColor + ", outAlpha)", stage); + emitLine("if (outAlpha < " + HW::T_ALPHA_THRESHOLD + ") ", stage, false); + emitScopeBegin(stage); + emitLine("discard", stage); + emitScopeEnd(stage); + } + else + { + emitLine(outputSocket->getVariable() + " = vec4f(" + outColor + ", 1.0)", stage); + } + } + else + { + string outValue = outputConnection->getVariable(); + if (context.getOptions().hwSrgbEncodeOutput && outputSocket->getType().isFloat3()) + { + outValue = "mx_srgb_encode(" + outValue + ")"; + } + if (!outputSocket->getType().isFloat4()) + { + toVec4Wgsl(outputSocket->getType(), outValue); + } + emitLine(outputSocket->getVariable() + " = " + outValue, stage); + } + } + else + { + string outputValue = outputSocket->getValue() ? + _syntax->getValue(outputSocket->getType(), *outputSocket->getValue()) : + _syntax->getDefaultValue(outputSocket->getType()); + if (!outputSocket->getType().isFloat4()) + { + string finalOutput = outputSocket->getVariable() + "_tmp"; + emitLine("var " + finalOutput + ": " + _syntax->getTypeName(outputSocket->getType()) + " = " + outputValue, stage); + toVec4Wgsl(outputSocket->getType(), finalOutput); + emitLine(outputSocket->getVariable() + " = " + finalOutput, stage); + } + else + { + emitLine(outputSocket->getVariable() + " = " + outputValue, stage); + } + } + } + + emitLine("return " + outputSocket->getVariable(), stage); + emitFunctionBodyEnd(graph, context, stage); +} + +// +// WGSL token mechanics +// + +void WgslShaderGenerator::emitVariableDeclaration(const ShaderPort* variable, const string& qualifier, + GenContext&, ShaderStage& stage, bool assignValue) const +{ + if (variable->getType() == Type::FILENAME) + { + emitString("var " + variable->getVariable() + ": texture_2d", stage); + return; + } + + string typeName = _syntax->getTypeName(variable->getType()); + if (variable->getType().isArray() && variable->getValue()) + typeName += _syntax->getArrayVariableSuffix(variable->getType(), *variable->getValue()); + + const bool isConst = (qualifier == _syntax->getConstantQualifier()); + string line = isConst ? "const " : "var "; + line += variable->getVariable() + ": " + typeName; + + if (assignValue && !isConst && qualifier == _syntax->getUniformQualifier()) + { + // Uniforms are not assigned inline. + emitString(line, stage); + return; + } + if (assignValue) + { + const string valueStr = variable->getValue() ? + _syntax->getValue(variable->getType(), *variable->getValue()) : + _syntax->getDefaultValue(variable->getType()); + if (!valueStr.empty()) + line += " = " + valueStr; + } + emitString(line, stage); +} + +void WgslShaderGenerator::emitOutput(const ShaderOutput* output, bool includeType, bool assignValue, GenContext& context, ShaderStage& stage) const +{ + if (includeType) + { + stage.addString("var " + output->getVariable() + ": " + _syntax->getTypeName(output->getType())); + } + else + { + // As a function-call argument an output is passed by pointer. + stage.addString(assignValue ? output->getVariable() : "&" + output->getVariable()); + } + + string suffix; + context.getOutputSuffix(output, suffix); + if (!suffix.empty()) + stage.addString(suffix); + + if (assignValue) + { + const string& value = _syntax->getDefaultValue(output->getType()); + if (!value.empty()) + stage.addString(" = " + value); + } +} + +void WgslShaderGenerator::emitFunctionDefinitionParameter(const ShaderPort* shaderPort, bool isOutput, GenContext&, ShaderStage& stage) const +{ + if (isOutput) + return; + if (shaderPort->getType() == Type::FILENAME) + { + const string& varName = shaderPort->getVariable(); + HwShaderGenerator::emitString(varName + "_texture: texture_2d, " + varName + "_sampler: sampler", stage); + return; + } + HwShaderGenerator::emitString(shaderPort->getVariable() + ": " + _syntax->getTypeName(shaderPort->getType()), stage); +} + +void WgslShaderGenerator::emitInput(const ShaderInput* input, GenContext& context, ShaderStage& stage) const +{ + if (input->getType() == Type::FILENAME) + { + const string base = getUpstreamResult(input, context); + HwShaderGenerator::emitString(base + "_texture, " + base + "_sampler", stage); + return; + } + if (input->getType() == Type::BOOLEAN) + { + // Boolean public uniforms are stored as u32 (bool is not host-shareable), so + // convert to bool at the use site. bool(bool) is an identity conversion, so this + // is also safe for boolean values originating from computed locals or literals. + HwShaderGenerator::emitString("bool(" + getUpstreamResult(input, context) + ")", stage); + return; + } + HwShaderGenerator::emitInput(input, context, stage); +} + +void WgslShaderGenerator::emitClosureDataParameter(const ShaderNode& node, GenContext&, ShaderStage& stage) const +{ + if (nodeNeedsClosureData(node)) + { + // WGSL parameter syntax: "closureData: ClosureData, ". + emitString(HW::CLOSURE_DATA_ARG + ": " + HW::CLOSURE_DATA_TYPE + ", ", stage); + } +} + +void WgslShaderGenerator::emitBlock(const string& str, const FilePath& sourceFilename, GenContext& context, ShaderStage& stage) const +{ + stage.addBlock(str, sourceFilename, context); + + if (sourceFilename.getExtension() == "wgsl" && !str.empty()) + { + auto data = context.getUserData(WGSL_EMITTED_FUNCTIONS); + if (!data) + { + data = std::make_shared(); + context.pushUserData(WGSL_EMITTED_FUNCTIONS, data); + } + trackEmittedFunctions(str, data->names); + } +} + +MATERIALX_NAMESPACE_END diff --git a/source/MaterialXGenWgsl/WgslShaderGenerator.h b/source/MaterialXGenWgsl/WgslShaderGenerator.h new file mode 100644 index 0000000000..6fb98e9439 --- /dev/null +++ b/source/MaterialXGenWgsl/WgslShaderGenerator.h @@ -0,0 +1,140 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#ifndef MATERIALX_WGSLSHADERGENERATOR_H +#define MATERIALX_WGSLSHADERGENERATOR_H + +/// @file +/// Native WGSL (WebGPU Shading Language) shader generator. +/// +/// Produces complete, standalone WGSL vertex + fragment shaders (with @vertex / @fragment +/// entry points and standard @group/@binding resources) from MaterialX node graphs, backed by +/// hand-written WGSL node implementation libraries under the `genwgsl` target. +/// +/// The class derives directly from HwShaderGenerator (not the GLSL hierarchy) and emits native +/// WGSL syntax. Its complete-shader structure mirrors SlangShaderGenerator, while the WGSL token +/// mechanics (var declarations, pointer outputs, split texture/sampler) follow the WebGPU spec. + +#include + +#include +#include + +MATERIALX_NAMESPACE_BEGIN + +using WgslShaderGeneratorPtr = shared_ptr; + +/// @class WgslShaderGenerator +/// Native WGSL shader generator for the `genwgsl` target. +class MX_GENWGSL_API WgslShaderGenerator : public HwShaderGenerator +{ + public: + WgslShaderGenerator(TypeSystemPtr typeSystem); + + /// Creator function. + static ShaderGeneratorPtr create(TypeSystemPtr typeSystem = nullptr) + { + return std::make_shared(typeSystem ? typeSystem : TypeSystem::create()); + } + + /// Generate a complete WGSL shader starting from the given element. + ShaderPtr generate(const string& name, ElementPtr element, GenContext& context) const override; + + /// Return the target identifier ("genwgsl"). + const string& getTarget() const override { return TARGET; } + + /// Return the WGSL version string. + virtual const string& getVersion() const { return VERSION; } + + /// "type" is a WGSL reserved word, so the LightData type member is renamed to "light_type". + const string& getLightDataTypevarString() const override { return LIGHTDATA_TYPEVAR_STRING; } + + /// Emit a WGSL variable declaration: "var name: type [= value]" / "const name: type = value". + void emitVariableDeclaration(const ShaderPort* variable, const string& qualifier, GenContext& context, ShaderStage& stage, + bool assignValue = true) const override; + + /// Emit a shader output, in WGSL syntax. As a function-call argument an output is passed by + /// pointer (&var) to match the ptr out-parameter convention. + void emitOutput(const ShaderOutput* output, bool includeType, bool assignValue, GenContext& context, ShaderStage& stage) const override; + + /// Emit a WGSL function parameter ("name: type"); output params are skipped (handled by callers). + /// For Type::FILENAME emit a texture_2d + sampler pair. + void emitFunctionDefinitionParameter(const ShaderPort* shaderPort, bool isOutput, GenContext& context, ShaderStage& stage) const override; + + /// Emit a function-call input argument. For Type::FILENAME emit (var_texture, var_sampler). + void emitInput(const ShaderInput* input, GenContext& context, ShaderStage& stage) const override; + + /// Emit the closure-data function parameter in WGSL syntax ("closureData: ClosureData, "). + void emitClosureDataParameter(const ShaderNode& node, GenContext& context, ShaderStage& stage) const override; + + static const string TARGET; + static const string VERSION; + + protected: + static const string LIGHTDATA_TYPEVAR_STRING; + + /// Emit the complete WGSL vertex stage (struct IO + @vertex entry point). + virtual void emitVertexStage(const ShaderGraph& graph, GenContext& context, ShaderStage& stage) const; + + /// Emit the complete WGSL pixel stage (type defs, uniforms, lighting, @fragment entry point). + virtual void emitPixelStage(const ShaderGraph& graph, GenContext& context, ShaderStage& stage) const; + + /// Emit the vertex input struct (VERTEX) or the shared vertex-data struct (PIXEL). + virtual void emitInputs(GenContext& context, ShaderStage& stage) const; + + /// Emit the shared vertex-data output struct (VERTEX) or the pixel output declaration (PIXEL). + virtual void emitOutputs(GenContext& context, ShaderStage& stage) const; + + /// Emit value/texture uniform resource bindings (skips the LightData block). + virtual void emitUniforms(GenContext& context, ShaderStage& stage) const; + + /// Emit the LightData struct + uniform array binding and MAX_LIGHT_SOURCES constant. + virtual void emitLightData(GenContext& context, ShaderStage& stage) const; + + /// Emit module-level constants (M_PI, M_FLOAT_EPS, CLOSURE_TYPE_*, graph constants). + virtual void emitConstants(GenContext& context, ShaderStage& stage) const; + + /// Emit closure / shader struct type definitions (surfaceshader, displacementshader, lightshader) + /// plus the closure type library (ClosureData, BSDF, FresnelData, makeClosureData). + void emitTypeDefinitions(GenContext& context, ShaderStage& stage) const override; + + /// Vertex data is accessed via the "vd." prefix. + string getVertexDataPrefix(const VariableBlock& vertexData) const override; + + bool requiresLighting(const ShaderGraph& graph) const override; + + /// Emit the specular environment library (FIS by default for standalone shaders). + virtual void emitSpecularEnvironment(GenContext& context, ShaderStage& stage) const; + + /// Emit the transmission rendering library (refraction or opacity). + virtual void emitTransmissionRender(GenContext& context, ShaderStage& stage) const; + + /// WGSL has no preprocessor directives; this is a structural no-op. + virtual void emitDirectives(GenContext& context, ShaderStage& stage) const; + + /// Emit light sampling function definitions when the graph requires lighting. + void emitLightFunctionDefinitions(const ShaderGraph& graph, GenContext& context, ShaderStage& stage) const; + + /// Override emitBlock to track emitted WGSL functions and avoid duplicate definitions. + void emitBlock(const string& str, const FilePath& sourceFilename, GenContext& context, ShaderStage& stage) const override; + + /// Use the WGSL-native compound / light-compound node implementations. + ShaderNodeImplPtr createShaderNodeImplForNodeGraph(const NodeGraph& nodegraph) const override; + + /// Use the WGSL-native source-code node implementation (WGSL declaration order). + ShaderNodeImplPtr createShaderNodeImplForImplementation(const Implementation& implementation) const override; + + /// Get the resource binding context from the GenContext. + HwResourceBindingContextPtr getResourceBindingContext(GenContext& context) const; + + /// Register the genwgsl node implementations. + void registerImplementations(const string& target); + + vector _lightSamplingNodes; +}; + +MATERIALX_NAMESPACE_END + +#endif diff --git a/source/MaterialXGenWgsl/WgslSyntax.cpp b/source/MaterialXGenWgsl/WgslSyntax.cpp new file mode 100644 index 0000000000..ae000aff65 --- /dev/null +++ b/source/MaterialXGenWgsl/WgslSyntax.cpp @@ -0,0 +1,398 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include + +MATERIALX_NAMESPACE_BEGIN + +namespace +{ + +// WGSL has no string type, so MaterialX string/enum inputs are represented as integers. +class WgslStringTypeSyntax : public StringTypeSyntax +{ + public: + WgslStringTypeSyntax(const Syntax* parent) : + StringTypeSyntax(parent, "i32", "0", "0") { } + + string getValue(const Value& /*value*/, bool /*uniform*/) const override + { + return "0"; + } +}; + +// WGSL aggregate construction uses the type name as the constructor: vec3f(...), BSDF(...). +class WgslAggregateTypeSyntax : public AggregateTypeSyntax +{ + public: + WgslAggregateTypeSyntax(const Syntax* parent, const string& name, const string& defaultValue, const string& uniformDefaultValue, + const string& typeAlias = EMPTY_STRING, const string& typeDefinition = EMPTY_STRING, + const StringVec& members = EMPTY_MEMBERS) : + AggregateTypeSyntax(parent, name, defaultValue, uniformDefaultValue, typeAlias, typeDefinition, members) + { + } + + string getValue(const Value& value, bool uniform) const override + { + const string valueString = value.getValueString(); + if (uniform) + { + return valueString; + } + return valueString.empty() ? valueString : getName() + "(" + valueString + ")"; + } +}; + +} // anonymous namespace + +const string WgslSyntax::INPUT_QUALIFIER = ""; +const string WgslSyntax::OUTPUT_QUALIFIER = ""; +// WGSL uniforms are declared via var in the resource binding context, so there is +// no per-variable uniform keyword. +const string WgslSyntax::UNIFORM_QUALIFIER = "uniform"; +const string WgslSyntax::CONSTANT_QUALIFIER = "const"; +// Integer varyings need @interpolate(flat); handled where varyings are emitted. +const string WgslSyntax::FLAT_QUALIFIER = "@interpolate(flat)"; +const string WgslSyntax::SOURCE_FILE_EXTENSION = ".wgsl"; +const StringVec WgslSyntax::VEC2_MEMBERS = { ".x", ".y" }; +const StringVec WgslSyntax::VEC3_MEMBERS = { ".x", ".y", ".z" }; +const StringVec WgslSyntax::VEC4_MEMBERS = { ".x", ".y", ".z", ".w" }; + +// +// WgslSyntax methods +// + +WgslSyntax::WgslSyntax(TypeSystemPtr typeSystem) : + Syntax(typeSystem) +{ + // Register WGSL keywords and reserved words (https://www.w3.org/TR/WGSL/#keyword-summary). + registerReservedWords({ + // Keywords + "alias", "break", "case", "const", "const_assert", "continue", "continuing", + "default", "diagnostic", "discard", "else", "enable", "false", "fn", "for", + "if", "let", "loop", "override", "requires", "return", "struct", "switch", + "true", "var", "while", + // Reserved words + "NULL", "Self", "abstract", "active", "alignas", "alignof", "as", "asm", + "asm_fragment", "async", "attribute", "auto", "await", "become", "cast", "catch", + "class", "co_await", "co_return", "co_yield", "coherent", "column_major", "common", + "compile", "compile_fragment", "concept", "const_cast", "consteval", "constexpr", + "constinit", "crate", "debugger", "decltype", "delete", "demote", "demote_to_helper", + "do", "dynamic_cast", "enum", "explicit", "export", "extends", "extern", "external", + "fallthrough", "filter", "final", "finally", "friend", "from", "fxgroup", "get", + "goto", "groupshared", "highp", "impl", "implements", "import", "inline", "instanceof", + "interface", "layout", "lowp", "macro", "macro_rules", "match", "mediump", "meta", + "mod", "module", "move", "mut", "mutable", "namespace", "new", "nil", "noexcept", + "noinline", "nointerpolation", "non_coherent", "noncoherent", "noperspective", "null", + "nullptr", "of", "operator", "package", "packoffset", "partition", "pass", "patch", + "pixelfragment", "precise", "precision", "premerge", "priv", "protected", "pub", + "public", "readonly", "ref", "regardless", "register", "reinterpret_cast", "require", + "resource", "restrict", "self", "set", "shared", "sizeof", "smooth", "snorm", "static", + "static_assert", "static_cast", "std", "subroutine", "super", "target", "template", + "this", "thread_local", "throw", "trait", "try", "type", "typedef", "typeid", "typename", + "typeof", "union", "unless", "unorm", "unsafe", "unsized", "use", "using", "varying", + "virtual", "volatile", "wgsl", "where", "with", "writeonly", "yield", + // Common built-in type/intrinsic names that must not collide with generated variables. + "f32", "i32", "u32", "vec2f", "vec3f", "vec4f", "mat3x3f", "mat4x4f", "texture_2d", + "sampler", "mix", "select" }); + + // + // Register syntax handlers for each data type. + // + + registerTypeSyntax( + Type::FLOAT, + std::make_shared( + this, + "f32", + "0.0", + "0.0")); + + registerTypeSyntax( + Type::FLOATARRAY, + std::make_shared( + this, + "f32", + EMPTY_STRING, + EMPTY_STRING)); + + registerTypeSyntax( + Type::INTEGER, + std::make_shared( + this, + "i32", + "0", + "0")); + + registerTypeSyntax( + Type::INTEGERARRAY, + std::make_shared( + this, + "i32", + EMPTY_STRING, + EMPTY_STRING)); + + registerTypeSyntax( + Type::BOOLEAN, + std::make_shared( + this, + "bool", + "false", + "false")); + + registerTypeSyntax( + Type::COLOR3, + std::make_shared( + this, + "vec3f", + "vec3f(0.0)", + "0.0, 0.0, 0.0", + EMPTY_STRING, + EMPTY_STRING, + VEC3_MEMBERS)); + + registerTypeSyntax( + Type::COLOR4, + std::make_shared( + this, + "vec4f", + "vec4f(0.0)", + "0.0, 0.0, 0.0, 0.0", + EMPTY_STRING, + EMPTY_STRING, + VEC4_MEMBERS)); + + registerTypeSyntax( + Type::VECTOR2, + std::make_shared( + this, + "vec2f", + "vec2f(0.0)", + "0.0, 0.0", + EMPTY_STRING, + EMPTY_STRING, + VEC2_MEMBERS)); + + registerTypeSyntax( + Type::VECTOR3, + std::make_shared( + this, + "vec3f", + "vec3f(0.0)", + "0.0, 0.0, 0.0", + EMPTY_STRING, + EMPTY_STRING, + VEC3_MEMBERS)); + + registerTypeSyntax( + Type::VECTOR4, + std::make_shared( + this, + "vec4f", + "vec4f(0.0)", + "0.0, 0.0, 0.0, 0.0", + EMPTY_STRING, + EMPTY_STRING, + VEC4_MEMBERS)); + + registerTypeSyntax( + Type::MATRIX33, + std::make_shared( + this, + "mat3x3f", + "mat3x3f(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)", + "1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0")); + + registerTypeSyntax( + Type::MATRIX44, + std::make_shared( + this, + "mat4x4f", + "mat4x4f(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0)", + "1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0")); + + registerTypeSyntax( + Type::STRING, + std::make_shared(this)); + + // File textures are split into a texture_2d + sampler pair; the generator handles + // FILENAME specially in emitVariableDeclaration / emitFunctionDefinitionParameter / emitInput. + registerTypeSyntax( + Type::FILENAME, + std::make_shared( + this, + "texture_2d", + EMPTY_STRING, + EMPTY_STRING)); + + registerTypeSyntax( + Type::BSDF, + std::make_shared( + this, + "BSDF", + "BSDF(vec3f(0.0), vec3f(1.0))", + EMPTY_STRING, + EMPTY_STRING, + EMPTY_STRING)); + + registerTypeSyntax( + Type::EDF, + std::make_shared( + this, + "EDF", + "EDF(0.0)", + "0.0, 0.0, 0.0", + "vec3f", + EMPTY_STRING)); + + registerTypeSyntax( + Type::VDF, + std::make_shared( + this, + "VDF", + "VDF(vec3f(0.0), vec3f(1.0))", + EMPTY_STRING, + EMPTY_STRING, + EMPTY_STRING)); + + registerTypeSyntax( + Type::SURFACESHADER, + std::make_shared( + this, + "surfaceshader", + "surfaceshader(vec3f(0.0), vec3f(0.0))", + EMPTY_STRING, + EMPTY_STRING, + EMPTY_STRING)); + + registerTypeSyntax( + Type::VOLUMESHADER, + std::make_shared( + this, + "volumeshader", + "volumeshader(vec3f(0.0), vec3f(0.0))", + EMPTY_STRING, + EMPTY_STRING, + EMPTY_STRING)); + + registerTypeSyntax( + Type::DISPLACEMENTSHADER, + std::make_shared( + this, + "displacementshader", + "displacementshader(vec3f(0.0), 1.0)", + EMPTY_STRING, + EMPTY_STRING, + EMPTY_STRING)); + + registerTypeSyntax( + Type::LIGHTSHADER, + std::make_shared( + this, + "lightshader", + "lightshader(vec3f(0.0), vec3f(0.0))", + EMPTY_STRING, + EMPTY_STRING, + EMPTY_STRING)); + + registerTypeSyntax( + Type::MATERIAL, + std::make_shared( + this, + "material", + "material(vec3f(0.0), vec3f(0.0))", + EMPTY_STRING, + "surfaceshader", + EMPTY_STRING)); +} + +void WgslSyntax::makeValidName(string& name) const +{ + Syntax::makeValidName(name); + // WGSL identifiers cannot begin with a digit. + if (!name.empty() && std::isdigit(static_cast(name[0]))) + name = "v_" + name; +} + +bool WgslSyntax::remapEnumeration(const string& value, TypeDesc type, const string& enumNames, std::pair& result) const +{ + // Early out if not an enum input. + if (enumNames.empty()) + { + return false; + } + + // Don't convert already supported types. + if (type != Type::STRING) + { + return false; + } + + // Early out if no valid value provided. + if (value.empty()) + { + return false; + } + + // WGSL has no strings, so enum strings are remapped to an integer index. + result.first = Type::INTEGER; + result.second = nullptr; + + StringVec valueElemEnumsVec = splitString(enumNames, ","); + for (size_t i = 0; i < valueElemEnumsVec.size(); i++) + { + valueElemEnumsVec[i] = trimSpaces(valueElemEnumsVec[i]); + } + auto pos = std::find(valueElemEnumsVec.begin(), valueElemEnumsVec.end(), value); + if (pos == valueElemEnumsVec.end()) + { + throw ExceptionShaderGenError("Given value '" + value + "' is not a valid enum value for input."); + } + const int index = static_cast(std::distance(valueElemEnumsVec.begin(), pos)); + result.second = Value::createValue(index); + + return true; +} + +StructTypeSyntaxPtr WgslSyntax::createStructSyntax(const string& structTypeName, const string& defaultValue, + const string& uniformDefaultValue, const string& typeAlias, + const string& typeDefinition) const +{ + return std::make_shared( + this, + structTypeName, + defaultValue, + uniformDefaultValue, + typeAlias, + typeDefinition); +} + +string WgslStructTypeSyntax::getValue(const Value& value, bool /* uniform */) const +{ + const AggregateValue& aggValue = static_cast(value); + + string result = aggValue.getTypeString() + "("; + + string separator = ""; + for (const auto& memberValue : aggValue.getMembers()) + { + result += separator; + separator = ", "; + + const string& memberTypeName = memberValue->getTypeString(); + const TypeDesc memberTypeDesc = _parent->getType(memberTypeName); + + // Recursively use the syntax to generate the output, so nested structs are supported. + result += _parent->getValue(memberTypeDesc, *memberValue, true); + } + + result += ")"; + + return result; +} + +MATERIALX_NAMESPACE_END diff --git a/source/MaterialXGenWgsl/WgslSyntax.h b/source/MaterialXGenWgsl/WgslSyntax.h new file mode 100644 index 0000000000..8cbd4e0686 --- /dev/null +++ b/source/MaterialXGenWgsl/WgslSyntax.h @@ -0,0 +1,69 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#ifndef MATERIALX_WGSL_SYNTAX_H +#define MATERIALX_WGSL_SYNTAX_H + +/// @file +/// WGSL (WebGPU Shading Language) syntax class + +#include + +#include + +MATERIALX_NAMESPACE_BEGIN + +/// Syntax class for the WebGPU Shading Language (WGSL). +/// +/// Unlike the legacy GLSL-derived WGSL flavor, this class derives directly from the +/// base Syntax and registers native WGSL type names (f32, vec3f, mat4x4f, ...). It is +/// modeled on SlangSyntax, the sibling complete-shader, non-GLSL syntax class. +class MX_GENWGSL_API WgslSyntax : public Syntax +{ + public: + WgslSyntax(TypeSystemPtr typeSystem); + + static SyntaxPtr create(TypeSystemPtr typeSystem) { return std::make_shared(typeSystem); } + + const string& getInputQualifier() const override { return INPUT_QUALIFIER; } + const string& getOutputQualifier() const override { return OUTPUT_QUALIFIER; } + const string& getConstantQualifier() const override { return CONSTANT_QUALIFIER; }; + const string& getUniformQualifier() const override { return UNIFORM_QUALIFIER; }; + const string& getSourceFileExtension() const override { return SOURCE_FILE_EXTENSION; }; + + void makeValidName(string& name) const override; + + /// Given an input specification attempt to remap this to an enumeration which is accepted by + /// the shader generator. The enumeration may be converted to a different type than the input. + bool remapEnumeration(const string& value, TypeDesc type, const string& enumNames, std::pair& result) const override; + + StructTypeSyntaxPtr createStructSyntax(const string& structTypeName, const string& defaultValue, + const string& uniformDefaultValue, const string& typeAlias, + const string& typeDefinition) const override; + + static const string INPUT_QUALIFIER; + static const string OUTPUT_QUALIFIER; + static const string UNIFORM_QUALIFIER; + static const string CONSTANT_QUALIFIER; + static const string FLAT_QUALIFIER; + static const string SOURCE_FILE_EXTENSION; + + static const StringVec VEC2_MEMBERS; + static const StringVec VEC3_MEMBERS; + static const StringVec VEC4_MEMBERS; +}; + +/// Specialization of TypeSyntax for aggregate (struct) types in WGSL. +class MX_GENWGSL_API WgslStructTypeSyntax : public StructTypeSyntax +{ + public: + using StructTypeSyntax::StructTypeSyntax; + + string getValue(const Value& value, bool uniform) const override; +}; + +MATERIALX_NAMESPACE_END + +#endif diff --git a/source/MaterialXGenWgsl/tools/README.md b/source/MaterialXGenWgsl/tools/README.md new file mode 100644 index 0000000000..8ac0539f16 --- /dev/null +++ b/source/MaterialXGenWgsl/tools/README.md @@ -0,0 +1,112 @@ +# genwgsl library transpiler + +`glsl_to_wgsl.py` transpiles the MaterialX **genglsl shader-node** fragments +(`libraries/{stdlib,pbrlib,lights}/genglsl/mx_*.glsl`) into their **genwgsl** WGSL equivalents. +It exists to keep the WGSL node library in sync with the GLSL originals: the genwgsl nodes were +originally hand-ported and repeatedly drifted as genglsl improved, so most of them are now +generated, and re-running the tool (then diffing) surfaces drift. + +``` +python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out build/genwgsl_generated +python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --only mx_noise3d_float mx_sheen_bsdf +``` + +Requires [`naga`](https://github.com/gfx-rs/wgpu/tree/trunk/naga) (`naga-cli`, v29+) on `PATH` or at +`%NAGA%`. Output is written to a staging dir (default `build/genwgsl_generated/...`); diff it against +the committed `libraries/.../genwgsl/*.wgsl` before copying over. + +## Scope + +* **In:** standalone node `.glsl` files referenced by `file=` impls in stdlib/pbrlib/lights. +* **Out:** core `lib/` math/helper files (hand-maintained), texture/image and light nodes (naga's + GLSL frontend has no sampler / dynamic-LightData support — auto-skipped), and a handful of nodes + that depend on **hand-adapted** lib helpers (see below) — those stay hand-written. + +The result is a *reduced* library: generated nodes for everything that resolves cleanly against the +genwgsl lib, hand-written nodes for the rest. + +## How it works + +**Mental model (read this first).** naga does the actual GLSL→WGSL translation — the `naga` +subprocess call is a single line. Everything else in the script is two adapters around it: + +* a **pre-processor** that manufactures a complete, compilable shader naga will accept from an + incomplete node fragment, and +* a **post-processor** that reconciles naga's (correct but verbose) output with the conventions of + the *hand-written* genwgsl library. + +The tool does **not** reimplement transpilation, and it is a *partial* generator by design (see +Scope): most nodes generate, a known few stay hand-written. The regex/bracket-matching is deliberate +string surgery, not a real parser — naga's own run is the correctness gate, and output is +deterministic (no timestamps; overload stubs indexed by declaration order) so re-running is +byte-identical and `git diff` = drift. + +**What a reviewer should focus on:** the two hand-maintained tables — `CALL_MAP` (GLSL overload → +genwgsl name) and `EXPECTED_FALLBACK` (nodes intentionally not generated). They encode the genwgsl +library's divergences from genglsl and must be kept in step with it; everything else is mechanical. + +The pipeline, per node function: + +1. **Pre-process** — a node fragment has no `main`, `#include`s helpers, carries MaterialX + `$`-tokens, and uses generator-emitted closure structs, so none of it is valid standalone GLSL. + Build a naga-parseable translation unit: shared `#define`/`const`/`struct` context + function + *prototypes* (so helper calls resolve) + closure structs (`BSDF`/`surfaceshader`, from + `CLOSURE_PREAMBLE`) + `$`-token→placeholder swaps + the one function body + a dummy `main`. The + parsing (`parse_functions`, `build_context`) exists only to synthesize that context. +2. **Transpile** — run `naga --input-kind glsl --shader-stage frag` (which also validates). +3. **Post-process** — clean up naga's SSA-style output into a readable fragment (collapse param-copy + shadows, inline single-use temps, `vec3`→`vec3f`, drop float-literal `f` suffixes, ...). + +WGSL has no function overloading, and the hand-written genwgsl `lib/` diverges from genglsl in two +further ways a naive transpile can't satisfy. Two post-process passes bridge this: + +1. **`remap_calls` / `CALL_MAP`** — overloaded GLSL helpers get a distinct type-suffixed name in the + genwgsl lib (`mx_square_f32`, `mx_perlin_noise_float_3d`, `mx_matrix_mul_mat4_vec4`, ...). + `CALL_MAP` maps `(glsl_helper, parameter_types) → genwgsl name`. naga numbers overload stubs + `_N` by **prototype-declaration order** (kept and indexed deterministically even when unused), + and prototypes are emitted sorted, so each call is rewritten to the right lib function. + A `None` entry marks an intentionally-unsupported (adapted) overload. + +2. **`check_lib_arity`** — some genwgsl helpers were hand-adapted beyond overloading + (e.g. `mx_ggx_energy_compensation` takes a precomputed `vec3f`, not `FresnelData`; + `mx_subsurface_scattering_approx` grew a `curvature` parameter). This pass parses the real + genwgsl `lib/*.wgsl` signatures and fails any node whose helper-call arity disagrees, so it + falls back to hand-written instead of producing a shader that fails to link. + +A node that hits either case is reported as `FAIL ... (kept hand-written)` and left out of the +generated set — that is expected, not an error in the tool. The set of such nodes is listed in +`EXPECTED_FALLBACK`; the tool's exit code is non-zero only when a node *outside* that set fails +(a regression — e.g. a genglsl change that breaks a previously-generable node, or a new naga +error). If a node in `EXPECTED_FALLBACK` starts transpiling cleanly, the tool prints a `WARN` so it +can be removed from the set and its generated version committed. + +## Build-time validation (CMake) + +Configure with `-DMATERIALX_GENERATE_WGSL_LIBRARY=ON` (requires `MATERIALX_BUILD_GEN_WGSL`, a Python +interpreter, and the `naga` CLI) to add a `MaterialXGenWgslLibrary` target that runs this tool over +`libraries/` on every build, emitting to `${CMAKE_BINARY_DIR}/genwgsl_generated`. Transpile/naga +errors and regressions then surface as **build errors** (the committed `.wgsl` files stay the source +of truth — the build-tree output is for validation only). The option defaults `OFF` so ordinary +builds need neither Python nor naga. + +CMake finds naga from, in order: `-DMATERIALX_NAGA_EXECUTABLE=/path/to/naga`, the `NAGA` environment +variable, `PATH`, and the standard cargo bin directories (`$CARGO_HOME/bin`, `~/.cargo/bin`, +`%USERPROFILE%\.cargo\bin`) — so an existing `cargo install naga-cli` is picked up even when +`~/.cargo/bin` is not on `PATH`. + +If naga is still not found, CMake locates **cargo** the same way EMSDK is located +(`-DMATERIALX_CARGO_PATH=`, then `$CARGO_HOME`, then the standard `~/.cargo` / +`%USERPROFILE%\.cargo` locations, then `PATH`) and builds naga into the build tree +(`/naga`) via `cargo install naga-cli`. This install runs at **build** time (not configure, +so configure stays fast) and is cached — it recompiles only if the binary is missing. cargo must +already be installed; MaterialX does not bootstrap the Rust toolchain. If cargo cannot be found, +generation is skipped with a warning. + +## Sync workflow + +1. Regenerate to staging: `python glsl_to_wgsl.py --libraries libraries --out build/genwgsl_generated`. +2. Diff `build/genwgsl_generated/**/*.wgsl` against `libraries/**/genwgsl/*.wgsl`. +3. Copy the generated files over the committed ones (the hand-written fallbacks and `lib/` are not + produced by the tool, so they are preserved). +4. Rebuild + restage test data, run the `[genwgsl]` tests, and `naga`-validate the emitted + `Default.{vertex,pixel}.wgsl`. diff --git a/source/MaterialXGenWgsl/tools/glsl_to_wgsl.py b/source/MaterialXGenWgsl/tools/glsl_to_wgsl.py new file mode 100644 index 0000000000..73db0ad8fc --- /dev/null +++ b/source/MaterialXGenWgsl/tools/glsl_to_wgsl.py @@ -0,0 +1,631 @@ +#!/usr/bin/env python3 +# +# Copyright Contributors to the MaterialX Project +# SPDX-License-Identifier: Apache-2.0 +# +""" +Transpile MaterialX genglsl *shader-node* library fragments to genwgsl (WGSL). + +The genglsl node fragments (e.g. libraries/pbrlib/genglsl/mx_dielectric_bsdf.glsl) are not +complete shaders: they use `#include`, `$`-tokens, and define functions with no entry point. +This tool turns each node function into a complete GLSL translation unit, runs `naga` +(GLSL -> WGSL), then strips scaffolding and cleans up naga's SSA-style output into a readable +genwgsl fragment that mirrors the hand-written convention. + +Strategy (key to readable output): transpile ONE node function at a time, supplying the +included lib context as *struct definitions + function prototypes + #defines* rather than full +bodies. With a single real function body in the module, naga keeps clean parameter names and we +avoid both name-collision suffixes and `$`-tokens that live inside lib bodies. + +Re-running keeps genwgsl in sync with genglsl; diffing the output against committed files +surfaces drift. + +Scope: standalone node `.glsl` files referenced by `file=` impls in stdlib/pbrlib/lights. +Out of scope: core `lib/` helpers (hand-maintained) and image/texture nodes (naga's GLSL +frontend has no sampler support) -- the latter are auto-skipped. + +Usage: + python glsl_to_wgsl.py --libraries libraries --out build/genwgsl_generated + python glsl_to_wgsl.py --libraries libraries --only mx_conductor_bsdf mx_normalmap +""" + +import argparse +import os +import re +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +# The naga CLI (GLSL->WGSL). Resolved in main() as: --naga arg, then $NAGA, then a `naga` on PATH. +# Install with `cargo install naga-cli` (see https://github.com/gfx-rs/wgpu/tree/trunk/naga). +NAGA = os.environ.get("NAGA") or "naga" + + +def resolve_naga(explicit=None): + """Return a runnable naga command: explicit --naga, else $NAGA, else `naga` from PATH.""" + return explicit or os.environ.get("NAGA") or shutil.which("naga") or "naga" + + +def naga_version(naga): + """Return naga's version string if it runs, else None (used to fail early with guidance).""" + try: + r = subprocess.run([naga, "--version"], capture_output=True, text=True) + return r.stdout.strip() if r.returncode == 0 else None + except OSError: + return None + + +def discover_libs(libroot): + """Library folders to process: every immediate subdirectory of --libraries that has a `genglsl` + node directory. Today that is stdlib/pbrlib/lights (bxdf is nodegraph-based, nprlib/targets have + no genglsl node fragments), but auto-discovering means new or downstream libraries are picked up + without editing this tool. Sorted for stable output ordering.""" + libroot = Path(libroot) + if not libroot.is_dir(): + return [] + return sorted(p.name for p in libroot.iterdir() if (p / "genglsl").is_dir()) + + +SKIP_PATTERNS = [re.compile(p) for p in ( + r"image", # texture nodes: naga's GLSL frontend has no sampler support + r"hextiled", # texture nodes + r"_light$", # light shaders take the dynamically-generated LightData struct +)] + +# Nodes that are intentionally NOT generated and remain hand-written, by file stem. These call lib +# helpers the genwgsl lib hand-adapted (FresnelData-taking ggx helpers, mx_subsurface_scattering_approx's +# extra `curvature` param) or hit a naga limitation (chiang hair). They are EXPECTED to fail transpile, +# so they don't constitute a build error -- only an *unexpected* failure (a previously-generable node +# that broke, e.g. after a genglsl change) sets a non-zero exit. Keep this in sync with the committed +# hand-written .wgsl files: if one starts transpiling cleanly, the tool warns so it can be removed here. +EXPECTED_FALLBACK = { + "mx_chiang_hair_bsdf", + "mx_conductor_bsdf", + "mx_dielectric_bsdf", + "mx_generalized_schlick_bsdf", + "mx_subsurface_bsdf", +} + +TYPE_FIXUPS = [ + (re.compile(r"\bvec([234])"), r"vec\1f"), + (re.compile(r"\bvec([234])"), r"vec\1i"), + (re.compile(r"\bvec([234])"), r"vec\1u"), + (re.compile(r"\bmat([234])x\1"), r"mat\1x\1f"), +] + +# WGSL has no function overloading, so the hand-written genwgsl lib gives each GLSL overload a +# distinct, type-suffixed name. This table maps a GLSL helper call -- keyed by (name, parameter +# types) exactly as build_context extracts them -- to the genwgsl function it resolves to. +# naga disambiguates overloaded calls with a deterministic `_N` suffix in prototype-declaration +# order (verified: stubs are kept and indexed by declaration order even when unused), so the +# remap pass below can rewrite each call to its genwgsl name. +# * value None -> intentionally unsupported (the genwgsl lib adapted this signature, e.g. the +# FresnelData-taking BSDF helpers); a node calling it stays hand-written. +# * missing key -> an unmapped overload; treated like None (node stays hand-written) and logged. +CALL_MAP = { + ("mx_square", ("float",)): "mx_square_f32", + ("mx_square", ("vec2",)): "mx_square_vec2", + ("mx_square", ("vec3",)): "mx_square_vec3", + + ("mx_f0_to_ior", ("float",)): "mx_f0_to_ior", + ("mx_f0_to_ior", ("vec3",)): "mx_f0_to_ior_vec3", + + ("mx_fresnel_schlick", ("float", "float")): "mx_fresnel_schlick_f32", + ("mx_fresnel_schlick", ("float", "vec3")): "mx_fresnel_schlick_vec3", + ("mx_fresnel_schlick", ("float", "float", "float")): "mx_fresnel_schlick_f32_f90", + ("mx_fresnel_schlick", ("float", "vec3", "vec3")): "mx_fresnel_schlick_vec3_f90", + ("mx_fresnel_schlick", ("float", "float", "float", "float")): "mx_fresnel_schlick_f32_exp", + ("mx_fresnel_schlick", ("float", "vec3", "vec3", "float")): "mx_fresnel_schlick_vec3_exp", + + ("mx_perlin_noise_float", ("vec2",)): "mx_perlin_noise_float_2d", + ("mx_perlin_noise_float", ("vec3",)): "mx_perlin_noise_float_3d", + ("mx_perlin_noise_vec3", ("vec2",)): "mx_perlin_noise_vec3_2d", + ("mx_perlin_noise_vec3", ("vec3",)): "mx_perlin_noise_vec3_3d", + + ("mx_cell_noise_float", ("float",)): "mx_cell_noise_float_f32", + ("mx_cell_noise_float", ("vec2",)): "mx_cell_noise_float_vec2", + ("mx_cell_noise_float", ("vec3",)): "mx_cell_noise_float_vec3", + ("mx_cell_noise_float", ("vec4",)): "mx_cell_noise_float_vec4", + + ("mx_worley_noise_float", ("vec2", "float", "int", "int")): "mx_worley_noise_float_2d", + ("mx_worley_noise_float", ("vec3", "float", "int", "int")): "mx_worley_noise_float_3d", + ("mx_worley_noise_vec2", ("vec2", "float", "int", "int")): "mx_worley_noise_vec2_2d", + ("mx_worley_noise_vec2", ("vec3", "float", "int", "int")): "mx_worley_noise_vec2_3d", + ("mx_worley_noise_vec3", ("vec2", "float", "int", "int")): "mx_worley_noise_vec3_2d", + ("mx_worley_noise_vec3", ("vec3", "float", "int", "int")): "mx_worley_noise_vec3_3d", + + ("mx_matrix_mul", ("vec2", "mat2")): "mx_matrix_mul_vec2_mat2", + ("mx_matrix_mul", ("vec3", "mat3")): "mx_matrix_mul_vec3_mat3", + ("mx_matrix_mul", ("vec4", "mat4")): "mx_matrix_mul_vec4_mat4", + ("mx_matrix_mul", ("mat2", "vec2")): "mx_matrix_mul_mat2_vec2", + ("mx_matrix_mul", ("mat3", "vec3")): "mx_matrix_mul_mat3_vec3", + ("mx_matrix_mul", ("mat4", "vec4")): "mx_matrix_mul_mat4_vec4", + ("mx_matrix_mul", ("mat2", "mat2")): "mx_matrix_mul_mat2_mat2", + ("mx_matrix_mul", ("mat3", "mat3")): "mx_matrix_mul_mat3_mat3", + ("mx_matrix_mul", ("mat4", "mat4")): "mx_matrix_mul_mat4_mat4", + + # vec3/vec3-F90 directional albedo maps cleanly; the float and FresnelData overloads were + # adapted in the genwgsl lib (caller precomputes), so nodes using them stay hand-written. + ("mx_ggx_dir_albedo", ("float", "float", "vec3", "vec3")): "mx_ggx_dir_albedo", + ("mx_ggx_dir_albedo", ("float", "float", "float", "float")): None, + ("mx_ggx_dir_albedo", ("float", "float", "FresnelData")): None, + # Single GLSL signature, but the genwgsl lib takes a precomputed Fss (vec3f), not FresnelData. + ("mx_ggx_energy_compensation", ("float", "float", "FresnelData")): None, +} + + +# ---------------------------------------------------------------------------- parsing + +# These two scanners do depth-aware bracket matching so we can carve functions and argument lists +# out of source text without a full parser. They are the foundation the rest of the parsing relies +# on -- naga gives us no AST, so everything here is string surgery over balanced brackets. + +def _match_brace(text, i): + """Given index of an opening '{', return index just past the matching '}'.""" + depth = 0 + while i < len(text): + if text[i] == "{": + depth += 1 + elif text[i] == "}": + depth -= 1 + if depth == 0: + return i + 1 + i += 1 + return len(text) + + +def _match_paren(text, i): + """Given index of an opening '(', return the index OF the matching ')' (-1 if unbalanced). + + Note the asymmetry with _match_brace: this returns the closing paren's own index (callers slice + text[open+1:close] to get the contents), whereas _match_brace returns one past the '}'.""" + depth = 0 + while i < len(text): + if text[i] == "(": + depth += 1 + elif text[i] == ")": + depth -= 1 + if depth == 0: + return i + i += 1 + return -1 + + +GLSL_KEYWORDS = {"if", "else", "for", "while", "switch", "do", "return", "case", "default"} + + +def parse_functions(text): + """Return [{ret, name, params, full}] for every top-level function *definition*. + + Heuristic, not a real parser: we match the ` (` shape, then confirm it is a + definition (not a call or a prototype) by checking that a `{` follows the closing paren. The + keyword filter rejects control-flow that looks like ` (` (e.g. `else if (`), and the + `{`-lookahead rejects prototypes ending in `;`. `full` is the entire definition text (signature + through matching `}`) so callers can re-emit or wrap it.""" + fns = [] + for m in re.finditer(r"\b([A-Za-z_]\w*)\s+([A-Za-z_]\w*)\s*\(", text): + if m.group(1) in GLSL_KEYWORDS or m.group(2) in GLSL_KEYWORDS: + continue # control-flow construct (e.g. `else if (...) {`), not a definition + open_p = text.index("(", m.start(2)) + close_p = _match_paren(text, open_p) + if close_p < 0: + continue + # Skip whitespace after `)`; a definition has `{` here, a call/prototype has `;` or `,`. + j = close_p + 1 + while j < len(text) and text[j].isspace(): + j += 1 + if j >= len(text) or text[j] != "{": + continue # a call/prototype, not a definition + end = _match_brace(text, j) + fns.append({"ret": m.group(1), "name": m.group(2), + "params": text[open_p + 1:close_p].strip(), "full": text[m.start(1):end]}) + return fns + + +# Closure/shader types the generator emits (GlslShaderGenerator::emitTypeDefinitions), not the +# lib files -- supply them so node BSDF/EDF signatures parse. +CLOSURE_PREAMBLE = """ +struct BSDF { vec3 response; vec3 throughput; }; +struct VDF { vec3 response; vec3 throughput; }; +#define EDF vec3 +struct surfaceshader { vec3 color; vec3 transparency; }; +struct volumeshader { vec3 color; vec3 transparency; }; +struct displacementshader { vec3 offset; float scale; }; +struct lightshader { vec3 intensity; vec3 direction; }; +#define material surfaceshader +""" + + +def build_context(libroot, libs): + """Build the naga parse context. Returns (base, protos, overloaded): + base - CLOSURE_PREAMBLE + dedup'd #defines/consts/struct-defs from every genglsl lib. + protos - {(name, types): "prototype;"} for every function defined in any genglsl lib OR + node file, so helpers AND cross-node calls (e.g. mx_burn_color3 -> mx_burn_float) + and generator-supplied helpers (mx_environment_radiance) all resolve. + overloaded - the set of names appearing with more than one signature (informational; the + actual overload remapping is driven by CALL_MAP in remap_calls). + Per node we emit `base` + every prototype except the one being defined.""" + defines, consts, structs, protos = {}, {}, {}, {} + # Scan lib/ helpers and the node files themselves (the latter for cross-node prototypes). + sources = [] + for lib in libs: + gldir = libroot / lib / "genglsl" + if gldir.is_dir(): + sources += sorted((gldir / "lib").glob("*.glsl")) + # Node files too (for cross-node prototypes), minus the skipped ones (light shaders + # reference the dynamic LightData struct; texture nodes carry $-tokens). + sources += [f for f in sorted(gldir.glob("mx_*.glsl")) + if not any(p.search(f.stem) for p in SKIP_PATTERNS)] + # Collect #defines, consts, struct definitions, and function prototypes across all sources. + # `setdefault` keeps the first occurrence, so a symbol defined in several files is deduped. + for f in sources: + txt = f.read_text(encoding="utf-8") + for l in txt.splitlines(): + m = re.match(r"#define\s+(\w+)", l.lstrip()) + if m: + defines.setdefault(m.group(1), l.strip()) + for m in re.finditer(r"^\s*const\s+[\w<>]+\s+(\w+)\s*=[^;{]*;", txt, re.M): + consts.setdefault(m.group(1), m.group(0).strip()) + for m in re.finditer(r"\bstruct\s+(\w+)\s*\{[^}]*\}\s*;?", txt): + structs.setdefault(m.group(1), m.group(0)) + for fn in parse_functions(txt): + # Dedupe by name + parameter *types* (GLSL overload identity, ignoring the + # `const`/`in` qualifiers naga also ignores), so the same helper defined in + # several lib files collapses to one prototype while real overloads survive. + types = [] + for p in fn["params"].split(","): + p = re.sub(r"^\s*(const|in)\s+", "", p.strip()) # drop leading qualifier + if p: + # Strip the parameter NAME (and any `[N]` array suffix) off the end, leaving the + # bare type; collapse internal whitespace. e.g. "vec3 F0" -> "vec3". + types.append(re.sub(r"\s+", " ", re.sub(r"\s+[A-Za-z_]\w*\s*(\[\d*\])?$", "", p)).strip()) + protos.setdefault((fn["name"], tuple(types)), f"{fn['ret']} {fn['name']}({fn['params']});") + base_items = list(defines.values()) + list(consts.values()) + list(structs.values()) + base = CLOSURE_PREAMBLE + "\n".join(s for s in base_items if "$" not in s) + protos = {k: v for k, v in protos.items() if "$" not in v} + # Names with more than one signature are GLSL-overloaded. WGSL has no overloading and the + # hand-written genwgsl lib renames/consolidates these (e.g. mx_square_f32, a single + # mx_ggx_dir_albedo), so the util cannot guarantee a transpiled call resolves against the lib. + proto_names = [nm for (nm, _t) in protos] + overloaded = {nm for nm in proto_names if proto_names.count(nm) > 1} + return base, protos, overloaded + + +# ---------------------------------------------------------------------------- cleanup + +# naga emits SSA-style output: function parameters are immutable in its IR, so it copies each into a +# mutable local shadow, and it spills every subexpression into a numbered `let _eN`. Faithful but +# unreadable. The two passes below undo that churn with conservative, single-assignment-safe rewrites +# (anything ambiguous is left as-is -- correct but verbose), so the committed WGSL reads like the +# hand-written files. + +def collapse_param_copies(body, params): + """Fold naga's read-only param shadows: `var p_1: T; p_1 = p;` -> use `p` directly. + + Only safe when the shadow is assigned exactly once (the `p_1 = p;` init). If naga reassigns the + shadow later, the single-assignment test fails and we leave it alone.""" + for p in params: + for m in re.finditer(r"\bvar\s+(" + re.escape(p) + r"_\d+)\s*:\s*[\w<>]+\s*;", body): + shadow = m.group(1) + if len(re.findall(r"\b" + re.escape(shadow) + r"\s*=", body)) == 1 and \ + re.search(r"\b" + re.escape(shadow) + r"\s*=\s*" + re.escape(p) + r"\s*;", body): + body = re.sub(r"[ \t]*\bvar\s+" + re.escape(shadow) + r"\s*:\s*[\w<>]+\s*;\n?", "", body) + body = re.sub(r"[ \t]*\b" + re.escape(shadow) + r"\s*=\s*" + re.escape(p) + r"\s*;\n?", "", body) + body = re.sub(r"\b" + re.escape(shadow) + r"\b", p, body) + return body + + +def inline_single_use_temps(body): + """Inline naga's single-use `let _eN = expr;` temporaries. + + Iterates to a fixpoint: each pass inlines one temp used exactly once (the `- 1` discounts the + definition itself from the match count), then restarts because inlining can make another temp + single-use. The expr is wrapped in parens when it contains an operator, so precedence is + preserved at the splice site.""" + changed = True + while changed: + changed = False + for m in re.finditer(r"[ \t]*let\s+(_e\d+)\s*=\s*(.+?);\n", body): + name, expr = m.group(1), m.group(2) + if len(re.findall(r"\b" + re.escape(name) + r"\b", body)) - 1 == 1: + repl = "(" + expr + ")" if re.search(r"[-+*/ ]", expr.strip()) else expr + body = body[:m.start()] + body[m.end():] + body = re.sub(r"\b" + re.escape(name) + r"\b", lambda _m: repl, body, count=1) + changed = True + break # body changed; restart the scan from the top + return body + + +def cleanup_function(fn_text): + """Run both readability passes over one WGSL function, in dependency order (param shadows first, + then temps, since collapsing a shadow can leave a temp single-use).""" + sig = re.match(r"fn\s+\w+\s*\(([^)]*)\)", fn_text, re.S) + params = re.findall(r"(\w+)\s*:", sig.group(1)) if sig else [] + return inline_single_use_temps(collapse_param_copies(fn_text, params)) + + +def fn_base(name): + """naga may append `_`/`_N` to disambiguate; strip it to recover the source name.""" + return re.sub(r"_\d*$", "", name) + + +def _count_items(s, brackets="()[]<>"): + """Count top-level comma-separated items in `s` (0 if blank), ignoring commas nested inside + the given bracket pairs.""" + s = s.strip() + if not s: + return 0 + opens = brackets[0::2] + closes = brackets[1::2] + depth = 0 + n = 1 + for ch in s: + if ch in opens: + depth += 1 + elif ch in closes: + depth = max(0, depth - 1) + elif ch == "," and depth == 0: + n += 1 + return n + + +def build_wgsl_lib_symbols(libroot, libs): + """Parse the hand-written genwgsl `lib/` files into {function name: parameter count}. + + The genwgsl lib has been hand-adapted in places that diverge from genglsl beyond overloading + (e.g. mx_subsurface_scattering_approx grew a `curvature` parameter, mx_ggx_energy_compensation + takes a precomputed Fss). A transpiled node faithfully follows the GLSL call, so we validate + every helper call against the real lib arity and fall the node back to hand-written on a + mismatch -- this auto-detects such divergences instead of discovering them at naga link time.""" + symbols = {} + for lib in libs: + wdir = libroot / lib / "genwgsl" / "lib" + if not wdir.is_dir(): + continue + for f in sorted(wdir.glob("*.wgsl")): + txt = f.read_text(encoding="utf-8") + for m in re.finditer(r"\bfn\s+([A-Za-z_]\w*)\s*\(", txt): + op = txt.index("(", m.start()) + cp = _match_paren(txt, op) + if cp >= 0: + symbols[m.group(1)] = _count_items(txt[op + 1:cp]) + return symbols + + +def check_lib_arity(text, lib_symbols): + """Return a list of calls whose arg count disagrees with the genwgsl lib's parameter count.""" + bad = [] + for m in re.finditer(r"\b(mx_\w+)\s*\(", text): + name = m.group(1) + if name not in lib_symbols: + continue # cross-node helper, builtin, or the node's own fn -- not a lib symbol + op = text.index("(", m.start()) + cp = _match_paren(text, op) + if cp < 0: + continue + nargs = _count_items(text[op + 1:cp], "()[]") # call args: no generics, keep '<'/'>' literal + if nargs != lib_symbols[name]: + bad.append(f"{name} (call has {nargs}, lib expects {lib_symbols[name]})") + return bad + + +def remap_calls(text, node_fn_name, protos): + """Rewrite overloaded/diverged helper calls to their genwgsl names via CALL_MAP. + + naga numbers overload stubs (`mx_foo`, `mx_foo_1`, ...) by prototype-declaration order, and + proto_block is emitted sorted by (name, types), so the i-th call name corresponds to the i-th + entry of the base's sorted type list. Returns (text, unsupported) where unsupported lists any + call whose CALL_MAP entry is None/missing -- the caller fails the node so it stays hand-written. + """ + by_base = {} + for (nm, types) in protos: + by_base.setdefault(nm, []).append(types) + for nm in by_base: + by_base[nm] = sorted(by_base[nm]) + + unsupported = [] + for base in sorted({b for (b, _t) in CALL_MAP}, key=len, reverse=True): + if base == node_fn_name or base not in by_base: + continue + for i, types in enumerate(by_base[base]): + callname = base if i == 0 else f"{base}_{i}" + if not re.search(r"\b" + re.escape(callname) + r"\s*\(", text): + continue + target = CALL_MAP.get((base, types)) + if not target: # None (adapted) or missing (unmapped overload) -> keep hand-written + unsupported.append(callname + "(" + ", ".join(types) + ")") + continue + text = re.sub(r"\b" + re.escape(callname) + r"\s*\(", target + "(", text) + return text, unsupported + + +def top_level_fns(wgsl): + """Yield (name, text) for each top-level `fn` in a WGSL module.""" + for m in re.finditer(r"\bfn\s+([A-Za-z_]\w*)\s*\(", wgsl): + brace = wgsl.index("{", m.start()) + yield m.group(1), wgsl[m.start():_match_brace(wgsl, brace)] + + +# ---------------------------------------------------------------------------- driver + +def transpile(node_path, out_path, base, protos, lib_symbols): + node_src = node_path.read_text(encoding="utf-8") + node_fns = parse_functions(node_src) + if not node_fns: + print(f" SKIP {node_path.name}: no functions found") + return False + + all_names = {nm for (nm, _t) in protos} + + outputs = [] + for fn in node_fns: + # Supply every known prototype except the function being defined here (siblings and + # cross-node helpers are all in `protos`, keyed by name+types). + # Emit sorted by (name, types) so each overloaded base's prototypes are declared in the + # same order remap_calls indexes them (naga numbers overload stubs by declaration order). + proto_block = "\n".join(p for (nm, _t), p in sorted(protos.items()) if nm != fn["name"]) + body = fn["full"] + + # MaterialX `$`-tokens (e.g. $blur, $albedoTable) aren't valid GLSL identifiers, so naga + # can't parse them. Swap each `$tok` for a legal placeholder `MTLXTOK_tok` before transpiling + # and restore it afterwards; token_map records the reverse mapping for this function. + token_map = {} + + def sentinel(m): + s = "MTLXTOK_" + m.group(1) + token_map[s] = m.group(0) + return s + body = re.sub(r"\$([A-Za-z_]\w*)", sentinel, body) + + # Assemble a complete, naga-parseable fragment shader: version + shared context (structs, + # #defines, prototypes) + this one real function body + a do-nothing entry point. naga's GLSL + # frontend requires a staged shader with a main(); it keeps non-entry functions in the output, + # which is exactly the one body we want back. + glsl = ("#version 450\n" + base + "\n" + proto_block + "\n" + body + + "\nlayout(location=0) out vec4 mtlx_o;\nvoid main() { mtlx_o = vec4(0.0); }\n") + + with tempfile.TemporaryDirectory() as td: + frag = Path(td) / "in.frag" + wtmp = Path(td) / "out.wgsl" + frag.write_text(glsl, encoding="utf-8") + r = subprocess.run([NAGA, "--input-kind", "glsl", "--shader-stage", "frag", + str(frag), str(wtmp)], capture_output=True, text=True) + if r.returncode != 0: + err = next((l for l in r.stderr.splitlines() if "error" in l.lower()), "naga error") + print(f" FAIL {node_path.name}::{fn['name']}: {err.strip()}") + if os.environ.get("MTLX_DEBUG"): + dbg = out_path.parent / f"_debug_{fn['name']}.frag" + dbg.parent.mkdir(parents=True, exist_ok=True) + dbg.write_text(glsl, encoding="utf-8") + print(f" wrote {dbg}") + return False + wgsl = wtmp.read_text(encoding="utf-8") + + # Pull our one real function back out of naga's module. The context prototypes become empty + # stub functions in the output, so match by source name (fn_base strips any naga `_N` suffix) + # AND require a non-trivial body: `"{\n}"` excludes the empty stubs and the `> 1` line count + # guards against a one-liner stub slipping through. Then rename the suffixed fn back to clean. + text = None + for name, t in top_level_fns(wgsl): + if fn_base(name) == fn["name"] and "{\n}" not in t and t.count("\n") > 1: + text = re.sub(r"(\bfn\s+)" + re.escape(name) + r"\b", r"\1" + fn["name"], t) + break + if text is None: + print(f" FAIL {node_path.name}::{fn['name']}: not found in naga output") + return False + + text = cleanup_function(text) + for rx, repl in TYPE_FIXUPS: + text = rx.sub(repl, text) + # Float literal suffixes: drop `f` from decimals first (so the int rule can't bite a + # decimal's fractional digits, e.g. 0.00000001f -> 0.00000001), then pad bare ints (2f -> 2.0). + text = re.sub(r"\b(\d+\.\d+(?:[eE][-+]?\d+)?)f\b", r"\1", text) + text = re.sub(r"(?.wgsl extension swap, so the generated + # fragment pulls in the same lib helpers (now their WGSL versions) the GLSL source did. A node + # file may define several functions (e.g. a vector2 + float variant); join them in source order. + includes = [f'#include "{inc.replace(".glsl", ".wgsl")}"' + for inc in re.findall(r'#include\s+"([^"]+)"', node_src)] + header = ("\n".join(includes) + "\n\n") if includes else "" + + # Banner marking the file as generated, so it is visually distinct from the hand-written + # genwgsl nodes it sits next to. Names the genglsl source (lib-relative, so the line is stable + # regardless of where --libraries points) and the tool. No timestamp: re-running must produce + # byte-identical output so `git diff` cleanly surfaces drift. + src_rel = f"libraries/{node_path.parent.parent.name}/genglsl/{node_path.name}" + banner = (f"// Generated from {src_rel} by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py.\n" + f"// Do not edit -- re-run the transpiler to regenerate " + f"(see source/MaterialXGenWgsl/README.md).\n\n") + + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(banner + header + "\n\n".join(outputs) + "\n", encoding="utf-8") + print(f" OK {node_path.name} -> {out_path}") + return True + + +def main(): + ap = argparse.ArgumentParser(description="Transpile genglsl node fragments to genwgsl.") + ap.add_argument("--libraries", default="libraries") + ap.add_argument("--out", default="build/genwgsl_generated") + ap.add_argument("--only", nargs="*") + ap.add_argument("--naga", help="Path to the naga CLI (overrides the NAGA env var / PATH lookup).") + args = ap.parse_args() + + # Resolve and preflight naga so a missing tool fails once, with guidance, instead of per node. + global NAGA + NAGA = resolve_naga(args.naga) + version = naga_version(NAGA) + if not version: + print(f"ERROR: naga CLI not found or not runnable (tried '{NAGA}').\n" + " Install it with `cargo install naga-cli`, or pass --naga / set the\n" + " NAGA environment variable. See https://github.com/gfx-rs/wgpu/tree/trunk/naga.", + file=sys.stderr) + return 2 + print(f"Using naga: {NAGA} ({version})") + + libroot, outroot = Path(args.libraries), Path(args.out) + libs = discover_libs(libroot) + # Build the two reference tables once, up front: the GLSL parse context (shared by every node) + # and the genwgsl lib's real signatures (for the arity check). `_overloaded` is unused here -- + # remap_calls drives off CALL_MAP -- but build_context returns it for callers that want it. + base, protos, _overloaded = build_context(libroot, libs) + lib_symbols = build_wgsl_lib_symbols(libroot, libs) + ok = skip = 0 + expected, unexpected = [], [] # node stems that failed transpile, by category + for lib in libs: + gldir = libroot / lib / "genglsl" + if not gldir.is_dir(): + continue + for glsl in sorted(gldir.glob("mx_*.glsl")): + base_name = glsl.stem + if args.only and base_name not in args.only: + continue + if any(p.search(base_name) for p in SKIP_PATTERNS): + print(f" SKIP {glsl.name}: texture/sampler node (unsupported by naga)") + skip += 1 + continue + out = outroot / lib / "genwgsl" / (base_name + ".wgsl") + if transpile(glsl, out, base, protos, lib_symbols): + ok += 1 + if base_name in EXPECTED_FALLBACK: + # A node we expected to stay hand-written now transpiles cleanly -- the lib + # divergence it depended on was probably resolved. Not fatal, but worth flagging. + print(f" WARN {glsl.name}: now transpiles cleanly; remove it from " + f"EXPECTED_FALLBACK and commit the generated version.") + elif base_name in EXPECTED_FALLBACK: + expected.append(base_name) # known hand-written fallback -- not a build error + else: + unexpected.append(base_name) # regression: should have generated, but didn't + + print(f"\nDone: {ok} transpiled, {skip} skipped, " + f"{len(expected)} expected fallbacks, {len(unexpected)} unexpected failures.") + if unexpected: + # Surface as an error so a CMake build step fails (see MATERIALX_GENERATE_WGSL_LIBRARY). + print("ERROR: nodes that should transpile failed: " + ", ".join(sorted(unexpected))) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/source/MaterialXTest/CMakeLists.txt b/source/MaterialXTest/CMakeLists.txt index 2cb7c1330a..7d408050e5 100644 --- a/source/MaterialXTest/CMakeLists.txt +++ b/source/MaterialXTest/CMakeLists.txt @@ -46,7 +46,7 @@ endif() add_subdirectory(MaterialXGenShader) target_link_libraries(MaterialXTest MaterialXGenShader) -if(MATERIALX_BUILD_GEN_GLSL OR MATERIALX_BUILD_GEN_OSL OR MATERIALX_BUILD_GEN_MDL OR MATERIALX_BUILD_GEN_MSL OR MATERIALX_BUILD_GEN_SLANG) +if(MATERIALX_BUILD_GEN_GLSL OR MATERIALX_BUILD_GEN_OSL OR MATERIALX_BUILD_GEN_MDL OR MATERIALX_BUILD_GEN_MSL OR MATERIALX_BUILD_GEN_SLANG OR MATERIALX_BUILD_GEN_WGSL) if(MATERIALX_BUILD_GEN_GLSL) add_subdirectory(MaterialXGenGlsl) target_link_libraries(MaterialXTest MaterialXGenGlsl) @@ -55,6 +55,10 @@ if(MATERIALX_BUILD_GEN_GLSL OR MATERIALX_BUILD_GEN_OSL OR MATERIALX_BUILD_GEN_MD add_subdirectory(MaterialXGenSlang) target_link_libraries(MaterialXTest MaterialXGenSlang) endif() + if(MATERIALX_BUILD_GEN_WGSL) + add_subdirectory(MaterialXGenWgsl) + target_link_libraries(MaterialXTest MaterialXGenWgsl) + endif() if(MATERIALX_BUILD_GEN_OSL) add_subdirectory(MaterialXGenOsl) target_link_libraries(MaterialXTest MaterialXGenOsl) diff --git a/source/MaterialXTest/MaterialXGenGlsl/GenGlsl.cpp b/source/MaterialXTest/MaterialXGenGlsl/GenGlsl.cpp index e99d83a452..a39e2600f9 100644 --- a/source/MaterialXTest/MaterialXGenGlsl/GenGlsl.cpp +++ b/source/MaterialXTest/MaterialXGenGlsl/GenGlsl.cpp @@ -19,7 +19,6 @@ #include #include #include -#include #include namespace mx = MaterialX; @@ -141,8 +140,7 @@ enum class GlslType Essl, Glsl, GlslLayout, - GlslVulkan, - GlslWgsl + GlslVulkan }; static void generateGlslCode(GlslType type) @@ -163,10 +161,6 @@ static void generateGlslCode(GlslType type) { generator = mx::VkShaderGenerator::create(); } - else if (type == GlslType::GlslWgsl) - { - generator = mx::WgslShaderGenerator::create(); - } else { generator = mx::GlslShaderGenerator::create(); @@ -177,8 +171,7 @@ static void generateGlslCode(GlslType type) { GlslType::Essl, "essl" }, { GlslType::Glsl, "glsl" }, { GlslType::GlslLayout, "glsl_layout" }, - { GlslType::GlslVulkan, "glsl_vulkan" }, - { GlslType::GlslWgsl , "glsl_wgsl" } + { GlslType::GlslVulkan, "glsl_vulkan" } }; const mx::FilePath logPath("genglsl_" + TYPE_NAME_MAP.at(type) + "_generate_test.txt"); GlslShaderGeneratorTester tester(generator, testRootPaths, searchPath, logPath, false); @@ -215,8 +208,3 @@ TEST_CASE("GenShader: Vulkan GLSL Shader Generation", "[genglsl]") { generateGlslCode(GlslType::GlslVulkan); } - -TEST_CASE("GenShader: Wgsl GLSL Shader Generation", "[genglsl]") -{ - generateGlslCode(GlslType::GlslWgsl); -} diff --git a/source/MaterialXTest/MaterialXGenWgsl/CMakeLists.txt b/source/MaterialXTest/MaterialXGenWgsl/CMakeLists.txt new file mode 100644 index 0000000000..f42e36e06d --- /dev/null +++ b/source/MaterialXTest/MaterialXGenWgsl/CMakeLists.txt @@ -0,0 +1,9 @@ +file(GLOB_RECURSE source "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") +file(GLOB_RECURSE headers "${CMAKE_CURRENT_SOURCE_DIR}/*.h") + +target_sources(MaterialXTest PUBLIC ${source} ${headers}) + +add_tests("${source}") + +assign_source_group("Source Files" ${source}) +assign_source_group("Header Files" ${headers}) diff --git a/source/MaterialXTest/MaterialXGenWgsl/GenWgsl.cpp b/source/MaterialXTest/MaterialXGenWgsl/GenWgsl.cpp new file mode 100644 index 0000000000..347afd8e1d --- /dev/null +++ b/source/MaterialXTest/MaterialXGenWgsl/GenWgsl.cpp @@ -0,0 +1,88 @@ +// +// 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; + +TEST_CASE("GenShader: WGSL Syntax Check", "[genwgsl]") +{ + mx::TypeSystemPtr typeSystem = mx::TypeSystem::create(); + mx::SyntaxPtr syntax = mx::WgslSyntax::create(typeSystem); + + REQUIRE(syntax->getTypeName(mx::Type::FLOAT) == "f32"); + REQUIRE(syntax->getTypeName(mx::Type::COLOR3) == "vec3f"); + REQUIRE(syntax->getTypeName(mx::Type::VECTOR4) == "vec4f"); + REQUIRE(syntax->getTypeName(mx::Type::MATRIX44) == "mat4x4f"); + REQUIRE(syntax->getSourceFileExtension() == ".wgsl"); +} + +TEST_CASE("GenShader: WGSL Target Registration", "[genwgsl]") +{ + mx::ShaderGeneratorPtr generator = mx::WgslShaderGenerator::create(); + REQUIRE(generator->getTarget() == "genwgsl"); +} + +TEST_CASE("GenShader: WGSL Shader Generation", "[genwgsl]") +{ + mx::FileSearchPath searchPath = mx::getDefaultDataSearchPath(); + + mx::DocumentPtr doc = mx::createDocument(); + mx::loadLibraries({ "libraries" }, searchPath, doc); + + // Load a representative lit surface material (standard_surface). + mx::FilePath examplePath = searchPath.find("resources/Materials/Examples/StandardSurface/standard_surface_default.mtlx"); + REQUIRE(!examplePath.isEmpty()); + mx::readFromXmlFile(doc, examplePath); + + mx::ShaderGeneratorPtr generator = mx::WgslShaderGenerator::create(); + mx::GenContext context(generator); + context.registerSourceCodeSearchPath(searchPath); + context.getOptions().hwSpecularEnvironmentMethod = mx::SPECULAR_ENVIRONMENT_FIS; + context.getOptions().hwMaxActiveLightSources = 1; + + std::vector renderables = mx::findRenderableElements(doc); + REQUIRE(!renderables.empty()); + + size_t generated = 0; + for (const mx::TypedElementPtr& element : renderables) + { + const std::string name = mx::createValidName(element->getNamePath()); + + mx::ShaderPtr shader; + REQUIRE_NOTHROW(shader = generator->generate(name, element, context)); + REQUIRE(shader != nullptr); + + const std::string& vertexCode = shader->getSourceCode(mx::Stage::VERTEX); + const std::string& pixelCode = shader->getSourceCode(mx::Stage::PIXEL); + + REQUIRE(!vertexCode.empty()); + REQUIRE(!pixelCode.empty()); + REQUIRE(vertexCode.find("@vertex") != std::string::npos); + REQUIRE(vertexCode.find("fn vertexMain") != std::string::npos); + REQUIRE(pixelCode.find("@fragment") != std::string::npos); + REQUIRE(pixelCode.find("fn fragmentMain") != std::string::npos); + + // Write the generated WGSL to files for manual inspection / external validation. + std::ofstream(name + ".vertex.wgsl") << vertexCode; + std::ofstream(name + ".pixel.wgsl") << pixelCode; + generated++; + } + REQUIRE(generated > 0); +} diff --git a/source/PyMaterialX/PyMaterialXGenGlsl/PyGlslShaderGenerator.cpp b/source/PyMaterialX/PyMaterialXGenGlsl/PyGlslShaderGenerator.cpp index 87726352ff..ae28d9890f 100644 --- a/source/PyMaterialX/PyMaterialXGenGlsl/PyGlslShaderGenerator.cpp +++ b/source/PyMaterialX/PyMaterialXGenGlsl/PyGlslShaderGenerator.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include @@ -33,10 +32,6 @@ namespace { return mx::VkShaderGenerator::create(); } - mx::ShaderGeneratorPtr WgslShaderGenerator_create() - { - return mx::WgslShaderGenerator::create(); - } } void bindPyGlslShaderGenerator(py::module& mod) @@ -78,14 +73,3 @@ void bindPyVkShaderGenerator(py::module& mod) .def("getTarget", &mx::VkShaderGenerator::getTarget) .def("getVersion", &mx::VkShaderGenerator::getVersion); } - -// Glsl Wgsl shader generator bindings - -void bindPyWgslShaderGenerator(py::module& mod) -{ - py::class_(mod, "WgslShaderGenerator") - .def_static("create", &WgslShaderGenerator_create) - .def("generate", &mx::WgslShaderGenerator::generate) - .def("getTarget", &mx::WgslShaderGenerator::getTarget) - .def("getVersion", &mx::WgslShaderGenerator::getVersion); -} diff --git a/source/PyMaterialX/PyMaterialXGenGlsl/PyModule.cpp b/source/PyMaterialX/PyMaterialXGenGlsl/PyModule.cpp index 4e020301db..448e1f4321 100644 --- a/source/PyMaterialX/PyMaterialXGenGlsl/PyModule.cpp +++ b/source/PyMaterialX/PyMaterialXGenGlsl/PyModule.cpp @@ -11,7 +11,6 @@ void bindPyGlslShaderGenerator(py::module& mod); void bindPyGlslResourceBindingContext(py::module &mod); void bindPyEsslShaderGenerator(py::module& mod); void bindPyVkShaderGenerator(py::module& mod); -void bindPyWgslShaderGenerator(py::module& mod); PYBIND11_MODULE(PyMaterialXGenGlsl, mod) { @@ -25,5 +24,4 @@ PYBIND11_MODULE(PyMaterialXGenGlsl, mod) bindPyEsslShaderGenerator(mod); bindPyVkShaderGenerator(mod); - bindPyWgslShaderGenerator(mod); } From 8122045b65cc62cdee2b6b82f32c9a0ecfb6f199 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Thu, 2 Jul 2026 17:47:07 -0700 Subject: [PATCH 02/19] Add wgsl data library Added genwgsl library what was transpiled offline. Add an opt-in `MATERIALX_GENERATE_WGSL_LIBRARY` build option that regenerates and validates the library at build time. This needs Cargo the official build tool and package manager for Rust. MATERIALX_CARGO_PATH can be set to define path to Cargo. See https://doc.rust-lang.org/cargo/getting-started/installation.html --- CMakeLists.txt | 5 + .../lights/genwgsl/lights_genwgsl_impl.mtlx | 19 + .../lights/genwgsl/mx_directional_light.wgsl | 4 + libraries/lights/genwgsl/mx_point_light.wgsl | 7 + libraries/lights/genwgsl/mx_spot_light.wgsl | 12 + .../pbrlib/genwgsl/lib/mx_closure_type.wgsl | 49 ++ .../genwgsl/lib/mx_environment_fis.wgsl | 72 ++ .../genwgsl/lib/mx_environment_none.wgsl | 12 + .../genwgsl/lib/mx_generate_albedo_table.wgsl | 15 + .../pbrlib/genwgsl/lib/mx_microfacet.wgsl | 98 +++ .../genwgsl/lib/mx_microfacet_diffuse.wgsl | 128 +++ .../genwgsl/lib/mx_microfacet_sheen.wgsl | 72 ++ .../genwgsl/lib/mx_microfacet_specular.wgsl | 388 +++++++++ libraries/pbrlib/genwgsl/lib/mx_shadow.wgsl | 22 + .../genwgsl/lib/mx_shadow_platform.wgsl | 11 + .../genwgsl/lib/mx_transmission_opacity.wgsl | 6 + .../genwgsl/lib/mx_transmission_refract.wgsl | 19 + libraries/pbrlib/genwgsl/mx_add_bsdf.wgsl | 17 + libraries/pbrlib/genwgsl/mx_add_edf.wgsl | 16 + .../pbrlib/genwgsl/mx_anisotropic_vdf.wgsl | 23 + libraries/pbrlib/genwgsl/mx_artistic_ior.wgsl | 24 + libraries/pbrlib/genwgsl/mx_blackbody.wgsl | 56 ++ .../genwgsl/mx_burley_diffuse_bsdf.wgsl | 54 ++ .../pbrlib/genwgsl/mx_chiang_hair_bsdf.wgsl | 288 +++++++ .../pbrlib/genwgsl/mx_conductor_bsdf.wgsl | 54 ++ .../pbrlib/genwgsl/mx_dielectric_bsdf.wgsl | 61 ++ .../pbrlib/genwgsl/mx_displacement_float.wgsl | 13 + .../genwgsl/mx_displacement_vector3.wgsl | 13 + .../genwgsl/mx_generalized_schlick_bsdf.wgsl | 87 ++ .../genwgsl/mx_generalized_schlick_edf.wgsl | 33 + libraries/pbrlib/genwgsl/mx_layer_bsdf.wgsl | 17 + libraries/pbrlib/genwgsl/mx_layer_vdf.wgsl | 17 + libraries/pbrlib/genwgsl/mx_mix_bsdf.wgsl | 19 + libraries/pbrlib/genwgsl/mx_mix_edf.wgsl | 18 + .../genwgsl/mx_multiply_bsdf_color3.wgsl | 19 + .../genwgsl/mx_multiply_bsdf_float.wgsl | 19 + .../genwgsl/mx_multiply_edf_color3.wgsl | 16 + .../pbrlib/genwgsl/mx_multiply_edf_float.wgsl | 16 + .../genwgsl/mx_oren_nayar_diffuse_bsdf.wgsl | 69 ++ .../genwgsl/mx_roughness_anisotropy.wgsl | 27 + .../pbrlib/genwgsl/mx_roughness_dual.wgsl | 16 + libraries/pbrlib/genwgsl/mx_sheen_bsdf.wgsl | 83 ++ .../pbrlib/genwgsl/mx_subsurface_bsdf.wgsl | 39 + .../pbrlib/genwgsl/mx_translucent_bsdf.wgsl | 46 ++ libraries/pbrlib/genwgsl/mx_uniform_edf.wgsl | 20 + .../pbrlib/genwgsl/pbrlib_genwgsl_impl.mtlx | 89 ++ libraries/stdlib/genwgsl/lib/mx_flake.wgsl | 116 +++ libraries/stdlib/genwgsl/lib/mx_geometry.wgsl | 36 + libraries/stdlib/genwgsl/lib/mx_hextile.wgsl | 145 ++++ libraries/stdlib/genwgsl/lib/mx_hsv.wgsl | 92 +++ libraries/stdlib/genwgsl/lib/mx_math.wgsl | 70 ++ libraries/stdlib/genwgsl/lib/mx_noise.wgsl | 716 ++++++++++++++++ .../stdlib/genwgsl/lib/mx_transform_uv.wgsl | 4 + .../genwgsl/lib/mx_transform_uv_vflip.wgsl | 5 + libraries/stdlib/genwgsl/mx_aastep.wgsl | 11 + libraries/stdlib/genwgsl/mx_burn_color3.wgsl | 22 + libraries/stdlib/genwgsl/mx_burn_color4.wgsl | 24 + libraries/stdlib/genwgsl/mx_burn_float.wgsl | 20 + .../stdlib/genwgsl/mx_cellnoise2d_float.wgsl | 12 + .../stdlib/genwgsl/mx_cellnoise3d_float.wgsl | 12 + .../mx_creatematrix_vector3_matrix33.wgsl | 14 + .../mx_creatematrix_vector3_matrix44.wgsl | 16 + .../mx_creatematrix_vector4_matrix44.wgsl | 16 + .../genwgsl/mx_disjointover_color4.wgsl | 52 ++ libraries/stdlib/genwgsl/mx_dodge_color3.wgsl | 22 + libraries/stdlib/genwgsl/mx_dodge_color4.wgsl | 24 + libraries/stdlib/genwgsl/mx_dodge_float.wgsl | 20 + libraries/stdlib/genwgsl/mx_flake2d.wgsl | 26 + libraries/stdlib/genwgsl/mx_flake3d.wgsl | 24 + .../stdlib/genwgsl/mx_fractal2d_float.wgsl | 22 + .../stdlib/genwgsl/mx_fractal2d_vector2.wgsl | 22 + .../stdlib/genwgsl/mx_fractal2d_vector3.wgsl | 22 + .../stdlib/genwgsl/mx_fractal2d_vector4.wgsl | 22 + .../stdlib/genwgsl/mx_fractal3d_float.wgsl | 22 + .../stdlib/genwgsl/mx_fractal3d_vector2.wgsl | 22 + .../stdlib/genwgsl/mx_fractal3d_vector3.wgsl | 22 + .../stdlib/genwgsl/mx_fractal3d_vector4.wgsl | 22 + .../genwgsl/mx_heighttonormal_vector3.wgsl | 36 + .../stdlib/genwgsl/mx_hextiledimage.wgsl | 80 ++ .../stdlib/genwgsl/mx_hextilednormalmap.wgsl | 64 ++ .../stdlib/genwgsl/mx_hsvtorgb_color3.wgsl | 12 + .../stdlib/genwgsl/mx_hsvtorgb_color4.wgsl | 13 + libraries/stdlib/genwgsl/mx_image_color3.wgsl | 6 + libraries/stdlib/genwgsl/mx_image_color4.wgsl | 6 + libraries/stdlib/genwgsl/mx_image_float.wgsl | 6 + .../stdlib/genwgsl/mx_image_vector2.wgsl | 6 + .../stdlib/genwgsl/mx_image_vector3.wgsl | 6 + .../stdlib/genwgsl/mx_image_vector4.wgsl | 6 + .../stdlib/genwgsl/mx_luminance_color3.wgsl | 12 + .../stdlib/genwgsl/mx_luminance_color4.wgsl | 13 + .../stdlib/genwgsl/mx_mix_surfaceshader.wgsl | 13 + .../stdlib/genwgsl/mx_noise2d_float.wgsl | 18 + .../stdlib/genwgsl/mx_noise2d_vector2.wgsl | 18 + .../stdlib/genwgsl/mx_noise2d_vector3.wgsl | 18 + .../stdlib/genwgsl/mx_noise2d_vector4.wgsl | 21 + .../stdlib/genwgsl/mx_noise3d_float.wgsl | 18 + .../stdlib/genwgsl/mx_noise3d_vector2.wgsl | 18 + .../stdlib/genwgsl/mx_noise3d_vector3.wgsl | 18 + .../stdlib/genwgsl/mx_noise3d_vector4.wgsl | 21 + libraries/stdlib/genwgsl/mx_normalmap.wgsl | 42 + .../stdlib/genwgsl/mx_premult_color4.wgsl | 11 + libraries/stdlib/genwgsl/mx_ramplr_float.wgsl | 14 + .../stdlib/genwgsl/mx_ramplr_vector2.wgsl | 14 + .../stdlib/genwgsl/mx_ramplr_vector3.wgsl | 14 + .../stdlib/genwgsl/mx_ramplr_vector4.wgsl | 14 + libraries/stdlib/genwgsl/mx_ramptb_float.wgsl | 14 + .../stdlib/genwgsl/mx_ramptb_vector2.wgsl | 14 + .../stdlib/genwgsl/mx_ramptb_vector3.wgsl | 14 + .../stdlib/genwgsl/mx_ramptb_vector4.wgsl | 14 + .../stdlib/genwgsl/mx_rgbtohsv_color3.wgsl | 12 + .../stdlib/genwgsl/mx_rgbtohsv_color4.wgsl | 13 + .../stdlib/genwgsl/mx_rotate_vector2.wgsl | 18 + .../stdlib/genwgsl/mx_rotate_vector3.wgsl | 23 + .../stdlib/genwgsl/mx_smoothstep_float.wgsl | 20 + .../stdlib/genwgsl/mx_splitlr_float.wgsl | 18 + .../stdlib/genwgsl/mx_splitlr_vector2.wgsl | 18 + .../stdlib/genwgsl/mx_splitlr_vector3.wgsl | 18 + .../stdlib/genwgsl/mx_splitlr_vector4.wgsl | 18 + .../stdlib/genwgsl/mx_splittb_float.wgsl | 18 + .../stdlib/genwgsl/mx_splittb_vector2.wgsl | 18 + .../stdlib/genwgsl/mx_splittb_vector3.wgsl | 18 + .../stdlib/genwgsl/mx_splittb_vector4.wgsl | 18 + .../stdlib/genwgsl/mx_surface_unlit.wgsl | 9 + .../genwgsl/mx_transformmatrix_vector2M3.wgsl | 15 + .../genwgsl/mx_transformmatrix_vector3M4.wgsl | 15 + .../stdlib/genwgsl/mx_unpremult_color4.wgsl | 11 + .../genwgsl/mx_worleynoise2d_float.wgsl | 16 + .../genwgsl/mx_worleynoise2d_vector2.wgsl | 16 + .../genwgsl/mx_worleynoise2d_vector3.wgsl | 16 + .../genwgsl/mx_worleynoise3d_float.wgsl | 16 + .../genwgsl/mx_worleynoise3d_vector2.wgsl | 16 + .../genwgsl/mx_worleynoise3d_vector3.wgsl | 16 + .../stdlib/genwgsl/stdlib_genwgsl_impl.mtlx | 768 ++++++++++++++++++ libraries/targets/genwgsl.mtlx | 14 + 134 files changed, 5635 insertions(+) create mode 100644 libraries/lights/genwgsl/lights_genwgsl_impl.mtlx create mode 100644 libraries/lights/genwgsl/mx_directional_light.wgsl create mode 100644 libraries/lights/genwgsl/mx_point_light.wgsl create mode 100644 libraries/lights/genwgsl/mx_spot_light.wgsl create mode 100644 libraries/pbrlib/genwgsl/lib/mx_closure_type.wgsl create mode 100644 libraries/pbrlib/genwgsl/lib/mx_environment_fis.wgsl create mode 100644 libraries/pbrlib/genwgsl/lib/mx_environment_none.wgsl create mode 100644 libraries/pbrlib/genwgsl/lib/mx_generate_albedo_table.wgsl create mode 100644 libraries/pbrlib/genwgsl/lib/mx_microfacet.wgsl create mode 100644 libraries/pbrlib/genwgsl/lib/mx_microfacet_diffuse.wgsl create mode 100644 libraries/pbrlib/genwgsl/lib/mx_microfacet_sheen.wgsl create mode 100644 libraries/pbrlib/genwgsl/lib/mx_microfacet_specular.wgsl create mode 100644 libraries/pbrlib/genwgsl/lib/mx_shadow.wgsl create mode 100644 libraries/pbrlib/genwgsl/lib/mx_shadow_platform.wgsl create mode 100644 libraries/pbrlib/genwgsl/lib/mx_transmission_opacity.wgsl create mode 100644 libraries/pbrlib/genwgsl/lib/mx_transmission_refract.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_add_bsdf.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_add_edf.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_anisotropic_vdf.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_artistic_ior.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_blackbody.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_burley_diffuse_bsdf.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_chiang_hair_bsdf.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_conductor_bsdf.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_dielectric_bsdf.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_displacement_float.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_displacement_vector3.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_generalized_schlick_bsdf.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_generalized_schlick_edf.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_layer_bsdf.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_layer_vdf.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_mix_bsdf.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_mix_edf.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_multiply_bsdf_color3.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_multiply_bsdf_float.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_multiply_edf_color3.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_multiply_edf_float.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_oren_nayar_diffuse_bsdf.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_roughness_anisotropy.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_roughness_dual.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_sheen_bsdf.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_subsurface_bsdf.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_translucent_bsdf.wgsl create mode 100644 libraries/pbrlib/genwgsl/mx_uniform_edf.wgsl create mode 100644 libraries/pbrlib/genwgsl/pbrlib_genwgsl_impl.mtlx create mode 100644 libraries/stdlib/genwgsl/lib/mx_flake.wgsl create mode 100644 libraries/stdlib/genwgsl/lib/mx_geometry.wgsl create mode 100644 libraries/stdlib/genwgsl/lib/mx_hextile.wgsl create mode 100644 libraries/stdlib/genwgsl/lib/mx_hsv.wgsl create mode 100644 libraries/stdlib/genwgsl/lib/mx_math.wgsl create mode 100644 libraries/stdlib/genwgsl/lib/mx_noise.wgsl create mode 100644 libraries/stdlib/genwgsl/lib/mx_transform_uv.wgsl create mode 100644 libraries/stdlib/genwgsl/lib/mx_transform_uv_vflip.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_aastep.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_burn_color3.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_burn_color4.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_burn_float.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_cellnoise2d_float.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_cellnoise3d_float.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_creatematrix_vector3_matrix33.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_creatematrix_vector3_matrix44.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_creatematrix_vector4_matrix44.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_disjointover_color4.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_dodge_color3.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_dodge_color4.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_dodge_float.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_flake2d.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_flake3d.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_fractal2d_float.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_fractal2d_vector2.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_fractal2d_vector3.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_fractal2d_vector4.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_fractal3d_float.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_fractal3d_vector2.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_fractal3d_vector3.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_fractal3d_vector4.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_heighttonormal_vector3.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_hextiledimage.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_hextilednormalmap.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_hsvtorgb_color3.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_hsvtorgb_color4.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_image_color3.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_image_color4.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_image_float.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_image_vector2.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_image_vector3.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_image_vector4.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_luminance_color3.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_luminance_color4.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_mix_surfaceshader.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_noise2d_float.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_noise2d_vector2.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_noise2d_vector3.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_noise2d_vector4.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_noise3d_float.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_noise3d_vector2.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_noise3d_vector3.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_noise3d_vector4.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_normalmap.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_premult_color4.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_ramplr_float.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_ramplr_vector2.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_ramplr_vector3.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_ramplr_vector4.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_ramptb_float.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_ramptb_vector2.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_ramptb_vector3.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_ramptb_vector4.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_rgbtohsv_color3.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_rgbtohsv_color4.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_rotate_vector2.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_rotate_vector3.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_smoothstep_float.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_splitlr_float.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_splitlr_vector2.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_splitlr_vector3.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_splitlr_vector4.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_splittb_float.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_splittb_vector2.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_splittb_vector3.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_splittb_vector4.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_surface_unlit.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_transformmatrix_vector2M3.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_transformmatrix_vector3M4.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_unpremult_color4.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_worleynoise2d_float.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_worleynoise2d_vector2.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_worleynoise2d_vector3.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_worleynoise3d_float.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_worleynoise3d_vector2.wgsl create mode 100644 libraries/stdlib/genwgsl/mx_worleynoise3d_vector3.wgsl create mode 100644 libraries/stdlib/genwgsl/stdlib_genwgsl_impl.mtlx create mode 100644 libraries/targets/genwgsl.mtlx diff --git a/CMakeLists.txt b/CMakeLists.txt index a55bd667d6..f7a196e2a9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ 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_GEN_SLANG "Build the Slang shader generator back-end." ON) option(MATERIALX_BUILD_GEN_WGSL "Build the WGSL (WebGPU) shader generator back-end." ON) +option(MATERIALX_GENERATE_WGSL_LIBRARY "Regenerate the genwgsl shader-node library from genglsl at build time (requires Python and naga); surfaces transpile/validation failures as build errors." OFF) option(MATERIALX_BUILD_RENDER "Build the MaterialX Render modules." ON) option(MATERIALX_BUILD_RENDER_PLATFORMS "Build platform-specific render modules for each shader generator." ON) option(MATERIALX_BUILD_OIIO "Build OpenImageIO support for MaterialXRender." OFF) @@ -127,6 +128,8 @@ set(MATERIALX_PYTHON_EXECUTABLE "" CACHE FILEPATH "Python executable to be used in building the MaterialX Python package (e.g. 'C:/Python39/python.exe').") set(MATERIALX_PYTHON_PYBIND11_DIR "" CACHE PATH "Path to a folder containing the PyBind11 source to be used in building MaterialX Python.") +set(MATERIALX_CARGO_PATH "" CACHE PATH + "Path to a Rust cargo installation (the CARGO_HOME directory containing bin/cargo, e.g. '~/.cargo'); used to obtain the naga CLI when MATERIALX_GENERATE_WGSL_LIBRARY is on and naga is not already found.") option(MATERIALX_PYTHON_FORCE_REPLACE_DOCS "Force replace existing docstrings when generating Python binding documentation from Doxygen." OFF) # Settings to define installation layout @@ -178,6 +181,8 @@ mark_as_advanced(MATERIALX_BUILD_DOCS) mark_as_advanced(MATERIALX_BUILD_GEN_GLSL) mark_as_advanced(MATERIALX_BUILD_GEN_SLANG) mark_as_advanced(MATERIALX_BUILD_GEN_WGSL) +mark_as_advanced(MATERIALX_GENERATE_WGSL_LIBRARY) +mark_as_advanced(MATERIALX_CARGO_PATH) mark_as_advanced(MATERIALX_BUILD_GEN_OSL) mark_as_advanced(MATERIALX_BUILD_GEN_MDL) mark_as_advanced(MATERIALX_BUILD_GEN_MSL) diff --git a/libraries/lights/genwgsl/lights_genwgsl_impl.mtlx b/libraries/lights/genwgsl/lights_genwgsl_impl.mtlx new file mode 100644 index 0000000000..2e45ee9ccd --- /dev/null +++ b/libraries/lights/genwgsl/lights_genwgsl_impl.mtlx @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/libraries/lights/genwgsl/mx_directional_light.wgsl b/libraries/lights/genwgsl/mx_directional_light.wgsl new file mode 100644 index 0000000000..e4e24e760d --- /dev/null +++ b/libraries/lights/genwgsl/mx_directional_light.wgsl @@ -0,0 +1,4 @@ +fn mx_directional_light(light: LightData, position: vec3f, result: ptr) { + (*result).direction = -light.direction; + (*result).intensity = light.color * light.intensity; +} diff --git a/libraries/lights/genwgsl/mx_point_light.wgsl b/libraries/lights/genwgsl/mx_point_light.wgsl new file mode 100644 index 0000000000..e9a5e4f730 --- /dev/null +++ b/libraries/lights/genwgsl/mx_point_light.wgsl @@ -0,0 +1,7 @@ +fn mx_point_light(light: LightData, position: vec3f, result: ptr) { + (*result).direction = light.position - position; + let distance = length((*result).direction) + M_FLOAT_EPS; + let attenuation = pow(distance + 1.0, light.decay_rate + M_FLOAT_EPS); + (*result).intensity = light.color * light.intensity / attenuation; + (*result).direction = (*result).direction / distance; +} diff --git a/libraries/lights/genwgsl/mx_spot_light.wgsl b/libraries/lights/genwgsl/mx_spot_light.wgsl new file mode 100644 index 0000000000..4cdaac19bd --- /dev/null +++ b/libraries/lights/genwgsl/mx_spot_light.wgsl @@ -0,0 +1,12 @@ +fn mx_spot_light(light: LightData, position: vec3f, result: ptr) { + (*result).direction = light.position - position; + let distance = length((*result).direction) + M_FLOAT_EPS; + let attenuation = pow(distance + 1.0, light.decay_rate + M_FLOAT_EPS); + (*result).intensity = light.color * light.intensity / attenuation; + (*result).direction = (*result).direction / distance; + let low = min(light.inner_angle, light.outer_angle); + let high = light.inner_angle; + let cosDir = dot((*result).direction, -light.direction); + let spotAttenuation = smoothstep(low, high, cosDir); + (*result).intensity = (*result).intensity * spotAttenuation; +} diff --git a/libraries/pbrlib/genwgsl/lib/mx_closure_type.wgsl b/libraries/pbrlib/genwgsl/lib/mx_closure_type.wgsl new file mode 100644 index 0000000000..02075bdf5e --- /dev/null +++ b/libraries/pbrlib/genwgsl/lib/mx_closure_type.wgsl @@ -0,0 +1,49 @@ +struct ClosureData { + closureType: i32, + L: vec3f, + V: vec3f, + N: vec3f, + P: vec3f, + occlusion: f32, +} + +struct BSDF { + response: vec3f, + throughput: vec3f, +} + +struct VDF { + response: vec3f, + throughput: vec3f, +} + +// EDF (Emission Distribution Function) is represented as a simple vec3 color value. +// In GLSL this was `#define EDF vec3`; in WGSL we use a type alias. +alias EDF = vec3f; + +// In GLSL this was `#define material surfaceshader`; in WGSL we use a type alias. +// The surfaceshader struct must be defined before this file is included. +alias material = surfaceshader; + +struct FresnelData { + // Fresnel model (FRESNEL_MODEL_DIELECTRIC / _CONDUCTOR / _SCHLICK) and thin-film flag. + model: i32, + airy: bool, + // Physical Fresnel. + ior: vec3f, + extinction: vec3f, + // Generalized Schlick Fresnel. + F0: vec3f, + F82: vec3f, + F90: vec3f, + exponent: f32, + // Thin film. + thinfilm_thickness: f32, + thinfilm_ior: f32, + // Selects the refracted environment lookup over the reflected one (surface transmission). + refraction: bool, +} + +fn makeClosureData(closureType: i32, L: vec3f, V: vec3f, N: vec3f, P: vec3f, occlusion: f32) -> ClosureData { + return ClosureData(closureType, L, V, N, P, occlusion); +} diff --git a/libraries/pbrlib/genwgsl/lib/mx_environment_fis.wgsl b/libraries/pbrlib/genwgsl/lib/mx_environment_fis.wgsl new file mode 100644 index 0000000000..e2a78be4ae --- /dev/null +++ b/libraries/pbrlib/genwgsl/lib/mx_environment_fis.wgsl @@ -0,0 +1,72 @@ +#include "mx_microfacet_specular.wgsl" + +// Filtered Importance Sampling environment lighting. +// http://cgg.mff.cuni.cz/~jaroslav/papers/2008-egsr-fis/2008-egsr-fis-final-embedded.pdf +// +// Iterates envRadianceSamples per pixel with GGX VNDF importance sampling. +// +// Requires (provided by the generator's shader template): +// envRadiance : texture_2d — lat-long radiance environment map +// envRadianceSampler : sampler +// envIrradiance : texture_2d — lat-long irradiance environment map +// envIrradianceSampler : sampler +// envRadianceMips : i32 — number of mip levels in envRadiance +// envRadianceSamples : i32 — number of importance samples +// envMatrix : mat4x4f — environment rotation matrix +// envLightIntensity : f32 — environment light intensity multiplier +// +// Dependencies from mx_microfacet_specular.wgsl: +// mx_average_alpha, mx_ggx_smith_G1, mx_ggx_smith_G2, mx_ggx_NDF, +// mx_ggx_importance_sample_VNDF, mx_compute_fresnel, +// mx_refraction_solid_sphere, mx_latlong_map_lookup, mx_latlong_compute_lod +// Dependencies from mx_microfacet.wgsl: +// mx_spherical_fibonacci + +fn mx_environment_radiance(N: vec3f, V_in: vec3f, X_in: vec3f, alpha: vec2f, distribution: i32, fd: FresnelData) -> vec3f { + let X = normalize(X_in - dot(X_in, N) * N); + let Y = cross(N, X); + let tangentToWorld = mat3x3f(X, Y, N); + + let V = vec3f(dot(V_in, X), dot(V_in, Y), dot(V_in, N)); + + let NdotV = clamp(V.z, M_FLOAT_EPS, 1.0); + let avgAlpha = mx_average_alpha(alpha); + let G1V = mx_ggx_smith_G1(NdotV, avgAlpha); + + var radiance = vec3f(0.0); + let envRadianceSamples: i32 = $envRadianceSamples; + for (var i: i32 = 0; i < envRadianceSamples; i++) { + let Xi = mx_spherical_fibonacci(i, envRadianceSamples); + + let H = mx_ggx_importance_sample_VNDF(Xi, V, alpha); + // For surface transmission, follow the refracted ray through a solid sphere; otherwise reflect. + let L = select(-reflect(V, H), mx_refraction_solid_sphere(-V, H, fd.ior.x), fd.refraction); + + let NdotL = clamp(L.z, M_FLOAT_EPS, 1.0); + let VdotH = clamp(dot(V, H), M_FLOAT_EPS, 1.0); + + let Lw = tangentToWorld * L; + let pdf = mx_ggx_NDF(H, alpha) * G1V / (4.0 * NdotV); + let lod = mx_latlong_compute_lod(Lw, pdf, f32($envRadianceMips - 1), envRadianceSamples); + let sampleColor = mx_latlong_map_lookup(Lw, $envMatrix, lod, $envRadiance, $envRadianceSampler); + + let F = mx_compute_fresnel(VdotH, fd); + let G = mx_ggx_smith_G2(NdotL, NdotV, avgAlpha); + // The combined FG term simplifies to inverted Fresnel for refraction. + let FG = select(F * G, vec3f(1.0) - F, fd.refraction); + + radiance += sampleColor * FG; + } + + radiance /= G1V * f32(envRadianceSamples); + + if ($envRadianceSamples == 0) { + return vec3f(0.0); + } + return radiance * $envLightIntensity; +} + +fn mx_environment_irradiance(N: vec3f) -> vec3f { + let Li = mx_latlong_map_lookup(N, $envMatrix, 0.0, $envIrradiance, $envIrradianceSampler); + return Li * $envLightIntensity; +} diff --git a/libraries/pbrlib/genwgsl/lib/mx_environment_none.wgsl b/libraries/pbrlib/genwgsl/lib/mx_environment_none.wgsl new file mode 100644 index 0000000000..27b07650c9 --- /dev/null +++ b/libraries/pbrlib/genwgsl/lib/mx_environment_none.wgsl @@ -0,0 +1,12 @@ +// Environment lighting with no IBL contribution. +// WGSL equivalent of genglsl/lib/mx_environment_none.glsl. + +fn mx_environment_radiance(N: vec3f, V: vec3f, X: vec3f, roughness: vec2f, distribution: i32, fd: FresnelData) -> vec3f +{ + return vec3f(0.0); +} + +fn mx_environment_irradiance(N: vec3f) -> vec3f +{ + return vec3f(0.0); +} diff --git a/libraries/pbrlib/genwgsl/lib/mx_generate_albedo_table.wgsl b/libraries/pbrlib/genwgsl/lib/mx_generate_albedo_table.wgsl new file mode 100644 index 0000000000..e908f395f6 --- /dev/null +++ b/libraries/pbrlib/genwgsl/lib/mx_generate_albedo_table.wgsl @@ -0,0 +1,15 @@ +#include "mx_microfacet_sheen.wgsl" +#include "mx_microfacet_specular.wgsl" + +// Bake shader: generates a directional albedo lookup table. +// The generator should provide $albedoTableSize as a uniform or constant. +// +// Usage: dispatch as a fullscreen fragment shader; gl_FragCoord maps to table UV. +// Output: vec3(ggxDirAlbedo.x, ggxDirAlbedo.y, sheenDirAlbedo) + +fn mx_generate_dir_albedo_table(fragCoord: vec2f, albedoTableSize: f32) -> vec3f { + let uv = fragCoord / albedoTableSize; + let ggxDirAlbedo = mx_ggx_dir_albedo(uv.x, uv.y, vec3f(1.0, 0.0, 0.0), vec3f(0.0, 1.0, 0.0)).xy; + let sheenDirAlbedo = mx_imageworks_sheen_dir_albedo(uv.x, uv.y); + return vec3f(ggxDirAlbedo.x, ggxDirAlbedo.y, sheenDirAlbedo); +} diff --git a/libraries/pbrlib/genwgsl/lib/mx_microfacet.wgsl b/libraries/pbrlib/genwgsl/lib/mx_microfacet.wgsl new file mode 100644 index 0000000000..abe67b7ceb --- /dev/null +++ b/libraries/pbrlib/genwgsl/lib/mx_microfacet.wgsl @@ -0,0 +1,98 @@ +// Microfacet utility functions (WGSL port of genglsl/lib/mx_microfacet.glsl) + +const M_PI_INV: f32 = 1.0 / 3.1415926535897932; + +fn mx_pow5(x: f32) -> f32 { + let x2 = x * x; + return x2 * x2 * x; +} + +fn mx_pow6(x: f32) -> f32 { + let x2 = x * x; + return x2 * x2 * x2; +} + +// Fresnel-Schlick variants — suffixed by return type and parameter set. + +// F0 only (scalar) +fn mx_fresnel_schlick_f32(cosTheta: f32, F0: f32) -> f32 { + let x = clamp(1.0 - cosTheta, 0.0, 1.0); + return F0 + (1.0 - F0) * mx_pow5(x); +} + +// F0 only (vec3) +fn mx_fresnel_schlick_vec3(cosTheta: f32, F0: vec3f) -> vec3f { + let x = clamp(1.0 - cosTheta, 0.0, 1.0); + return F0 + (vec3f(1.0) - F0) * mx_pow5(x); +} + +// F0 + F90 (scalar) +fn mx_fresnel_schlick_f32_f90(cosTheta: f32, F0: f32, F90: f32) -> f32 { + let x = clamp(1.0 - cosTheta, 0.0, 1.0); + return mix(F0, F90, mx_pow5(x)); +} + +// F0 + F90 (vec3) +fn mx_fresnel_schlick_vec3_f90(cosTheta: f32, F0: vec3f, F90: vec3f) -> vec3f { + let x = clamp(1.0 - cosTheta, 0.0, 1.0); + return mix(F0, F90, vec3f(mx_pow5(x))); +} + +// F0 + F90 + exponent (scalar) +fn mx_fresnel_schlick_f32_exp(cosTheta: f32, F0: f32, F90: f32, exponent: f32) -> f32 { + let x = clamp(1.0 - cosTheta, 0.0, 1.0); + return mix(F0, F90, pow(x, exponent)); +} + +// F0 + F90 + exponent (vec3) +fn mx_fresnel_schlick_vec3_exp(cosTheta: f32, F0: vec3f, F90: vec3f, exponent: f32) -> vec3f { + let x = clamp(1.0 - cosTheta, 0.0, 1.0); + return mix(F0, F90, vec3f(pow(x, exponent))); +} + +fn mx_forward_facing_normal(N: vec3f, V: vec3f) -> vec3f { + if (dot(N, V) < 0.0) { + return -N; + } + return N; +} + +// Spherical Fibonacci sampling utilities. +fn mx_golden_ratio_sequence(i: i32) -> f32 { + let GOLDEN_RATIO = 1.618034; + return fract((f32(i) + 1.0) * GOLDEN_RATIO); +} + +fn mx_spherical_fibonacci(i: i32, numSamples: i32) -> vec2f { + return vec2f((f32(i) + 0.5) / f32(numSamples), mx_golden_ratio_sequence(i)); +} + +fn mx_uniform_sample_hemisphere(Xi: vec2f) -> vec3f { + let phi = 2.0 * 3.1415926535897932 * Xi.x; + let cosTheta = 1.0 - Xi.y; + let sinTheta = sqrt(1.0 - mx_square_f32(cosTheta)); + return vec3f(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta); +} + +fn mx_cosine_sample_hemisphere(Xi: vec2f) -> vec3f { + let phi = 2.0 * 3.1415926535897932 * Xi.x; + let cosTheta = sqrt(Xi.y); + let sinTheta = sqrt(1.0 - Xi.y); + return vec3f(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta); +} + +// Constructs an orthonormal basis (T, B, N) from a unit normal N. +// Uses the Duff et al. (JCGT 2017) / Frisvad construction with the sign branch +// at N.z = -1 instead of N.z = 0. The original Naga-transpiled version branched +// at N.z < 0, which placed a tangent-frame discontinuity at the equator of any +// Z-up sphere -- causing visible sawtooth artifacts on glossy/metallic materials. +// Moving the branch to N.z < -0.9999999 pushes the singularity to a tiny region +// near the south pole where it is virtually invisible. +fn mx_orthonormal_basis(N: vec3f) -> mat3x3f { + let s = select(1.0, -1.0, N.z < -0.9999999); + let a = -1.0 / (s + N.z); + let b = N.x * N.y * a; + let X = vec3f(1.0 + s * N.x * N.x * a, s * b, -s * N.x); + let Y = vec3f(b, s + N.y * N.y * a, -N.y); + return mat3x3f(X, Y, N); +} diff --git a/libraries/pbrlib/genwgsl/lib/mx_microfacet_diffuse.wgsl b/libraries/pbrlib/genwgsl/lib/mx_microfacet_diffuse.wgsl new file mode 100644 index 0000000000..113aad9518 --- /dev/null +++ b/libraries/pbrlib/genwgsl/lib/mx_microfacet_diffuse.wgsl @@ -0,0 +1,128 @@ +#include "mx_microfacet.wgsl" + +// Qualitative Oren-Nayar diffuse with simplified math: +// https://www1.cs.columbia.edu/CAVE/publications/pdfs/Oren_SIGGRAPH94.pdf +fn mx_oren_nayar_diffuse(NdotV: f32, NdotL: f32, LdotV: f32, roughness: f32) -> f32 { + let s = LdotV - NdotL * NdotV; + var stinv: f32; + if (s > 0.0) { + stinv = s / max(NdotL, NdotV); + } else { + stinv = 0.0; + } + + let sigma2 = mx_square_f32(roughness); + let A = 1.0 - 0.5 * (sigma2 / (sigma2 + 0.33)); + let B = 0.45 * sigma2 / (sigma2 + 0.09); + + return A + B * stinv; +} + +// Missing MaterialX functions - Oren-Nayar compensated diffuse +const FUJII_CONSTANT_1: f32 = 0.5 - 2.0 / (3.0 * 3.1415926535897932); +const FUJII_CONSTANT_2: f32 = 2.0 / 3.0 - 28.0 / (15.0 * 3.1415926535897932); + +fn mx_oren_nayar_fujii_diffuse_dir_albedo(cosTheta: f32, roughness: f32) -> f32 { + let A = 1.0 / (1.0 + FUJII_CONSTANT_1 * roughness); + let B = roughness * A; + let Si = sqrt(max(0.0, 1.0 - mx_square_f32(cosTheta))); + let cosThetaClamped = clamp(cosTheta, -1.0, 1.0); + let G = Si * (acos(cosThetaClamped) - Si * cosTheta) + 2.0 * ((Si / max(cosTheta, 0.0001)) * (1.0 - Si * Si * Si) - Si) / 3.0; + return A + (B * G * (1.0 / 3.1415926535897932)); +} + +fn mx_oren_nayar_fujii_diffuse_avg_albedo(roughness: f32) -> f32 { + let A = 1.0 / (1.0 + FUJII_CONSTANT_1 * roughness); + return A * (1.0 + FUJII_CONSTANT_2 * roughness); +} + +fn mx_oren_nayar_compensated_diffuse(NdotV: f32, NdotL: f32, LdotV: f32, roughness: f32, color: vec3f) -> vec3f { + let s = LdotV - NdotL * NdotV; + var stinv: f32; + if (s > 0.0) { + stinv = s / max(NdotL, NdotV); + } else { + stinv = s; + } + + let A = 1.0 / (1.0 + FUJII_CONSTANT_1 * roughness); + let lobeSingleScatter = color * A * (1.0 + roughness * stinv); + + let dirAlbedoV = mx_oren_nayar_fujii_diffuse_dir_albedo(NdotV, roughness); + let dirAlbedoL = mx_oren_nayar_fujii_diffuse_dir_albedo(NdotL, roughness); + let avgAlbedo = mx_oren_nayar_fujii_diffuse_avg_albedo(roughness); + let colorMultiScatter = mx_square_vec3(color) * avgAlbedo / (vec3(1.0) - color * max(0.0, 1.0 - avgAlbedo)); + let lobeMultiScatter = colorMultiScatter * max(0.00000001, 1.0 - dirAlbedoV) * max(0.00000001, 1.0 - dirAlbedoL) / max(0.00000001, 1.0 - avgAlbedo); + + return lobeSingleScatter + lobeMultiScatter; +} + +fn mx_oren_nayar_compensated_diffuse_dir_albedo(cosTheta: f32, roughness: f32, color: vec3f) -> vec3f { + let dirAlbedo = mx_oren_nayar_fujii_diffuse_dir_albedo(cosTheta, roughness); + let avgAlbedo = mx_oren_nayar_fujii_diffuse_avg_albedo(roughness); + let colorMultiScatter = mx_square_vec3(color) * avgAlbedo / (vec3(1.0) - color * max(0.0, 1.0 - avgAlbedo)); + return mix(colorMultiScatter, color, dirAlbedo); +} + +fn mx_oren_nayar_diffuse_dir_albedo(NdotV: f32, roughness: f32) -> f32 { + let x = NdotV; + let y = roughness; + let r = vec2(1.0, 1.0) + vec2(-0.4297, -0.6076) * y + vec2(-0.7632, -0.4993) * x * y + vec2(1.4385, 2.0315) * mx_square_f32(y); + return clamp(r.x / r.y, 0.0, 1.0); +} + +// Burley diffusion profile for subsurface scattering approximation. +// Ported from genglsl/lib/mx_microfacet_diffuse.glsl. +fn mx_burley_diffusion_profile(dist: f32, shape: vec3f) -> vec3f { + let num1 = exp(-shape * dist); + let num2 = exp(-shape * dist / 3.0); + let denom = max(dist, M_FLOAT_EPS); + return (num1 + num2) / denom; +} + +// Integrate the Burley diffusion profile over a sphere of the given radius. +// Inspired by Eric Penner's presentation in http://advances.realtimerendering.com/s2011/ +fn mx_integrate_burley_diffusion(N: vec3f, L: vec3f, radius: f32, mfp: vec3f) -> vec3f { + let theta = acos(clamp(dot(N, L), -1.0, 1.0)); + + // Estimate the Burley diffusion shape from mean free path. + let shape = vec3f(1.0) / max(mfp, vec3f(0.1)); + + // Integrate the profile over the sphere. + var sumD = vec3f(0.0); + var sumR = vec3f(0.0); + let SAMPLE_COUNT: i32 = 32; + let SAMPLE_WIDTH: f32 = (2.0 * M_PI) / f32(SAMPLE_COUNT); + for (var i: i32 = 0; i < SAMPLE_COUNT; i++) { + let x = -M_PI + (f32(i) + 0.5) * SAMPLE_WIDTH; + let dist = radius * abs(2.0 * sin(x * 0.5)); + let R = mx_burley_diffusion_profile(dist, shape); + sumD += R * max(cos(theta + x), 0.0); + sumR += R; + } + + return sumD / sumR; +} + +fn mx_subsurface_scattering_approx(N: vec3f, L: vec3f, P: vec3f, albedo: vec3f, mfp: vec3f, curvature: f32) -> vec3f { + let radius = 1.0 / max(curvature, 0.01); + return albedo * mx_integrate_burley_diffusion(N, L, radius, mfp) / vec3f(M_PI); +} + +// Disney Burley diffuse BRDF. +// https://media.disneyanimation.com/uploads/production/publication_asset/48/asset/s2012_pbs_disney_brdf_notes_v3.pdf +// Section 5.3 +fn mx_burley_diffuse(NdotV: f32, NdotL: f32, LdotH: f32, roughness: f32) -> f32 { + let F90 = 0.5 + 2.0 * roughness * mx_square_f32(LdotH); + let refL = mx_fresnel_schlick_f32_f90(NdotL, 1.0, F90); + let refV = mx_fresnel_schlick_f32_f90(NdotV, 1.0, F90); + return refL * refV; +} + +// Directional albedo for Burley diffuse. Curve fit by Stephen Hill. +fn mx_burley_diffuse_dir_albedo(NdotV: f32, roughness: f32) -> f32 { + let x = NdotV; + let fit0 = 0.97619 - 0.488095 * mx_pow5(1.0 - x); + let fit1 = 1.55754 + (-2.02221 + (2.56283 - 1.06244 * x) * x) * x; + return mix(fit0, fit1, roughness); +} diff --git a/libraries/pbrlib/genwgsl/lib/mx_microfacet_sheen.wgsl b/libraries/pbrlib/genwgsl/lib/mx_microfacet_sheen.wgsl new file mode 100644 index 0000000000..92c907c2ca --- /dev/null +++ b/libraries/pbrlib/genwgsl/lib/mx_microfacet_sheen.wgsl @@ -0,0 +1,72 @@ +#include "mx_microfacet.wgsl" + +// Imageworks sheen NDF +fn mx_imageworks_sheen_NDF(NdotH: f32, roughness: f32) -> f32 { + let invRoughness = 1.0 / max(roughness, 0.005); + let cos2 = NdotH * NdotH; + let sin2 = 1.0 - cos2; + return (2.0 + invRoughness) * pow(sin2, invRoughness * 0.5) / (2.0 * 3.1415926535897932); +} + +// Missing MaterialX functions - Sheen BRDF and directional albedo +fn mx_imageworks_sheen_brdf(NdotL: f32, NdotV: f32, NdotH: f32, roughness: f32) -> f32 { + let D = mx_imageworks_sheen_NDF(NdotH, roughness); + let F = 1.0; + let G = 1.0; + return D * F * G / (4.0 * (NdotL + NdotV - NdotL * NdotV)); +} + +fn mx_imageworks_sheen_dir_albedo(NdotV: f32, roughness: f32) -> f32 { + let x = NdotV; + let y = roughness; + let r = vec2(13.67300, 1.0) + vec2(-68.78018, 61.57746) * x + vec2(799.08825, 442.78211) * y + vec2(-905.00061, 2597.49308) * x * y + vec2(60.28956, 121.81241) * mx_square_f32(x) + vec2(1086.96473, 3045.55075) * mx_square_f32(y); + return clamp(r.x / r.y, 0.0, 1.0); +} + +fn mx_zeltner_sheen_dir_albedo(x: f32, y: f32) -> f32 { + let s = y * (0.0206607 + 1.58491 * y) / (0.0379424 + y * (1.32227 + y)); + let m = y * (-0.193854 + y * (-1.14885 + y * (1.7932 - 0.95943 * y * y))) / (0.046391 + y); + let o = y * (0.000654023 + (-0.0207818 + 0.119681 * y) * y) / (1.26264 + y * (-1.92021 + y)); + return exp(-0.5 * mx_square_f32((x - m) / s)) / (s * sqrt(2.0 * 3.1415926535897932)) + o; +} + +fn mx_zeltner_sheen_ltc_aInv(x: f32, y: f32) -> f32 { + return (2.58126 * x + 0.813703 * y) * y / (1.0 + 0.310327 * x * x + 2.60994 * x * y); +} + +fn mx_zeltner_sheen_ltc_bInv(x: f32, y: f32) -> f32 { + return sqrt(1.0 - x) * (y - 1.0) * y * y * y / (0.0000254053 + 1.71228 * x - 1.71506 * x * y + 1.34174 * y * y); +} + +fn mx_transpose_mat3(m: mat3x3f) -> mat3x3f { + return mat3x3f( + vec3f(m[0].x, m[1].x, m[2].x), + vec3f(m[0].y, m[1].y, m[2].y), + vec3f(m[0].z, m[1].z, m[2].z) + ); +} + +fn mx_orthonormal_basis_ltc(V: vec3f, N: vec3f, NdotV: f32) -> mat3x3f { + var X = V - N * NdotV; + let lenSqr = dot(X, X); + if (lenSqr > 0.0) { + X = X * (1.0 / sqrt(lenSqr)); + let Y = cross(N, X); + return mat3x3f(vec3f(X.x, Y.x, N.x), vec3f(X.y, Y.y, N.y), vec3f(X.z, Y.z, N.z)); + } + return mx_orthonormal_basis(N); +} + +fn mx_zeltner_sheen_brdf(L: vec3f, V: vec3f, N: vec3f, NdotV: f32, roughness: f32) -> f32 { + let basis = mx_orthonormal_basis_ltc(V, N, NdotV); + let toLTC = mx_transpose_mat3(basis); + let w = (toLTC * L); + + let aInv = mx_zeltner_sheen_ltc_aInv(NdotV, roughness); + let bInv = mx_zeltner_sheen_ltc_bInv(NdotV, roughness); + + let wo = vec3f(aInv * w.x + bInv * w.z, aInv * w.y, w.z); + let lenSqr = dot(wo, wo); + + return max(wo.z, 0.0) * (1.0 / 3.1415926535897932) * mx_square_f32(aInv / lenSqr); +} diff --git a/libraries/pbrlib/genwgsl/lib/mx_microfacet_specular.wgsl b/libraries/pbrlib/genwgsl/lib/mx_microfacet_specular.wgsl new file mode 100644 index 0000000000..9e74a7a2ca --- /dev/null +++ b/libraries/pbrlib/genwgsl/lib/mx_microfacet_specular.wgsl @@ -0,0 +1,388 @@ +#include "mx_microfacet.wgsl" + +fn mx_average_alpha(alpha: vec2f) -> f32 { + return sqrt(alpha.x * alpha.y); +} + +// Convert IOR to F0 (normal-incidence reflectivity) +fn mx_ior_to_f0(ior: f32) -> f32 { + return mx_square_f32((ior - 1f) / (ior + 1f)); +} + +// Convert F0 to IOR (for transmission) +fn mx_f0_to_ior(F0: f32) -> f32 { + let sqrtF0 = sqrt(clamp(F0, 0.01, 0.99)); + return (1.0 + sqrtF0) / (1.0 - sqrtF0); +} + +fn mx_f0_to_ior_vec3(F0: vec3f) -> vec3f { + let sqrtF0 = sqrt(clamp(F0, vec3f(0.01), vec3f(0.99))); + return (vec3f(1.0) + sqrtF0) / (vec3f(1.0) - sqrtF0); +} + +// Fresnel models, mirroring mx_microfacet_specular.glsl. +const FRESNEL_MODEL_DIELECTRIC: i32 = 0; +const FRESNEL_MODEL_CONDUCTOR: i32 = 1; +const FRESNEL_MODEL_SCHLICK: i32 = 2; + +// Number of terms in the thin-film Airy series (generator option hwAiryFresnelIterations; default 2). +const AIRY_FRESNEL_ITERATIONS: i32 = 2; + +fn mx_init_fresnel_dielectric(ior: f32, thinfilm_thickness: f32, thinfilm_ior: f32) -> FresnelData { + var fd: FresnelData; + fd.model = FRESNEL_MODEL_DIELECTRIC; + fd.airy = thinfilm_thickness > 0.0; + let F0_scalar = mx_ior_to_f0(ior); + fd.F0 = vec3f(F0_scalar); + fd.F82 = vec3f(F0_scalar); + fd.F90 = vec3f(1f); + fd.exponent = 5f; + fd.thinfilm_thickness = thinfilm_thickness; + fd.thinfilm_ior = thinfilm_ior; + fd.ior = vec3f(ior); + fd.extinction = vec3f(0.0); + return fd; +} + +// Initialize FresnelData for a conductor material. +// Computes F0 from complex IOR (n, k) using the exact Fresnel equation at normal incidence: +// F0 = ((n - 1)^2 + k^2) / ((n + 1)^2 + k^2) +fn mx_init_fresnel_conductor(ior: vec3f, extinction: vec3f, thinfilm_thickness: f32, thinfilm_ior: f32) -> FresnelData { + var fd: FresnelData; + fd.model = FRESNEL_MODEL_CONDUCTOR; + fd.airy = thinfilm_thickness > 0.0; + fd.ior = ior; + fd.extinction = extinction; + let n_minus_1 = ior - vec3f(1.0); + let n_plus_1 = ior + vec3f(1.0); + let k2 = extinction * extinction; + fd.F0 = (n_minus_1 * n_minus_1 + k2) / (n_plus_1 * n_plus_1 + k2); + fd.F82 = fd.F0; + fd.F90 = vec3f(1.0); + fd.exponent = 5.0; + fd.thinfilm_thickness = thinfilm_thickness; + fd.thinfilm_ior = thinfilm_ior; + return fd; +} + +fn mx_saturate(x: f32) -> f32 { + return clamp(x, 0.0, 1.0); +} + +fn mx_saturate_v3(x: vec3f) -> vec3f { + return clamp(x, vec3f(0.0), vec3f(1.0)); +} + +// Initialize Fresnel data for generalized Schlick model. +fn mx_init_fresnel_schlick( + color0: vec3f, + color82: vec3f, + color90: vec3f, + exponent: f32, + thinfilm_thickness: f32, + thinfilm_ior: f32 +) -> FresnelData { + var fd: FresnelData; + fd.model = FRESNEL_MODEL_SCHLICK; + fd.airy = thinfilm_thickness > 0.0; + fd.F0 = mx_saturate_v3(color0); + fd.F82 = mx_saturate_v3(color82); + fd.F90 = mx_saturate_v3(color90); + fd.exponent = exponent; + fd.thinfilm_thickness = thinfilm_thickness; + fd.thinfilm_ior = thinfilm_ior; + return fd; +} + +// https://renderwonk.com/publications/wp-generalization-adobe/gen-adobe.pdf +fn mx_fresnel_hoffman_schlick(cosTheta: f32, fd: FresnelData) -> vec3f { + let COS_THETA_MAX: f32 = 1.0 / 7.0; + let COS_THETA_FACTOR: f32 = 1.0 / (COS_THETA_MAX * pow(1.0 - COS_THETA_MAX, 6.0)); + + let x = clamp(cosTheta, 0.0, 1.0); + let a = mix(fd.F0, fd.F90, pow(1.0 - COS_THETA_MAX, fd.exponent)) * (vec3f(1.0) - fd.F82) * COS_THETA_FACTOR; + return mix(fd.F0, fd.F90, pow(1.0 - x, fd.exponent)) - a * x * mx_pow6(1.0 - x); +} + +// https://seblagarde.wordpress.com/2013/04/29/memo-on-fresnel-equations/ +fn mx_fresnel_dielectric(cosTheta: f32, ior: f32) -> f32 { + let c = cosTheta; + let g2 = ior * ior + c * c - 1.0; + if (g2 < 0.0) { + // Total internal reflection + return 1.0; + } + let g = sqrt(g2); + return 0.5 * mx_square_f32((g - c) / (g + c)) * + (1.0 + mx_square_f32(((g + c) * c - 1.0) / ((g - c) * c + 1.0))); +} + +// https://seblagarde.wordpress.com/2013/04/29/memo-on-fresnel-equations/ +fn mx_fresnel_dielectric_polarized(cosTheta: f32, ior: f32) -> vec2f { + let cosTheta2 = mx_square_f32(clamp(cosTheta, 0.0, 1.0)); + let sinTheta2 = 1.0 - cosTheta2; + + let t0 = max(ior * ior - sinTheta2, 0.0); + let t1 = t0 + cosTheta2; + let t2 = 2.0 * sqrt(t0) * cosTheta; + let Rs = (t1 - t2) / (t1 + t2); + + let t3 = cosTheta2 * t0 + sinTheta2 * sinTheta2; + let t4 = t2 * sinTheta2; + let Rp = Rs * (t3 - t4) / (t3 + t4); + + return vec2f(Rp, Rs); +} + +// https://seblagarde.wordpress.com/2013/04/29/memo-on-fresnel-equations/ +fn mx_fresnel_conductor_polarized(cosTheta: f32, n: vec3f, k: vec3f, Rp: ptr, Rs: ptr) { + let cosTheta2 = mx_square_f32(clamp(cosTheta, 0.0, 1.0)); + let sinTheta2 = 1.0 - cosTheta2; + let n2 = n * n; + let k2 = k * k; + + let t0 = n2 - k2 - vec3f(sinTheta2); + let a2plusb2 = sqrt(t0 * t0 + 4.0 * n2 * k2); + let t1 = a2plusb2 + vec3f(cosTheta2); + let a = sqrt(max(0.5 * (a2plusb2 + t0), vec3f(0.0))); + let t2 = 2.0 * a * cosTheta; + (*Rs) = (t1 - t2) / (t1 + t2); + + let t3 = cosTheta2 * a2plusb2 + vec3f(sinTheta2 * sinTheta2); + let t4 = t2 * sinTheta2; + (*Rp) = (*Rs) * (t3 - t4) / (t3 + t4); +} + +fn mx_fresnel_conductor(cosTheta: f32, n: vec3f, k: vec3f) -> vec3f { + var Rp: vec3f; + var Rs: vec3f; + mx_fresnel_conductor_polarized(cosTheta, n, k, &Rp, &Rs); + return 0.5 * (Rp + Rs); +} + +// https://belcour.github.io/blog/research/publication/2017/05/01/brdf-thin-film.html +fn mx_fresnel_conductor_phase_polarized(cosTheta: f32, eta1: f32, eta2: vec3f, kappa2: vec3f, phiP: ptr, phiS: ptr) { + let k2 = kappa2 / eta2; + let sinThetaSqr = vec3f(1.0 - cosTheta * cosTheta); + let A = eta2 * eta2 * (vec3f(1.0) - k2 * k2) - eta1 * eta1 * sinThetaSqr; + let B = sqrt(A * A + mx_square_vec3(2.0 * eta2 * eta2 * k2)); + let U = sqrt((A + B) / 2.0); + let V = max(vec3f(0.0), sqrt((B - A) / 2.0)); + + (*phiS) = atan2(2.0 * eta1 * V * cosTheta, U * U + V * V - vec3f(mx_square_f32(eta1 * cosTheta))); + (*phiP) = atan2(2.0 * eta1 * eta2 * eta2 * cosTheta * (2.0 * k2 * U - (vec3f(1.0) - k2 * k2) * V), + mx_square_vec3(eta2 * eta2 * (vec3f(1.0) + k2 * k2) * cosTheta) - eta1 * eta1 * (U * U + V * V)); +} + +// https://belcour.github.io/blog/research/publication/2017/05/01/brdf-thin-film.html +fn mx_eval_sensitivity(opd: f32, shift: vec3f) -> vec3f { + // Use Gaussian fits, given by 3 parameters: val, pos and var. + let phase = 2.0 * M_PI * opd; + let val = vec3f(5.4856e-13, 4.4201e-13, 5.2481e-13); + let pos = vec3f(1.6810e+06, 1.7953e+06, 2.2084e+06); + let vari = vec3f(4.3278e+09, 9.3046e+09, 6.6121e+09); + var xyz = val * sqrt(2.0 * M_PI * vari) * cos(pos * phase + shift) * exp(-vari * phase * phase); + xyz.x = xyz.x + 9.7470e-14 * sqrt(2.0 * M_PI * 4.5282e+09) * cos(2.2399e+06 * phase + shift.x) * exp(-4.5282e+09 * phase * phase); + return xyz / 1.0685e-7; +} + +// A Practical Extension to Microfacet Theory for the Modeling of Varying Iridescence +// https://belcour.github.io/blog/research/publication/2017/05/01/brdf-thin-film.html +fn mx_fresnel_airy(cosTheta: f32, fd: FresnelData) -> vec3f { + // XYZ to CIE 1931 RGB color space (using neutral E illuminant). + let XYZ_TO_RGB = mat3x3f(2.3706743, -0.5138850, 0.0052982, -0.9000405, 1.4253036, -0.0146949, -0.4706338, 0.0885814, 1.0093968); + + // Assume vacuum on the outside. + let eta1 = 1.0; + let eta2 = max(fd.thinfilm_ior, eta1); + let eta3 = select(fd.ior, mx_f0_to_ior_vec3(fd.F0), fd.model == FRESNEL_MODEL_SCHLICK); + let kappa3 = select(fd.extinction, vec3f(0.0), fd.model == FRESNEL_MODEL_SCHLICK); + let cosThetaT = sqrt(1.0 - (1.0 - mx_square_f32(cosTheta)) * mx_square_f32(eta1 / eta2)); + + // First interface. + var R12 = mx_fresnel_dielectric_polarized(cosTheta, eta2 / eta1); + if (cosThetaT <= 0.0) { + // Total internal reflection + R12 = vec2f(1.0); + } + let T121 = vec2f(1.0) - R12; + + // Second interface. + var R23p: vec3f; + var R23s: vec3f; + if (fd.model == FRESNEL_MODEL_SCHLICK) { + let f = mx_fresnel_hoffman_schlick(cosThetaT, fd); + R23p = 0.5 * f; + R23s = 0.5 * f; + } else { + mx_fresnel_conductor_polarized(cosThetaT, eta3 / eta2, kappa3 / eta2, &R23p, &R23s); + } + + // Phase shift. + let cosB = cos(atan(eta2 / eta1)); + let phi21 = vec2f(select(M_PI, 0.0, cosTheta < cosB), M_PI); + var phi23p: vec3f; + var phi23s: vec3f; + if (fd.model == FRESNEL_MODEL_SCHLICK) { + phi23p = vec3f(select(0.0, M_PI, eta3.x < eta2), + select(0.0, M_PI, eta3.y < eta2), + select(0.0, M_PI, eta3.z < eta2)); + phi23s = phi23p; + } else { + mx_fresnel_conductor_phase_polarized(cosThetaT, eta2, eta3, kappa3, &phi23p, &phi23s); + } + let r123p = max(sqrt(R12.x * R23p), vec3f(0.0)); + let r123s = max(sqrt(R12.y * R23s), vec3f(0.0)); + + // Iridescence term. + var I = vec3f(0.0); + var Cm: vec3f; + var Sm: vec3f; + + // Optical path difference. + let distMeters = fd.thinfilm_thickness * 1.0e-9; + let opd = 2.0 * eta2 * cosThetaT * distMeters; + + // Iridescence term using spectral antialiasing for parallel polarization. + let Rsp = (mx_square_f32(T121.x) * R23p) / (vec3f(1.0) - R12.x * R23p); + I += vec3f(R12.x) + Rsp; + Cm = Rsp - vec3f(T121.x); + for (var m: i32 = 1; m <= AIRY_FRESNEL_ITERATIONS; m++) { + Cm *= r123p; + Sm = 2.0 * mx_eval_sensitivity(f32(m) * opd, f32(m) * (phi23p + vec3f(phi21.x))); + I += Cm * Sm; + } + + // Iridescence term using spectral antialiasing for perpendicular polarization. + let Rpp = (mx_square_f32(T121.y) * R23s) / (vec3f(1.0) - R12.y * R23s); + I += vec3f(R12.y) + Rpp; + Cm = Rpp - vec3f(T121.y); + for (var m: i32 = 1; m <= AIRY_FRESNEL_ITERATIONS; m++) { + Cm *= r123s; + Sm = 2.0 * mx_eval_sensitivity(f32(m) * opd, f32(m) * (phi23s + vec3f(phi21.y))); + I += Cm * Sm; + } + + // Average parallel and perpendicular polarization. + I *= 0.5; + + // Convert back to RGB reflectance. + I = clamp(XYZ_TO_RGB * I, vec3f(0.0), vec3f(1.0)); + + return I; +} + +// Fresnel computation dispatching on the Fresnel model, with thin-film (Airy) iridescence. +fn mx_compute_fresnel(cosTheta: f32, fd: FresnelData) -> vec3f { + if (fd.airy) { + return mx_fresnel_airy(cosTheta, fd); + } else if (fd.model == FRESNEL_MODEL_DIELECTRIC) { + return vec3f(mx_fresnel_dielectric(cosTheta, fd.ior.x)); + } else if (fd.model == FRESNEL_MODEL_CONDUCTOR) { + return mx_fresnel_conductor(cosTheta, fd.ior, fd.extinction); + } + return mx_fresnel_hoffman_schlick(cosTheta, fd); +} + +// https://media.disneyanimation.com/uploads/production/publication_asset/48/asset/s2012_pbs_disney_brdf_notes_v3.pdf +// Appendix B.2 Equation 13 +fn mx_ggx_NDF(H: vec3f, alpha: vec2f) -> f32 { + let He = H.xy / alpha; + let denom = dot(He, He) + mx_square_f32(H.z); + return 1.0 / (M_PI * alpha.x * alpha.y * mx_square_f32(denom)); +} + +// Height-correlated Smith G2 for GGX. +fn mx_ggx_smith_G2(NdotL: f32, NdotV: f32, alpha: f32) -> f32 { + let alpha2 = mx_square_f32(alpha); + let lambdaL = sqrt(alpha2 + (1.0 - alpha2) * mx_square_f32(NdotL)); + let lambdaV = sqrt(alpha2 + (1.0 - alpha2) * mx_square_f32(NdotV)); + return (2.0 * NdotL * NdotV) / (lambdaL * NdotV + lambdaV * NdotL); +} + +// Rational quadratic fit to Monte Carlo data for GGX directional albedo. +fn mx_ggx_dir_albedo_analytic(NdotV: f32, alpha: f32, F0: vec3f, F90: vec3f) -> vec3f { + let x = NdotV; + let y = alpha; + let x2 = mx_square_f32(x); + let y2 = mx_square_f32(y); + + // Rational polynomial fit — accumulate terms incrementally to avoid + // deeply nested parenthesized expressions that exceed WGSL parser limits. + var r = vec4f(0.1003, 0.9345, 1.0, 1.0); + r = r + vec4f(-0.6303, -2.323, -1.765, 0.2281) * x; + r = r + vec4f(9.748, 2.229, 8.263, 15.94) * y; + r = r + vec4f(-2.038, -3.748, 11.53, -55.83) * (x * y); + r = r + vec4f(29.34, 1.424, 28.96, 13.08) * x2; + r = r + vec4f(-8.245, -0.7684, -7.507, 41.26) * y2; + r = r + vec4f(-26.44, 1.436, -36.11, 54.9) * (x2 * y); + r = r + vec4f(19.99, 0.2913, 15.86, 300.2) * (x * y2); + r = r + vec4f(-5.448, 0.6286, 33.37, -285.1) * (x2 * y2); + + let AB = clamp(r.xy / r.zw, vec2f(0.0), vec2f(1.0)); + return F0 * AB.x + F90 * AB.y; +} + +fn mx_ggx_dir_albedo(NdotV: f32, alpha: f32, F0: vec3f, F90: vec3f) -> vec3f { + return mx_ggx_dir_albedo_analytic(NdotV, alpha, F0, F90); +} + +// GGX energy compensation: accounts for energy lost to multiple scattering. +fn mx_ggx_energy_compensation(NdotV: f32, alpha: f32, Fss: vec3f) -> vec3f { + let Ess = mx_ggx_dir_albedo(NdotV, alpha, vec3f(1.0), vec3f(1.0)); + return vec3f(1.0) + Fss * (vec3f(1.0) - Ess) / max(Ess, vec3f(M_FLOAT_EPS)); +} + +// https://ggx-research.github.io/publication/2023/06/09/publication-ggx.html +fn mx_ggx_importance_sample_VNDF(Xi: vec2f, V: vec3f, alpha: vec2f) -> vec3f { + let V_h = normalize(vec3f(V.xy * alpha, V.z)); + + let phi = 2.0 * M_PI * Xi.x; + let z = (1.0 - Xi.y) * (1.0 + V_h.z) - V_h.z; + let sinTheta = sqrt(clamp(1.0 - z * z, 0.0, 1.0)); + let x = sinTheta * cos(phi); + let y = sinTheta * sin(phi); + let c = vec3f(x, y, z); + + let H = c + V_h; + return normalize(vec3f(H.xy * alpha, max(H.z, 0.0))); +} + +// https://www.cs.cornell.edu/~srm/publications/EGSR07-btdf.pdf +// Equation 34 +fn mx_ggx_smith_G1(cosTheta: f32, alpha: f32) -> f32 { + let cosTheta2 = mx_square_f32(cosTheta); + let tanTheta2 = (1.0 - cosTheta2) / cosTheta2; + return 2.0 / (1.0 + sqrt(1.0 + mx_square_f32(alpha) * tanTheta2)); +} + +// Compute the refraction of a ray through a solid sphere. +fn mx_refraction_solid_sphere(R_in: vec3f, N: vec3f, ior: f32) -> vec3f { + let R = refract(R_in, N, 1.0 / ior); + let N1 = normalize(R * dot(R, N) - N * 0.5); + return refract(R, N1, ior); +} + +fn mx_latlong_projection(dir: vec3f) -> vec2f { + let latitude = -asin(dir.y) * M_PI_INV + 0.5; + let longitude = atan2(dir.x, -dir.z) * M_PI_INV * 0.5 + 0.5; + return vec2f(longitude, latitude); +} + +// Sample a lat-long environment map with the given direction, transform, and mip level. +// The texture and sampler are provided by the generator ($texSamplerSignature). +fn mx_latlong_map_lookup(dir: vec3f, transform: mat4x4f, lod: f32, envTex: texture_2d, envSampler: sampler) -> vec3f { + let envDir = normalize((transform * vec4f(dir, 0.0)).xyz); + let uv = mx_latlong_projection(envDir); + return textureSampleLevel(envTex, envSampler, uv, lod).rgb; +} + +// Return the mip level with appropriate coverage for a filtered importance sample. +// https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch20.html +// Section 20.4 Equation 13 +fn mx_latlong_compute_lod(dir: vec3f, pdf: f32, maxMipLevel: f32, envSamples: i32) -> f32 { + let MIP_LEVEL_OFFSET = 1.5; + let effectiveMaxMipLevel = maxMipLevel - MIP_LEVEL_OFFSET; + let distortion = sqrt(1.0 - mx_square_f32(dir.y)); + return max(effectiveMaxMipLevel - 0.5 * log2(f32(envSamples) * pdf * distortion), 0.0); +} diff --git a/libraries/pbrlib/genwgsl/lib/mx_shadow.wgsl b/libraries/pbrlib/genwgsl/lib/mx_shadow.wgsl new file mode 100644 index 0000000000..ff560baa8e --- /dev/null +++ b/libraries/pbrlib/genwgsl/lib/mx_shadow.wgsl @@ -0,0 +1,22 @@ +// Variance shadow mapping utilities. +// https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-8-summed-area-variance-shadow-maps + +fn mx_variance_shadow_occlusion(moments: vec2f, fragmentDepth: f32) -> f32 { + let MIN_VARIANCE = 0.00001; + + // One-tailed inequality valid if fragmentDepth > moments.x. + var p: f32; + if (fragmentDepth <= moments.x) { + p = 1.0; + } else { + p = 0.0; + } + + // Compute variance. + let variance = max(moments.y - mx_square_f32(moments.x), MIN_VARIANCE); + + // Compute probabilistic upper bound. + let d = fragmentDepth - moments.x; + let pMax = variance / (variance + mx_square_f32(d)); + return max(p, pMax); +} diff --git a/libraries/pbrlib/genwgsl/lib/mx_shadow_platform.wgsl b/libraries/pbrlib/genwgsl/lib/mx_shadow_platform.wgsl new file mode 100644 index 0000000000..089338b693 --- /dev/null +++ b/libraries/pbrlib/genwgsl/lib/mx_shadow_platform.wgsl @@ -0,0 +1,11 @@ +// Platform-specific shadow map lookup. +// Requires (provided by the renderer's shader template): +// shadowMap : texture_2d — variance shadow map +// shadowMapSampler : sampler — sampler for shadowMap + +fn mx_shadow_occlusion(shadow_matrix: mat4x4f, world_position: vec3f) -> f32 { + let shadowCoord4 = (shadow_matrix * vec4f(world_position, 1.0)); + let shadowCoord = shadowCoord4.xyz / shadowCoord4.w * 0.5 + 0.5; + let shadowMoments = textureSample(shadowMap, shadowMapSampler, shadowCoord.xy).xy; + return mx_variance_shadow_occlusion(shadowMoments, shadowCoord.z); +} diff --git a/libraries/pbrlib/genwgsl/lib/mx_transmission_opacity.wgsl b/libraries/pbrlib/genwgsl/lib/mx_transmission_opacity.wgsl new file mode 100644 index 0000000000..235429b624 --- /dev/null +++ b/libraries/pbrlib/genwgsl/lib/mx_transmission_opacity.wgsl @@ -0,0 +1,6 @@ +#include "mx_microfacet_specular.wgsl" + +// Opacity-only surface transmission: no refraction, just tint. +fn mx_surface_transmission(N: vec3f, V: vec3f, X: vec3f, alpha: vec2f, distribution: i32, fd: FresnelData, tint: vec3f) -> vec3f { + return tint; +} diff --git a/libraries/pbrlib/genwgsl/lib/mx_transmission_refract.wgsl b/libraries/pbrlib/genwgsl/lib/mx_transmission_refract.wgsl new file mode 100644 index 0000000000..8515cb709e --- /dev/null +++ b/libraries/pbrlib/genwgsl/lib/mx_transmission_refract.wgsl @@ -0,0 +1,19 @@ +#include "mx_microfacet_specular.wgsl" + +fn mx_surface_transmission(N: vec3f, V: vec3f, X: vec3f, alpha: vec2f, distribution: i32, fd: FresnelData, tint_in: vec3f) -> vec3f +{ + // Approximate the appearance of surface transmission as glossy environment map + // refraction, ignoring any scene geometry that might be visible through the surface. + // Setting fd.refraction makes mx_environment_radiance sample along the refracted ray + // (mx_refraction_solid_sphere) with an inverted Fresnel weight, rather than reflecting. + var fd_refract: FresnelData = fd; + fd_refract.refraction = true; + + var tint: vec3f = tint_in; + + if (bool($refractionTwoSided)) { + tint = mx_square_vec3(tint); + } + + return mx_environment_radiance(N, V, X, alpha, distribution, fd_refract) * tint; +} diff --git a/libraries/pbrlib/genwgsl/mx_add_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_add_bsdf.wgsl new file mode 100644 index 0000000000..f954d627eb --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_add_bsdf.wgsl @@ -0,0 +1,17 @@ +// Generated from libraries/pbrlib/genglsl/mx_add_bsdf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_closure_type.wgsl" + +fn mx_add_bsdf(closureData_21: ClosureData, in1_8: BSDF, in2_8: BSDF, result_82: ptr) { + var closureData_22: ClosureData; + var in1_9: BSDF; + var in2_9: BSDF; + + closureData_22 = closureData_21; + in1_9 = in1_8; + in2_9 = in2_8; + (*result_82).response = (in1_9.response + in2_9.response); + (*result_82).throughput = max(((in1_9.throughput + in2_9.throughput) - vec3(1.0)), vec3(0.0)); + return; +} diff --git a/libraries/pbrlib/genwgsl/mx_add_edf.wgsl b/libraries/pbrlib/genwgsl/mx_add_edf.wgsl new file mode 100644 index 0000000000..604bcc00d2 --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_add_edf.wgsl @@ -0,0 +1,16 @@ +// Generated from libraries/pbrlib/genglsl/mx_add_edf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_closure_type.wgsl" + +fn mx_add_edf(closureData_21: ClosureData, in1_8: vec3f, in2_8: vec3f, result_82: ptr) { + var closureData_22: ClosureData; + var in1_9: vec3f; + var in2_9: vec3f; + + closureData_22 = closureData_21; + in1_9 = in1_8; + in2_9 = in2_8; + (*result_82) = (in1_9 + in2_9); + return; +} diff --git a/libraries/pbrlib/genwgsl/mx_anisotropic_vdf.wgsl b/libraries/pbrlib/genwgsl/mx_anisotropic_vdf.wgsl new file mode 100644 index 0000000000..63e7f5edd3 --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_anisotropic_vdf.wgsl @@ -0,0 +1,23 @@ +// Generated from libraries/pbrlib/genglsl/mx_anisotropic_vdf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_closure_type.wgsl" + +fn mx_anisotropic_vdf(closureData_21: ClosureData, absorption_2: vec3f, scattering: vec3f, anisotropy_2: f32, vdf: ptr) { + var closureData_22: ClosureData; + var absorption_3: vec3f; + var anisotropy_3: f32; + + closureData_22 = closureData_21; + absorption_3 = absorption_2; + anisotropy_3 = anisotropy_2; + if (closureData_22.closureType == 2i) { + { + (*vdf).response = vec3(0.0); + (*vdf).throughput = exp(-(absorption_3)); + return; + } + } else { + return; + } +} diff --git a/libraries/pbrlib/genwgsl/mx_artistic_ior.wgsl b/libraries/pbrlib/genwgsl/mx_artistic_ior.wgsl new file mode 100644 index 0000000000..c67a1a6591 --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_artistic_ior.wgsl @@ -0,0 +1,24 @@ +// Generated from libraries/pbrlib/genglsl/mx_artistic_ior.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_artistic_ior(reflectivity: vec3f, edge_color: vec3f, ior_8: ptr, extinction_1: ptr) { + var r_4: vec3f; + var r_sqrt: vec3f; + var n_min: vec3f; + var n_max: vec3f; + var np1_: vec3f; + var nm1_: vec3f; + var k2_: vec3f; + + r_4 = clamp(reflectivity, vec3(0.0), vec3(0.99)); + r_sqrt = sqrt(r_4); + n_min = ((vec3(1.0) - r_4) / (vec3(1.0) + r_4)); + n_max = ((vec3(1.0) + r_sqrt) / (vec3(1.0) - r_sqrt)); + (*ior_8) = mix(n_max, n_min, edge_color); + np1_ = (((*ior_8)) + vec3(1.0)); + nm1_ = (((*ior_8)) - vec3(1.0)); + k2_ = ((((np1_ * np1_) * r_4) - (nm1_ * nm1_)) / (vec3(1.0) - r_4)); + k2_ = max(k2_, vec3(0.0)); + (*extinction_1) = sqrt(k2_); + return; +} diff --git a/libraries/pbrlib/genwgsl/mx_blackbody.wgsl b/libraries/pbrlib/genwgsl/mx_blackbody.wgsl new file mode 100644 index 0000000000..27f58b5742 --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_blackbody.wgsl @@ -0,0 +1,56 @@ +// Generated from libraries/pbrlib/genglsl/mx_blackbody.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_blackbody(temperatureKelvin: f32, colorValue: ptr) { + var temperatureKelvin_1: f32; + var xc: f32; + var yc: f32; + var t_6: f32; + var t2_: f32; + var t3_: f32; + var xc2_: f32; + var xc3_: f32; + var XYZ: vec3f; + + temperatureKelvin_1 = temperatureKelvin; + temperatureKelvin_1 = clamp(temperatureKelvin_1, 800.0, 25000.0); + t_6 = (1000.0 / temperatureKelvin_1); + t2_ = (t_6 * t_6); + t3_ = ((t_6 * t_6) * t_6); + if (temperatureKelvin_1 < 4000.0) { + { + xc = ((((-0.2661239 * t3_) - (0.234358 * t2_)) + (0.8776956 * t_6)) + 0.17991); + } + } else { + { + xc = ((((-3.025847 * t3_) + (2.1070378 * t2_)) + (0.2226347 * t_6)) + 0.24039); + } + } + xc2_ = (xc * xc); + xc3_ = ((xc * xc) * xc); + if (temperatureKelvin_1 < 2222.0) { + { + yc = ((((-1.1063814 * xc3_) - (1.3481102 * xc2_)) + (2.1855583 * xc)) - 0.20219684); + } + } else { + if (temperatureKelvin_1 < 4000.0) { + { + yc = ((((-0.9549476 * xc3_) - (1.3741859 * xc2_)) + (2.09137 * xc)) - 0.16748866); + } + } else { + { + yc = ((((3.081758 * xc3_) - (5.873387 * xc2_)) + (3.7511299 * xc)) - 0.37001482); + } + } + } + if (yc <= 0.0) { + { + (*colorValue) = vec3(1.0); + return; + } + } + XYZ = vec3f((xc / yc), 1.0, (((1.0 - xc) - yc) / yc)); + (*colorValue) = (mx_matrix_mul_mat3_vec3(XYZ_to_RGB, XYZ)); + (*colorValue) = max(((*colorValue)), vec3(0.0)); + return; +} diff --git a/libraries/pbrlib/genwgsl/mx_burley_diffuse_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_burley_diffuse_bsdf.wgsl new file mode 100644 index 0000000000..f6b7d15d0f --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_burley_diffuse_bsdf.wgsl @@ -0,0 +1,54 @@ +// Generated from libraries/pbrlib/genglsl/mx_burley_diffuse_bsdf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_closure_type.wgsl" +#include "lib/mx_microfacet_diffuse.wgsl" + +fn mx_burley_diffuse_bsdf(closureData_21: ClosureData, weight_7: f32, color_9: vec3f, roughness_29: f32, N_24: vec3f, bsdf_8: ptr) { + var closureData_22: ClosureData; + var weight_8: f32; + var color_10: vec3f; + var roughness_30: f32; + var N_25: vec3f; + var V_8: vec3f; + var L_4: vec3f; + var NdotV_24: f32; + var NdotL_5: f32; + var LdotH_1: f32; + var Li: vec3f; + + closureData_22 = closureData_21; + weight_8 = weight_7; + color_10 = color_9; + roughness_30 = roughness_29; + N_25 = N_24; + (*bsdf_8).throughput = vec3(0.0); + if (weight_8 < 0.00000001) { + { + return; + } + } + V_8 = closureData_22.V; + L_4 = closureData_22.L; + N_25 = (mx_forward_facing_normal(N_25, V_8)); + NdotV_24 = clamp(dot(N_25, V_8), 0.00000001, 1.0); + if (closureData_22.closureType == 1i) { + { + NdotL_5 = clamp(dot(N_25, L_4), 0.00000001, 1.0); + LdotH_1 = clamp(dot(L_4, normalize((L_4 + V_8))), 0.00000001, 1.0); + (*bsdf_8).response = ((((color_10 * closureData_22.occlusion) * weight_8) * NdotL_5) * 0.31830987); + (*bsdf_8).response = (((*bsdf_8)).response * (mx_burley_diffuse(NdotV_24, NdotL_5, LdotH_1, roughness_30))); + return; + } + } else { + if (closureData_22.closureType == 3i) { + { + Li = (mx_environment_irradiance(N_25) * (mx_burley_diffuse_dir_albedo(NdotV_24, roughness_30))); + (*bsdf_8).response = ((Li * color_10) * weight_8); + return; + } + } else { + return; + } + } +} diff --git a/libraries/pbrlib/genwgsl/mx_chiang_hair_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_chiang_hair_bsdf.wgsl new file mode 100644 index 0000000000..2ead9a244a --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_chiang_hair_bsdf.wgsl @@ -0,0 +1,288 @@ +#include "lib/mx_closure_type.wgsl" +#include "lib/mx_microfacet_specular.wgsl" + +// https://eugenedeon.com/pdfs/egsrhair.pdf +fn mx_deon_hair_absorption_from_melanin( + melanin_concentration: f32, + melanin_redness: f32, + // constants converted to color via exp(-c). the defaults are lin_rec709 colors, they may be + // transformed to scene-linear rendering color space. + eumelanin_color: vec3f, // default: (0.657704, 0.498077, 0.254106) == exp(-(0.419, 0.697, 1.37)) + pheomelanin_color: vec3f, // default: (0.829443, 0.670320, 0.349937) == exp(-(0.187, 0.4, 1.05)) + absorption: ptr) +{ + var melanin: f32 = -log(max(1.0 - melanin_concentration, 0.0001)); + var eumelanin: f32 = melanin * (1.0 - melanin_redness); + var pheomelanin: f32 = melanin * melanin_redness; + *absorption = max( + eumelanin * -log(eumelanin_color) + pheomelanin * -log(pheomelanin_color), + vec3f(0.0) + ); +} + +// https://media.disneyanimation.com/uploads/production/publication_asset/152/asset/eurographics2016Fur_Smaller.pdf +fn mx_chiang_hair_absorption_from_color(color: vec3f, betaN: f32, absorption: ptr) +{ + var b2: f32 = betaN* betaN; + var b4: f32 = b2 * b2; + var b_fac: f32 = + 5.969 - + (0.215 * betaN) + + (2.532 * b2) - + (10.73 * b2 * betaN) + + (5.574 * b4) + + (0.245 * b4 * betaN); + var sigma: vec3f = log(min(max(color, 0.001), vec3f(1.0))) / b_fac; + *absorption = sigma * sigma; +} + +fn mx_chiang_hair_roughness( + longitudinal: f32, + azimuthal: f32, + scale_TT: f32, // empirical roughness scale from Marschner et al. (2003). + scale_TRT: f32, // default: scale_TT = 0.5, scale_TRT = 2.0 + roughness_R: ptr, + roughness_TT: ptr, + roughness_TRT: ptr) +{ + var lr: f32 = clamp(longitudinal, 0.001, 1.0); + var ar: f32 = clamp(azimuthal, 0.001, 1.0); + + // longitudinal variance + var v: f32 = 0.726 * lr + 0.812 * lr * lr + 3.7 * pow(lr, 20.0); + v = v * v; + + var s: f32 = 0.265 * ar + 1.194 * ar * ar + 5.372 * pow(ar, 22.0); + + *roughness_R = vec2f(v, s); + *roughness_TT = vec2f(v * scale_TT * scale_TT, s); + *roughness_TRT = vec2f(v * scale_TRT * scale_TRT, s); +} + +fn mx_hair_transform_sin_cos(x: f32) -> f32 +{ + return sqrt(max(1.0 - x * x, 0.0)); +} + +fn mx_hair_I0(x: f32) -> f32 +{ + var v: f32 = 1.0; + var n: f32 = 1.0; + var d: f32 = 1.0; + var f: f32 = 1.0; + var x2: f32 = x * x; + for (var i: i32 = 0; i < 9 ; i++) + { + d *= 4.0 * (f * f); + n *= x2; + v += n / d; + f += 1.0; + } + return v; +} + +fn mx_hair_log_I0(x: f32) -> f32 +{ + if (x > 12.0) + return x + 0.5 * (-log(2.0 * M_PI) + log(1.0 / x) + 1.0 / (8.0 * x)); + else + return log(mx_hair_I0(x)); +} + +fn mx_hair_logistic(x: f32, s: f32) -> f32 +{ + var x_val: f32 = x; + if (x_val > 0.0) + x_val = -x_val; + var f: f32 = exp(x_val / s); + return f / (s * (1.0 + f) * (1.0 + f)); +} + +fn mx_hair_logistic_cdf(x: f32, s: f32) -> f32 +{ + return 1.0 / (1.0 + exp(-x / s)); +} + +fn mx_hair_trimmed_logistic(x: f32, s: f32, a: f32, b: f32) -> f32 +{ + // the constant can be found in Chiang et al. (2016) Appendix A, eq. (12) + var s_val: f32 = s * 0.626657; // sqrt(M_PI/8) + return mx_hair_logistic(x, s_val) / (mx_hair_logistic_cdf(b, s_val) - mx_hair_logistic_cdf(a, s_val)); +} + +fn mx_hair_phi(p: i32, gammaO: f32, gammaT: f32) -> f32 +{ + var fP: f32 = f32(p); + return 2.0 * fP * gammaT - 2.0 * gammaO + fP * M_PI; +} + +fn mx_hair_longitudinal_scattering( // Mp + sinThetaI: f32, + cosThetaI: f32, + sinThetaO: f32, + cosThetaO: f32, + v: f32) -> f32 +{ + var inv_v: f32 = 1.0 / v; + var a: f32 = cosThetaO * cosThetaI * inv_v; + var b: f32 = sinThetaO * sinThetaI * inv_v; + if (v < 0.1) + return exp(mx_hair_log_I0(a) - b - inv_v + 0.6931 + log(0.5 * inv_v)); + else + return ((exp(-b) * mx_hair_I0(a)) / (2.0 * v * sinh(inv_v))); +} + +fn mx_hair_azimuthal_scattering( // Np + phi: f32, + p: i32, + s: f32, + gammaO: f32, + gammaT: f32) -> f32 +{ + if (p >= 3) + return 0.5 / M_PI; + + var dphi: f32 = phi - mx_hair_phi(p, gammaO, gammaT); + if (isinf(dphi)) + return 0.5 / M_PI; + + while (dphi > M_PI) dphi -= (2.0 * M_PI); + while (dphi < (-M_PI)) dphi += (2.0 * M_PI); + + return mx_hair_trimmed_logistic(dphi, s, -M_PI, M_PI); +} + +fn mx_hair_alpha_angles( + alpha: f32, + sinThetaI: f32, + cosThetaI: f32, + angles: ptr>) +{ + // 0:R, 1:TT, 2:TRT, 3:TRRT+ + for (var i: i32 = 0; i <= 3; i++) + { + if (alpha == 0.0 || i == 3) + (*angles)[i] = vec2f(sinThetaI, cosThetaI); + else + { + var m: f32 = 2.0 - f32(i) * 3.0; + var sa: f32 = sin(m * alpha); + var ca: f32 = cos(m * alpha); + (*angles)[i].x = sinThetaI * ca + cosThetaI * sa; + (*angles)[i].y = cosThetaI * ca - sinThetaI * sa; + } + } +} + +fn mx_hair_attenuation(f: f32, T: vec3f, Ap: ptr>) // Ap +{ + // 0:R, 1:TT, 2:TRT, 3:TRRT+ + (*Ap)[0] = vec3f(f); + (*Ap)[1] = (1.0 - f) * (1.0 - f) * T; + (*Ap)[2] = (*Ap)[1] * T * f; + (*Ap)[3] = (*Ap)[2] * T * f / (vec3f(1.0) - T * f); +} + +fn mx_chiang_hair_bsdf(closureData: ClosureData, tint_R: vec3f, tint_TT: vec3f, tint_TRT: vec3f, ior: f32, + roughness_R: vec2f, roughness_TT: vec2f, roughness_TRT: vec2f, cuticle_angle: f32, + absorption_coefficient: vec3f, N: vec3f, X: vec3f, bsdf: ptr) +{ + var V: vec3f = closureData.V; + var L: vec3f = closureData.L; + var N_: vec3f = mx_forward_facing_normal(N, V); // mutable copy (WGSL params are immutable) + var X_: vec3f = X; + + (*bsdf).throughput = vec3f(0.0); + + if (closureData.closureType == CLOSURE_TYPE_REFLECTION) + { + X_ = normalize(X_ - dot(X_, N_) * N_); + var Y: vec3f = cross(N_, X_); + + var sinThetaO: f32 = dot(V, X_); + var sinThetaI: f32 = dot(L, X_); + var cosThetaO: f32 = mx_hair_transform_sin_cos(sinThetaO); + var cosThetaI: f32 = mx_hair_transform_sin_cos(sinThetaI); + + var y1: f32 = dot(L, N_); + var x1: f32 = dot(L, Y); + var y2: f32 = dot(V, N_); + var x2: f32 = dot(V, Y); + var phi: f32 = atan2(y1 * x2 - y2 * x1, x1 * x2 + y1 * y2); + + var k1_p: vec3f = normalize(V - X_ * dot(V, X_)); + var cosGammaO: f32 = dot(N_, k1_p); + var sinGammaO: f32 = mx_hair_transform_sin_cos(cosGammaO); + if (dot(k1_p, Y) > 0.0) + sinGammaO = -sinGammaO; + var gammaO: f32 = asin(sinGammaO); + + var sinThetaT: f32 = sinThetaO / ior; + var cosThetaT: f32 = mx_hair_transform_sin_cos(sinThetaT); + var etaP: f32 = sqrt(max(ior * ior - sinThetaO * sinThetaO, 0.0)) / max(cosThetaO, M_FLOAT_EPS); + var sinGammaT: f32 = max(min(sinGammaO / etaP, 1.0), -1.0); + var cosGammaT: f32 = sqrt(1.0 - sinGammaT * sinGammaT); + var gammaT: f32 = asin(sinGammaT); + + // attenuation + var Ap: array; + var fresnel: f32 = mx_fresnel_dielectric(cosThetaO * cosGammaO, ior); + var T: vec3f = exp(-absorption_coefficient * (2.0 * cosGammaT / cosThetaT)); + mx_hair_attenuation(fresnel, T, &(Ap)); + + // parameters for each lobe + var angles: array; + var alpha: f32 = cuticle_angle * M_PI - (M_PI / 2.0); // remap [0, 1] to [-PI/2, PI/2] + mx_hair_alpha_angles(alpha, sinThetaI, cosThetaI, &(angles)); + + var tint: array; + tint[0] = tint_R; + tint[1] = tint_TT; + tint[2] = tint_TRT; + tint[3] = tint_TRT; + + var roughness_R_clamped: vec2f = clamp(roughness_R, 0.001, 1.0); + var roughness_TT_clamped: vec2f = clamp(roughness_TT, 0.001, 1.0); + var roughness_TRT_clamped: vec2f = clamp(roughness_TRT, 0.001, 1.0); + + var vs: array; + vs[0] = roughness_R_clamped; + vs[1] = roughness_TT_clamped; + vs[2] = roughness_TRT_clamped; + vs[3] = roughness_TRT_clamped; + + // R, TT, TRT, TRRT+ + var F: vec3f = vec3f(0.0); + for (var i: i32 = 0; i <= 3; i++) + { + tint[i] = max(tint[i], vec3f(0.0)); + var Mp: f32 = mx_hair_longitudinal_scattering(angles[i].x, angles[i].y, sinThetaO, cosThetaO, vs[i].x); + var Np: f32 = select(mx_hair_azimuthal_scattering(phi, i, vs[i].y, gammaO, gammaT), (1.0 / 2.0 * M_PI), (i == 3)); + F += Mp * Np * tint[i] * Ap[i]; + } + + (*bsdf).response = F * closureData.occlusion * (1.0 / M_PI); + } + else if (closureData.closureType == CLOSURE_TYPE_INDIRECT) + { + // This indirect term is a *very* rough approximation. + + var NdotV: f32 = clamp(dot(N_, V), M_FLOAT_EPS, 1.0); + var fd: FresnelData = mx_init_fresnel_dielectric(ior, 0.0, 1.0); + var F: vec3f = mx_compute_fresnel(NdotV, fd); + + var roughness: vec2f = (roughness_R + roughness_TT + roughness_TRT) / vec2f(3.0); // ? + var safeAlpha: vec2f = clamp(roughness, vec2f(M_FLOAT_EPS), vec2f(1.0)); + var avgAlpha: f32 = mx_average_alpha(safeAlpha); + + // Use GGX to match the behavior of mx_environment_radiance. + var F0: f32 = mx_ior_to_f0(ior); + var comp: vec3f = mx_ggx_energy_compensation(NdotV, avgAlpha, F); + var dirAlbedo: vec3f = mx_ggx_dir_albedo(NdotV, avgAlpha, F0, 1.0) * comp; + + var Li: vec3f = mx_environment_radiance(N_, V, X_, safeAlpha, 0, fd); + var tint: vec3f = (tint_R + tint_TT + tint_TRT) / vec3f(3.0); // ? + + (*bsdf).response = Li * comp * tint; + } +} diff --git a/libraries/pbrlib/genwgsl/mx_conductor_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_conductor_bsdf.wgsl new file mode 100644 index 0000000000..af5f2deec0 --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_conductor_bsdf.wgsl @@ -0,0 +1,54 @@ +#include "lib/mx_closure_type.wgsl" +#include "lib/mx_microfacet_specular.wgsl" + +fn mx_conductor_bsdf(closureData: ClosureData, weight: f32, ior_n: vec3f, ior_k: vec3f, roughness: vec2f, retroreflective: bool, thinfilm_thickness: f32, thinfilm_ior: f32, N: vec3f, X: vec3f, distribution: i32, bsdf: ptr) +{ + (*bsdf).throughput = vec3f(0.0); + + if (weight < M_FLOAT_EPS) + { + return; + } + + var V: vec3f = closureData.V; + var L: vec3f = closureData.L; + var N_: vec3f = mx_forward_facing_normal(N, V); // mutable copy (WGSL params are immutable) + var X_: vec3f = X; + + var NdotV: f32 = clamp(dot(N_, V), M_FLOAT_EPS, 1.0); + + var fd: FresnelData = mx_init_fresnel_conductor(ior_n, ior_k, thinfilm_thickness, thinfilm_ior); + + var safeAlpha: vec2f = clamp(roughness, vec2f(M_FLOAT_EPS), vec2f(1.0)); + var avgAlpha: f32 = mx_average_alpha(safeAlpha); + + if (closureData.closureType == CLOSURE_TYPE_REFLECTION) + { + X_ = normalize(X_ - dot(X_, N_) * N_); + var Y: vec3f = cross(N_, X_); + var H: vec3f = normalize(L + V); + + var NdotL: f32 = clamp(dot(N_, L), M_FLOAT_EPS, 1.0); + var VdotH: f32 = clamp(dot(V, H), M_FLOAT_EPS, 1.0); + + var Ht: vec3f = vec3f(dot(H, X_), dot(H, Y), dot(H, N_)); + + var F: vec3f = mx_compute_fresnel(VdotH, fd); + var D: f32 = mx_ggx_NDF(Ht, safeAlpha); + var G: f32 = mx_ggx_smith_G2(NdotL, NdotV, avgAlpha); + + var comp: vec3f = mx_ggx_energy_compensation(NdotV, avgAlpha, F); + + // Note: NdotL is cancelled out + (*bsdf).response = D * F * G * comp * closureData.occlusion * weight / (4.0 * NdotV); + } + else if (closureData.closureType == CLOSURE_TYPE_INDIRECT) + { + var F: vec3f = mx_compute_fresnel(NdotV, fd); + var comp: vec3f = mx_ggx_energy_compensation(NdotV, avgAlpha, F); + // FIS bakes the Fresnel term into Li, so weight only by the energy-compensation term + // (matching mx_conductor_bsdf.glsl); multiplying by dirAlbedo would double-count Fresnel. + var Li: vec3f = mx_environment_radiance(N_, V, X_, safeAlpha, distribution, fd); + (*bsdf).response = Li * comp * weight; + } +} diff --git a/libraries/pbrlib/genwgsl/mx_dielectric_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_dielectric_bsdf.wgsl new file mode 100644 index 0000000000..cfb49af486 --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_dielectric_bsdf.wgsl @@ -0,0 +1,61 @@ +#include "lib/mx_closure_type.wgsl" +#include "lib/mx_microfacet_specular.wgsl" + +fn mx_dielectric_bsdf(closureData: ClosureData, weight: f32, tint: vec3f, ior: f32, roughness: vec2f, retroreflective: bool, thinfilm_thickness: f32, thinfilm_ior: f32, N_in: vec3f, X_in: vec3f, distribution: i32, scatter_mode: i32, bsdf: ptr) { + if (weight < M_FLOAT_EPS) { return; } + if (closureData.closureType != CLOSURE_TYPE_TRANSMISSION && scatter_mode == 1) { return; } + + let V = closureData.V; + let L = closureData.L; + let N = mx_forward_facing_normal(N_in, V); + let NdotV = clamp(dot(N, V), 1e-8, 1.0); + + let fd = mx_init_fresnel_dielectric(ior, thinfilm_thickness, thinfilm_ior); + let F0 = mx_ior_to_f0(ior); + + let safeAlpha = clamp(roughness, vec2f(1e-8), vec2f(1.0)); + let avgAlpha = mx_average_alpha(safeAlpha); + let safeTint = max(tint, vec3f(0.0)); + + if (closureData.closureType == CLOSURE_TYPE_REFLECTION) { + var X = normalize(X_in - dot(X_in, N) * N); + let Y = cross(N, X); + let H = normalize(L + V); + + let NdotL = clamp(dot(N, L), 1e-8, 1.0); + let VdotH = clamp(dot(V, H), 1e-8, 1.0); + + let Ht = vec3f(dot(H, X), dot(H, Y), dot(H, N)); + + let F = mx_compute_fresnel(VdotH, fd); + let D = mx_ggx_NDF(Ht, safeAlpha); + let G = mx_ggx_smith_G2(NdotL, NdotV, avgAlpha); + + let comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F); + let dirAlbedo = mx_ggx_dir_albedo(NdotV, avgAlpha, vec3f(F0), vec3f(1.0)) * comp; + (*bsdf).throughput = vec3f(1.0) - dirAlbedo * weight; + + (*bsdf).response = D * F * G * comp * safeTint * closureData.occlusion * weight / (4.0 * NdotV); + } else if (closureData.closureType == CLOSURE_TYPE_TRANSMISSION) { + let F = mx_compute_fresnel(NdotV, fd); + + let comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F); + let dirAlbedo = mx_ggx_dir_albedo(NdotV, avgAlpha, vec3f(F0), vec3f(1.0)) * comp; + (*bsdf).throughput = vec3f(1.0) - dirAlbedo * weight; + + if (scatter_mode != 0) { + (*bsdf).response = mx_surface_transmission(N, V, X_in, safeAlpha, distribution, fd, safeTint) * weight; + } + } else if (closureData.closureType == CLOSURE_TYPE_INDIRECT) { + let F = mx_compute_fresnel(NdotV, fd); + + let comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F); + let dirAlbedo = mx_ggx_dir_albedo(NdotV, avgAlpha, vec3f(F0), vec3f(1.0)) * comp; + (*bsdf).throughput = vec3f(1.0) - dirAlbedo * weight; + + // FIS bakes the Fresnel term into Li, so weight only by the energy-compensation term + // (matching mx_dielectric_bsdf.glsl); dirAlbedo would double-count Fresnel. + let Li = mx_environment_radiance(N, V, X_in, safeAlpha, distribution, fd); + (*bsdf).response = Li * safeTint * comp * weight; + } +} diff --git a/libraries/pbrlib/genwgsl/mx_displacement_float.wgsl b/libraries/pbrlib/genwgsl/mx_displacement_float.wgsl new file mode 100644 index 0000000000..791eccc4bd --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_displacement_float.wgsl @@ -0,0 +1,13 @@ +// Generated from libraries/pbrlib/genglsl/mx_displacement_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_displacement_float(disp_1: f32, scale_3: f32, result_82: ptr) { + var disp_2: f32; + var scale_4: f32; + + disp_2 = disp_1; + scale_4 = scale_3; + (*result_82).offset = vec3(disp_2); + (*result_82).scale = scale_4; + return; +} diff --git a/libraries/pbrlib/genwgsl/mx_displacement_vector3.wgsl b/libraries/pbrlib/genwgsl/mx_displacement_vector3.wgsl new file mode 100644 index 0000000000..328da6a7cd --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_displacement_vector3.wgsl @@ -0,0 +1,13 @@ +// Generated from libraries/pbrlib/genglsl/mx_displacement_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_displacement_vector3(disp_1: vec3f, scale_3: f32, result_82: ptr) { + var disp_2: vec3f; + var scale_4: f32; + + disp_2 = disp_1; + scale_4 = scale_3; + (*result_82).offset = disp_2; + (*result_82).scale = scale_4; + return; +} diff --git a/libraries/pbrlib/genwgsl/mx_generalized_schlick_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_generalized_schlick_bsdf.wgsl new file mode 100644 index 0000000000..30b0e8e0b9 --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_generalized_schlick_bsdf.wgsl @@ -0,0 +1,87 @@ +#include "lib/mx_closure_type.wgsl" +#include "lib/mx_microfacet_specular.wgsl" + +fn mx_generalized_schlick_bsdf( + closureData: ClosureData, + weight: f32, + color0: vec3f, + color82: vec3f, + color90: vec3f, + exponent: f32, + roughness: vec2f, + retroreflective: bool, + thinfilm_thickness: f32, + thinfilm_ior: f32, + N: vec3f, + X: vec3f, + distribution: i32, + scatter_mode: i32, + bsdf: ptr +) { + if (weight < M_FLOAT_EPS) { + return; + } + + if (closureData.closureType != CLOSURE_TYPE_TRANSMISSION && scatter_mode == 1) { + return; + } + + let V = closureData.V; + let L = closureData.L; + + var N_facing = mx_forward_facing_normal(N, V); + let NdotV = mx_saturate(dot(N_facing, V)); + + let safeColor0 = max(color0, vec3f(0.0)); + let safeColor82 = max(color82, vec3f(0.0)); + let safeColor90 = max(color90, vec3f(0.0)); + let fd = mx_init_fresnel_schlick(safeColor0, safeColor82, safeColor90, exponent, thinfilm_thickness, thinfilm_ior); + + let safeAlpha = clamp(roughness, vec2f(M_FLOAT_EPS), vec2f(1.0)); + let avgAlpha = mx_average_alpha(safeAlpha); + + if (closureData.closureType == CLOSURE_TYPE_REFLECTION) { + var X_ortho = normalize(X - dot(X, N_facing) * N_facing); + let Y = cross(N_facing, X_ortho); + let H = normalize(L + V); + + let NdotL = mx_saturate(dot(N_facing, L)); + let VdotH = mx_saturate(dot(V, H)); + + let Ht = vec3f(dot(H, X_ortho), dot(H, Y), dot(H, N_facing)); + + let F = mx_compute_fresnel(VdotH, fd); + let D = mx_ggx_NDF(Ht, safeAlpha); + let G = mx_ggx_smith_G2(NdotL, NdotV, avgAlpha); + + let comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F); + let dirAlbedo = mx_ggx_dir_albedo(NdotV, avgAlpha, safeColor0, safeColor90) * comp; + let avgDirAlbedo = dot(dirAlbedo, vec3f(1.0 / 3.0)); + (*bsdf).throughput = vec3f(1.0 - avgDirAlbedo * weight); + + (*bsdf).response = D * F * G * comp * closureData.occlusion * weight / (4.0 * NdotV); + } else if (closureData.closureType == CLOSURE_TYPE_TRANSMISSION) { + let F = mx_compute_fresnel(NdotV, fd); + + let comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F); + let dirAlbedo = mx_ggx_dir_albedo(NdotV, avgAlpha, safeColor0, safeColor90) * comp; + let avgDirAlbedo = dot(dirAlbedo, vec3f(1.0 / 3.0)); + (*bsdf).throughput = vec3f(1.0 - avgDirAlbedo * weight); + + if (scatter_mode != 0) { + (*bsdf).response = mx_surface_transmission(N_facing, V, X, safeAlpha, distribution, fd, vec3f(1.0)) * weight; + } + } else if (closureData.closureType == CLOSURE_TYPE_INDIRECT) { + let F = mx_compute_fresnel(NdotV, fd); + + let comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F); + let dirAlbedo = mx_ggx_dir_albedo(NdotV, avgAlpha, safeColor0, safeColor90) * comp; + let avgDirAlbedo = dot(dirAlbedo, vec3f(1.0 / 3.0)); + (*bsdf).throughput = vec3f(1.0 - avgDirAlbedo * weight); + + // FIS bakes the Fresnel term into Li, so weight only by the energy-compensation term + // (matching mx_generalized_schlick_bsdf.glsl); dirAlbedo would double-count Fresnel. + let Li = mx_environment_radiance(N_facing, V, X, safeAlpha, distribution, fd); + (*bsdf).response = Li * comp * weight; + } +} diff --git a/libraries/pbrlib/genwgsl/mx_generalized_schlick_edf.wgsl b/libraries/pbrlib/genwgsl/mx_generalized_schlick_edf.wgsl new file mode 100644 index 0000000000..3eae1d27f2 --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_generalized_schlick_edf.wgsl @@ -0,0 +1,33 @@ +// Generated from libraries/pbrlib/genglsl/mx_generalized_schlick_edf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_closure_type.wgsl" +#include "lib/mx_microfacet.wgsl" + +fn mx_generalized_schlick_edf(closureData_21: ClosureData, color0_1: vec3f, color90_1: vec3f, exponent_4: f32, base_2: vec3f, result_82: ptr) { + var closureData_22: ClosureData; + var color0_2: vec3f; + var color90_2: vec3f; + var exponent_5: f32; + var base_3: vec3f; + var N_25: vec3f; + var NdotV_24: f32; + var f_1: vec3f; + + closureData_22 = closureData_21; + color0_2 = color0_1; + color90_2 = color90_1; + exponent_5 = exponent_4; + base_3 = base_2; + if (closureData_22.closureType == 4i) { + { + N_25 = (mx_forward_facing_normal(closureData_22.N, closureData_22.V)); + NdotV_24 = clamp(dot(N_25, closureData_22.V), 0.00000001, 1.0); + f_1 = (mx_fresnel_schlick_vec3_exp(NdotV_24, color0_2, color90_2, exponent_5)); + (*result_82) = (base_3 * f_1); + return; + } + } else { + return; + } +} diff --git a/libraries/pbrlib/genwgsl/mx_layer_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_layer_bsdf.wgsl new file mode 100644 index 0000000000..335e625e68 --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_layer_bsdf.wgsl @@ -0,0 +1,17 @@ +// Generated from libraries/pbrlib/genglsl/mx_layer_bsdf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_closure_type.wgsl" + +fn mx_layer_bsdf(closureData_21: ClosureData, top_1: BSDF, base_2: BSDF, result_82: ptr) { + var closureData_22: ClosureData; + var top_2: BSDF; + var base_3: BSDF; + + closureData_22 = closureData_21; + top_2 = top_1; + base_3 = base_2; + (*result_82).response = (top_2.response + (base_3.response * top_2.throughput)); + (*result_82).throughput = (top_2.throughput * base_3.throughput); + return; +} diff --git a/libraries/pbrlib/genwgsl/mx_layer_vdf.wgsl b/libraries/pbrlib/genwgsl/mx_layer_vdf.wgsl new file mode 100644 index 0000000000..166a03b5b0 --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_layer_vdf.wgsl @@ -0,0 +1,17 @@ +// Generated from libraries/pbrlib/genglsl/mx_layer_vdf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_closure_type.wgsl" + +fn mx_layer_vdf(closureData_21: ClosureData, top_1: BSDF, base_2: VDF, result_82: ptr) { + var closureData_22: ClosureData; + var top_2: BSDF; + var base_3: VDF; + + closureData_22 = closureData_21; + top_2 = top_1; + base_3 = base_2; + (*result_82).response = (top_2.response * base_3.throughput); + (*result_82).throughput = (top_2.throughput * base_3.throughput); + return; +} diff --git a/libraries/pbrlib/genwgsl/mx_mix_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_mix_bsdf.wgsl new file mode 100644 index 0000000000..4db2230dcf --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_mix_bsdf.wgsl @@ -0,0 +1,19 @@ +// Generated from libraries/pbrlib/genglsl/mx_mix_bsdf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_closure_type.wgsl" + +fn mx_mix_bsdf(closureData_21: ClosureData, fg_9: BSDF, bg_9: BSDF, mixValue_1: f32, result_82: ptr) { + var closureData_22: ClosureData; + var fg_10: BSDF; + var bg_10: BSDF; + var mixValue_2: f32; + + closureData_22 = closureData_21; + fg_10 = fg_9; + bg_10 = bg_9; + mixValue_2 = mixValue_1; + (*result_82).response = mix(bg_10.response, fg_10.response, vec3(mixValue_2)); + (*result_82).throughput = mix(bg_10.throughput, fg_10.throughput, vec3(mixValue_2)); + return; +} diff --git a/libraries/pbrlib/genwgsl/mx_mix_edf.wgsl b/libraries/pbrlib/genwgsl/mx_mix_edf.wgsl new file mode 100644 index 0000000000..8a749c995b --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_mix_edf.wgsl @@ -0,0 +1,18 @@ +// Generated from libraries/pbrlib/genglsl/mx_mix_edf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_closure_type.wgsl" + +fn mx_mix_edf(closureData_21: ClosureData, fg_9: vec3f, bg_9: vec3f, mixValue_1: f32, result_82: ptr) { + var closureData_22: ClosureData; + var fg_10: vec3f; + var bg_10: vec3f; + var mixValue_2: f32; + + closureData_22 = closureData_21; + fg_10 = fg_9; + bg_10 = bg_9; + mixValue_2 = mixValue_1; + (*result_82) = mix(bg_10, fg_10, vec3(mixValue_2)); + return; +} diff --git a/libraries/pbrlib/genwgsl/mx_multiply_bsdf_color3.wgsl b/libraries/pbrlib/genwgsl/mx_multiply_bsdf_color3.wgsl new file mode 100644 index 0000000000..14d2a9db8d --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_multiply_bsdf_color3.wgsl @@ -0,0 +1,19 @@ +// Generated from libraries/pbrlib/genglsl/mx_multiply_bsdf_color3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_closure_type.wgsl" + +fn mx_multiply_bsdf_color3(closureData_21: ClosureData, in1_8: BSDF, in2_8: vec3f, result_82: ptr) { + var closureData_22: ClosureData; + var in1_9: BSDF; + var in2_9: vec3f; + var tint_2: vec3f; + + closureData_22 = closureData_21; + in1_9 = in1_8; + in2_9 = in2_8; + tint_2 = clamp(in2_9, vec3(0.0), vec3(1.0)); + (*result_82).response = (in1_9.response * tint_2); + (*result_82).throughput = in1_9.throughput; + return; +} diff --git a/libraries/pbrlib/genwgsl/mx_multiply_bsdf_float.wgsl b/libraries/pbrlib/genwgsl/mx_multiply_bsdf_float.wgsl new file mode 100644 index 0000000000..c0e84e82bb --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_multiply_bsdf_float.wgsl @@ -0,0 +1,19 @@ +// Generated from libraries/pbrlib/genglsl/mx_multiply_bsdf_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_closure_type.wgsl" + +fn mx_multiply_bsdf_float(closureData_21: ClosureData, in1_8: BSDF, in2_8: f32, result_82: ptr) { + var closureData_22: ClosureData; + var in1_9: BSDF; + var in2_9: f32; + var weight_8: f32; + + closureData_22 = closureData_21; + in1_9 = in1_8; + in2_9 = in2_8; + weight_8 = clamp(in2_9, 0.0, 1.0); + (*result_82).response = (in1_9.response * weight_8); + (*result_82).throughput = in1_9.throughput; + return; +} diff --git a/libraries/pbrlib/genwgsl/mx_multiply_edf_color3.wgsl b/libraries/pbrlib/genwgsl/mx_multiply_edf_color3.wgsl new file mode 100644 index 0000000000..af5b33dd81 --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_multiply_edf_color3.wgsl @@ -0,0 +1,16 @@ +// Generated from libraries/pbrlib/genglsl/mx_multiply_edf_color3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_closure_type.wgsl" + +fn mx_multiply_edf_color3(closureData_21: ClosureData, in1_8: vec3f, in2_8: vec3f, result_82: ptr) { + var closureData_22: ClosureData; + var in1_9: vec3f; + var in2_9: vec3f; + + closureData_22 = closureData_21; + in1_9 = in1_8; + in2_9 = in2_8; + (*result_82) = (in1_9 * in2_9); + return; +} diff --git a/libraries/pbrlib/genwgsl/mx_multiply_edf_float.wgsl b/libraries/pbrlib/genwgsl/mx_multiply_edf_float.wgsl new file mode 100644 index 0000000000..39f7087dab --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_multiply_edf_float.wgsl @@ -0,0 +1,16 @@ +// Generated from libraries/pbrlib/genglsl/mx_multiply_edf_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_closure_type.wgsl" + +fn mx_multiply_edf_float(closureData_21: ClosureData, in1_8: vec3f, in2_8: f32, result_82: ptr) { + var closureData_22: ClosureData; + var in1_9: vec3f; + var in2_9: f32; + + closureData_22 = closureData_21; + in1_9 = in1_8; + in2_9 = in2_8; + (*result_82) = (in1_9 * in2_9); + return; +} diff --git a/libraries/pbrlib/genwgsl/mx_oren_nayar_diffuse_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_oren_nayar_diffuse_bsdf.wgsl new file mode 100644 index 0000000000..cfb9ba5f4d --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_oren_nayar_diffuse_bsdf.wgsl @@ -0,0 +1,69 @@ +// Generated from libraries/pbrlib/genglsl/mx_oren_nayar_diffuse_bsdf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_closure_type.wgsl" +#include "lib/mx_microfacet_diffuse.wgsl" + +fn mx_oren_nayar_diffuse_bsdf(closureData_21: ClosureData, weight_7: f32, color_9: vec3f, roughness_29: f32, N_24: vec3f, energy_compensation: bool, bsdf_8: ptr) { + var closureData_22: ClosureData; + var weight_8: f32; + var color_10: vec3f; + var roughness_30: f32; + var N_25: vec3f; + var V_8: vec3f; + var L_4: vec3f; + var NdotV_24: f32; + var NdotL_5: f32; + var LdotV_2: f32; + var local: vec3f; + var diffuse: vec3f; + var local_1: vec3f; + var diffuse_1: vec3f; + var Li: vec3f; + + closureData_22 = closureData_21; + weight_8 = weight_7; + color_10 = color_9; + roughness_30 = roughness_29; + N_25 = N_24; + (*bsdf_8).throughput = vec3(0.0); + if (weight_8 < 0.00000001) { + { + return; + } + } + V_8 = closureData_22.V; + L_4 = closureData_22.L; + N_25 = (mx_forward_facing_normal(N_25, V_8)); + NdotV_24 = clamp(dot(N_25, V_8), 0.00000001, 1.0); + if (closureData_22.closureType == 1i) { + { + NdotL_5 = clamp(dot(N_25, L_4), 0.00000001, 1.0); + LdotV_2 = clamp(dot(L_4, V_8), 0.00000001, 1.0); + if energy_compensation { + local = (mx_oren_nayar_compensated_diffuse(NdotV_24, NdotL_5, LdotV_2, roughness_30, color_10)); + } else { + local = ((mx_oren_nayar_diffuse(NdotV_24, NdotL_5, LdotV_2, roughness_30)) * color_10); + } + diffuse = local; + (*bsdf_8).response = ((((diffuse * closureData_22.occlusion) * weight_8) * NdotL_5) * 0.31830987); + return; + } + } else { + if (closureData_22.closureType == 3i) { + { + if energy_compensation { + local_1 = (mx_oren_nayar_compensated_diffuse_dir_albedo(NdotV_24, roughness_30, color_10)); + } else { + local_1 = ((mx_oren_nayar_diffuse_dir_albedo(NdotV_24, roughness_30)) * color_10); + } + diffuse_1 = local_1; + Li = mx_environment_irradiance(N_25); + (*bsdf_8).response = ((Li * diffuse_1) * weight_8); + return; + } + } else { + return; + } + } +} diff --git a/libraries/pbrlib/genwgsl/mx_roughness_anisotropy.wgsl b/libraries/pbrlib/genwgsl/mx_roughness_anisotropy.wgsl new file mode 100644 index 0000000000..085895817c --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_roughness_anisotropy.wgsl @@ -0,0 +1,27 @@ +// Generated from libraries/pbrlib/genglsl/mx_roughness_anisotropy.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_roughness_anisotropy(roughness_29: f32, anisotropy_2: f32, result_82: ptr) { + var roughness_30: f32; + var anisotropy_3: f32; + var roughness_sqr: f32; + var aspect: f32; + + roughness_30 = roughness_29; + anisotropy_3 = anisotropy_2; + roughness_sqr = clamp((roughness_30 * roughness_30), 0.00000001, 1.0); + if (anisotropy_3 > 0.0) { + { + aspect = sqrt((1.0 - clamp(anisotropy_3, 0.0, 0.98))); + (*result_82).x = min((roughness_sqr / aspect), 1.0); + (*result_82).y = (roughness_sqr * aspect); + return; + } + } else { + { + (*result_82).x = roughness_sqr; + (*result_82).y = roughness_sqr; + return; + } + } +} diff --git a/libraries/pbrlib/genwgsl/mx_roughness_dual.wgsl b/libraries/pbrlib/genwgsl/mx_roughness_dual.wgsl new file mode 100644 index 0000000000..a45cacaa7f --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_roughness_dual.wgsl @@ -0,0 +1,16 @@ +// Generated from libraries/pbrlib/genglsl/mx_roughness_dual.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_roughness_dual(roughness_29: vec2f, result_82: ptr) { + var roughness_30: vec2f; + + roughness_30 = roughness_29; + if (roughness_30.y < 0.0) { + { + roughness_30.y = roughness_30.x; + } + } + (*result_82).x = clamp((roughness_30.x * roughness_30.x), 0.00000001, 1.0); + (*result_82).y = clamp((roughness_30.y * roughness_30.y), 0.00000001, 1.0); + return; +} diff --git a/libraries/pbrlib/genwgsl/mx_sheen_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_sheen_bsdf.wgsl new file mode 100644 index 0000000000..67fba4257a --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_sheen_bsdf.wgsl @@ -0,0 +1,83 @@ +// Generated from libraries/pbrlib/genglsl/mx_sheen_bsdf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_closure_type.wgsl" +#include "lib/mx_microfacet_sheen.wgsl" + +fn mx_sheen_bsdf(closureData_21: ClosureData, weight_7: f32, color_9: vec3f, roughness_29: f32, N_24: vec3f, mode: i32, bsdf_8: ptr) { + var closureData_22: ClosureData; + var weight_8: f32; + var color_10: vec3f; + var roughness_30: f32; + var N_25: vec3f; + var V_8: vec3f; + var L_4: vec3f; + var NdotV_24: f32; + var dirAlbedo: f32; + var H_2: vec3f; + var NdotL_5: f32; + var NdotH_2: f32; + var fr: vec3f; + var fr_1: vec3f; + var dirAlbedo_1: f32; + var Li: vec3f; + + closureData_22 = closureData_21; + weight_8 = weight_7; + color_10 = color_9; + roughness_30 = roughness_29; + N_25 = N_24; + if (weight_8 < 0.00000001) { + { + return; + } + } + V_8 = closureData_22.V; + L_4 = closureData_22.L; + N_25 = (mx_forward_facing_normal(N_25, V_8)); + NdotV_24 = clamp(dot(N_25, V_8), 0.00000001, 1.0); + if (closureData_22.closureType == 1i) { + { + if (mode == 0i) { + { + H_2 = normalize((L_4 + V_8)); + NdotL_5 = clamp(dot(N_25, L_4), 0.00000001, 1.0); + NdotH_2 = clamp(dot(N_25, H_2), 0.00000001, 1.0); + fr = (color_10 * (mx_imageworks_sheen_brdf(NdotL_5, NdotV_24, NdotH_2, roughness_30))); + dirAlbedo = (mx_imageworks_sheen_dir_albedo(NdotV_24, roughness_30)); + (*bsdf_8).response = (((fr * NdotL_5) * closureData_22.occlusion) * weight_8); + } + } else { + { + roughness_30 = clamp(roughness_30, 0.01, 1.0); + fr_1 = (color_10 * (mx_zeltner_sheen_brdf(L_4, V_8, N_25, NdotV_24, roughness_30))); + dirAlbedo = (mx_zeltner_sheen_dir_albedo(NdotV_24, roughness_30)); + (*bsdf_8).response = (((dirAlbedo * fr_1) * closureData_22.occlusion) * weight_8); + } + } + (*bsdf_8).throughput = vec3((1.0 - (dirAlbedo * weight_8))); + return; + } + } else { + if (closureData_22.closureType == 3i) { + { + if (mode == 0i) { + { + dirAlbedo_1 = (mx_imageworks_sheen_dir_albedo(NdotV_24, roughness_30)); + } + } else { + { + roughness_30 = clamp(roughness_30, 0.01, 1.0); + dirAlbedo_1 = (mx_zeltner_sheen_dir_albedo(NdotV_24, roughness_30)); + } + } + Li = mx_environment_irradiance(N_25); + (*bsdf_8).response = (((Li * color_10) * dirAlbedo_1) * weight_8); + (*bsdf_8).throughput = vec3((1.0 - (dirAlbedo_1 * weight_8))); + return; + } + } else { + return; + } + } +} diff --git a/libraries/pbrlib/genwgsl/mx_subsurface_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_subsurface_bsdf.wgsl new file mode 100644 index 0000000000..dcaeffd4e4 --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_subsurface_bsdf.wgsl @@ -0,0 +1,39 @@ +#include "lib/mx_closure_type.wgsl" +#include "lib/mx_microfacet_diffuse.wgsl" + +// NOTE: The GLSL version computes curvature via fwidth(N)/fwidth(P), but WGSL +// forbids fwidth inside non-uniform control flow (e.g. the light loop that +// calls this function). We use a flat curvature approximation (0.0) for now. +// TODO: wire proper curvature when the renderer can pre-compute it per-vertex. + +fn mx_subsurface_bsdf(closureData: ClosureData, weight: f32, color: vec3f, radius: vec3f, anisotropy: f32, N: vec3f, bsdf: ptr) +{ + (*bsdf).throughput = vec3f(0.0); + + if (weight < M_FLOAT_EPS) + { + return; + } + + var V: vec3f = closureData.V; + var L: vec3f = closureData.L; + var P: vec3f = closureData.P; + var occlusion: f32 = closureData.occlusion; + var N_: vec3f = mx_forward_facing_normal(N, V); + + if (closureData.closureType == CLOSURE_TYPE_REFLECTION) + { + // Flat curvature approximation — see note above. + var curvature: f32 = 0.0; + var sss: vec3f = mx_subsurface_scattering_approx(N_, L, P, color, radius, curvature); + var NdotL: f32 = clamp(dot(N_, L), M_FLOAT_EPS, 1.0); + var visibleOcclusion: f32 = 1.0 - NdotL * (1.0 - occlusion); + (*bsdf).response = sss * visibleOcclusion * weight; + } + else if (closureData.closureType == CLOSURE_TYPE_INDIRECT) + { + // For now, we render indirect subsurface as simple indirect diffuse. + var Li: vec3f = mx_environment_irradiance(N_); + (*bsdf).response = Li * color * weight; + } +} diff --git a/libraries/pbrlib/genwgsl/mx_translucent_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_translucent_bsdf.wgsl new file mode 100644 index 0000000000..c58ca23141 --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_translucent_bsdf.wgsl @@ -0,0 +1,46 @@ +// Generated from libraries/pbrlib/genglsl/mx_translucent_bsdf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_closure_type.wgsl" + +fn mx_translucent_bsdf(closureData_21: ClosureData, weight_7: f32, color_9: vec3f, N_24: vec3f, bsdf_8: ptr) { + var closureData_22: ClosureData; + var weight_8: f32; + var color_10: vec3f; + var N_25: vec3f; + var V_8: vec3f; + var L_4: vec3f; + var NdotL_5: f32; + var Li: vec3f; + + closureData_22 = closureData_21; + weight_8 = weight_7; + color_10 = color_9; + N_25 = N_24; + (*bsdf_8).throughput = vec3(0.0); + if (weight_8 < 0.00000001) { + { + return; + } + } + V_8 = closureData_22.V; + L_4 = closureData_22.L; + N_25 = -(N_25); + if (closureData_22.closureType == 1i) { + { + NdotL_5 = clamp(dot(N_25, L_4), 0.0, 1.0); + (*bsdf_8).response = (((color_10 * weight_8) * NdotL_5) * 0.31830987); + return; + } + } else { + if (closureData_22.closureType == 3i) { + { + Li = mx_environment_irradiance(N_25); + (*bsdf_8).response = ((Li * color_10) * weight_8); + return; + } + } else { + return; + } + } +} diff --git a/libraries/pbrlib/genwgsl/mx_uniform_edf.wgsl b/libraries/pbrlib/genwgsl/mx_uniform_edf.wgsl new file mode 100644 index 0000000000..64965b44b0 --- /dev/null +++ b/libraries/pbrlib/genwgsl/mx_uniform_edf.wgsl @@ -0,0 +1,20 @@ +// Generated from libraries/pbrlib/genglsl/mx_uniform_edf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_closure_type.wgsl" + +fn mx_uniform_edf(closureData_21: ClosureData, color_9: vec3f, result_82: ptr) { + var closureData_22: ClosureData; + var color_10: vec3f; + + closureData_22 = closureData_21; + color_10 = color_9; + if (closureData_22.closureType == 4i) { + { + (*result_82) = color_10; + return; + } + } else { + return; + } +} diff --git a/libraries/pbrlib/genwgsl/pbrlib_genwgsl_impl.mtlx b/libraries/pbrlib/genwgsl/pbrlib_genwgsl_impl.mtlx new file mode 100644 index 0000000000..c6a725f841 --- /dev/null +++ b/libraries/pbrlib/genwgsl/pbrlib_genwgsl_impl.mtlx @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libraries/stdlib/genwgsl/lib/mx_flake.wgsl b/libraries/stdlib/genwgsl/lib/mx_flake.wgsl new file mode 100644 index 0000000000..5d06489f09 --- /dev/null +++ b/libraries/stdlib/genwgsl/lib/mx_flake.wgsl @@ -0,0 +1,116 @@ +#include "mx_noise.wgsl" + +// "Fast Random Rotation Matrices" by James Arvo, Graphics Gems3 P.117 +fn mx_rotate_flake(p: vec3f, i: vec3f) -> vec3f { + let theta = M_PI * 2.0 * i.x; + let phi = M_PI * 2.0 * i.y; + let z = i.z * 2.0; + + let r = sqrt(z); + let vx = sin(phi) * r; + let vy = cos(phi) * r; + let vz = sqrt(2.0 - z); + + let s_theta = sin(theta); + let c_theta = cos(theta); + let sx = vx * c_theta - vy * s_theta; + let sy = vx * s_theta + vy * c_theta; + + let m = mat3x3f( + vx * sx - c_theta, vx * sy - s_theta, vx * vz, + vy * sx + s_theta, vy * sy - c_theta, vy * vz, + vz * sx , vz * sy , 1.0 - z + ); + + return m * p; +} + +fn mx_flake_density_to_probability(x: f32) -> f32 { + let abcd = vec4f(-26.19771808, 26.39663835, 85.53857017, -102.35069432); + let ef = vec2f(-101.42634862, 118.45082288); + let xx = x * x; + + return (abcd.x * xx + abcd.y * x) / (abcd.z * xx * x + abcd.w * xx + ef.x * x + ef.y); +} + +fn mx_flake( + size: f32, + roughness: f32, + coverage: f32, + position: vec3f, + normal: vec3f, + tangent: vec3f, + bitangent: vec3f, + id: ptr, + rand_out: ptr, + presence: ptr, + flakenormal: ptr +) { + let probability = mx_flake_density_to_probability(clamp(coverage, 0.0, 1.0)); + let flake_diameter = 1.5 / sqrt(3.0); + + let P = position / vec3f(size); + let base_P = floor(P); + + var flake_priority: f32 = 0.0; + var flake_cell: vec3f = vec3f(0.0); + + for (var i: i32 = -1; i < 2; i = i + 1) { + for (var j: i32 = -1; j < 2; j = j + 1) { + for (var k: i32 = -1; k < 2; k = k + 1) { + let cell_pos = base_P + vec3f(f32(i), f32(j), f32(k)); + + let PP_pre = P - cell_pos - vec3f(0.5); + if (dot(PP_pre, PP_pre) >= flake_diameter * flake_diameter * 3.0) { + continue; + } + + if (mx_cell_noise_float_vec3(cell_pos) > probability) { + continue; + } + + let priority = mx_cell_noise_float_vec4(vec4f(cell_pos, 3.0)); + if (priority < flake_priority) { + continue; + } + + let rot = mx_cell_noise_vec3_vec3(cell_pos); + let PP = mx_rotate_flake(PP_pre, rot); + + if (abs(PP.x) <= flake_diameter && + abs(PP.y) <= flake_diameter && + abs(PP.z) <= flake_diameter) { + flake_priority = priority; + flake_cell = cell_pos; + } + } + } + } + + if (flake_priority <= 0.0) { + *id = 0; + *rand_out = 0.0; + *presence = 0.0; + *flakenormal = normal; + return; + } + + let flake_noise = mx_cell_noise_vec3_vec4(vec4f(flake_cell, 2.0)); + let xi0 = flake_noise.x; + let xi1 = flake_noise.y; + + *rand_out = flake_noise.z; + *id = i32(*rand_out * 2147483647.0); + *presence = flake_priority; + + let phi = M_PI * 2.0 * xi0; + let tan_theta = roughness * roughness * sqrt(xi1) / sqrt(1.0 - xi1); + let sin_theta = tan_theta / sqrt(1.0 + tan_theta * tan_theta); + let cos_theta = sqrt(1.0 - sin_theta * sin_theta); + + *flakenormal = normalize( + tangent * cos(phi) * sin_theta + + bitangent * sin(phi) * sin_theta + + normal * cos_theta + ); +} diff --git a/libraries/stdlib/genwgsl/lib/mx_geometry.wgsl b/libraries/stdlib/genwgsl/lib/mx_geometry.wgsl new file mode 100644 index 0000000000..be6ed0bfd6 --- /dev/null +++ b/libraries/stdlib/genwgsl/lib/mx_geometry.wgsl @@ -0,0 +1,36 @@ +// Normal blending and axis rotation utilities. +// Port of genglsl/lib/mx_geometry.glsl + +// Blend 3 normals by blending the gradients. +// Morten S. Mikkelsen, Surface Gradient–Based Bump Mapping Framework, JCGT vol. 9, no. 3, 2020 +// http://jcgt.org/published/0009/03/04/ +fn mx_normals_to_gradient(N: vec3f, Np: vec3f) -> vec3f { + let d = dot(N, Np); + return (d * N - Np) / max(M_FLOAT_EPS, abs(d)); +} + +fn mx_gradient_blend_3_normals(N: vec3f, N1: vec3f, N1_weight: f32, N2: vec3f, N2_weight: f32, N3: vec3f, N3_weight: f32) -> vec3f { + let w1 = clamp(N1_weight, 0.0, 1.0); + let w2 = clamp(N2_weight, 0.0, 1.0); + let w3 = clamp(N3_weight, 0.0, 1.0); + + let g1 = mx_normals_to_gradient(N, N1); + let g2 = mx_normals_to_gradient(N, N2); + let g3 = mx_normals_to_gradient(N, N3); + + let gg = w1 * g1 + w2 * g2 + w3 * g3; + return normalize(N - gg); +} + +// Rotation matrix around an arbitrary axis. +// (Placed here rather than mx_math.wgsl to match MaterialX's workaround for MSL builds.) +fn mx_axis_rotation_matrix(a: vec3f, r: f32) -> mat3x3f { + let s = sin(r); + let c = cos(r); + let omc = 1.0 - c; + return mat3x3f( + a.x * a.x * omc + c, a.x * a.y * omc - a.z * s, a.x * a.z * omc + a.y * s, + a.y * a.x * omc + a.z * s, a.y * a.y * omc + c, a.y * a.z * omc - a.x * s, + a.z * a.x * omc - a.y * s, a.z * a.y * omc + a.x * s, a.z * a.z * omc + c + ); +} diff --git a/libraries/stdlib/genwgsl/lib/mx_hextile.wgsl b/libraries/stdlib/genwgsl/lib/mx_hextile.wgsl new file mode 100644 index 0000000000..f6a7d98d2c --- /dev/null +++ b/libraries/stdlib/genwgsl/lib/mx_hextile.wgsl @@ -0,0 +1,145 @@ +// Hex-tiling coordinate generation utilities. +// +// Morten S. Mikkelsen, Practical Real-Time Hex-Tiling, JCGT vol. 11, no. 2, 2022 +// http://jcgt.org/published/0011/03/05/ + +// https://www.shadertoy.com/view/4djSRW +fn mx_hextile_hash(p: vec2f) -> vec2f { + var p3 = fract(vec3f(p.x, p.y, p.x) * vec3f(0.1031, 0.1030, 0.0973)); + p3 += dot(p3, vec3f(p3.y, p3.z, p3.x) + 33.33); + return fract((vec2f(p3.x, p3.x) + vec2f(p3.y, p3.z)) * vec2f(p3.z, p3.y)); +} + +// Christophe Schlick. "Fast Alternatives to Perlin's Bias and Gain Functions". +// In Graphics Gems IV, Morgan Kaufmann, 1994, pages 401–403. +// https://dept-info.labri.fr/~schlick/DOC/gem2.html +fn mx_schlick_gain(x: f32, r: f32) -> f32 { + let rr = clamp(r, 0.001, 0.999); + let a = (1.0 / rr - 2.0) * (1.0 - 2.0 * x); + if (x < 0.5) { + return x / (a + 1.0); + } + return (a - x) / (a - 1.0); +} + +struct HextileData { + coords0: vec2f, + coords1: vec2f, + coords2: vec2f, + weights: vec3f, + rotations: vec3f, + ddx0: vec2f, + ddx1: vec2f, + ddx2: vec2f, + ddy0: vec2f, + ddy1: vec2f, + ddy2: vec2f, +}; + +// Helper to compute blend weights with optional falloff. +fn mx_hextile_compute_blend_weights(luminance_weights: vec3f, tile_weights: vec3f, falloff: f32) -> vec3f { + var w = luminance_weights * pow(tile_weights, vec3f(7.0)); + w /= (w.x + w.y + w.z); + + if (falloff != 0.5) { + w.x = mx_schlick_gain(w.x, falloff); + w.y = mx_schlick_gain(w.y, falloff); + w.z = mx_schlick_gain(w.z, falloff); + w /= (w.x + w.y + w.z); + } + return w; +} + +fn mx_hextile_coord( + coord: vec2f, + rotation: f32, + rotation_range: vec2f, + scale: f32, + scale_range: vec2f, + offset: f32, + offset_range: vec2f +) -> HextileData { + let sqrt3_2 = sqrt(3.0) * 2.0; + + // Scale coord to maintain the original fit. + let st = coord * sqrt3_2; + + // Skew input space into simplex triangle grid. + // (1, 0, -tan(30), 2*tan(30)) + let to_skewed = mat2x2f(1.0, 0.0, -0.57735027, 1.15470054); + let st_skewed = (to_skewed * st); + + // Barycentric weights. + let st_frac = fract(st_skewed); + var temp = vec3f(st_frac.x, st_frac.y, 0.0); + temp.z = 1.0 - temp.x - temp.y; + + let s = step(0.0, -temp.z); + let s2 = 2.0 * s - 1.0; + + let w1 = -temp.z * s2; + let w2 = s - temp.y * s2; + let w3 = s - temp.x * s2; + + // Vertex IDs. + let base_id = vec2(floor(st_skewed)); + let si = i32(s); + let id1 = base_id + vec2(si, si); + let id2 = base_id + vec2(si, 1 - si); + let id3 = base_id + vec2(1 - si, si); + + // Tile centers. + let inv_skewed = mat2x2f(1.0, 0.0, 0.5, 1.0 / 1.15470054); + let ctr1 = (inv_skewed * (vec2f(id1) / vec2f(sqrt3_2))); + let ctr2 = (inv_skewed * (vec2f(id2) / vec2f(sqrt3_2))); + let ctr3 = (inv_skewed * (vec2f(id3) / vec2f(sqrt3_2))); + + // Reuse hash for performance. + let seed_offset = vec2f(0.12345); + let rand1 = mx_hextile_hash(vec2f(id1) + seed_offset); + let rand2 = mx_hextile_hash(vec2f(id2) + seed_offset); + let rand3 = mx_hextile_hash(vec2f(id3) + seed_offset); + + // Randomized rotation. + let rr = radians(rotation_range); + let rand_x = vec3f(rand1.x, rand2.x, rand3.x); + let rotations = mix(vec3f(rr.x), vec3f(rr.y), rand_x * rotation); + let sin_r = sin(rotations); + let cos_r = cos(rotations); + let rm1 = mat2x2f(cos_r.x, -sin_r.x, sin_r.x, cos_r.x); + let rm2 = mat2x2f(cos_r.y, -sin_r.y, sin_r.y, cos_r.y); + let rm3 = mat2x2f(cos_r.z, -sin_r.z, sin_r.z, cos_r.z); + + // Randomized scale. + let rand_y = vec3f(rand1.y, rand2.y, rand3.y); + let scales = mix(vec3f(1.0), mix(vec3f(scale_range.x), vec3f(scale_range.y), rand_y), scale); + let scale1 = vec2f(scales.x); + let scale2 = vec2f(scales.y); + let scale3 = vec2f(scales.z); + + // Randomized offset. + let offset1 = mix(vec2f(offset_range.x), vec2f(offset_range.y), rand1 * offset); + let offset2 = mix(vec2f(offset_range.x), vec2f(offset_range.y), rand2 * offset); + let offset3 = mix(vec2f(offset_range.x), vec2f(offset_range.y), rand3 * offset); + + var tile_data: HextileData; + tile_data.weights = vec3f(w1, w2, w3); + tile_data.rotations = rotations; + + // Get tile-local coords. + tile_data.coords0 = (((coord - ctr1) * rm1) / scale1) + ctr1 + offset1; + tile_data.coords1 = (((coord - ctr2) * rm2) / scale2) + ctr2 + offset2; + tile_data.coords2 = (((coord - ctr3) * rm3) / scale3) + ctr3 + offset3; + + // Derivatives. + let ddx_coord = dpdx(coord); + let ddy_coord = dpdy(coord); + tile_data.ddx0 = (ddx_coord * rm1) / scale1; + tile_data.ddx1 = (ddx_coord * rm2) / scale2; + tile_data.ddx2 = (ddx_coord * rm3) / scale3; + tile_data.ddy0 = (ddy_coord * rm1) / scale1; + tile_data.ddy1 = (ddy_coord * rm2) / scale2; + tile_data.ddy2 = (ddy_coord * rm3) / scale3; + + return tile_data; +} diff --git a/libraries/stdlib/genwgsl/lib/mx_hsv.wgsl b/libraries/stdlib/genwgsl/lib/mx_hsv.wgsl new file mode 100644 index 0000000000..2f32986ab4 --- /dev/null +++ b/libraries/stdlib/genwgsl/lib/mx_hsv.wgsl @@ -0,0 +1,92 @@ +// Color transform functions (WGSL port of genglsl/lib/mx_hsv.glsl) +// +// These functions are modified versions of the color operators found in +// Open Shading Language: +// github.com/imageworks/OpenShadingLanguage/blob/master/src/liboslexec/opcolor.cpp +// +// It contains the subset of color operators needed to implement the MaterialX +// standard library. The modifications are conversions from C++ to GLSL to WGSL. +// +// Original copyright notice: +// ------------------------------------------------------------------------ +// Copyright (c) 2009-2010 Sony Pictures Imageworks Inc., et al. +// All Rights Reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Sony Pictures Imageworks nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// ------------------------------------------------------------------------ + +fn mx_hsvtorgb(hsv: vec3f) -> vec3f { + // Reference for this technique: Foley & van Dam + let h = hsv.x; + let s = hsv.y; + let v = hsv.z; + + if (s < 0.0001) { + return vec3f(v, v, v); + } + + let hue = 6.0 * (h - floor(h)); // expand to [0..6) + let hi = i32(trunc(hue)); + let f = hue - f32(hi); + let p = v * (1.0 - s); + let q = v * (1.0 - s * f); + let t = v * (1.0 - s * (1.0 - f)); + + if (hi == 0) { return vec3f(v, t, p); } + if (hi == 1) { return vec3f(q, v, p); } + if (hi == 2) { return vec3f(p, v, t); } + if (hi == 3) { return vec3f(p, q, v); } + if (hi == 4) { return vec3f(t, p, v); } + return vec3f(v, p, q); +} + +fn mx_rgbtohsv(c: vec3f) -> vec3f { + // See Foley & van Dam + let r = c.x; + let g = c.y; + let b = c.z; + let mincomp = min(r, min(g, b)); + let maxcomp = max(r, max(g, b)); + let delta = maxcomp - mincomp; // chroma + + let v = maxcomp; + var s: f32; + if (maxcomp > 0.0) { + s = delta / maxcomp; + } else { + s = 0.0; + } + + var h: f32; + if (s <= 0.0) { + h = 0.0; + } else { + if (r >= maxcomp) { h = (g - b) / delta; } + else if (g >= maxcomp) { h = 2.0 + (b - r) / delta; } + else { h = 4.0 + (r - g) / delta; } + h *= (1.0 / 6.0); + if (h < 0.0) { h += 1.0; } + } + return vec3f(h, s, v); +} diff --git a/libraries/stdlib/genwgsl/lib/mx_math.wgsl b/libraries/stdlib/genwgsl/lib/mx_math.wgsl new file mode 100644 index 0000000000..689081c8ed --- /dev/null +++ b/libraries/stdlib/genwgsl/lib/mx_math.wgsl @@ -0,0 +1,70 @@ +// Matrix-by-vector multiply used by the hardware geometric nodes (normal/tangent/ +// bitangent/transform/view-direction). WGSL has no function overloading, and every +// emitter uses the mat4x4 * vec4 form, so a single definition suffices. +fn mx_matrix_mul(m: mat4x4f, v: vec4f) -> vec4f { + return m * v; +} + +// Type-suffixed forms of GLSL's overloaded mx_matrix_mul, used by the transpiled library nodes +// (transformmatrix, blackbody, ...). vector*matrix and matrix*vector follow GLSL's argument order. +fn mx_matrix_mul_vec2_mat2(v: vec2f, m: mat2x2f) -> vec2f { return v * m; } +fn mx_matrix_mul_vec3_mat3(v: vec3f, m: mat3x3f) -> vec3f { return v * m; } +fn mx_matrix_mul_vec4_mat4(v: vec4f, m: mat4x4f) -> vec4f { return v * m; } +fn mx_matrix_mul_mat2_vec2(m: mat2x2f, v: vec2f) -> vec2f { return m * v; } +fn mx_matrix_mul_mat3_vec3(m: mat3x3f, v: vec3f) -> vec3f { return m * v; } +fn mx_matrix_mul_mat4_vec4(m: mat4x4f, v: vec4f) -> vec4f { return m * v; } +fn mx_matrix_mul_mat2_mat2(a: mat2x2f, b: mat2x2f) -> mat2x2f { return a * b; } +fn mx_matrix_mul_mat3_mat3(a: mat3x3f, b: mat3x3f) -> mat3x3f { return a * b; } +fn mx_matrix_mul_mat4_mat4(a: mat4x4f, b: mat4x4f) -> mat4x4f { return a * b; } + +// Square functions + +fn mx_square_f32(x: f32) -> f32 { + return (x * x); +} + +fn mx_square_vec2(x: vec2f) -> vec2f { + return (x * x); +} + +fn mx_square_vec3(x: vec3f) -> vec3f { + return (x * x); +} + +// Modulo with GLSL mod() semantics: x - y * floor(x / y) +// WGSL '%' operator is remainder (fmod), not modulo, so we need explicit functions. + +fn mx_mod_f32(x: f32, y: f32) -> f32 { + return x - y * floor(x / y); +} + +fn mx_mod_vec2(x: vec2f, y: vec2f) -> vec2f { + return x - y * floor(x / y); +} + +fn mx_mod_vec2_f32(x: vec2f, y: f32) -> vec2f { + return x - vec2f(y) * floor(x / vec2f(y)); +} + +fn mx_mod_vec3(x: vec3f, y: vec3f) -> vec3f { + return x - y * floor(x / y); +} + +fn mx_mod_vec3_f32(x: vec3f, y: f32) -> vec3f { + return x - vec3f(y) * floor(x / vec3f(y)); +} + +fn mx_mod_vec4(x: vec4f, y: vec4f) -> vec4f { + return x - y * floor(x / y); +} + +fn mx_mod_vec4_f32(x: vec4f, y: f32) -> vec4f { + return x - vec4f(y) * floor(x / vec4f(y)); +} + +fn mx_srgb_encode(color: vec3f) -> vec3f { + let isAbove = (color > vec3(0.0031308f)); + let linSeg = (color * 12.92f); + let powSeg = ((1.055f * pow(max(color, vec3(0f)), vec3(0.41666666f))) - vec3(0.055f)); + return select(linSeg, powSeg, isAbove); +} diff --git a/libraries/stdlib/genwgsl/lib/mx_noise.wgsl b/libraries/stdlib/genwgsl/lib/mx_noise.wgsl new file mode 100644 index 0000000000..38a9b4c3c6 --- /dev/null +++ b/libraries/stdlib/genwgsl/lib/mx_noise.wgsl @@ -0,0 +1,716 @@ +// Noise Library (WGSL port of genglsl/lib/mx_noise.glsl) +// +// This library is a modified version of the noise library found in +// Open Shading Language: +// github.com/imageworks/OpenShadingLanguage/blob/master/src/include/OSL/oslnoise.h +// +// It contains the subset of noise types needed to implement the MaterialX +// standard library. The modifications are conversions from C++ to GLSL to WGSL. +// Produced results should be identical to the OSL noise functions. +// +// Original copyright notice: +// ------------------------------------------------------------------------ +// Copyright (c) 2009-2010 Sony Pictures Imageworks Inc., et al. +// All Rights Reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Sony Pictures Imageworks nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// ------------------------------------------------------------------------ + +fn mx_select(b: bool, t: f32, f: f32) -> f32 { + return select(f, t, b); +} + +fn mx_negate_if(val: f32, b: bool) -> f32 { + return select(val, -val, b); +} + +fn mx_floor(x: f32) -> i32 { + return i32(floor(x)); +} + +// Return mx_floor as well as the fractional remainder. +fn mx_floorfrac(x: f32, i: ptr) -> f32 { + *i = mx_floor(x); + return x - f32(*i); +} + +// --------------------------------------------------------------------------- +// Bilinear / trilinear interpolation +// --------------------------------------------------------------------------- + +fn mx_bilerp_f32(v0: f32, v1: f32, v2: f32, v3: f32, s: f32, t: f32) -> f32 { + let s1 = 1.0 - s; + return (1.0 - t) * (v0 * s1 + v1 * s) + t * (v2 * s1 + v3 * s); +} + +fn mx_bilerp_vec3(v0: vec3f, v1: vec3f, v2: vec3f, v3: vec3f, s: f32, t: f32) -> vec3f { + let s1 = 1.0 - s; + return (1.0 - t) * (v0 * s1 + v1 * s) + t * (v2 * s1 + v3 * s); +} + +fn mx_trilerp_f32(v0: f32, v1: f32, v2: f32, v3: f32, + v4: f32, v5: f32, v6: f32, v7: f32, + s: f32, t: f32, r: f32) -> f32 { + let s1 = 1.0 - s; + let t1 = 1.0 - t; + let r1 = 1.0 - r; + return r1 * (t1 * (v0 * s1 + v1 * s) + t * (v2 * s1 + v3 * s)) + + r * (t1 * (v4 * s1 + v5 * s) + t * (v6 * s1 + v7 * s)); +} + +fn mx_trilerp_vec3(v0: vec3f, v1: vec3f, v2: vec3f, v3: vec3f, + v4: vec3f, v5: vec3f, v6: vec3f, v7: vec3f, + s: f32, t: f32, r: f32) -> vec3f { + let s1 = 1.0 - s; + let t1 = 1.0 - t; + let r1 = 1.0 - r; + return r1 * (t1 * (v0 * s1 + v1 * s) + t * (v2 * s1 + v3 * s)) + + r * (t1 * (v4 * s1 + v5 * s) + t * (v6 * s1 + v7 * s)); +} + +// --------------------------------------------------------------------------- +// Gradient functions +// --------------------------------------------------------------------------- +// 2D and 3D gradient functions — perform a dot product against a randomly +// chosen vector. The gradient vector is not normalized, but this only affects +// the overall "scale" of the result, so we account for it by multiplying in +// the corresponding "perlin" function. + +// 2D gradient: 8 possible directions (+-1,+-2) and (+-2,+-1) +fn mx_gradient_float_2d(hash: u32, x: f32, y: f32) -> f32 { + let h = hash & 7u; + let u = select(y, x, h < 4u); + let v = 2.0 * select(x, y, h < 4u); + return mx_negate_if(u, bool(h & 1u)) + mx_negate_if(v, bool(h & 2u)); +} + +// 3D gradient: vectors pointing to the edges of the cube +fn mx_gradient_float_3d(hash: u32, x: f32, y: f32, z: f32) -> f32 { + let h = hash & 15u; + let u = select(y, x, h < 8u); + let v = select(select(z, x, (h == 12u) || (h == 14u)), y, h < 4u); + return mx_negate_if(u, bool(h & 1u)) + mx_negate_if(v, bool(h & 2u)); +} + +// 2D vec3 gradient +fn mx_gradient_vec3_2d(hash: vec3, x: f32, y: f32) -> vec3f { + return vec3f( + mx_gradient_float_2d(hash.x, x, y), + mx_gradient_float_2d(hash.y, x, y), + mx_gradient_float_2d(hash.z, x, y) + ); +} + +// 3D vec3 gradient +fn mx_gradient_vec3_3d(hash: vec3, x: f32, y: f32, z: f32) -> vec3f { + return vec3f( + mx_gradient_float_3d(hash.x, x, y, z), + mx_gradient_float_3d(hash.y, x, y, z), + mx_gradient_float_3d(hash.z, x, y, z) + ); +} + +// Scaling factors to normalize the result of gradients above. +// Experimentally calculated: 2D = 0.6616, 3D = 0.9820. +fn mx_gradient_scale2d_f32(v: f32) -> f32 { return 0.6616 * v; } +fn mx_gradient_scale3d_f32(v: f32) -> f32 { return 0.9820 * v; } +fn mx_gradient_scale2d_vec3(v: vec3f) -> vec3f { return 0.6616 * v; } +fn mx_gradient_scale3d_vec3(v: vec3f) -> vec3f { return 0.9820 * v; } + +// --------------------------------------------------------------------------- +// Bob Jenkins hash (bjmix / bjfinal) +// --------------------------------------------------------------------------- + +// Bitwise circular rotation left by k bits (32-bit unsigned). +fn mx_rotl32(x: u32, k: i32) -> u32 { + return (x << u32(k)) | (x >> u32(32 - k)); +} + +fn mx_bjmix(a: ptr, b: ptr, c: ptr) { + *a -= *c; *a ^= mx_rotl32(*c, 4); *c += *b; + *b -= *a; *b ^= mx_rotl32(*a, 6); *a += *c; + *c -= *b; *c ^= mx_rotl32(*b, 8); *b += *a; + *a -= *c; *a ^= mx_rotl32(*c, 16); *c += *b; + *b -= *a; *b ^= mx_rotl32(*a, 19); *a += *c; + *c -= *b; *c ^= mx_rotl32(*b, 4); *b += *a; +} + +// Mix up and combine the bits of a, b, and c (returns a hash of those three +// original values without changing them). +fn mx_bjfinal(a: u32, b: u32, c: u32) -> u32 { + var a_ = a; var b_ = b; var c_ = c; + c_ ^= b_; c_ -= mx_rotl32(b_, 14); + a_ ^= c_; a_ -= mx_rotl32(c_, 11); + b_ ^= a_; b_ -= mx_rotl32(a_, 25); + c_ ^= b_; c_ -= mx_rotl32(b_, 16); + a_ ^= c_; a_ -= mx_rotl32(c_, 4); + b_ ^= a_; b_ -= mx_rotl32(a_, 14); + c_ ^= b_; c_ -= mx_rotl32(b_, 24); + return c_; +} + +// Convert a 32-bit integer into a floating point number in [0,1]. +fn mx_bits_to_01(bits: u32) -> f32 { + return f32(bits) / f32(0xffffffffu); +} + +// Quintic fade curve: 6t^5 - 15t^4 + 10t^3 +fn mx_fade(t: f32) -> f32 { + return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); +} + +// --------------------------------------------------------------------------- +// Hash functions +// --------------------------------------------------------------------------- +// WGSL has no function overloading, so we suffix by argument count: +// mx_hash_int_i1(x), mx_hash_int_i2(x,y), mx_hash_int_i3(x,y,z), etc. + +fn mx_hash_int_i1(x: i32) -> u32 { + let len = 1u; + let seed = 0xdeadbeefu + (len << 2u) + 13u; + return mx_bjfinal(seed + u32(x), seed, seed); +} + +fn mx_hash_int_i2(x: i32, y: i32) -> u32 { + let len = 2u; + var a = 0xdeadbeefu + (len << 2u) + 13u; + var b = a; var c = a; + a += u32(x); + b += u32(y); + return mx_bjfinal(a, b, c); +} + +fn mx_hash_int_i3(x: i32, y: i32, z: i32) -> u32 { + let len = 3u; + var a = 0xdeadbeefu + (len << 2u) + 13u; + var b = a; var c = a; + a += u32(x); + b += u32(y); + c += u32(z); + return mx_bjfinal(a, b, c); +} + +fn mx_hash_int_i4(x: i32, y: i32, z: i32, xx: i32) -> u32 { + let len = 4u; + var a = 0xdeadbeefu + (len << 2u) + 13u; + var b = a; var c = a; + a += u32(x); + b += u32(y); + c += u32(z); + mx_bjmix(&a, &b, &c); + a += u32(xx); + return mx_bjfinal(a, b, c); +} + +fn mx_hash_int_i5(x: i32, y: i32, z: i32, xx: i32, yy: i32) -> u32 { + let len = 5u; + var a = 0xdeadbeefu + (len << 2u) + 13u; + var b = a; var c = a; + a += u32(x); + b += u32(y); + c += u32(z); + mx_bjmix(&a, &b, &c); + a += u32(xx); + b += u32(yy); + return mx_bjfinal(a, b, c); +} + +// vec3 hash from 2 int args +fn mx_hash_vec3_i2(x: i32, y: i32) -> vec3 { + let h = mx_hash_int_i2(x, y); + return vec3( + (h ) & 0xFFu, + (h >> 8 ) & 0xFFu, + (h >> 16) & 0xFFu + ); +} + +// vec3 hash from 3 int args +fn mx_hash_vec3_i3(x: i32, y: i32, z: i32) -> vec3 { + let h = mx_hash_int_i3(x, y, z); + return vec3( + (h ) & 0xFFu, + (h >> 8 ) & 0xFFu, + (h >> 16) & 0xFFu + ); +} + +// --------------------------------------------------------------------------- +// Perlin noise +// --------------------------------------------------------------------------- + +// 2D Perlin noise (float output) +fn mx_perlin_noise_float_2d(p: vec2f) -> f32 { + var X: i32; var Y: i32; + let fx = mx_floorfrac(p.x, &X); + let fy = mx_floorfrac(p.y, &Y); + let u = mx_fade(fx); + let v = mx_fade(fy); + let result = mx_bilerp_f32( + mx_gradient_float_2d(mx_hash_int_i2(X, Y ), fx, fy ), + mx_gradient_float_2d(mx_hash_int_i2(X+1, Y ), fx - 1.0, fy ), + mx_gradient_float_2d(mx_hash_int_i2(X, Y+1), fx, fy - 1.0), + mx_gradient_float_2d(mx_hash_int_i2(X+1, Y+1), fx - 1.0, fy - 1.0), + u, v); + return mx_gradient_scale2d_f32(result); +} + +// 3D Perlin noise (float output) +fn mx_perlin_noise_float_3d(p: vec3f) -> f32 { + var X: i32; var Y: i32; var Z: i32; + let fx = mx_floorfrac(p.x, &X); + let fy = mx_floorfrac(p.y, &Y); + let fz = mx_floorfrac(p.z, &Z); + let u = mx_fade(fx); + let v = mx_fade(fy); + let w = mx_fade(fz); + let result = mx_trilerp_f32( + mx_gradient_float_3d(mx_hash_int_i3(X, Y, Z ), fx, fy, fz ), + mx_gradient_float_3d(mx_hash_int_i3(X+1, Y, Z ), fx - 1.0, fy, fz ), + mx_gradient_float_3d(mx_hash_int_i3(X, Y+1, Z ), fx, fy - 1.0, fz ), + mx_gradient_float_3d(mx_hash_int_i3(X+1, Y+1, Z ), fx - 1.0, fy - 1.0, fz ), + mx_gradient_float_3d(mx_hash_int_i3(X, Y, Z+1), fx, fy, fz - 1.0), + mx_gradient_float_3d(mx_hash_int_i3(X+1, Y, Z+1), fx - 1.0, fy, fz - 1.0), + mx_gradient_float_3d(mx_hash_int_i3(X, Y+1, Z+1), fx, fy - 1.0, fz - 1.0), + mx_gradient_float_3d(mx_hash_int_i3(X+1, Y+1, Z+1), fx - 1.0, fy - 1.0, fz - 1.0), + u, v, w); + return mx_gradient_scale3d_f32(result); +} + +// 2D Perlin noise (vec3 output) +fn mx_perlin_noise_vec3_2d(p: vec2f) -> vec3f { + var X: i32; var Y: i32; + let fx = mx_floorfrac(p.x, &X); + let fy = mx_floorfrac(p.y, &Y); + let u = mx_fade(fx); + let v = mx_fade(fy); + let result = mx_bilerp_vec3( + mx_gradient_vec3_2d(mx_hash_vec3_i2(X, Y ), fx, fy ), + mx_gradient_vec3_2d(mx_hash_vec3_i2(X+1, Y ), fx - 1.0, fy ), + mx_gradient_vec3_2d(mx_hash_vec3_i2(X, Y+1), fx, fy - 1.0), + mx_gradient_vec3_2d(mx_hash_vec3_i2(X+1, Y+1), fx - 1.0, fy - 1.0), + u, v); + return mx_gradient_scale2d_vec3(result); +} + +// 3D Perlin noise (vec3 output) +fn mx_perlin_noise_vec3_3d(p: vec3f) -> vec3f { + var X: i32; var Y: i32; var Z: i32; + let fx = mx_floorfrac(p.x, &X); + let fy = mx_floorfrac(p.y, &Y); + let fz = mx_floorfrac(p.z, &Z); + let u = mx_fade(fx); + let v = mx_fade(fy); + let w = mx_fade(fz); + let result = mx_trilerp_vec3( + mx_gradient_vec3_3d(mx_hash_vec3_i3(X, Y, Z ), fx, fy, fz ), + mx_gradient_vec3_3d(mx_hash_vec3_i3(X+1, Y, Z ), fx - 1.0, fy, fz ), + mx_gradient_vec3_3d(mx_hash_vec3_i3(X, Y+1, Z ), fx, fy - 1.0, fz ), + mx_gradient_vec3_3d(mx_hash_vec3_i3(X+1, Y+1, Z ), fx - 1.0, fy - 1.0, fz ), + mx_gradient_vec3_3d(mx_hash_vec3_i3(X, Y, Z+1), fx, fy, fz - 1.0), + mx_gradient_vec3_3d(mx_hash_vec3_i3(X+1, Y, Z+1), fx - 1.0, fy, fz - 1.0), + mx_gradient_vec3_3d(mx_hash_vec3_i3(X, Y+1, Z+1), fx, fy - 1.0, fz - 1.0), + mx_gradient_vec3_3d(mx_hash_vec3_i3(X+1, Y+1, Z+1), fx - 1.0, fy - 1.0, fz - 1.0), + u, v, w); + return mx_gradient_scale3d_vec3(result); +} + +// --------------------------------------------------------------------------- +// Cell noise +// --------------------------------------------------------------------------- + +// f32 input → f32 output +fn mx_cell_noise_float_f32(p: f32) -> f32 { + let ix = mx_floor(p); + return mx_bits_to_01(mx_hash_int_i1(ix)); +} + +// vec2 input → f32 output +fn mx_cell_noise_float_vec2(p: vec2f) -> f32 { + let ix = mx_floor(p.x); + let iy = mx_floor(p.y); + return mx_bits_to_01(mx_hash_int_i2(ix, iy)); +} + +// vec3 input → f32 output +fn mx_cell_noise_float_vec3(p: vec3f) -> f32 { + let ix = mx_floor(p.x); + let iy = mx_floor(p.y); + let iz = mx_floor(p.z); + return mx_bits_to_01(mx_hash_int_i3(ix, iy, iz)); +} + +// vec4 input → f32 output +fn mx_cell_noise_float_vec4(p: vec4f) -> f32 { + let ix = mx_floor(p.x); + let iy = mx_floor(p.y); + let iz = mx_floor(p.z); + let iw = mx_floor(p.w); + return mx_bits_to_01(mx_hash_int_i4(ix, iy, iz, iw)); +} + +// f32 input → vec3 output +fn mx_cell_noise_vec3_f32(p: f32) -> vec3f { + let ix = mx_floor(p); + return vec3f( + mx_bits_to_01(mx_hash_int_i2(ix, 0)), + mx_bits_to_01(mx_hash_int_i2(ix, 1)), + mx_bits_to_01(mx_hash_int_i2(ix, 2)) + ); +} + +// vec2 input → vec3 output +fn mx_cell_noise_vec3_vec2(p: vec2f) -> vec3f { + let ix = mx_floor(p.x); + let iy = mx_floor(p.y); + return vec3f( + mx_bits_to_01(mx_hash_int_i3(ix, iy, 0)), + mx_bits_to_01(mx_hash_int_i3(ix, iy, 1)), + mx_bits_to_01(mx_hash_int_i3(ix, iy, 2)) + ); +} + +// vec3 input → vec3 output +fn mx_cell_noise_vec3_vec3(p: vec3f) -> vec3f { + let ix = mx_floor(p.x); + let iy = mx_floor(p.y); + let iz = mx_floor(p.z); + return vec3f( + mx_bits_to_01(mx_hash_int_i4(ix, iy, iz, 0)), + mx_bits_to_01(mx_hash_int_i4(ix, iy, iz, 1)), + mx_bits_to_01(mx_hash_int_i4(ix, iy, iz, 2)) + ); +} + +// vec4 input → vec3 output +fn mx_cell_noise_vec3_vec4(p: vec4f) -> vec3f { + let ix = mx_floor(p.x); + let iy = mx_floor(p.y); + let iz = mx_floor(p.z); + let iw = mx_floor(p.w); + return vec3f( + mx_bits_to_01(mx_hash_int_i5(ix, iy, iz, iw, 0)), + mx_bits_to_01(mx_hash_int_i5(ix, iy, iz, iw, 1)), + mx_bits_to_01(mx_hash_int_i5(ix, iy, iz, iw, 2)) + ); +} + +// --------------------------------------------------------------------------- +// Fractal noise (fBm) +// --------------------------------------------------------------------------- + +fn mx_fractal2d_noise_float(p: vec2f, octaves: i32, lacunarity: f32, diminish: f32) -> f32 { + var result = 0.0; + var amplitude = 1.0; + var pp = p; + for (var i = 0; i < octaves; i++) { + result += amplitude * mx_perlin_noise_float_2d(pp); + amplitude *= diminish; + pp *= lacunarity; + } + return result; +} + +fn mx_fractal2d_noise_vec3(p: vec2f, octaves: i32, lacunarity: f32, diminish: f32) -> vec3f { + var result = vec3f(0.0); + var amplitude = 1.0; + var pp = p; + for (var i = 0; i < octaves; i++) { + result += amplitude * mx_perlin_noise_vec3_2d(pp); + amplitude *= diminish; + pp *= lacunarity; + } + return result; +} + +fn mx_fractal2d_noise_vec2(p: vec2f, octaves: i32, lacunarity: f32, diminish: f32) -> vec2f { + return vec2f( + mx_fractal2d_noise_float(p, octaves, lacunarity, diminish), + mx_fractal2d_noise_float(p + vec2f(19.0, 193.0), octaves, lacunarity, diminish) + ); +} + +fn mx_fractal2d_noise_vec4(p: vec2f, octaves: i32, lacunarity: f32, diminish: f32) -> vec4f { + let c = mx_fractal2d_noise_vec3(p, octaves, lacunarity, diminish); + let f = mx_fractal2d_noise_float(p + vec2f(19.0, 193.0), octaves, lacunarity, diminish); + return vec4f(c, f); +} + +fn mx_fractal3d_noise_float(p: vec3f, octaves: i32, lacunarity: f32, diminish: f32) -> f32 { + var result = 0.0; + var amplitude = 1.0; + var pp = p; + for (var i = 0; i < octaves; i++) { + result += amplitude * mx_perlin_noise_float_3d(pp); + amplitude *= diminish; + pp *= lacunarity; + } + return result; +} + +fn mx_fractal3d_noise_vec3(p: vec3f, octaves: i32, lacunarity: f32, diminish: f32) -> vec3f { + var result = vec3f(0.0); + var amplitude = 1.0; + var pp = p; + for (var i = 0; i < octaves; i++) { + result += amplitude * mx_perlin_noise_vec3_3d(pp); + amplitude *= diminish; + pp *= lacunarity; + } + return result; +} + +fn mx_fractal3d_noise_vec2(p: vec3f, octaves: i32, lacunarity: f32, diminish: f32) -> vec2f { + return vec2f( + mx_fractal3d_noise_float(p, octaves, lacunarity, diminish), + mx_fractal3d_noise_float(p + vec3f(19.0, 193.0, 17.0), octaves, lacunarity, diminish) + ); +} + +fn mx_fractal3d_noise_vec4(p: vec3f, octaves: i32, lacunarity: f32, diminish: f32) -> vec4f { + let c = mx_fractal3d_noise_vec3(p, octaves, lacunarity, diminish); + let f = mx_fractal3d_noise_float(p + vec3f(19.0, 193.0, 17.0), octaves, lacunarity, diminish); + return vec4f(c, f); +} + +// --------------------------------------------------------------------------- +// Worley noise +// --------------------------------------------------------------------------- + +// 2D cell position for Worley noise +fn mx_worley_cell_position_2d(x: i32, y: i32, xoff: i32, yoff: i32, jitter: f32) -> vec2f { + let tmp = mx_cell_noise_vec3_vec2(vec2f(f32(x + xoff), f32(y + yoff))); + var off = vec2f(tmp.x, tmp.y); + off -= 0.5; + off *= jitter; + off += 0.5; + return vec2f(f32(x), f32(y)) + off; +} + +// 3D cell position for Worley noise +fn mx_worley_cell_position_3d(x: i32, y: i32, z: i32, xoff: i32, yoff: i32, zoff: i32, jitter: f32) -> vec3f { + var off = mx_cell_noise_vec3_vec3(vec3f(f32(x + xoff), f32(y + yoff), f32(z + zoff))); + off -= 0.5; + off *= jitter; + off += 0.5; + return vec3f(f32(x), f32(y), f32(z)) + off; +} + +// 2D Worley distance +fn mx_worley_distance_2d(p: vec2f, x: i32, y: i32, xoff: i32, yoff: i32, jitter: f32, metric: i32) -> f32 { + let cellpos = mx_worley_cell_position_2d(x, y, xoff, yoff, jitter); + let diff = cellpos - p; + if (metric == 2) { + return abs(diff.x) + abs(diff.y); // Manhattan + } + if (metric == 3) { + return max(abs(diff.x), abs(diff.y)); // Chebyshev + } + return dot(diff, diff); // Euclidean^2 +} + +// 3D Worley distance +fn mx_worley_distance_3d(p: vec3f, x: i32, y: i32, z: i32, xoff: i32, yoff: i32, zoff: i32, jitter: f32, metric: i32) -> f32 { + let cellpos = mx_worley_cell_position_3d(x, y, z, xoff, yoff, zoff, jitter); + let diff = cellpos - p; + if (metric == 2) { + return abs(diff.x) + abs(diff.y) + abs(diff.z); // Manhattan + } + if (metric == 3) { + return max(max(abs(diff.x), abs(diff.y)), abs(diff.z)); // Chebyshev + } + return dot(diff, diff); // Euclidean^2 +} + +// 2D Worley noise (float output) +fn mx_worley_noise_float_2d(p: vec2f, jitter: f32, style: i32, metric: i32) -> f32 { + var X: i32; var Y: i32; + let localpos = vec2f(mx_floorfrac(p.x, &X), mx_floorfrac(p.y, &Y)); + var sqdist = 1e6; + var minpos = vec2f(0.0, 0.0); + for (var x = -1; x <= 1; x++) { + for (var y = -1; y <= 1; y++) { + let dist = mx_worley_distance_2d(localpos, x, y, X, Y, jitter, metric); + let cellpos = mx_worley_cell_position_2d(x, y, X, Y, jitter) - localpos; + if (dist < sqdist) { + sqdist = dist; + minpos = cellpos; + } + } + } + if (style == 1) { + return mx_cell_noise_float_vec2(minpos + p); + } else { + if (metric == 0) { sqdist = sqrt(sqdist); } + return sqdist; + } +} + +// 2D Worley noise (vec2 output) +fn mx_worley_noise_vec2_2d(p: vec2f, jitter: f32, style: i32, metric: i32) -> vec2f { + var X: i32; var Y: i32; + let localpos = vec2f(mx_floorfrac(p.x, &X), mx_floorfrac(p.y, &Y)); + var sqdist = vec2f(1e6, 1e6); + var minpos = vec2f(0.0, 0.0); + for (var x = -1; x <= 1; x++) { + for (var y = -1; y <= 1; y++) { + let dist = mx_worley_distance_2d(localpos, x, y, X, Y, jitter, metric); + let cellpos = mx_worley_cell_position_2d(x, y, X, Y, jitter) - localpos; + if (dist < sqdist.x) { + sqdist.y = sqdist.x; + sqdist.x = dist; + minpos = cellpos; + } else if (dist < sqdist.y) { + sqdist.y = dist; + } + } + } + if (style == 1) { + let tmp = mx_cell_noise_vec3_vec2(minpos + p); + return vec2f(tmp.x, tmp.y); + } else { + if (metric == 0) { sqdist = sqrt(sqdist); } + return sqdist; + } +} + +// 2D Worley noise (vec3 output) +fn mx_worley_noise_vec3_2d(p: vec2f, jitter: f32, style: i32, metric: i32) -> vec3f { + var X: i32; var Y: i32; + let localpos = vec2f(mx_floorfrac(p.x, &X), mx_floorfrac(p.y, &Y)); + var sqdist = vec3f(1e6, 1e6, 1e6); + var minpos = vec2f(0.0, 0.0); + for (var x = -1; x <= 1; x++) { + for (var y = -1; y <= 1; y++) { + let dist = mx_worley_distance_2d(localpos, x, y, X, Y, jitter, metric); + let cellpos = mx_worley_cell_position_2d(x, y, X, Y, jitter) - localpos; + if (dist < sqdist.x) { + sqdist.z = sqdist.y; + sqdist.y = sqdist.x; + sqdist.x = dist; + minpos = cellpos; + } else if (dist < sqdist.y) { + sqdist.z = sqdist.y; + sqdist.y = dist; + } else if (dist < sqdist.z) { + sqdist.z = dist; + } + } + } + if (style == 1) { + return mx_cell_noise_vec3_vec2(minpos + p); + } else { + if (metric == 0) { sqdist = sqrt(sqdist); } + return sqdist; + } +} + +// 3D Worley noise (float output) +fn mx_worley_noise_float_3d(p: vec3f, jitter: f32, style: i32, metric: i32) -> f32 { + var X: i32; var Y: i32; var Z: i32; + let localpos = vec3f(mx_floorfrac(p.x, &X), mx_floorfrac(p.y, &Y), mx_floorfrac(p.z, &Z)); + var sqdist = 1e6; + var minpos = vec3f(0.0, 0.0, 0.0); + for (var x = -1; x <= 1; x++) { + for (var y = -1; y <= 1; y++) { + for (var z = -1; z <= 1; z++) { + let dist = mx_worley_distance_3d(localpos, x, y, z, X, Y, Z, jitter, metric); + let cellpos = mx_worley_cell_position_3d(x, y, z, X, Y, Z, jitter) - localpos; + if (dist < sqdist) { + sqdist = dist; + minpos = cellpos; + } + } + } + } + if (style == 1) { + return mx_cell_noise_float_vec3(minpos + p); + } else { + if (metric == 0) { sqdist = sqrt(sqdist); } + return sqdist; + } +} + +// 3D Worley noise (vec2 output) +fn mx_worley_noise_vec2_3d(p: vec3f, jitter: f32, style: i32, metric: i32) -> vec2f { + var X: i32; var Y: i32; var Z: i32; + let localpos = vec3f(mx_floorfrac(p.x, &X), mx_floorfrac(p.y, &Y), mx_floorfrac(p.z, &Z)); + var sqdist = vec2f(1e6, 1e6); + var minpos = vec3f(0.0, 0.0, 0.0); + for (var x = -1; x <= 1; x++) { + for (var y = -1; y <= 1; y++) { + for (var z = -1; z <= 1; z++) { + let dist = mx_worley_distance_3d(localpos, x, y, z, X, Y, Z, jitter, metric); + let cellpos = mx_worley_cell_position_3d(x, y, z, X, Y, Z, jitter) - localpos; + if (dist < sqdist.x) { + sqdist.y = sqdist.x; + sqdist.x = dist; + minpos = cellpos; + } else if (dist < sqdist.y) { + sqdist.y = dist; + } + } + } + } + if (style == 1) { + let tmp = mx_cell_noise_vec3_vec3(minpos + p); + return vec2f(tmp.x, tmp.y); + } else { + if (metric == 0) { sqdist = sqrt(sqdist); } + return sqdist; + } +} + +// 3D Worley noise (vec3 output) +fn mx_worley_noise_vec3_3d(p: vec3f, jitter: f32, style: i32, metric: i32) -> vec3f { + var X: i32; var Y: i32; var Z: i32; + let localpos = vec3f(mx_floorfrac(p.x, &X), mx_floorfrac(p.y, &Y), mx_floorfrac(p.z, &Z)); + var sqdist = vec3f(1e6, 1e6, 1e6); + var minpos = vec3f(0.0, 0.0, 0.0); + for (var x = -1; x <= 1; x++) { + for (var y = -1; y <= 1; y++) { + for (var z = -1; z <= 1; z++) { + let dist = mx_worley_distance_3d(localpos, x, y, z, X, Y, Z, jitter, metric); + let cellpos = mx_worley_cell_position_3d(x, y, z, X, Y, Z, jitter) - localpos; + if (dist < sqdist.x) { + sqdist.z = sqdist.y; + sqdist.y = sqdist.x; + sqdist.x = dist; + minpos = cellpos; + } else if (dist < sqdist.y) { + sqdist.z = sqdist.y; + sqdist.y = dist; + } else if (dist < sqdist.z) { + sqdist.z = dist; + } + } + } + } + if (style == 1) { + return mx_cell_noise_vec3_vec3(minpos + p); + } else { + if (metric == 0) { sqdist = sqrt(sqdist); } + return sqdist; + } +} diff --git a/libraries/stdlib/genwgsl/lib/mx_transform_uv.wgsl b/libraries/stdlib/genwgsl/lib/mx_transform_uv.wgsl new file mode 100644 index 0000000000..5d6d0725bb --- /dev/null +++ b/libraries/stdlib/genwgsl/lib/mx_transform_uv.wgsl @@ -0,0 +1,4 @@ +// UV transform: scale and offset. +fn mx_transform_uv(uv: vec2f, uv_scale: vec2f, uv_offset: vec2f) -> vec2f { + return uv * uv_scale + uv_offset; +} diff --git a/libraries/stdlib/genwgsl/lib/mx_transform_uv_vflip.wgsl b/libraries/stdlib/genwgsl/lib/mx_transform_uv_vflip.wgsl new file mode 100644 index 0000000000..76459ca49e --- /dev/null +++ b/libraries/stdlib/genwgsl/lib/mx_transform_uv_vflip.wgsl @@ -0,0 +1,5 @@ +// UV transform with vertical flip: scale, offset, then flip V. +fn mx_transform_uv(uv: vec2f, uv_scale: vec2f, uv_offset: vec2f) -> vec2f { + let transformed = uv * uv_scale + uv_offset; + return vec2f(transformed.x, 1.0 - transformed.y); +} diff --git a/libraries/stdlib/genwgsl/mx_aastep.wgsl b/libraries/stdlib/genwgsl/mx_aastep.wgsl new file mode 100644 index 0000000000..e37477e62d --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_aastep.wgsl @@ -0,0 +1,11 @@ +// Generated from libraries/stdlib/genglsl/mx_aastep.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_aastep(threshold: f32, value_2: f32) -> f32 { + var value_3: f32; + var afwidth: f32; + + value_3 = value_2; + afwidth = (length(vec2f(dpdx(value_3), dpdy(value_3))) * 0.70710677); + return smoothstep((threshold - afwidth), (threshold + afwidth), value_3); +} diff --git a/libraries/stdlib/genwgsl/mx_burn_color3.wgsl b/libraries/stdlib/genwgsl/mx_burn_color3.wgsl new file mode 100644 index 0000000000..77bbff968b --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_burn_color3.wgsl @@ -0,0 +1,22 @@ +// Generated from libraries/stdlib/genglsl/mx_burn_color3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "mx_burn_float.wgsl" + +fn mx_burn_color3(fg_9: vec3f, bg_9: vec3f, mixval_6: f32, result_82: ptr) { + var fg_10: vec3f; + var bg_10: vec3f; + var mixval_7: f32; + var f_1: f32; + + fg_10 = fg_9; + bg_10 = bg_9; + mixval_7 = mixval_6; + mx_burn_float(fg_10.x, bg_10.x, mixval_7, (&f_1)); + (*result_82).x = f_1; + mx_burn_float(fg_10.y, bg_10.y, mixval_7, (&f_1)); + (*result_82).y = f_1; + mx_burn_float(fg_10.z, bg_10.z, mixval_7, (&f_1)); + (*result_82).z = f_1; + return; +} diff --git a/libraries/stdlib/genwgsl/mx_burn_color4.wgsl b/libraries/stdlib/genwgsl/mx_burn_color4.wgsl new file mode 100644 index 0000000000..bff6caef96 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_burn_color4.wgsl @@ -0,0 +1,24 @@ +// Generated from libraries/stdlib/genglsl/mx_burn_color4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "mx_burn_float.wgsl" + +fn mx_burn_color4(fg_9: vec4f, bg_9: vec4f, mixval_6: f32, result_82: ptr) { + var fg_10: vec4f; + var bg_10: vec4f; + var mixval_7: f32; + var f_1: f32; + + fg_10 = fg_9; + bg_10 = bg_9; + mixval_7 = mixval_6; + mx_burn_float(fg_10.x, bg_10.x, mixval_7, (&f_1)); + (*result_82).x = f_1; + mx_burn_float(fg_10.y, bg_10.y, mixval_7, (&f_1)); + (*result_82).y = f_1; + mx_burn_float(fg_10.z, bg_10.z, mixval_7, (&f_1)); + (*result_82).z = f_1; + mx_burn_float(fg_10.w, bg_10.w, mixval_7, (&f_1)); + (*result_82).w = f_1; + return; +} diff --git a/libraries/stdlib/genwgsl/mx_burn_float.wgsl b/libraries/stdlib/genwgsl/mx_burn_float.wgsl new file mode 100644 index 0000000000..f7f56f1af0 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_burn_float.wgsl @@ -0,0 +1,20 @@ +// Generated from libraries/stdlib/genglsl/mx_burn_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_burn_float(fg_9: f32, bg_9: f32, mixval_6: f32, result_82: ptr) { + var fg_10: f32; + var bg_10: f32; + var mixval_7: f32; + + fg_10 = fg_9; + bg_10 = bg_9; + mixval_7 = mixval_6; + if (abs(fg_10) < 0.00000001) { + { + (*result_82) = 0.0; + return; + } + } + (*result_82) = ((mixval_7 * (1.0 - ((1.0 - bg_10) / fg_10))) + ((1.0 - mixval_7) * bg_10)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_cellnoise2d_float.wgsl b/libraries/stdlib/genwgsl/mx_cellnoise2d_float.wgsl new file mode 100644 index 0000000000..07ad01d3d9 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_cellnoise2d_float.wgsl @@ -0,0 +1,12 @@ +// Generated from libraries/stdlib/genglsl/mx_cellnoise2d_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_cellnoise2d_float(texcoord_29: vec2f, result_82: ptr) { + var texcoord_30: vec2f; + + texcoord_30 = texcoord_29; + (*result_82) = mx_cell_noise_float_vec2(texcoord_30); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_cellnoise3d_float.wgsl b/libraries/stdlib/genwgsl/mx_cellnoise3d_float.wgsl new file mode 100644 index 0000000000..5155c7cafb --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_cellnoise3d_float.wgsl @@ -0,0 +1,12 @@ +// Generated from libraries/stdlib/genglsl/mx_cellnoise3d_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_cellnoise3d_float(position_13: vec3f, result_82: ptr) { + var position_14: vec3f; + + position_14 = position_13; + (*result_82) = mx_cell_noise_float_vec3(position_14); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_creatematrix_vector3_matrix33.wgsl b/libraries/stdlib/genwgsl/mx_creatematrix_vector3_matrix33.wgsl new file mode 100644 index 0000000000..399cad8414 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_creatematrix_vector3_matrix33.wgsl @@ -0,0 +1,14 @@ +// Generated from libraries/stdlib/genglsl/mx_creatematrix_vector3_matrix33.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_creatematrix_vector3_matrix33(in1_8: vec3f, in2_8: vec3f, in3_2: vec3f, result_82: ptr) { + var in1_9: vec3f; + var in2_9: vec3f; + var in3_3: vec3f; + + in1_9 = in1_8; + in2_9 = in2_8; + in3_3 = in3_2; + (*result_82) = mat3x3f(vec3f(in1_9.x, in1_9.y, in1_9.z), vec3f(in2_9.x, in2_9.y, in2_9.z), vec3f(in3_3.x, in3_3.y, in3_3.z)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_creatematrix_vector3_matrix44.wgsl b/libraries/stdlib/genwgsl/mx_creatematrix_vector3_matrix44.wgsl new file mode 100644 index 0000000000..93c12d72c1 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_creatematrix_vector3_matrix44.wgsl @@ -0,0 +1,16 @@ +// Generated from libraries/stdlib/genglsl/mx_creatematrix_vector3_matrix44.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_creatematrix_vector3_matrix44(in1_8: vec3f, in2_8: vec3f, in3_2: vec3f, in4_1: vec3f, result_82: ptr) { + var in1_9: vec3f; + var in2_9: vec3f; + var in3_3: vec3f; + var in4_2: vec3f; + + in1_9 = in1_8; + in2_9 = in2_8; + in3_3 = in3_2; + in4_2 = in4_1; + (*result_82) = mat4x4f(vec4f(in1_9.x, in1_9.y, in1_9.z, 0.0), vec4f(in2_9.x, in2_9.y, in2_9.z, 0.0), vec4f(in3_3.x, in3_3.y, in3_3.z, 0.0), vec4f(in4_2.x, in4_2.y, in4_2.z, 1.0)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_creatematrix_vector4_matrix44.wgsl b/libraries/stdlib/genwgsl/mx_creatematrix_vector4_matrix44.wgsl new file mode 100644 index 0000000000..90b73ed8e5 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_creatematrix_vector4_matrix44.wgsl @@ -0,0 +1,16 @@ +// Generated from libraries/stdlib/genglsl/mx_creatematrix_vector4_matrix44.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_creatematrix_vector4_matrix44(in1_8: vec4f, in2_8: vec4f, in3_2: vec4f, in4_1: vec4f, result_82: ptr) { + var in1_9: vec4f; + var in2_9: vec4f; + var in3_3: vec4f; + var in4_2: vec4f; + + in1_9 = in1_8; + in2_9 = in2_8; + in3_3 = in3_2; + in4_2 = in4_1; + (*result_82) = mat4x4f(vec4f(in1_9.x, in1_9.y, in1_9.z, in1_9.w), vec4f(in2_9.x, in2_9.y, in2_9.z, in2_9.w), vec4f(in3_3.x, in3_3.y, in3_3.z, in3_3.w), vec4f(in4_2.x, in4_2.y, in4_2.z, in4_2.w)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_disjointover_color4.wgsl b/libraries/stdlib/genwgsl/mx_disjointover_color4.wgsl new file mode 100644 index 0000000000..3e78aff978 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_disjointover_color4.wgsl @@ -0,0 +1,52 @@ +// Generated from libraries/stdlib/genglsl/mx_disjointover_color4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_disjointover_color4(fg_9: vec4f, bg_9: vec4f, mixval_6: f32, result_82: ptr) { + var fg_10: vec4f; + var bg_10: vec4f; + var mixval_7: f32; + var summedAlpha: f32; + var x_34: f32; + + fg_10 = fg_9; + bg_10 = bg_9; + mixval_7 = mixval_6; + summedAlpha = (fg_10.w + bg_10.w); + if (summedAlpha <= 1.0) { + { + let _e34 = (*result_82); + let _e40 = (fg_10.xyz + bg_10.xyz); + (*result_82).x = _e40.x; + (*result_82).y = _e40.y; + (*result_82).z = _e40.z; + } + } else { + { + if (abs(bg_10.w) < 0.00000001) { + { + let _e52 = (*result_82); + (*result_82).x = 0.0; + (*result_82).y = 0.0; + (*result_82).z = 0.0; + } + } else { + { + x_34 = ((1.0 - fg_10.w) / bg_10.w); + let _e67 = (*result_82); + let _e75 = (fg_10.xyz + (bg_10.xyz * x_34)); + (*result_82).x = _e75.x; + (*result_82).y = _e75.y; + (*result_82).z = _e75.z; + } + } + } + } + (*result_82).w = min(summedAlpha, 1.0); + let _e86 = (*result_82); + let _e98 = ((((*result_82)).xyz * mixval_7) + ((1.0 - mixval_7) * bg_10.xyz)); + (*result_82).x = _e98.x; + (*result_82).y = _e98.y; + (*result_82).z = _e98.z; + (*result_82).w = ((((*result_82)).w * mixval_7) + ((1.0 - mixval_7) * bg_10.w)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_dodge_color3.wgsl b/libraries/stdlib/genwgsl/mx_dodge_color3.wgsl new file mode 100644 index 0000000000..0470383f9e --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_dodge_color3.wgsl @@ -0,0 +1,22 @@ +// Generated from libraries/stdlib/genglsl/mx_dodge_color3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "mx_dodge_float.wgsl" + +fn mx_dodge_color3(fg_9: vec3f, bg_9: vec3f, mixval_6: f32, result_82: ptr) { + var fg_10: vec3f; + var bg_10: vec3f; + var mixval_7: f32; + var f_1: f32; + + fg_10 = fg_9; + bg_10 = bg_9; + mixval_7 = mixval_6; + mx_dodge_float(fg_10.x, bg_10.x, mixval_7, (&f_1)); + (*result_82).x = f_1; + mx_dodge_float(fg_10.y, bg_10.y, mixval_7, (&f_1)); + (*result_82).y = f_1; + mx_dodge_float(fg_10.z, bg_10.z, mixval_7, (&f_1)); + (*result_82).z = f_1; + return; +} diff --git a/libraries/stdlib/genwgsl/mx_dodge_color4.wgsl b/libraries/stdlib/genwgsl/mx_dodge_color4.wgsl new file mode 100644 index 0000000000..04491d3c55 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_dodge_color4.wgsl @@ -0,0 +1,24 @@ +// Generated from libraries/stdlib/genglsl/mx_dodge_color4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "mx_dodge_float.wgsl" + +fn mx_dodge_color4(fg_9: vec4f, bg_9: vec4f, mixval_6: f32, result_82: ptr) { + var fg_10: vec4f; + var bg_10: vec4f; + var mixval_7: f32; + var f_1: f32; + + fg_10 = fg_9; + bg_10 = bg_9; + mixval_7 = mixval_6; + mx_dodge_float(fg_10.x, bg_10.x, mixval_7, (&f_1)); + (*result_82).x = f_1; + mx_dodge_float(fg_10.y, bg_10.y, mixval_7, (&f_1)); + (*result_82).y = f_1; + mx_dodge_float(fg_10.z, bg_10.z, mixval_7, (&f_1)); + (*result_82).z = f_1; + mx_dodge_float(fg_10.w, bg_10.w, mixval_7, (&f_1)); + (*result_82).w = f_1; + return; +} diff --git a/libraries/stdlib/genwgsl/mx_dodge_float.wgsl b/libraries/stdlib/genwgsl/mx_dodge_float.wgsl new file mode 100644 index 0000000000..a137e171c0 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_dodge_float.wgsl @@ -0,0 +1,20 @@ +// Generated from libraries/stdlib/genglsl/mx_dodge_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_dodge_float(fg_9: f32, bg_9: f32, mixval_6: f32, result_82: ptr) { + var fg_10: f32; + var bg_10: f32; + var mixval_7: f32; + + fg_10 = fg_9; + bg_10 = bg_9; + mixval_7 = mixval_6; + if (abs((1.0 - fg_10)) < 0.00000001) { + { + (*result_82) = 0.0; + return; + } + } + (*result_82) = ((mixval_7 * (bg_10 / (1.0 - fg_10))) + ((1.0 - mixval_7) * bg_10)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_flake2d.wgsl b/libraries/stdlib/genwgsl/mx_flake2d.wgsl new file mode 100644 index 0000000000..98905e1fdf --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_flake2d.wgsl @@ -0,0 +1,26 @@ +// Generated from libraries/stdlib/genglsl/mx_flake2d.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_flake.wgsl" + +fn mx_flake2d(size_2: f32, roughness_29: f32, coverage_2: f32, texcoord_29: vec2f, normal_2: vec3f, tangent_2: vec3f, bitangent_2: vec3f, id_2: ptr, rand_2: ptr, presence_2: ptr, flakenormal_2: ptr) { + var size_3: f32; + var roughness_30: f32; + var coverage_3: f32; + var texcoord_30: vec2f; + var normal_3: vec3f; + var tangent_3: vec3f; + var bitangent_3: vec3f; + var position_14: vec3f; + + size_3 = size_2; + roughness_30 = roughness_29; + coverage_3 = coverage_2; + texcoord_30 = texcoord_29; + normal_3 = normal_2; + tangent_3 = tangent_2; + bitangent_3 = bitangent_2; + position_14 = vec3f(texcoord_30.x, texcoord_30.y, 0.0); + mx_flake(size_3, roughness_30, coverage_3, position_14, normal_3, tangent_3, bitangent_3, id_2, rand_2, presence_2, flakenormal_2); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_flake3d.wgsl b/libraries/stdlib/genwgsl/mx_flake3d.wgsl new file mode 100644 index 0000000000..b0cc93fa1a --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_flake3d.wgsl @@ -0,0 +1,24 @@ +// Generated from libraries/stdlib/genglsl/mx_flake3d.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_flake.wgsl" + +fn mx_flake3d(size_2: f32, roughness_29: f32, coverage_2: f32, position_13: vec3f, normal_2: vec3f, tangent_2: vec3f, bitangent_2: vec3f, id_2: ptr, rand_2: ptr, presence_2: ptr, flakenormal_2: ptr) { + var size_3: f32; + var roughness_30: f32; + var coverage_3: f32; + var position_14: vec3f; + var normal_3: vec3f; + var tangent_3: vec3f; + var bitangent_3: vec3f; + + size_3 = size_2; + roughness_30 = roughness_29; + coverage_3 = coverage_2; + position_14 = position_13; + normal_3 = normal_2; + tangent_3 = tangent_2; + bitangent_3 = bitangent_2; + mx_flake(size_3, roughness_30, coverage_3, position_14, normal_3, tangent_3, bitangent_3, id_2, rand_2, presence_2, flakenormal_2); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_fractal2d_float.wgsl b/libraries/stdlib/genwgsl/mx_fractal2d_float.wgsl new file mode 100644 index 0000000000..603a4f564d --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_fractal2d_float.wgsl @@ -0,0 +1,22 @@ +// Generated from libraries/stdlib/genglsl/mx_fractal2d_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_fractal2d_float(amplitude_15: f32, octaves_15: i32, lacunarity_15: f32, diminish_15: f32, texcoord_29: vec2f, result_82: ptr) { + var amplitude_16: f32; + var octaves_16: i32; + var lacunarity_16: f32; + var diminish_16: f32; + var texcoord_30: vec2f; + var value_3: f32; + + amplitude_16 = amplitude_15; + octaves_16 = octaves_15; + lacunarity_16 = lacunarity_15; + diminish_16 = diminish_15; + texcoord_30 = texcoord_29; + value_3 = (mx_fractal2d_noise_float(texcoord_30, octaves_16, lacunarity_16, diminish_16)); + (*result_82) = (value_3 * amplitude_16); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_fractal2d_vector2.wgsl b/libraries/stdlib/genwgsl/mx_fractal2d_vector2.wgsl new file mode 100644 index 0000000000..0f3741d6ff --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_fractal2d_vector2.wgsl @@ -0,0 +1,22 @@ +// Generated from libraries/stdlib/genglsl/mx_fractal2d_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_fractal2d_vector2(amplitude_15: vec2f, octaves_15: i32, lacunarity_15: f32, diminish_15: f32, texcoord_29: vec2f, result_82: ptr) { + var amplitude_16: vec2f; + var octaves_16: i32; + var lacunarity_16: f32; + var diminish_16: f32; + var texcoord_30: vec2f; + var value_3: vec2f; + + amplitude_16 = amplitude_15; + octaves_16 = octaves_15; + lacunarity_16 = lacunarity_15; + diminish_16 = diminish_15; + texcoord_30 = texcoord_29; + value_3 = (mx_fractal2d_noise_vec2(texcoord_30, octaves_16, lacunarity_16, diminish_16)); + (*result_82) = (value_3 * amplitude_16); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_fractal2d_vector3.wgsl b/libraries/stdlib/genwgsl/mx_fractal2d_vector3.wgsl new file mode 100644 index 0000000000..553224e099 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_fractal2d_vector3.wgsl @@ -0,0 +1,22 @@ +// Generated from libraries/stdlib/genglsl/mx_fractal2d_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_fractal2d_vector3(amplitude_15: vec3f, octaves_15: i32, lacunarity_15: f32, diminish_15: f32, texcoord_29: vec2f, result_82: ptr) { + var amplitude_16: vec3f; + var octaves_16: i32; + var lacunarity_16: f32; + var diminish_16: f32; + var texcoord_30: vec2f; + var value_3: vec3f; + + amplitude_16 = amplitude_15; + octaves_16 = octaves_15; + lacunarity_16 = lacunarity_15; + diminish_16 = diminish_15; + texcoord_30 = texcoord_29; + value_3 = (mx_fractal2d_noise_vec3(texcoord_30, octaves_16, lacunarity_16, diminish_16)); + (*result_82) = (value_3 * amplitude_16); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_fractal2d_vector4.wgsl b/libraries/stdlib/genwgsl/mx_fractal2d_vector4.wgsl new file mode 100644 index 0000000000..72c3c93d21 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_fractal2d_vector4.wgsl @@ -0,0 +1,22 @@ +// Generated from libraries/stdlib/genglsl/mx_fractal2d_vector4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_fractal2d_vector4(amplitude_15: vec4f, octaves_15: i32, lacunarity_15: f32, diminish_15: f32, texcoord_29: vec2f, result_82: ptr) { + var amplitude_16: vec4f; + var octaves_16: i32; + var lacunarity_16: f32; + var diminish_16: f32; + var texcoord_30: vec2f; + var value_3: vec4f; + + amplitude_16 = amplitude_15; + octaves_16 = octaves_15; + lacunarity_16 = lacunarity_15; + diminish_16 = diminish_15; + texcoord_30 = texcoord_29; + value_3 = (mx_fractal2d_noise_vec4(texcoord_30, octaves_16, lacunarity_16, diminish_16)); + (*result_82) = (value_3 * amplitude_16); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_fractal3d_float.wgsl b/libraries/stdlib/genwgsl/mx_fractal3d_float.wgsl new file mode 100644 index 0000000000..d1fe4fba3e --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_fractal3d_float.wgsl @@ -0,0 +1,22 @@ +// Generated from libraries/stdlib/genglsl/mx_fractal3d_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_fractal3d_float(amplitude_15: f32, octaves_15: i32, lacunarity_15: f32, diminish_15: f32, position_13: vec3f, result_82: ptr) { + var amplitude_16: f32; + var octaves_16: i32; + var lacunarity_16: f32; + var diminish_16: f32; + var position_14: vec3f; + var value_3: f32; + + amplitude_16 = amplitude_15; + octaves_16 = octaves_15; + lacunarity_16 = lacunarity_15; + diminish_16 = diminish_15; + position_14 = position_13; + value_3 = (mx_fractal3d_noise_float(position_14, octaves_16, lacunarity_16, diminish_16)); + (*result_82) = (value_3 * amplitude_16); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_fractal3d_vector2.wgsl b/libraries/stdlib/genwgsl/mx_fractal3d_vector2.wgsl new file mode 100644 index 0000000000..4c775f30b6 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_fractal3d_vector2.wgsl @@ -0,0 +1,22 @@ +// Generated from libraries/stdlib/genglsl/mx_fractal3d_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_fractal3d_vector2(amplitude_15: vec2f, octaves_15: i32, lacunarity_15: f32, diminish_15: f32, position_13: vec3f, result_82: ptr) { + var amplitude_16: vec2f; + var octaves_16: i32; + var lacunarity_16: f32; + var diminish_16: f32; + var position_14: vec3f; + var value_3: vec2f; + + amplitude_16 = amplitude_15; + octaves_16 = octaves_15; + lacunarity_16 = lacunarity_15; + diminish_16 = diminish_15; + position_14 = position_13; + value_3 = (mx_fractal3d_noise_vec2(position_14, octaves_16, lacunarity_16, diminish_16)); + (*result_82) = (value_3 * amplitude_16); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_fractal3d_vector3.wgsl b/libraries/stdlib/genwgsl/mx_fractal3d_vector3.wgsl new file mode 100644 index 0000000000..cc9ad60d7d --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_fractal3d_vector3.wgsl @@ -0,0 +1,22 @@ +// Generated from libraries/stdlib/genglsl/mx_fractal3d_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_fractal3d_vector3(amplitude_15: vec3f, octaves_15: i32, lacunarity_15: f32, diminish_15: f32, position_13: vec3f, result_82: ptr) { + var amplitude_16: vec3f; + var octaves_16: i32; + var lacunarity_16: f32; + var diminish_16: f32; + var position_14: vec3f; + var value_3: vec3f; + + amplitude_16 = amplitude_15; + octaves_16 = octaves_15; + lacunarity_16 = lacunarity_15; + diminish_16 = diminish_15; + position_14 = position_13; + value_3 = (mx_fractal3d_noise_vec3(position_14, octaves_16, lacunarity_16, diminish_16)); + (*result_82) = (value_3 * amplitude_16); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_fractal3d_vector4.wgsl b/libraries/stdlib/genwgsl/mx_fractal3d_vector4.wgsl new file mode 100644 index 0000000000..d54256c823 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_fractal3d_vector4.wgsl @@ -0,0 +1,22 @@ +// Generated from libraries/stdlib/genglsl/mx_fractal3d_vector4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_fractal3d_vector4(amplitude_15: vec4f, octaves_15: i32, lacunarity_15: f32, diminish_15: f32, position_13: vec3f, result_82: ptr) { + var amplitude_16: vec4f; + var octaves_16: i32; + var lacunarity_16: f32; + var diminish_16: f32; + var position_14: vec3f; + var value_3: vec4f; + + amplitude_16 = amplitude_15; + octaves_16 = octaves_15; + lacunarity_16 = lacunarity_15; + diminish_16 = diminish_15; + position_14 = position_13; + value_3 = (mx_fractal3d_noise_vec4(position_14, octaves_16, lacunarity_16, diminish_16)); + (*result_82) = (value_3 * amplitude_16); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_heighttonormal_vector3.wgsl b/libraries/stdlib/genwgsl/mx_heighttonormal_vector3.wgsl new file mode 100644 index 0000000000..f0f9d43e3c --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_heighttonormal_vector3.wgsl @@ -0,0 +1,36 @@ +// Generated from libraries/stdlib/genglsl/mx_heighttonormal_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_heighttonormal_vector3(height: f32, scale_3: f32, texcoord_29: vec2f, result_82: ptr) { + var scale_4: f32; + var texcoord_30: vec2f; + var SOBEL_SCALE_FACTOR: f32 = 0.0625; + var dHdS: vec2f; + var dUdS: vec2f; + var dVdS: vec2f; + var tangent_3: vec3f; + var bitangent_3: vec3f; + var n_2: vec3f; + + scale_4 = scale_3; + texcoord_30 = texcoord_29; + dHdS = ((vec2f(dpdx(height), dpdy(height)) * scale_4) * SOBEL_SCALE_FACTOR); + dUdS = vec2f(dpdx(texcoord_30.x), dpdy(texcoord_30.x)); + dVdS = vec2f(dpdx(texcoord_30.y), dpdy(texcoord_30.y)); + tangent_3 = vec3f(dUdS.x, dVdS.x, dHdS.x); + bitangent_3 = vec3f(dUdS.y, dVdS.y, dHdS.y); + n_2 = cross(tangent_3, bitangent_3); + if (dot(n_2, n_2) < 0.0000000000000001) { + { + n_2 = vec3f(0.0, 0.0, 1.0); + } + } else { + if (n_2.z < 0.0) { + { + n_2 = (n_2 * -1.0); + } + } + } + (*result_82) = ((normalize(n_2) * 0.5) + vec3(0.5)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_hextiledimage.wgsl b/libraries/stdlib/genwgsl/mx_hextiledimage.wgsl new file mode 100644 index 0000000000..785511a7d6 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_hextiledimage.wgsl @@ -0,0 +1,80 @@ +#include "lib/$fileTransformUv" +#include "lib/mx_hextile.wgsl" + +// +// Morten S. Mikkelsen, Practical Real-Time Hex-Tiling, JCGT vol. 11, no. 2, 2022 +// http://jcgt.org/published/0011/03/05/ + +fn mx_hextiledimage_color3( + tex: texture_2d, + tex_sampler: sampler, + default_value: vec3f, + tex_coord: vec2f, + tiling: vec2f, + rotation: f32, + rotation_range: vec2f, + scale: f32, + scale_range: vec2f, + offset_val: f32, + offset_range: vec2f, + falloff: f32, + falloff_contrast: f32, + lumacoeffs: vec3f, + result: ptr +) { + let coord = mx_transform_uv(tex_coord, tiling, vec2f(0.0)); + + let tile_data = mx_hextile_coord(coord, rotation, rotation_range, scale, scale_range, offset_val, offset_range); + + let c1 = textureSampleGrad(tex, tex_sampler, tile_data.coords0, tile_data.ddx0, tile_data.ddy0).rgb; + let c2 = textureSampleGrad(tex, tex_sampler, tile_data.coords1, tile_data.ddx1, tile_data.ddy1).rgb; + let c3 = textureSampleGrad(tex, tex_sampler, tile_data.coords2, tile_data.ddx2, tile_data.ddy2).rgb; + + // Luminance as weights. + var cw = vec3f(dot(c1, lumacoeffs), dot(c2, lumacoeffs), dot(c3, lumacoeffs)); + cw = mix(vec3f(1.0), cw, vec3f(falloff_contrast)); + + let w = mx_hextile_compute_blend_weights(cw, tile_data.weights, falloff); + + *result = w.x * c1 + w.y * c2 + w.z * c3; +} + +fn mx_hextiledimage_color4( + tex: texture_2d, + tex_sampler: sampler, + default_value: vec4f, + tex_coord: vec2f, + tiling: vec2f, + rotation: f32, + rotation_range: vec2f, + scale: f32, + scale_range: vec2f, + offset_val: f32, + offset_range: vec2f, + falloff: f32, + falloff_contrast: f32, + lumacoeffs: vec3f, + result: ptr +) { + let coord = mx_transform_uv(tex_coord, tiling, vec2f(0.0)); + + let tile_data = mx_hextile_coord(coord, rotation, rotation_range, scale, scale_range, offset_val, offset_range); + + let c1 = textureSampleGrad(tex, tex_sampler, tile_data.coords0, tile_data.ddx0, tile_data.ddy0); + let c2 = textureSampleGrad(tex, tex_sampler, tile_data.coords1, tile_data.ddx1, tile_data.ddy1); + let c3 = textureSampleGrad(tex, tex_sampler, tile_data.coords2, tile_data.ddx2, tile_data.ddy2); + + // Luminance as weights. + var cw = vec3f(dot(c1.rgb, lumacoeffs), dot(c2.rgb, lumacoeffs), dot(c3.rgb, lumacoeffs)); + cw = mix(vec3f(1.0), cw, vec3f(falloff_contrast)); + + let w = mx_hextile_compute_blend_weights(cw, tile_data.weights, falloff); + + // Alpha is averaged, then optionally gain-adjusted. + var a = (c1.a + c2.a + c3.a) / 3.0; + if (falloff != 0.5) { + a = mx_schlick_gain(a, falloff); + } + + (*result) = vec4f(w.x * c1.rgb + w.y * c2.rgb + w.z * c3.rgb, a); +} diff --git a/libraries/stdlib/genwgsl/mx_hextilednormalmap.wgsl b/libraries/stdlib/genwgsl/mx_hextilednormalmap.wgsl new file mode 100644 index 0000000000..c580cab41f --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_hextilednormalmap.wgsl @@ -0,0 +1,64 @@ +#include "lib/$fileTransformUv" +#include "lib/mx_hextile.wgsl" +#include "lib/mx_geometry.wgsl" + +// +// Morten S. Mikkelsen, Practical Real-Time Hex-Tiling, JCGT vol. 11, no. 2, 2022 +// http://jcgt.org/published/0011/03/05/ + +fn mx_hextilednormalmap_vector3( + tex: texture_2d, + tex_sampler: sampler, + default_value: vec3f, + tex_coord: vec2f, + tiling: vec2f, + rotation: f32, + rotation_range: vec2f, + scale: f32, + scale_range: vec2f, + offset_val: f32, + offset_range: vec2f, + falloff: f32, + strength: f32, + flip_g: bool, + N: vec3f, + T: vec3f, + B: vec3f, + result: ptr +) { + let coord = mx_transform_uv(tex_coord, tiling, vec2f(0.0)); + + let tile_data = mx_hextile_coord(coord, rotation, rotation_range, scale, scale_range, offset_val, offset_range); + + var nm1 = textureSampleGrad(tex, tex_sampler, tile_data.coords0, tile_data.ddx0, tile_data.ddy0).xyz; + var nm2 = textureSampleGrad(tex, tex_sampler, tile_data.coords1, tile_data.ddx1, tile_data.ddy1).xyz; + var nm3 = textureSampleGrad(tex, tex_sampler, tile_data.coords2, tile_data.ddx2, tile_data.ddy2).xyz; + + if (flip_g) { + nm1.y = 1.0 - nm1.y; + nm2.y = 1.0 - nm2.y; + nm3.y = 1.0 - nm3.y; + } + + // Normal map to shading normal. + nm1 = 2.0 * nm1 - 1.0; + nm2 = 2.0 * nm2 - 1.0; + nm3 = 2.0 * nm3 - 1.0; + let tangent_rot_mat1 = mx_axis_rotation_matrix(N, -tile_data.rotations.x); + let tangent_rot_mat2 = mx_axis_rotation_matrix(N, -tile_data.rotations.y); + let tangent_rot_mat3 = mx_axis_rotation_matrix(N, -tile_data.rotations.z); + let T1 = (tangent_rot_mat1 * T) * strength; + let T2 = (tangent_rot_mat2 * T) * strength; + let T3 = (tangent_rot_mat3 * T) * strength; + let B1 = (tangent_rot_mat1 * B) * strength; + let B2 = (tangent_rot_mat2 * B) * strength; + let B3 = (tangent_rot_mat3 * B) * strength; + let N1 = normalize(T1 * nm1.x + B1 * nm1.y + N * nm1.z); + let N2 = normalize(T2 * nm2.x + B2 * nm2.y + N * nm2.z); + let N3 = normalize(T3 * nm3.x + B3 * nm3.y + N * nm3.z); + + // Blend weights. + let w = mx_hextile_compute_blend_weights(vec3f(1.0), tile_data.weights, falloff); + + *result = mx_gradient_blend_3_normals(N, N1, w.x, N2, w.y, N3, w.z); +} diff --git a/libraries/stdlib/genwgsl/mx_hsvtorgb_color3.wgsl b/libraries/stdlib/genwgsl/mx_hsvtorgb_color3.wgsl new file mode 100644 index 0000000000..f6688b1260 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_hsvtorgb_color3.wgsl @@ -0,0 +1,12 @@ +// Generated from libraries/stdlib/genglsl/mx_hsvtorgb_color3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_hsv.wgsl" + +fn mx_hsvtorgb_color3(_in_9: vec3f, result_82: ptr) { + var _in_10: vec3f; + + _in_10 = _in_9; + (*result_82) = mx_hsvtorgb(_in_10); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_hsvtorgb_color4.wgsl b/libraries/stdlib/genwgsl/mx_hsvtorgb_color4.wgsl new file mode 100644 index 0000000000..fe29caf1e6 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_hsvtorgb_color4.wgsl @@ -0,0 +1,13 @@ +// Generated from libraries/stdlib/genglsl/mx_hsvtorgb_color4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_hsv.wgsl" + +fn mx_hsvtorgb_color4(_in_9: vec4f, result_82: ptr) { + var _in_10: vec4f; + + _in_10 = _in_9; + let _e23 = mx_hsvtorgb(_in_10.xyz); + (*result_82) = vec4f(_e23.x, _e23.y, _e23.z, _in_10.w); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_image_color3.wgsl b/libraries/stdlib/genwgsl/mx_image_color3.wgsl new file mode 100644 index 0000000000..2c1ce40e71 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_image_color3.wgsl @@ -0,0 +1,6 @@ +#include "lib/$fileTransformUv" + +fn mx_image_color3(tex: texture_2d, samp: sampler, layer: i32, defaultval: vec3f, texcoord: vec2f, uaddressmode: i32, vaddressmode: i32, filtertype: i32, framerange: i32, frameoffset: i32, frameendaction: i32, uv_scale: vec2f, uv_offset: vec2f, result: ptr) { + let uv = mx_transform_uv(texcoord, uv_scale, uv_offset); + *result = textureSample(tex, samp, uv).rgb; +} diff --git a/libraries/stdlib/genwgsl/mx_image_color4.wgsl b/libraries/stdlib/genwgsl/mx_image_color4.wgsl new file mode 100644 index 0000000000..fe96f67a96 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_image_color4.wgsl @@ -0,0 +1,6 @@ +#include "lib/$fileTransformUv" + +fn mx_image_color4(tex: texture_2d, samp: sampler, layer: i32, defaultval: vec4f, texcoord: vec2f, uaddressmode: i32, vaddressmode: i32, filtertype: i32, framerange: i32, frameoffset: i32, frameendaction: i32, uv_scale: vec2f, uv_offset: vec2f, result: ptr) { + let uv = mx_transform_uv(texcoord, uv_scale, uv_offset); + *result = textureSample(tex, samp, uv); +} diff --git a/libraries/stdlib/genwgsl/mx_image_float.wgsl b/libraries/stdlib/genwgsl/mx_image_float.wgsl new file mode 100644 index 0000000000..99d184dbe6 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_image_float.wgsl @@ -0,0 +1,6 @@ +#include "lib/$fileTransformUv" + +fn mx_image_float(tex: texture_2d, samp: sampler, layer: i32, defaultval: f32, texcoord: vec2f, uaddressmode: i32, vaddressmode: i32, filtertype: i32, framerange: i32, frameoffset: i32, frameendaction: i32, uv_scale: vec2f, uv_offset: vec2f, result: ptr) { + let uv = mx_transform_uv(texcoord, uv_scale, uv_offset); + *result = textureSample(tex, samp, uv).r; +} diff --git a/libraries/stdlib/genwgsl/mx_image_vector2.wgsl b/libraries/stdlib/genwgsl/mx_image_vector2.wgsl new file mode 100644 index 0000000000..77da96a3e7 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_image_vector2.wgsl @@ -0,0 +1,6 @@ +#include "lib/$fileTransformUv" + +fn mx_image_vector2(tex: texture_2d, samp: sampler, layer: i32, defaultval: vec2f, texcoord: vec2f, uaddressmode: i32, vaddressmode: i32, filtertype: i32, framerange: i32, frameoffset: i32, frameendaction: i32, uv_scale: vec2f, uv_offset: vec2f, result: ptr) { + let uv = mx_transform_uv(texcoord, uv_scale, uv_offset); + *result = textureSample(tex, samp, uv).rg; +} diff --git a/libraries/stdlib/genwgsl/mx_image_vector3.wgsl b/libraries/stdlib/genwgsl/mx_image_vector3.wgsl new file mode 100644 index 0000000000..08b67446ea --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_image_vector3.wgsl @@ -0,0 +1,6 @@ +#include "lib/$fileTransformUv" + +fn mx_image_vector3(tex: texture_2d, samp: sampler, layer: i32, defaultval: vec3f, texcoord: vec2f, uaddressmode: i32, vaddressmode: i32, filtertype: i32, framerange: i32, frameoffset: i32, frameendaction: i32, uv_scale: vec2f, uv_offset: vec2f, result: ptr) { + let uv = mx_transform_uv(texcoord, uv_scale, uv_offset); + *result = textureSample(tex, samp, uv).rgb; +} diff --git a/libraries/stdlib/genwgsl/mx_image_vector4.wgsl b/libraries/stdlib/genwgsl/mx_image_vector4.wgsl new file mode 100644 index 0000000000..19b0f9c456 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_image_vector4.wgsl @@ -0,0 +1,6 @@ +#include "lib/$fileTransformUv" + +fn mx_image_vector4(tex: texture_2d, samp: sampler, layer: i32, defaultval: vec4f, texcoord: vec2f, uaddressmode: i32, vaddressmode: i32, filtertype: i32, framerange: i32, frameoffset: i32, frameendaction: i32, uv_scale: vec2f, uv_offset: vec2f, result: ptr) { + let uv = mx_transform_uv(texcoord, uv_scale, uv_offset); + *result = textureSample(tex, samp, uv); +} diff --git a/libraries/stdlib/genwgsl/mx_luminance_color3.wgsl b/libraries/stdlib/genwgsl/mx_luminance_color3.wgsl new file mode 100644 index 0000000000..eaa7ae9019 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_luminance_color3.wgsl @@ -0,0 +1,12 @@ +// Generated from libraries/stdlib/genglsl/mx_luminance_color3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_luminance_color3(_in_9: vec3f, lumacoeffs_1: vec3f, result_82: ptr) { + var _in_10: vec3f; + var lumacoeffs_2: vec3f; + + _in_10 = _in_9; + lumacoeffs_2 = lumacoeffs_1; + (*result_82) = vec3(dot(_in_10, lumacoeffs_2)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_luminance_color4.wgsl b/libraries/stdlib/genwgsl/mx_luminance_color4.wgsl new file mode 100644 index 0000000000..887bf86291 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_luminance_color4.wgsl @@ -0,0 +1,13 @@ +// Generated from libraries/stdlib/genglsl/mx_luminance_color4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_luminance_color4(_in_9: vec4f, lumacoeffs_1: vec3f, result_82: ptr) { + var _in_10: vec4f; + var lumacoeffs_2: vec3f; + + _in_10 = _in_9; + lumacoeffs_2 = lumacoeffs_1; + let _e27 = vec3(dot(_in_10.xyz, lumacoeffs_2)); + (*result_82) = vec4f(_e27.x, _e27.y, _e27.z, _in_10.w); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_mix_surfaceshader.wgsl b/libraries/stdlib/genwgsl/mx_mix_surfaceshader.wgsl new file mode 100644 index 0000000000..aeecfd342c --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_mix_surfaceshader.wgsl @@ -0,0 +1,13 @@ +// Generated from libraries/stdlib/genglsl/mx_mix_surfaceshader.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_mix_surfaceshader(fg_9: surfaceshader, bg_9: surfaceshader, w: f32, returnshader: ptr) { + var fg_10: surfaceshader; + var bg_10: surfaceshader; + + fg_10 = fg_9; + bg_10 = bg_9; + (*returnshader).color = mix(bg_10.color, fg_10.color, vec3(w)); + (*returnshader).transparency = mix(bg_10.transparency, fg_10.transparency, vec3(w)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_noise2d_float.wgsl b/libraries/stdlib/genwgsl/mx_noise2d_float.wgsl new file mode 100644 index 0000000000..0be9584b9d --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_noise2d_float.wgsl @@ -0,0 +1,18 @@ +// Generated from libraries/stdlib/genglsl/mx_noise2d_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_noise2d_float(amplitude_15: f32, pivot_7: f32, texcoord_29: vec2f, result_82: ptr) { + var amplitude_16: f32; + var pivot_8: f32; + var texcoord_30: vec2f; + var value_3: f32; + + amplitude_16 = amplitude_15; + pivot_8 = pivot_7; + texcoord_30 = texcoord_29; + value_3 = mx_perlin_noise_float_2d(texcoord_30); + (*result_82) = ((value_3 * amplitude_16) + pivot_8); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_noise2d_vector2.wgsl b/libraries/stdlib/genwgsl/mx_noise2d_vector2.wgsl new file mode 100644 index 0000000000..620e389426 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_noise2d_vector2.wgsl @@ -0,0 +1,18 @@ +// Generated from libraries/stdlib/genglsl/mx_noise2d_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_noise2d_vector2(amplitude_15: vec2f, pivot_7: f32, texcoord_29: vec2f, result_82: ptr) { + var amplitude_16: vec2f; + var pivot_8: f32; + var texcoord_30: vec2f; + var value_3: vec3f; + + amplitude_16 = amplitude_15; + pivot_8 = pivot_7; + texcoord_30 = texcoord_29; + value_3 = mx_perlin_noise_vec3_2d(texcoord_30); + (*result_82) = ((value_3.xy * amplitude_16) + vec2(pivot_8)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_noise2d_vector3.wgsl b/libraries/stdlib/genwgsl/mx_noise2d_vector3.wgsl new file mode 100644 index 0000000000..561929ea07 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_noise2d_vector3.wgsl @@ -0,0 +1,18 @@ +// Generated from libraries/stdlib/genglsl/mx_noise2d_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_noise2d_vector3(amplitude_15: vec3f, pivot_7: f32, texcoord_29: vec2f, result_82: ptr) { + var amplitude_16: vec3f; + var pivot_8: f32; + var texcoord_30: vec2f; + var value_3: vec3f; + + amplitude_16 = amplitude_15; + pivot_8 = pivot_7; + texcoord_30 = texcoord_29; + value_3 = mx_perlin_noise_vec3_2d(texcoord_30); + (*result_82) = ((value_3 * amplitude_16) + vec3(pivot_8)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_noise2d_vector4.wgsl b/libraries/stdlib/genwgsl/mx_noise2d_vector4.wgsl new file mode 100644 index 0000000000..1abb915618 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_noise2d_vector4.wgsl @@ -0,0 +1,21 @@ +// Generated from libraries/stdlib/genglsl/mx_noise2d_vector4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_noise2d_vector4(amplitude_15: vec4f, pivot_7: f32, texcoord_29: vec2f, result_82: ptr) { + var amplitude_16: vec4f; + var pivot_8: f32; + var texcoord_30: vec2f; + var xyz: vec3f; + var w_1: f32; + + amplitude_16 = amplitude_15; + pivot_8 = pivot_7; + texcoord_30 = texcoord_29; + xyz = mx_perlin_noise_vec3_2d(texcoord_30); + w_1 = (mx_perlin_noise_float_2d((texcoord_30 + vec2f(19.0, 73.0)))); + let _e37 = xyz; + (*result_82) = ((vec4f(_e37.x, _e37.y, _e37.z, w_1) * amplitude_16) + vec4(pivot_8)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_noise3d_float.wgsl b/libraries/stdlib/genwgsl/mx_noise3d_float.wgsl new file mode 100644 index 0000000000..f1673c296c --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_noise3d_float.wgsl @@ -0,0 +1,18 @@ +// Generated from libraries/stdlib/genglsl/mx_noise3d_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_noise3d_float(amplitude_15: f32, pivot_7: f32, position_13: vec3f, result_82: ptr) { + var amplitude_16: f32; + var pivot_8: f32; + var position_14: vec3f; + var value_3: f32; + + amplitude_16 = amplitude_15; + pivot_8 = pivot_7; + position_14 = position_13; + value_3 = mx_perlin_noise_float_3d(position_14); + (*result_82) = ((value_3 * amplitude_16) + pivot_8); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_noise3d_vector2.wgsl b/libraries/stdlib/genwgsl/mx_noise3d_vector2.wgsl new file mode 100644 index 0000000000..ac959d4bfc --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_noise3d_vector2.wgsl @@ -0,0 +1,18 @@ +// Generated from libraries/stdlib/genglsl/mx_noise3d_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_noise3d_vector2(amplitude_15: vec2f, pivot_7: f32, position_13: vec3f, result_82: ptr) { + var amplitude_16: vec2f; + var pivot_8: f32; + var position_14: vec3f; + var value_3: vec3f; + + amplitude_16 = amplitude_15; + pivot_8 = pivot_7; + position_14 = position_13; + value_3 = mx_perlin_noise_vec3_3d(position_14); + (*result_82) = ((value_3.xy * amplitude_16) + vec2(pivot_8)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_noise3d_vector3.wgsl b/libraries/stdlib/genwgsl/mx_noise3d_vector3.wgsl new file mode 100644 index 0000000000..3da976aacb --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_noise3d_vector3.wgsl @@ -0,0 +1,18 @@ +// Generated from libraries/stdlib/genglsl/mx_noise3d_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_noise3d_vector3(amplitude_15: vec3f, pivot_7: f32, position_13: vec3f, result_82: ptr) { + var amplitude_16: vec3f; + var pivot_8: f32; + var position_14: vec3f; + var value_3: vec3f; + + amplitude_16 = amplitude_15; + pivot_8 = pivot_7; + position_14 = position_13; + value_3 = mx_perlin_noise_vec3_3d(position_14); + (*result_82) = ((value_3 * amplitude_16) + vec3(pivot_8)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_noise3d_vector4.wgsl b/libraries/stdlib/genwgsl/mx_noise3d_vector4.wgsl new file mode 100644 index 0000000000..b9e428531f --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_noise3d_vector4.wgsl @@ -0,0 +1,21 @@ +// Generated from libraries/stdlib/genglsl/mx_noise3d_vector4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_noise3d_vector4(amplitude_15: vec4f, pivot_7: f32, position_13: vec3f, result_82: ptr) { + var amplitude_16: vec4f; + var pivot_8: f32; + var position_14: vec3f; + var xyz: vec3f; + var w_1: f32; + + amplitude_16 = amplitude_15; + pivot_8 = pivot_7; + position_14 = position_13; + xyz = mx_perlin_noise_vec3_3d(position_14); + w_1 = (mx_perlin_noise_float_3d((position_14 + vec3f(19.0, 73.0, 29.0)))); + let _e39 = xyz; + (*result_82) = ((vec4f(_e39.x, _e39.y, _e39.z, w_1) * amplitude_16) + vec4(pivot_8)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_normalmap.wgsl b/libraries/stdlib/genwgsl/mx_normalmap.wgsl new file mode 100644 index 0000000000..c3d6ff922b --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_normalmap.wgsl @@ -0,0 +1,42 @@ +// Generated from libraries/stdlib/genglsl/mx_normalmap.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_normalmap_vector2(value_2: vec3f, normal_scale_1: vec2f, N_24: vec3f, T_1: vec3f, B_1: vec3f, result_82: ptr) { + var value_3: vec3f; + var normal_scale_2: vec2f; + var N_25: vec3f; + var T_2: vec3f; + var B_2: vec3f; + var local: vec3f; + + value_3 = value_2; + normal_scale_2 = normal_scale_1; + N_25 = N_24; + T_2 = T_1; + B_2 = B_1; + if (dot(value_3, value_3) == 0.0) { + local = vec3f(0.0, 0.0, 1.0); + } else { + local = ((value_3 * 2.0) - vec3(1.0)); + } + value_3 = local; + value_3 = ((((T_2 * value_3.x) * normal_scale_2.x) + ((B_2 * value_3.y) * normal_scale_2.y)) + (N_25 * value_3.z)); + (*result_82) = normalize(value_3); + return; +} + +fn mx_normalmap_float(value_2: vec3f, normal_scale_1: f32, N_24: vec3f, T_1: vec3f, B_1: vec3f, result_82: ptr) { + var value_3: vec3f; + var normal_scale_2: f32; + var N_25: vec3f; + var T_2: vec3f; + var B_2: vec3f; + + value_3 = value_2; + normal_scale_2 = normal_scale_1; + N_25 = N_24; + T_2 = T_1; + B_2 = B_1; + mx_normalmap_vector2(value_3, vec2(normal_scale_2), N_25, T_2, B_2, result_82); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_premult_color4.wgsl b/libraries/stdlib/genwgsl/mx_premult_color4.wgsl new file mode 100644 index 0000000000..fdf898606f --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_premult_color4.wgsl @@ -0,0 +1,11 @@ +// Generated from libraries/stdlib/genglsl/mx_premult_color4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_premult_color4(_in_9: vec4f, result_82: ptr) { + var _in_10: vec4f; + + _in_10 = _in_9; + let _e25 = (_in_10.xyz * _in_10.w); + (*result_82) = vec4f(_e25.x, _e25.y, _e25.z, _in_10.w); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_ramplr_float.wgsl b/libraries/stdlib/genwgsl/mx_ramplr_float.wgsl new file mode 100644 index 0000000000..337cf8b660 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_ramplr_float.wgsl @@ -0,0 +1,14 @@ +// Generated from libraries/stdlib/genglsl/mx_ramplr_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_ramplr_float(valuel_7: f32, valuer_7: f32, texcoord_29: vec2f, result_82: ptr) { + var valuel_8: f32; + var valuer_8: f32; + var texcoord_30: vec2f; + + valuel_8 = valuel_7; + valuer_8 = valuer_7; + texcoord_30 = texcoord_29; + (*result_82) = mix(valuel_8, valuer_8, clamp(texcoord_30.x, 0.0, 1.0)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_ramplr_vector2.wgsl b/libraries/stdlib/genwgsl/mx_ramplr_vector2.wgsl new file mode 100644 index 0000000000..00a3f356de --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_ramplr_vector2.wgsl @@ -0,0 +1,14 @@ +// Generated from libraries/stdlib/genglsl/mx_ramplr_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_ramplr_vector2(valuel_7: vec2f, valuer_7: vec2f, texcoord_29: vec2f, result_82: ptr) { + var valuel_8: vec2f; + var valuer_8: vec2f; + var texcoord_30: vec2f; + + valuel_8 = valuel_7; + valuer_8 = valuer_7; + texcoord_30 = texcoord_29; + (*result_82) = mix(valuel_8, valuer_8, vec2(clamp(texcoord_30.x, 0.0, 1.0))); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_ramplr_vector3.wgsl b/libraries/stdlib/genwgsl/mx_ramplr_vector3.wgsl new file mode 100644 index 0000000000..f82d090533 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_ramplr_vector3.wgsl @@ -0,0 +1,14 @@ +// Generated from libraries/stdlib/genglsl/mx_ramplr_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_ramplr_vector3(valuel_7: vec3f, valuer_7: vec3f, texcoord_29: vec2f, result_82: ptr) { + var valuel_8: vec3f; + var valuer_8: vec3f; + var texcoord_30: vec2f; + + valuel_8 = valuel_7; + valuer_8 = valuer_7; + texcoord_30 = texcoord_29; + (*result_82) = mix(valuel_8, valuer_8, vec3(clamp(texcoord_30.x, 0.0, 1.0))); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_ramplr_vector4.wgsl b/libraries/stdlib/genwgsl/mx_ramplr_vector4.wgsl new file mode 100644 index 0000000000..9f3fec7625 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_ramplr_vector4.wgsl @@ -0,0 +1,14 @@ +// Generated from libraries/stdlib/genglsl/mx_ramplr_vector4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_ramplr_vector4(valuel_7: vec4f, valuer_7: vec4f, texcoord_29: vec2f, result_82: ptr) { + var valuel_8: vec4f; + var valuer_8: vec4f; + var texcoord_30: vec2f; + + valuel_8 = valuel_7; + valuer_8 = valuer_7; + texcoord_30 = texcoord_29; + (*result_82) = mix(valuel_8, valuer_8, vec4(clamp(texcoord_30.x, 0.0, 1.0))); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_ramptb_float.wgsl b/libraries/stdlib/genwgsl/mx_ramptb_float.wgsl new file mode 100644 index 0000000000..e8d6d8c77b --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_ramptb_float.wgsl @@ -0,0 +1,14 @@ +// Generated from libraries/stdlib/genglsl/mx_ramptb_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_ramptb_float(valuet_7: f32, valueb_7: f32, texcoord_29: vec2f, result_82: ptr) { + var valuet_8: f32; + var valueb_8: f32; + var texcoord_30: vec2f; + + valuet_8 = valuet_7; + valueb_8 = valueb_7; + texcoord_30 = texcoord_29; + (*result_82) = mix(valueb_8, valuet_8, clamp(texcoord_30.y, 0.0, 1.0)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_ramptb_vector2.wgsl b/libraries/stdlib/genwgsl/mx_ramptb_vector2.wgsl new file mode 100644 index 0000000000..f5dc86276f --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_ramptb_vector2.wgsl @@ -0,0 +1,14 @@ +// Generated from libraries/stdlib/genglsl/mx_ramptb_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_ramptb_vector2(valuet_7: vec2f, valueb_7: vec2f, texcoord_29: vec2f, result_82: ptr) { + var valuet_8: vec2f; + var valueb_8: vec2f; + var texcoord_30: vec2f; + + valuet_8 = valuet_7; + valueb_8 = valueb_7; + texcoord_30 = texcoord_29; + (*result_82) = mix(valueb_8, valuet_8, vec2(clamp(texcoord_30.y, 0.0, 1.0))); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_ramptb_vector3.wgsl b/libraries/stdlib/genwgsl/mx_ramptb_vector3.wgsl new file mode 100644 index 0000000000..b80cf895bd --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_ramptb_vector3.wgsl @@ -0,0 +1,14 @@ +// Generated from libraries/stdlib/genglsl/mx_ramptb_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_ramptb_vector3(valuet_7: vec3f, valueb_7: vec3f, texcoord_29: vec2f, result_82: ptr) { + var valuet_8: vec3f; + var valueb_8: vec3f; + var texcoord_30: vec2f; + + valuet_8 = valuet_7; + valueb_8 = valueb_7; + texcoord_30 = texcoord_29; + (*result_82) = mix(valueb_8, valuet_8, vec3(clamp(texcoord_30.y, 0.0, 1.0))); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_ramptb_vector4.wgsl b/libraries/stdlib/genwgsl/mx_ramptb_vector4.wgsl new file mode 100644 index 0000000000..3f6da3a6a4 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_ramptb_vector4.wgsl @@ -0,0 +1,14 @@ +// Generated from libraries/stdlib/genglsl/mx_ramptb_vector4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_ramptb_vector4(valuet_7: vec4f, valueb_7: vec4f, texcoord_29: vec2f, result_82: ptr) { + var valuet_8: vec4f; + var valueb_8: vec4f; + var texcoord_30: vec2f; + + valuet_8 = valuet_7; + valueb_8 = valueb_7; + texcoord_30 = texcoord_29; + (*result_82) = mix(valueb_8, valuet_8, vec4(clamp(texcoord_30.y, 0.0, 1.0))); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_rgbtohsv_color3.wgsl b/libraries/stdlib/genwgsl/mx_rgbtohsv_color3.wgsl new file mode 100644 index 0000000000..18b7f9aaa7 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_rgbtohsv_color3.wgsl @@ -0,0 +1,12 @@ +// Generated from libraries/stdlib/genglsl/mx_rgbtohsv_color3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_hsv.wgsl" + +fn mx_rgbtohsv_color3(_in_9: vec3f, result_82: ptr) { + var _in_10: vec3f; + + _in_10 = _in_9; + (*result_82) = mx_rgbtohsv(_in_10); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_rgbtohsv_color4.wgsl b/libraries/stdlib/genwgsl/mx_rgbtohsv_color4.wgsl new file mode 100644 index 0000000000..ee63e187f3 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_rgbtohsv_color4.wgsl @@ -0,0 +1,13 @@ +// Generated from libraries/stdlib/genglsl/mx_rgbtohsv_color4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_hsv.wgsl" + +fn mx_rgbtohsv_color4(_in_9: vec4f, result_82: ptr) { + var _in_10: vec4f; + + _in_10 = _in_9; + let _e23 = mx_rgbtohsv(_in_10.xyz); + (*result_82) = vec4f(_e23.x, _e23.y, _e23.z, _in_10.w); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_rotate_vector2.wgsl b/libraries/stdlib/genwgsl/mx_rotate_vector2.wgsl new file mode 100644 index 0000000000..b4cd3c1f42 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_rotate_vector2.wgsl @@ -0,0 +1,18 @@ +// Generated from libraries/stdlib/genglsl/mx_rotate_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_rotate_vector2(_in_9: vec2f, amount_1: f32, result_82: ptr) { + var _in_10: vec2f; + var amount_2: f32; + var rotationRadians: f32; + var sa: f32; + var ca: f32; + + _in_10 = _in_9; + amount_2 = amount_1; + rotationRadians = radians(amount_2); + sa = sin(rotationRadians); + ca = cos(rotationRadians); + (*result_82) = vec2f(((ca * _in_10.x) + (sa * _in_10.y)), ((-(sa) * _in_10.x) + (ca * _in_10.y))); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_rotate_vector3.wgsl b/libraries/stdlib/genwgsl/mx_rotate_vector3.wgsl new file mode 100644 index 0000000000..5b48b639c6 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_rotate_vector3.wgsl @@ -0,0 +1,23 @@ +// Generated from libraries/stdlib/genglsl/mx_rotate_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_rotate_vector3(_in_9: vec3f, amount_1: f32, axis: vec3f, result_82: ptr) { + var _in_10: vec3f; + var amount_2: f32; + var axis_1: vec3f; + var rotationRadians: f32; + var s_8: f32; + var c_3: f32; + var oc: f32; + + _in_10 = _in_9; + amount_2 = amount_1; + axis_1 = axis; + axis_1 = normalize(axis_1); + rotationRadians = radians(amount_2); + s_8 = sin(rotationRadians); + c_3 = cos(rotationRadians); + oc = (1.0 - c_3); + (*result_82) = (((_in_10 * c_3) + (cross(_in_10, axis_1) * s_8)) + ((axis_1 * dot(axis_1, _in_10)) * oc)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_smoothstep_float.wgsl b/libraries/stdlib/genwgsl/mx_smoothstep_float.wgsl new file mode 100644 index 0000000000..41b3690ead --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_smoothstep_float.wgsl @@ -0,0 +1,20 @@ +// Generated from libraries/stdlib/genglsl/mx_smoothstep_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_smoothstep_float(val_3: f32, low: f32, high: f32, result_82: ptr) { + var val_4: f32; + + val_4 = val_3; + if (val_4 >= high) { + (*result_82) = 1.0; + return; + } else { + if (val_4 <= low) { + (*result_82) = 0.0; + return; + } else { + (*result_82) = smoothstep(low, high, val_4); + return; + } + } +} diff --git a/libraries/stdlib/genwgsl/mx_splitlr_float.wgsl b/libraries/stdlib/genwgsl/mx_splitlr_float.wgsl new file mode 100644 index 0000000000..9c43beb399 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_splitlr_float.wgsl @@ -0,0 +1,18 @@ +// Generated from libraries/stdlib/genglsl/mx_splitlr_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "mx_aastep.wgsl" + +fn mx_splitlr_float(valuel_7: f32, valuer_7: f32, center_7: f32, texcoord_29: vec2f, result_82: ptr) { + var valuel_8: f32; + var valuer_8: f32; + var center_8: f32; + var texcoord_30: vec2f; + + valuel_8 = valuel_7; + valuer_8 = valuer_7; + center_8 = center_7; + texcoord_30 = texcoord_29; + (*result_82) = mix(valuel_8, valuer_8, (mx_aastep(center_8, texcoord_30.x))); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_splitlr_vector2.wgsl b/libraries/stdlib/genwgsl/mx_splitlr_vector2.wgsl new file mode 100644 index 0000000000..4c79fad614 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_splitlr_vector2.wgsl @@ -0,0 +1,18 @@ +// Generated from libraries/stdlib/genglsl/mx_splitlr_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "mx_aastep.wgsl" + +fn mx_splitlr_vector2(valuel_7: vec2f, valuer_7: vec2f, center_7: f32, texcoord_29: vec2f, result_82: ptr) { + var valuel_8: vec2f; + var valuer_8: vec2f; + var center_8: f32; + var texcoord_30: vec2f; + + valuel_8 = valuel_7; + valuer_8 = valuer_7; + center_8 = center_7; + texcoord_30 = texcoord_29; + (*result_82) = mix(valuel_8, valuer_8, vec2((mx_aastep(center_8, texcoord_30.x)))); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_splitlr_vector3.wgsl b/libraries/stdlib/genwgsl/mx_splitlr_vector3.wgsl new file mode 100644 index 0000000000..b2edeb19ad --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_splitlr_vector3.wgsl @@ -0,0 +1,18 @@ +// Generated from libraries/stdlib/genglsl/mx_splitlr_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "mx_aastep.wgsl" + +fn mx_splitlr_vector3(valuel_7: vec3f, valuer_7: vec3f, center_7: f32, texcoord_29: vec2f, result_82: ptr) { + var valuel_8: vec3f; + var valuer_8: vec3f; + var center_8: f32; + var texcoord_30: vec2f; + + valuel_8 = valuel_7; + valuer_8 = valuer_7; + center_8 = center_7; + texcoord_30 = texcoord_29; + (*result_82) = mix(valuel_8, valuer_8, vec3((mx_aastep(center_8, texcoord_30.x)))); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_splitlr_vector4.wgsl b/libraries/stdlib/genwgsl/mx_splitlr_vector4.wgsl new file mode 100644 index 0000000000..b8a02f437a --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_splitlr_vector4.wgsl @@ -0,0 +1,18 @@ +// Generated from libraries/stdlib/genglsl/mx_splitlr_vector4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "mx_aastep.wgsl" + +fn mx_splitlr_vector4(valuel_7: vec4f, valuer_7: vec4f, center_7: f32, texcoord_29: vec2f, result_82: ptr) { + var valuel_8: vec4f; + var valuer_8: vec4f; + var center_8: f32; + var texcoord_30: vec2f; + + valuel_8 = valuel_7; + valuer_8 = valuer_7; + center_8 = center_7; + texcoord_30 = texcoord_29; + (*result_82) = mix(valuel_8, valuer_8, vec4((mx_aastep(center_8, texcoord_30.x)))); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_splittb_float.wgsl b/libraries/stdlib/genwgsl/mx_splittb_float.wgsl new file mode 100644 index 0000000000..2522215d32 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_splittb_float.wgsl @@ -0,0 +1,18 @@ +// Generated from libraries/stdlib/genglsl/mx_splittb_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "mx_aastep.wgsl" + +fn mx_splittb_float(valuet_7: f32, valueb_7: f32, center_7: f32, texcoord_29: vec2f, result_82: ptr) { + var valuet_8: f32; + var valueb_8: f32; + var center_8: f32; + var texcoord_30: vec2f; + + valuet_8 = valuet_7; + valueb_8 = valueb_7; + center_8 = center_7; + texcoord_30 = texcoord_29; + (*result_82) = mix(valueb_8, valuet_8, (mx_aastep(center_8, texcoord_30.y))); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_splittb_vector2.wgsl b/libraries/stdlib/genwgsl/mx_splittb_vector2.wgsl new file mode 100644 index 0000000000..e52470a29a --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_splittb_vector2.wgsl @@ -0,0 +1,18 @@ +// Generated from libraries/stdlib/genglsl/mx_splittb_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "mx_aastep.wgsl" + +fn mx_splittb_vector2(valuet_7: vec2f, valueb_7: vec2f, center_7: f32, texcoord_29: vec2f, result_82: ptr) { + var valuet_8: vec2f; + var valueb_8: vec2f; + var center_8: f32; + var texcoord_30: vec2f; + + valuet_8 = valuet_7; + valueb_8 = valueb_7; + center_8 = center_7; + texcoord_30 = texcoord_29; + (*result_82) = mix(valueb_8, valuet_8, vec2((mx_aastep(center_8, texcoord_30.y)))); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_splittb_vector3.wgsl b/libraries/stdlib/genwgsl/mx_splittb_vector3.wgsl new file mode 100644 index 0000000000..23ff900855 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_splittb_vector3.wgsl @@ -0,0 +1,18 @@ +// Generated from libraries/stdlib/genglsl/mx_splittb_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "mx_aastep.wgsl" + +fn mx_splittb_vector3(valuet_7: vec3f, valueb_7: vec3f, center_7: f32, texcoord_29: vec2f, result_82: ptr) { + var valuet_8: vec3f; + var valueb_8: vec3f; + var center_8: f32; + var texcoord_30: vec2f; + + valuet_8 = valuet_7; + valueb_8 = valueb_7; + center_8 = center_7; + texcoord_30 = texcoord_29; + (*result_82) = mix(valueb_8, valuet_8, vec3((mx_aastep(center_8, texcoord_30.y)))); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_splittb_vector4.wgsl b/libraries/stdlib/genwgsl/mx_splittb_vector4.wgsl new file mode 100644 index 0000000000..bf7cf37c26 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_splittb_vector4.wgsl @@ -0,0 +1,18 @@ +// Generated from libraries/stdlib/genglsl/mx_splittb_vector4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "mx_aastep.wgsl" + +fn mx_splittb_vector4(valuet_7: vec4f, valueb_7: vec4f, center_7: f32, texcoord_29: vec2f, result_82: ptr) { + var valuet_8: vec4f; + var valueb_8: vec4f; + var center_8: f32; + var texcoord_30: vec2f; + + valuet_8 = valuet_7; + valueb_8 = valueb_7; + center_8 = center_7; + texcoord_30 = texcoord_29; + (*result_82) = mix(valueb_8, valuet_8, vec4((mx_aastep(center_8, texcoord_30.y)))); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_surface_unlit.wgsl b/libraries/stdlib/genwgsl/mx_surface_unlit.wgsl new file mode 100644 index 0000000000..cb8ca75c32 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_surface_unlit.wgsl @@ -0,0 +1,9 @@ +// Generated from libraries/stdlib/genglsl/mx_surface_unlit.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_surface_unlit(emission: f32, emission_color: vec3f, transmission: f32, transmission_color: vec3f, opacity: f32, result_82: ptr) { + + (*result_82).color = ((emission * emission_color) * opacity); + (*result_82).transparency = mix(vec3(1.0), (transmission * transmission_color), vec3(opacity)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_transformmatrix_vector2M3.wgsl b/libraries/stdlib/genwgsl/mx_transformmatrix_vector2M3.wgsl new file mode 100644 index 0000000000..560d102e15 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_transformmatrix_vector2M3.wgsl @@ -0,0 +1,15 @@ +// Generated from libraries/stdlib/genglsl/mx_transformmatrix_vector2M3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_transformmatrix_vector2M3(val_3: vec2f, transform_1: mat3x3f, result_82: ptr) { + var val_4: vec2f; + var transform_2: mat3x3f; + var res: vec3f; + + val_4 = val_3; + transform_2 = transform_1; + let _e24 = val_4; + res = (mx_matrix_mul_mat3_vec3(transform_2, vec3f(_e24.x, _e24.y, 1.0))); + (*result_82) = res.xy; + return; +} diff --git a/libraries/stdlib/genwgsl/mx_transformmatrix_vector3M4.wgsl b/libraries/stdlib/genwgsl/mx_transformmatrix_vector3M4.wgsl new file mode 100644 index 0000000000..9ce61a9972 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_transformmatrix_vector3M4.wgsl @@ -0,0 +1,15 @@ +// Generated from libraries/stdlib/genglsl/mx_transformmatrix_vector3M4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_transformmatrix_vector3M4(val_3: vec3f, transform_1: mat4x4f, result_82: ptr) { + var val_4: vec3f; + var transform_2: mat4x4f; + var res: vec4f; + + val_4 = val_3; + transform_2 = transform_1; + let _e24 = val_4; + res = (mx_matrix_mul_mat4_vec4(transform_2, vec4f(_e24.x, _e24.y, _e24.z, 1.0))); + (*result_82) = res.xyz; + return; +} diff --git a/libraries/stdlib/genwgsl/mx_unpremult_color4.wgsl b/libraries/stdlib/genwgsl/mx_unpremult_color4.wgsl new file mode 100644 index 0000000000..38562e7371 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_unpremult_color4.wgsl @@ -0,0 +1,11 @@ +// Generated from libraries/stdlib/genglsl/mx_unpremult_color4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +fn mx_unpremult_color4(_in_9: vec4f, result_82: ptr) { + var _in_10: vec4f; + + _in_10 = _in_9; + let _e26 = (_in_10.xyz / vec3(_in_10.w)); + (*result_82) = vec4f(_e26.x, _e26.y, _e26.z, _in_10.w); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_worleynoise2d_float.wgsl b/libraries/stdlib/genwgsl/mx_worleynoise2d_float.wgsl new file mode 100644 index 0000000000..f0e609d959 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_worleynoise2d_float.wgsl @@ -0,0 +1,16 @@ +// Generated from libraries/stdlib/genglsl/mx_worleynoise2d_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_worleynoise2d_float(texcoord_29: vec2f, jitter_15: f32, style_11: i32, result_82: ptr) { + var texcoord_30: vec2f; + var jitter_16: f32; + var style_12: i32; + + texcoord_30 = texcoord_29; + jitter_16 = jitter_15; + style_12 = style_11; + (*result_82) = (mx_worley_noise_float_2d(texcoord_30, jitter_16, style_12, 0i)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_worleynoise2d_vector2.wgsl b/libraries/stdlib/genwgsl/mx_worleynoise2d_vector2.wgsl new file mode 100644 index 0000000000..aac4b61b68 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_worleynoise2d_vector2.wgsl @@ -0,0 +1,16 @@ +// Generated from libraries/stdlib/genglsl/mx_worleynoise2d_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_worleynoise2d_vector2(texcoord_29: vec2f, jitter_15: f32, style_11: i32, result_82: ptr) { + var texcoord_30: vec2f; + var jitter_16: f32; + var style_12: i32; + + texcoord_30 = texcoord_29; + jitter_16 = jitter_15; + style_12 = style_11; + (*result_82) = (mx_worley_noise_vec2_2d(texcoord_30, jitter_16, style_12, 0i)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_worleynoise2d_vector3.wgsl b/libraries/stdlib/genwgsl/mx_worleynoise2d_vector3.wgsl new file mode 100644 index 0000000000..60459bc30b --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_worleynoise2d_vector3.wgsl @@ -0,0 +1,16 @@ +// Generated from libraries/stdlib/genglsl/mx_worleynoise2d_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_worleynoise2d_vector3(texcoord_29: vec2f, jitter_15: f32, style_11: i32, result_82: ptr) { + var texcoord_30: vec2f; + var jitter_16: f32; + var style_12: i32; + + texcoord_30 = texcoord_29; + jitter_16 = jitter_15; + style_12 = style_11; + (*result_82) = (mx_worley_noise_vec3_2d(texcoord_30, jitter_16, style_12, 0i)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_worleynoise3d_float.wgsl b/libraries/stdlib/genwgsl/mx_worleynoise3d_float.wgsl new file mode 100644 index 0000000000..d357127090 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_worleynoise3d_float.wgsl @@ -0,0 +1,16 @@ +// Generated from libraries/stdlib/genglsl/mx_worleynoise3d_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_worleynoise3d_float(position_13: vec3f, jitter_15: f32, style_11: i32, result_82: ptr) { + var position_14: vec3f; + var jitter_16: f32; + var style_12: i32; + + position_14 = position_13; + jitter_16 = jitter_15; + style_12 = style_11; + (*result_82) = (mx_worley_noise_float_3d(position_14, jitter_16, style_12, 0i)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_worleynoise3d_vector2.wgsl b/libraries/stdlib/genwgsl/mx_worleynoise3d_vector2.wgsl new file mode 100644 index 0000000000..8de97aea46 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_worleynoise3d_vector2.wgsl @@ -0,0 +1,16 @@ +// Generated from libraries/stdlib/genglsl/mx_worleynoise3d_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_worleynoise3d_vector2(position_13: vec3f, jitter_15: f32, style_11: i32, result_82: ptr) { + var position_14: vec3f; + var jitter_16: f32; + var style_12: i32; + + position_14 = position_13; + jitter_16 = jitter_15; + style_12 = style_11; + (*result_82) = (mx_worley_noise_vec2_3d(position_14, jitter_16, style_12, 0i)); + return; +} diff --git a/libraries/stdlib/genwgsl/mx_worleynoise3d_vector3.wgsl b/libraries/stdlib/genwgsl/mx_worleynoise3d_vector3.wgsl new file mode 100644 index 0000000000..3ec74a4d10 --- /dev/null +++ b/libraries/stdlib/genwgsl/mx_worleynoise3d_vector3.wgsl @@ -0,0 +1,16 @@ +// Generated from libraries/stdlib/genglsl/mx_worleynoise3d_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. +// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). + +#include "lib/mx_noise.wgsl" + +fn mx_worleynoise3d_vector3(position_13: vec3f, jitter_15: f32, style_11: i32, result_82: ptr) { + var position_14: vec3f; + var jitter_16: f32; + var style_12: i32; + + position_14 = position_13; + jitter_16 = jitter_15; + style_12 = style_11; + (*result_82) = (mx_worley_noise_vec3_3d(position_14, jitter_16, style_12, 0i)); + return; +} diff --git a/libraries/stdlib/genwgsl/stdlib_genwgsl_impl.mtlx b/libraries/stdlib/genwgsl/stdlib_genwgsl_impl.mtlx new file mode 100644 index 0000000000..815e34d8cf --- /dev/null +++ b/libraries/stdlib/genwgsl/stdlib_genwgsl_impl.mtlx @@ -0,0 +1,768 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libraries/targets/genwgsl.mtlx b/libraries/targets/genwgsl.mtlx new file mode 100644 index 0000000000..438095a5f4 --- /dev/null +++ b/libraries/targets/genwgsl.mtlx @@ -0,0 +1,14 @@ + + + + + + + + + + + From f12bb7ea0c0e9ff7f60caf859e14dc13fcc5dcbf Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Thu, 2 Jul 2026 17:51:38 -0700 Subject: [PATCH 03/19] Add three.js/WebGPU viewer backend driven by WgslShaderGenerator Render MaterialX materials over WebGPU by using the native WgslShaderGenerator The main change here to support Three.js wgslFn: WGSL function node, a TSL function. https://threejs.org/docs/#FunctionNode - mxtsladapter.js: converts the complete WGSL module to TSL-portable format - wgslmanifest.js: reconstruct the reflection manifest in JS from the generated WGSL + Shader uniform ports - viewer.js/index.js: dual WebGL (ESSL) + WebGPU (WGSL) backends selected per bundle; WebGPU path uses WebGPURenderer + the TSL bridge. --- javascript/MaterialXView/source/helper.js | 9 +- javascript/MaterialXView/source/index.js | 137 +- .../MaterialXView/source/mxtsladapter.js | 1226 +++++++++++++++++ javascript/MaterialXView/source/viewer.js | 244 +++- .../MaterialXView/source/wgslmanifest.js | 218 +++ javascript/MaterialXView/webpack.config.js | 106 +- 6 files changed, 1857 insertions(+), 83 deletions(-) create mode 100644 javascript/MaterialXView/source/mxtsladapter.js create mode 100644 javascript/MaterialXView/source/wgslmanifest.js diff --git a/javascript/MaterialXView/source/helper.js b/javascript/MaterialXView/source/helper.js index 417512f666..a50153e7d3 100644 --- a/javascript/MaterialXView/source/helper.js +++ b/javascript/MaterialXView/source/helper.js @@ -21,7 +21,9 @@ export function prepareEnvTexture(texture, capabilities) { let newTexture = new THREE.DataTexture(texture.image.data, texture.image.width, texture.image.height, texture.format, texture.type); newTexture.wrapS = THREE.RepeatWrapping; - newTexture.anisotropy = capabilities.getMaxAnisotropy(); + // WebGLRenderer exposes getMaxAnisotropy(); guard for WebGPURenderer capabilities. + newTexture.anisotropy = (capabilities && typeof capabilities.getMaxAnisotropy === 'function') + ? capabilities.getMaxAnisotropy() : 1; newTexture.minFilter = THREE.LinearMipmapLinearFilter; newTexture.magFilter = THREE.LinearFilter; newTexture.generateMipmaps = true; @@ -270,7 +272,9 @@ export function getLightRotation() export function findLights(doc) { let lights = []; - for (let node of doc.getNodes()) + const nodes = doc.getNodes(); + if (!nodes) return lights; + for (let node of nodes) { if (node.getType() === "lightshader") lights.push(node); @@ -292,6 +296,7 @@ export function registerLights(mx, lights, genContext) const lightTypesBound = {}; const lightData = []; let lightId = 1; + if (!lights) return lightData; for (let light of lights) { let nodeDef = light.getNodeDef(); diff --git a/javascript/MaterialXView/source/index.js b/javascript/MaterialXView/source/index.js index 2840261a40..9998993147 100644 --- a/javascript/MaterialXView/source/index.js +++ b/javascript/MaterialXView/source/index.js @@ -8,7 +8,15 @@ import { Viewer } from './viewer.js' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { dropHandler, dragOverHandler, setLoadingCallback, setSceneLoadingCallback } from './dropHandling.js'; +// Rendering backend, selected at build time per bundle (see webpack.config.js). The WebGL +// bundle uses classic THREE.WebGLRenderer + RawShaderMaterial (ESSL); the WebGPU bundle +// aliases `three` to three/webgpu and uses WebGPURenderer + NodeMaterial (WGSL via the +// upstream WgslShaderGenerator). A toggle switches between the two HTML pages. +const BACKEND = (typeof __BACKEND__ !== 'undefined') ? __BACKEND__ : 'webgl'; +let TSL = null; // three/tsl namespace, loaded for the WebGPU backend only + let renderer, orbitControls; +let webgpuSupported = null; // cached result of navigator.gpu probe // FPS overlay state let fpsOverlay = null; @@ -50,8 +58,32 @@ function setFPSOverlayVisible(visible) { createFPSOverlay(); setFPSOverlayVisible(showFPS); -init(); -viewer.getEditor().updateProperties(0.9); +probeWebGPU().then(() => +{ + init(); + viewer.getEditor().updateProperties(0.9); +}); + +/** Probe WebGPU availability once; used by the backend toggle and init fallback. */ +async function probeWebGPU() +{ + if (webgpuSupported !== null) return webgpuSupported; + if (!navigator.gpu) + { + webgpuSupported = false; + return false; + } + try + { + const adapter = await navigator.gpu.requestAdapter(); + webgpuSupported = !!adapter; + } + catch + { + webgpuSupported = false; + } + return webgpuSupported; +} // Capture the current frame and save an image file. function captureFrame() @@ -94,11 +126,26 @@ function init() // Set up scene scene.initialize(); - // Set up renderer - renderer = new THREE.WebGLRenderer({ antialias: true, canvas }); + // Set up renderer for the selected backend. + if (BACKEND === 'webgpu') + { + if (!webgpuSupported) + { + showWebGPUFallbackBanner(); + return; + } + renderer = new THREE.WebGPURenderer({ antialias: true, canvas }); + } + else + { + renderer = new THREE.WebGLRenderer({ antialias: true, canvas }); + renderer.debug.checkShaderErrors = false; + } renderer.setSize(window.innerWidth, window.innerHeight); + // WebGL encodes sRGB in the pixel shader; WebGPU outputs linear and relies on this conversion. renderer.outputColorSpace = THREE.SRGBColorSpace; - renderer.debug.checkShaderErrors = false; + + addBackendToggle(webgpuSupported); window.addEventListener('resize', onWindowResize); @@ -134,8 +181,17 @@ function init() .then(({ default: MaterialX }) => MaterialX()) ]).then(async ([radianceTexture, irradianceTexture, lightRigXml, mxIn]) => { + // WebGPU: load the TSL namespace (used by the WGSL→NodeMaterial bridge) and + // initialize the device before the first render. + if (BACKEND === 'webgpu') + { + TSL = await import('three/tsl'); + await renderer.init(); + } + // Initialize viewer + lighting - await viewer.initialize(mxIn, renderer, radianceTexture, irradianceTexture, lightRigXml); + await viewer.initialize(mxIn, renderer, radianceTexture, irradianceTexture, lightRigXml, + { backend: BACKEND, TSL }); // Load geometry let scene = viewer.getScene(); @@ -154,7 +210,13 @@ function init() animate(); }).catch(err => { - console.error(Number.isInteger(err) ? this.getMx().getExceptionMessage(err) : err); + const mx = viewer.getMx(); + const message = (Number.isInteger(err) && mx) + ? mx.getExceptionMessage(err) + : (err && err.message ? err.message : err); + console.error(message, err); + if (BACKEND === 'webgpu') + showWebGPUFallbackBanner('WebGPU initialization failed. Use the WebGL view instead.'); }) // allow dropping files and directories @@ -187,6 +249,67 @@ function onWindowResize() renderer.setSize(window.innerWidth, window.innerHeight); } +// Floating toggle to switch rendering backend. Each backend is a separate bundle/page +// (different three build), so switching navigates between index.html and index-webgpu.html +// while preserving the current ?file/?geom query so the comparison stays on the same content. +function addBackendToggle(gpuAvailable) +{ + const search = window.location.search; + const wrap = document.createElement('div'); + // Bottom-center: clear of the material/geometry selectors (top-left), the property + // editor (top-right), and the FPS overlay (bottom-left). + wrap.style.cssText = 'position:fixed;bottom:10px;left:50%;transform:translateX(-50%);z-index:1000;' + + 'display:flex;gap:4px;flex-wrap:wrap;align-items:center;' + + 'font-family:sans-serif;font-size:12px;background:rgba(0,0,0,0.5);padding:4px 6px;border-radius:4px;max-width:90vw;'; + const label = document.createElement('span'); + label.textContent = 'Renderer:'; + label.style.cssText = 'color:#ccc;align-self:center;'; + wrap.appendChild(label); + + for (const [name, page] of [['WebGL', 'index.html'], ['WebGPU', 'index-webgpu.html']]) + { + const isWebGPU = (name === 'WebGPU'); + const active = (name.toLowerCase() === BACKEND); + const disabled = isWebGPU && gpuAvailable === false; + const btn = document.createElement('button'); + btn.textContent = name; + btn.title = disabled + ? 'WebGPU is not available in this browser' + : (isWebGPU + ? 'WebGPU uses MaterialX WGSL + TSL NodeMaterial (same light rig and IBL as WebGL)' + : 'WebGL uses full MaterialX light rig + GLSL ES shaders'); + btn.style.cssText = 'border:none;border-radius:3px;padding:3px 8px;' + + (active ? 'background:#4a9;color:#fff;font-weight:bold;' : + disabled ? 'background:#222;color:#666;cursor:not-allowed;' : + 'background:#333;color:#bbb;cursor:pointer;'); + if (!active && !disabled) + { + btn.addEventListener('click', () => + { + btn.textContent = '…'; + btn.disabled = true; + window.location.href = page + search; + }); + } + wrap.appendChild(btn); + } + document.body.appendChild(wrap); +} + +function showWebGPUFallbackBanner(message) +{ + const search = window.location.search; + const banner = document.createElement('div'); + banner.style.cssText = 'position:fixed;inset:0;display:flex;align-items:center;justify-content:center;' + + 'background:rgba(0,0,0,0.85);color:#eee;font-family:sans-serif;font-size:14px;z-index:2000;padding:24px;text-align:center;'; + const link = 'index.html' + search; + banner.innerHTML = '

' + + (message || 'WebGPU is not available in this browser.') + + '

Open WebGL view
'; + document.body.appendChild(banner); + addBackendToggle(false); +} + function animate() { requestAnimationFrame(animate); diff --git a/javascript/MaterialXView/source/mxtsladapter.js b/javascript/MaterialXView/source/mxtsladapter.js new file mode 100644 index 0000000000..5c4f16ee0c --- /dev/null +++ b/javascript/MaterialXView/source/mxtsladapter.js @@ -0,0 +1,1226 @@ +/** + * mxtsladapter + * + * Adapts MaterialX-generated WGSL into Three.js TSL / WebGPU. + * + * The MaterialX `WgslShaderGenerator` emits a complete, standard WGSL module: + * helper structs + functions, a uniform block + texture/sampler bindings declared + * with `@group(N) @binding(M)`, and a single material entry function that returns + * the surface result. Three.js never ingests such a module directly — its only raw + * WGSL entry point is `wgslFn`, which accepts exactly one function whose resources + * arrive as *parameters* (TSL owns the bindings). + * + * This module performs that reshaping ("TSL-portable WGSL"): + * - `convertToTslPortable(wgsl, manifest)` rewrites the entry function so that the + * uniform-block members and texture/sampler bindings become explicit parameters, + * and strips the now-unused `@group/@binding` declarations and uniform struct. + * Everything else (helper structs/functions) is returned verbatim as `includes`. + * - `createMxWgslMaterial({ THREE, TSL, ... })` wires the converted entry up + * with `wgslFn`, binding each parameter to the matching TSL node + * (`uniform`/`texture`/`sampler`/builtin), and returns a NodeMaterial. + * + * The manifest is emitted alongside the WGSL by the generator and makes the + * conversion deterministic (no fragile parsing of binding layouts). + */ + +// ── WGSL text utilities ───────────────────────────────────────────────────── + +function skipWs( src, i ) { + + while ( i < src.length && /\s/.test( src[ i ] ) ) i ++; + return i; + +} + +// Escape a string for safe use as a literal inside a RegExp. The function/resource names +// here are generator-emitted WGSL identifiers, but escaping keeps the matching correct if +// that ever changes and documents the intent. +function escapeRegExp( s ) { + + return s.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' ); + +} + +const WGSL_TYPE_ALIGN = { + i32: 4, u32: 4, f32: 4, bool: 4, + vec2f: 8, 'vec2': 8, + vec3f: 16, 'vec3': 16, + vec4f: 16, 'vec4': 16 +}; + +const WGSL_TYPE_SIZE = { + i32: 4, u32: 4, f32: 4, bool: 4, + vec2f: 8, 'vec2': 8, + vec3f: 12, 'vec3': 12, + vec4f: 16, 'vec4': 16 +}; + +/** WGSL uniform struct layout (WebGPU host-shareable rules). */ +function layoutUniformStruct( fields ) { + + let offset = 0; + let maxAlign = 4; + const placements = []; + for ( const field of fields ) { + + const align = WGSL_TYPE_ALIGN[ field.type ] || 4; + const size = WGSL_TYPE_SIZE[ field.type ] || 4; + offset = Math.ceil( offset / align ) * align; + placements.push( { name: field.name, type: field.type, offset } ); + offset += size; + maxAlign = Math.max( maxAlign, align ); + + } + const structSize = Math.ceil( offset / maxAlign ) * maxAlign; + return { placements, structSize, vec4Stride: structSize / 16 }; + +} + +/** Parse `array` from a manifest uniform type string. */ +function parseStructArrayType( typeStr, wgslSource, numLights, lightData, layout = null ) { + + const type = typeStr || ''; + const flatVec4 = type.match( /array/ ); + if ( flatVec4 ) { + + const flatCount = parseInt( flatVec4[ 1 ], 10 ); + const stride = layout?.vec4Stride || 1; + return { + structName: 'LightData', + count: Math.ceil( flatCount / stride ), + vec4Stride: stride, + flatCount + }; + + } + const numeric = type.match( /array<(\w+),\s*(\d+)>/ ); + if ( numeric ) { + + const count = parseInt( numeric[ 2 ], 10 ); + return { + structName: numeric[ 1 ], + count, + vec4Stride: layout?.vec4Stride || 1, + flatCount: layout ? count * layout.vec4Stride : count + }; + + } + const symbolic = type.match( /array<(\w+),\s*(\w+)>/ ); + const structName = symbolic ? symbolic[ 1 ] : 'LightData'; + let count = Math.max( numLights || 0, lightData?.length || 0, 1 ); + if ( symbolic && wgslSource ) { + + const constRe = new RegExp( `const\\s+${ escapeRegExp( symbolic[ 2 ] ) }:\\s*i32\\s*=\\s*(\\d+)` ); + const m = wgslSource.match( constRe ); + if ( m ) count = parseInt( m[ 1 ], 10 ); + + } + const vec4Stride = layout?.vec4Stride || 1; + return { structName, count, vec4Stride, flatCount: count * vec4Stride }; + +} + +/** Parse `struct LightData { ... }` from generated WGSL. */ +export function parseLightDataStruct( wgsl ) { + + const m = wgsl.match( /struct\s+LightData\s*\{([^}]+)\}/ ); + if ( ! m ) return null; + const fields = []; + for ( const line of m[ 1 ].split( '\n' ) ) { + + const trimmed = line.trim().replace( /,$/, '' ); + if ( ! trimmed ) continue; + const colon = trimmed.indexOf( ':' ); + if ( colon < 0 ) continue; + fields.push( { + name: trimmed.slice( 0, colon ).trim(), + type: trimmed.slice( colon + 1 ).trim() + } ); + + } + return fields.length ? layoutUniformStruct( fields ) : null; + +} + +/** + * TSL `uniformArray` only binds `array`. Flatten `array` to + * `array` and add an unpack helper for indexed struct access. + */ +function flattenLightDataUniform( wgsl ) { + + const layout = parseLightDataStruct( wgsl ); + if ( ! layout ) return { wgsl, layout: null }; + + const maxMatch = wgsl.match( /array/ ); + let maxCount = 1; + if ( maxMatch ) { + + const sym = maxMatch[ 1 ]; + const constM = wgsl.match( new RegExp( `const\\s+${ escapeRegExp( sym ) }:\\s*i32\\s*=\\s*(\\d+)` ) ); + maxCount = constM ? parseInt( constM[ 1 ], 10 ) : 1; + + } + const flatCount = maxCount * layout.vec4Stride; + const stride = layout.vec4Stride; + + const unpackLines = [ 'var out: LightData;' ]; + for ( const p of layout.placements ) { + + const vec4Index = Math.floor( p.offset / 16 ); + const comp = [ 'x', 'y', 'z', 'w' ][ ( p.offset % 16 ) / 4 ]; + if ( p.type === 'vec3f' || p.type === 'vec3' ) { + + unpackLines.push( `out.${ p.name } = data[base + ${ vec4Index }u].xyz;` ); + + } else if ( p.type === 'i32' ) { + + unpackLines.push( `out.${ p.name } = bitcast(data[base + ${ vec4Index }u].${ comp });` ); + + } else if ( p.type === 'f32' ) { + + unpackLines.push( `out.${ p.name } = data[base + ${ vec4Index }u].${ comp };` ); + + } + + } + const helper = [ + `fn mx_lightData_at(data: array, index: i32) -> LightData {`, + `\tlet base: u32 = u32(index) * ${ stride }u;`, + ...unpackLines.map( ( l ) => `\t${ l }` ), + '\treturn out;', + '}' + ].join( '\n' ); + + let out = wgsl.replace( /struct\s+LightData\s*\{/, `${ helper }\n\nstruct LightData {` ); + out = out.replace( /array]+>/g, `array` ); + out = out.replace( /u_lightData\[([^\]]+)\]/g, 'mx_lightData_at(u_lightData, $1)' ); + return { wgsl: out, layout }; + +} + +function vec3ToArray( v ) { + + if ( Array.isArray( v ) ) return v; + if ( v && v.x != null ) return [ v.x, v.y, v.z ]; + return [ 0, 0, 0 ]; + +} + +// Reused buffer for packing i32 fields into vec4f slots (WGSL unpack uses bitcast). +const _intBitsView = new DataView( new ArrayBuffer( 4 ) ); + +/** Store an i32 in a float32 uniform slot preserving bit pattern for WGSL bitcast. */ +function floatFromIntBits( i32 ) { + + _intBitsView.setInt32( 0, i32 | 0, true ); + return _intBitsView.getFloat32( 0, true ); + +} + +/** + * Pack light slots into a flat vec4 array matching WGSL uniform struct layout. + * Inactive slots are zeroed so the WGSL light loop can skip them via u_numActiveLightSources. + */ +export function packLightDataToVec4Array( lightData, maxCount, layout ) { + + const vec4Count = maxCount * layout.vec4Stride; + const floats = new Array( vec4Count * 4 ).fill( 0 ); + const floatStride = layout.vec4Stride * 4; + for ( let i = 0; i < maxCount; i ++ ) { + + const l = lightData && lightData[ i ]; + const base = i * floatStride; + if ( ! l ) continue; + for ( const p of layout.placements ) { + + const floatIndex = base + p.offset / 4; + if ( p.type === 'i32' || p.type === 'u32' ) { + + let intVal = 0; + if ( p.name === 'light_type' || p.name === 'type' ) { + + intVal = l.light_type != null ? l.light_type : ( l.type | 0 ); + + } else if ( l[ p.name ] != null ) { + + intVal = l[ p.name ] | 0; + + } + floats[ floatIndex ] = floatFromIntBits( intVal ); + + } else if ( p.name === 'direction' ) { + + const d = vec3ToArray( l.direction ); + floats[ floatIndex ] = d[ 0 ]; + floats[ floatIndex + 1 ] = d[ 1 ]; + floats[ floatIndex + 2 ] = d[ 2 ]; + + } else if ( p.name === 'color' ) { + + const c = vec3ToArray( l.color ); + floats[ floatIndex ] = c[ 0 ]; + floats[ floatIndex + 1 ] = c[ 1 ]; + floats[ floatIndex + 2 ] = c[ 2 ]; + + } else if ( p.name === 'intensity' ) { + + floats[ floatIndex ] = l.intensity != null ? l.intensity : 0; + + } + + } + + } + const out = []; + for ( let i = 0; i < vec4Count; i ++ ) { + + const b = i * 4; + out.push( { x: floats[ b ], y: floats[ b + 1 ], z: floats[ b + 2 ], w: floats[ b + 3 ] } ); + + } + return out; + +} + +/** + * Extract a top-level `fn ( ) -> { }` by name. + * Returns { start, end, params, ret, body } where [start,end) spans the whole + * definition in `src`. Throws if not found. + */ +function extractFunction( src, name ) { + + const re = new RegExp( `\\bfn\\s+${ escapeRegExp( name ) }\\s*\\(`, 'g' ); + const m = re.exec( src ); + if ( m === null ) throw new Error( `mxtsladapter: entry function '${ name }' not found` ); + + const start = m.index; + let i = m.index + m[ 0 ].length - 1; // at '(' + + // Match the parameter-list parentheses. + let depth = 0; + const paramStart = i + 1; + let paramEnd = - 1; + for ( ; i < src.length; i ++ ) { + + if ( src[ i ] === '(' ) depth ++; + else if ( src[ i ] === ')' ) { depth --; if ( depth === 0 ) { paramEnd = i; break; } } + + } + + if ( paramEnd < 0 ) throw new Error( `mxtsladapter: unterminated parameter list for '${ name }'` ); + + const params = src.slice( paramStart, paramEnd ); + + // Optional `-> type` then the body braces. + let j = skipWs( src, paramEnd + 1 ); + let ret = ''; + if ( src[ j ] === '-' && src[ j + 1 ] === '>' ) { + + j = skipWs( src, j + 2 ); + const retStart = j; + while ( j < src.length && src[ j ] !== '{' ) j ++; + ret = src.slice( retStart, j ).trim(); + + } + + if ( src[ j ] !== '{' ) throw new Error( `mxtsladapter: missing body for '${ name }'` ); + + const bodyStart = j; + depth = 0; + let bodyEnd = - 1; + for ( ; j < src.length; j ++ ) { + + if ( src[ j ] === '{' ) depth ++; + else if ( src[ j ] === '}' ) { depth --; if ( depth === 0 ) { bodyEnd = j + 1; break; } } + + } + + if ( bodyEnd < 0 ) throw new Error( `mxtsladapter: unterminated body for '${ name }'` ); + + return { + start, + end: bodyEnd, + params: params.trim(), + ret, + body: src.slice( bodyStart + 1, bodyEnd - 1 ) // inside the outer braces + }; + +} + +/** Remove every line that declares a `@group(...) @binding(...)` resource. */ +function removeBindingDecls( src ) { + + return src + .split( '\n' ) + .filter( ( line ) => ! /^\s*@group\s*\(/.test( line ) ) + .join( '\n' ); + +} + +/** Index of the `)` matching the `(` at `openIdx` in `src`. */ +function matchParen( src, openIdx ) { + + let depth = 0; + for ( let i = openIdx; i < src.length; i ++ ) { + + if ( src[ i ] === '(' ) depth ++; + else if ( src[ i ] === ')' ) { depth --; if ( depth === 0 ) return i; } + + } + return - 1; + +} + +/** + * Thread private/global HW resources through the call chain. + * + * The MaterialX library functions (IBL, transmission, lighting) reference the HW private + * uniforms + env maps as *module-scope globals* (`u_envMatrix`, `u_viewPosition`, + * `u_lightData`, the env textures, …). Three.js `wgslFn` forbids module-scope resources in + * `includes`, so any function that (transitively) touches one of them must receive it as a + * parameter, and every call site must forward it. + * + * Only these private/global resources are threaded. Public material inputs (semantic + * `uniform`) and material image textures (semantic `texture`/`sampler`) must NOT be threaded: + * they are part of the MaterialX node interface and are already passed explicitly down the + * node-graph call chain as named parameters (e.g. `NG_..._surfaceshader_100(base, base_color, …)`). + * Threading them would re-append them to functions that already declare them as parameters or + * use them as local variable names, producing WGSL "redeclaration of '…'" errors. + * + * The entry function is left unmodified — `convertToTslPortable` adds the resources to it as + * parameters from the manifest, and call sites in the entry body forward them. + * + * @param {string} wgsl - The full generated WGSL module. + * @param {Object} manifest - Generator manifest (uniforms + textures after normalization). + * @return {string} The rewritten module (unchanged if there are no private resources). + */ +export function threadEnvResources( wgsl, manifest ) { + + // The generator references geometry varyings through the MaterialX vertex-data instance + // `vd` (e.g. `vd.normalWorld`). The entry exposes the `VertexData` members as flat + // parameters (and the assembler binds them to TSL builtins), so flatten the struct access + // to bare member names module-wide. The varyings are then threaded like any other global + // below, so library helpers (e.g. the surface-shader node) that read them get them as + // parameters forwarded from the entry. `\bvd\.` only matches the vertex-data instance. + wgsl = wgsl.replace( /\bvd\./g, '' ); + + // The native WgslShaderGenerator threads the whole `vd: VertexData` struct into closure / + // surface-shader functions as an explicit parameter (and forwards `vd` at each call site), + // because standalone WGSL has no module-scope varyings. The TSL bridge instead threads the + // individual varyings (flattened to bare names above and re-added as params below), so the + // struct parameter and its forwarded argument are redundant — strip them. The entry's sole + // `vd: VertexData` parameter is intentionally left intact so `convertToTslPortable` can still + // detect the struct entry and rebuild it from the manifest's flat varying members. + wgsl = wgsl.replace( /\bvd\s*:\s*VertexData\s*,\s*/g, '' ); // first parameter of a helper + wgsl = wgsl.replace( /,\s*vd\s*:\s*VertexData\b/g, '' ); // later parameter of a helper + wgsl = wgsl.replace( /\(\s*vd\s*,\s*/g, '(' ); // forwarded `vd` as first argument + wgsl = wgsl.replace( /\(\s*vd\s*\)/g, '()' ); // forwarded `vd` as sole argument + + // A resource is "private/global" (referenced as a module-scope global inside library + // helpers, hence threaded) when it is an env map or a non-public uniform. Public material + // inputs (`uniform`) and material image textures (`texture`/`sampler`) flow through the + // node-graph parameter chain and are excluded. + const isPrivateTexture = ( t ) => ( t.semantic || '' ).startsWith( 'env:' ); + const isPrivateUniform = ( u ) => ( u.semantic || 'uniform' ) !== 'uniform'; + + // Ordered resource list (signature decls + call args), data-driven from the manifest. + const resourceParts = []; + + // Vertex-data varyings (now bare member names after the `vd.` flatten above). These are + // genuine module inputs that helpers reference globally, so they must be threaded too. + for ( const p of manifest.entryParams || [] ) { + + if ( ! ( p.semantic || '' ).startsWith( 'varying:' ) ) continue; + resourceParts.push( { decl: `${ p.name }: ${ p.type }`, arg: p.name } ); + + } + for ( const t of manifest.textures || [] ) { + + if ( ! isPrivateTexture( t ) ) continue; + resourceParts.push( { decl: `${ t.texture }: ${ t.wgslType }`, arg: t.texture } ); + resourceParts.push( { decl: `${ t.sampler }: sampler`, arg: t.sampler } ); + + } + for ( const u of manifest.uniforms || [] ) { + + if ( ! isPrivateUniform( u ) ) continue; + resourceParts.push( { decl: `${ u.name }: ${ u.type }`, arg: u.name } ); + + } + if ( resourceParts.length === 0 ) return wgsl; + + const resourceNames = resourceParts.map( ( p ) => p.arg ); + const paramsStr = resourceParts.map( ( p ) => p.decl ).join( ', ' ); + const argsStr = resourceParts.map( ( p ) => p.arg ).join( ', ' ); + + // Enumerate all top-level `fn name( params ) ... { body }` definitions. + const fnRe = /\bfn\s+([A-Za-z_]\w*)\s*\(/g; + const fns = []; + for ( let m = fnRe.exec( wgsl ); m !== null; m = fnRe.exec( wgsl ) ) { + + const openParen = m.index + m[ 0 ].length - 1; + const closeParen = matchParen( wgsl, openParen ); + const braceOpen = wgsl.indexOf( '{', closeParen ); + // Match the body braces to know where this function ends. + let depth = 0, braceEnd = - 1; + for ( let i = braceOpen; i < wgsl.length; i ++ ) { + + if ( wgsl[ i ] === '{' ) depth ++; + else if ( wgsl[ i ] === '}' ) { depth --; if ( depth === 0 ) { braceEnd = i; break; } } + + } + fns.push( { name: m[ 1 ], openParen, closeParen, bodyStart: braceOpen, bodyEnd: braceEnd } ); + + } + + const byName = Object.fromEntries( fns.map( ( f ) => [ f.name, f ] ) ); + const bodyOf = ( f ) => wgsl.slice( f.bodyStart, f.bodyEnd + 1 ); + + // Fixpoint: a function "needs" resources if its body references one directly, or + // calls another function that needs them. + const needs = new Set(); + const refsResource = ( body ) => resourceNames.some( ( n ) => new RegExp( `\\b${ escapeRegExp( n ) }\\b` ).test( body ) ); + for ( const f of fns ) if ( refsResource( bodyOf( f ) ) ) needs.add( f.name ); + + for ( let changed = true; changed; ) { + + changed = false; + for ( const f of fns ) { + + if ( needs.has( f.name ) ) continue; + const body = bodyOf( f ); + for ( const callee of needs ) { + + if ( new RegExp( `\\b${ escapeRegExp( callee ) }\\s*\\(` ).test( body ) ) { needs.add( f.name ); changed = true; break; } + + } + + } + + } + + if ( needs.size === 0 ) return wgsl; + + const entryName = manifest.entry; + + // Collect insertions (index -> text). Apply right-to-left so indices stay valid. + const inserts = []; + + // 1. Append params to the signature of each needing function (except the entry). + for ( const f of fns ) { + + if ( ! needs.has( f.name ) || f.name === entryName ) continue; + const hasParams = wgsl.slice( f.openParen + 1, f.closeParen ).trim().length > 0; + inserts.push( { at: f.closeParen, text: ( hasParams ? ', ' : '' ) + paramsStr } ); + + } + + // 2. Append args at every call site of a needing function (anywhere in the module). + for ( const callee of needs ) { + + const callRe = new RegExp( `\\b${ escapeRegExp( callee ) }\\s*\\(`, 'g' ); + for ( let m = callRe.exec( wgsl ); m !== null; m = callRe.exec( wgsl ) ) { + + // Skip the definition itself (`fn callee(`). + const before = wgsl.slice( Math.max( 0, m.index - 4 ), m.index ); + if ( /\bfn\s$/.test( before ) ) continue; + const open = m.index + m[ 0 ].length - 1; + const close = matchParen( wgsl, open ); + if ( close < 0 ) continue; + const hasArgs = wgsl.slice( open + 1, close ).trim().length > 0; + inserts.push( { at: close, text: ( hasArgs ? ', ' : '' ) + argsStr } ); + + } + + } + + inserts.sort( ( a, b ) => b.at - a.at ); + let out = wgsl; + for ( const ins of inserts ) out = out.slice( 0, ins.at ) + ins.text + out.slice( ins.at ); + return out; + +} + +// ── Reflection normalization (neutral C++ → TSL semantics) ───────────────── + +function varyingSemantic( name ) { + + if ( name.includes( 'normal' ) ) return 'varying:normalWorld'; + if ( name.includes( 'tangent' ) ) return 'varying:tangentWorld'; + if ( name.includes( 'position' ) ) return 'varying:positionWorld'; + return 'varying:uv'; + +} + +function buildTslSemantics( binding ) { + + const { name, role } = binding; + const HOST = { + u_viewPosition: 'camera:viewPosition', + u_numActiveLightSources: 'light:count', + u_lightData: 'light:data', + u_envMatrix: 'env:matrix', + u_envRadianceMips: 'env:radianceMips', + u_envRadianceSamples: 'env:radianceSamples', + u_envLightIntensity: 'env:lightIntensity' + }; + if ( HOST[ name ] ) return HOST[ name ]; + if ( name === 'u_lightDirection' ) return 'light:direction'; + if ( name === 'u_lightColor' ) return 'light:color'; + if ( role === 'lightData' ) return 'light:data'; + if ( role === 'uniform' ) return 'uniform'; + if ( role === 'texture' ) { + + if ( name.startsWith( 'u_envRadiance' ) ) return 'env:radiance'; + if ( name.startsWith( 'u_envIrradiance' ) ) return 'env:irradiance'; + return 'texture'; + + } + if ( role === 'sampler' ) { + + if ( name.startsWith( 'u_envRadiance' ) ) return 'env:radiance:sampler'; + if ( name.startsWith( 'u_envIrradiance' ) ) return 'env:irradiance:sampler'; + return 'sampler'; + + } + if ( role === 'host' ) return HOST[ name ] || 'host'; + return 'uniform'; + +} + +/** + * Normalize generator reflection into the legacy manifest shape expected by the + * TSL bridge. Accepts both the old manifest (entry string + uniforms/textures) + * and the new neutral format (entry object + bindings + struct entryParams). + * + * @param {Object} reflection - Generator reflection or legacy manifest. + * @return {Object} Manifest with TSL semantics on uniforms, textures, entryParams. + */ +export function normalizeReflection( reflection ) { + + if ( ! reflection ) return reflection; + + // Legacy manifests already carry TSL semantics — return unchanged. + if ( reflection.uniforms?.some( ( u ) => u.semantic ) || reflection.textures?.some( ( t ) => t.semantic ) ) + return reflection; + + if ( ! reflection.bindings ) return reflection; + + const entry = typeof reflection.entry === 'string' + ? reflection.entry + : reflection.entry?.pixel; + + const uniforms = []; + const textures = []; + const textureKeys = new Set(); + + for ( const b of reflection.bindings ) { + + const semantic = buildTslSemantics( b ); + if ( b.role === 'texture' ) { + + const key = b.key || b.name.replace( /_texture$/, '' ); + textures.push( { + key, + texture: b.name, + sampler: null, + wgslType: b.type, + semantic, + file: b.file + } ); + textureKeys.add( key ); + + } else if ( b.role === 'sampler' ) { + + const key = b.key || b.name.replace( /_sampler$/, '' ); + const tex = textures.find( ( t ) => t.key === key ); + if ( tex ) tex.sampler = b.name; + + } else { + + uniforms.push( { + name: b.name, + type: b.type, + semantic, + value: b.value + } ); + + } + + } + + for ( const t of textures ) { + + if ( ! t.sampler ) t.sampler = `${ t.key }_sampler`; + + } + + let entryParams = reflection.entryParams || []; + if ( entryParams.length === 1 && entryParams[ 0 ].members ) { + + entryParams = entryParams[ 0 ].members.map( ( m ) => ( { + name: m.name, + type: m.type, + semantic: varyingSemantic( m.name ) + } ) ); + + } else { + + entryParams = entryParams.map( ( p ) => ( { + ...p, + semantic: p.semantic || varyingSemantic( p.name ) + } ) ); + + } + + return { + entry, + output: reflection.output, + entryParams, + uniforms, + textures + }; + +} + +// ── Conversion ────────────────────────────────────────────────────────────── + +/** + * Convert a complete MaterialX WGSL module into a TSL-portable form. + * + * @param {string} wgsl - The generated WGSL module. + * @param {Object} manifest - Resource description emitted by the generator. + * @return {{ name:string, entry:string, includes:string, params:Array }} + */ +export function convertToTslPortable( wgsl, manifest ) { + + manifest = normalizeReflection( manifest ); + + // Thread module-scope resources through the call chain so `includes` receive them + // as parameters rather than globals (required by Three.js `wgslFn`). + wgsl = threadEnvResources( wgsl, manifest ); + + // Three.js uniformArray only supports primitive array types (vec4/mat4), not struct arrays. + const lightFlatten = flattenLightDataUniform( wgsl ); + wgsl = lightFlatten.wgsl; + const lightLayout = lightFlatten.layout; + + const entryName = manifest.entry; + const fn = extractFunction( wgsl, entryName ); + + const uniforms = manifest.uniforms || []; + const textures = manifest.textures || []; + const entryParams = manifest.entryParams || []; + + // The generated entry already takes the geometry varyings as parameters and + // references uniforms/textures by bare name as module-scope `@group/@binding` + // resources. Hoist those resources into the parameter list (bodies use bare + // names, so no in-body rewriting is needed) and strip the binding declarations. + const newParams = []; + const params = []; + + // 1. Existing entry parameters (varyings). Neutral reflection may pass vertex + // data as a single struct parameter — flatten it for wgslFn and rewrite the body. + const semanticByName = Object.fromEntries( entryParams.map( ( p ) => [ p.name, p.semantic ] ) ); + const flatParams = splitParams( fn.params ); + const isStructEntry = flatParams.length === 1 && entryParams.length > 0 && + entryParams[ 0 ].name !== flatParams[ 0 ].name; + let body = fn.body; + if ( isStructEntry ) { + + const structName = flatParams[ 0 ].name; + for ( const m of entryParams ) { + + newParams.push( `${ m.name }: ${ m.type }` ); + params.push( { name: m.name, type: m.type, semantic: m.semantic || varyingSemantic( m.name ) } ); + + } + body = body.replace( new RegExp( `\\b${ escapeRegExp( structName ) }\\.`, 'g' ), '' ); + + } else { + + for ( const p of flatParams ) { + + newParams.push( `${ p.name }: ${ p.type }` ); + params.push( { name: p.name, type: p.type, semantic: semanticByName[ p.name ] || 'varying:uv' } ); + + } + + } + + // 2. Uniforms become individual parameters (bare names already used in body). + // Most carry semantic 'uniform'; surface lighting inputs carry camera:/light:. + for ( const u of uniforms ) { + + let paramType = u.type; + if ( ( u.semantic || '' ) === 'light:data' && lightLayout ) { + + const { flatCount } = parseStructArrayType( u.type, wgsl, null, null, lightLayout ); + paramType = `array`; + + } + newParams.push( `${ u.name }: ${ paramType }` ); + params.push( { name: u.name, type: paramType, semantic: u.semantic || 'uniform', value: u.value } ); + + } + + // 3. Each texture contributes a texture + sampler parameter pair. Material image + // textures carry semantic 'texture'; env (IBL) maps carry 'env:radiance' / + // 'env:irradiance' so the assembler can bind them from the environment instead. + for ( const tex of textures ) { + + const texSemantic = tex.semantic && tex.semantic.startsWith( 'env:' ) ? tex.semantic : 'texture'; + const sampSemantic = texSemantic === 'texture' ? 'sampler' : texSemantic + ':sampler'; + newParams.push( `${ tex.texture }: ${ tex.wgslType }` ); + newParams.push( `${ tex.sampler }: sampler` ); + params.push( { name: tex.texture, type: tex.wgslType, semantic: texSemantic, key: tex.key } ); + params.push( { name: tex.sampler, type: 'sampler', semantic: sampSemantic, key: tex.key } ); + + } + + // Strip entry-point IO attributes (e.g. `@location(0)`, `@builtin(...)`) from the return + // type: they are required on the standalone `@fragment` entry (WGSL validation) but invalid + // on the plain function that Three.js `wgslFn` turns this into. + const retType = fn.ret ? fn.ret.replace( /@\w+\s*\([^)]*\)\s*/g, '' ).trim() : ''; + const retArrow = retType ? ` -> ${ retType }` : ''; + const entry = `fn ${ entryName }( ${ newParams.join( ', ' ) } )${ retArrow } {${ body }}`; + + // Includes: the original module minus the entry and the binding declarations. + let includes = wgsl.slice( 0, fn.start ) + wgsl.slice( fn.end ); + includes = removeBindingDecls( includes ).trim(); + + // The generator emits the stage attribute on the line *before* `fn ` (e.g. + // `@fragment\nfn fragmentMain(...)`) for standalone WGSL validation. `extractFunction` + // starts at `fn`, so that attribute is left dangling in the includes and would re-attach + // to the next function Three.js emits — turning a plain helper into a bogus entry point + // ("missing entry point IO attribute on parameter"). Strip any stray stage attributes. + includes = includes.replace( /@(?:fragment|vertex|compute)\b\s*/g, '' ).trim(); + + // The vertex-data struct is now unused: the entry takes flat varying parameters and the + // helper functions were de-threaded (see threadEnvResources). It also carries @builtin / + // @location IO attributes that are only valid at an entry boundary, so leaving its + // definition in the wgslFn includes would be rejected. Strip it. + if ( isStructEntry ) { + + const structType = flatParams[ 0 ].type; + includes = includes + .replace( new RegExp( `struct\\s+${ escapeRegExp( structType ) }\\s*\\{[^}]*\\}\\s*`, 'g' ), '' ) + .trim(); + + } + + return { name: entryName, entry, includes, params }; + +} + +/** Split a WGSL parameter list ("a: T1, b: T2") into [{name,type}]. */ +function splitParams( params ) { + + const out = []; + let depth = 0, start = 0; + const pieces = []; + for ( let i = 0; i < params.length; i ++ ) { + + const c = params[ i ]; + if ( c === '<' || c === '(' ) depth ++; + else if ( c === '>' || c === ')' ) depth --; + else if ( c === ',' && depth === 0 ) { pieces.push( params.slice( start, i ) ); start = i + 1; } + + } + const tail = params.slice( start ).trim(); + if ( tail ) pieces.push( tail ); + + for ( const piece of pieces ) { + + const colon = piece.indexOf( ':' ); + if ( colon < 0 ) continue; + out.push( { name: piece.slice( 0, colon ).trim(), type: piece.slice( colon + 1 ).trim() } ); + + } + return out; + +} + +// ── Material assembly ──────────────────────────────────────────────────────── + +/** + * Build a Three.js NodeMaterial from MaterialX WGSL + manifest. + * + * @param {Object} options + * @param {Object} options.THREE - The `three/webgpu` namespace. + * @param {Object} options.TSL - The `three/tsl` namespace. + * @param {string} options.wgsl - Generated WGSL module. + * @param {Object} options.manifest - Generator manifest. + * @param {Object} [options.textures] - Map of texture key -> Texture. + * @param {Node} [options.uvNode] - Optional uv node (defaults to TSL `uv()`). + * @param {boolean} [options.useGeometryTangent] - Use TSL `tangentWorld` when geometry tangents exist. + * @return {NodeMaterial} + */ +export function createMxWgslMaterial( { THREE, TSL, wgsl, manifest, textures = {}, uvNode = null, light = null, lightData = null, numLights = null, environment = null, useGeometryTangent = false } ) { + + manifest = normalizeReflection( manifest ); + + const { wgslFn, wgsl: wgslCode, uniform, uniformArray, texture, sampler, uv, normalWorld, positionWorld, cameraPosition, tangentWorld: tangentWorldBuiltin, vec3, float, select } = TSL; + + // IBL environment: equirect radiance (mipped) + irradiance maps and FIS parameters. + // When the shader was generated with FIS but no environment is supplied, bind a 1×1 + // black fallback so the module still compiles (IBL simply contributes nothing). + const env = environment || {}; + const fallbackEnvTex = () => { + + const t = new THREE.DataTexture( new Uint8Array( [ 0, 0, 0, 255 ] ), 1, 1 ); + t.needsUpdate = true; + return t; + + }; + const envRadianceTex = env.radiance || fallbackEnvTex(); + const envIrradianceTex = env.irradiance || envRadianceTex; + // HDR env maps must stay linear (see viewer buildEnvironment). + envRadianceTex.colorSpace = THREE.NoColorSpace; + envIrradianceTex.colorSpace = THREE.NoColorSpace; + const envTexFor = ( semantic ) => semantic.startsWith( 'env:irradiance' ) ? envIrradianceTex : envRadianceTex; + const fallbackMatTex = () => { + + const t = new THREE.DataTexture( new Uint8Array( [ 128, 128, 128, 255 ] ), 1, 1 ); + // DataTexture defaults to Nearest+Nearest; Three.js treats that as "unfilterable" and + // omits nodeUniformN_sampler bindings while MaterialX WGSL still passes samplers. + t.magFilter = THREE.LinearFilter; + t.minFilter = THREE.LinearFilter; + t.needsUpdate = true; + return t; + + }; + + // Three.js only emits sampler bindings for filterable textures (see WebGPUNodeBuilder + // needsSampler). MaterialX WGSL always declares matching sampler parameters. + const ensureFilterableTexture = ( tex ) => { + + if ( ! tex ) return tex; + if ( tex.magFilter === THREE.NearestFilter ) tex.magFilter = THREE.LinearFilter; + if ( tex.minFilter === THREE.NearestFilter || tex.minFilter === THREE.NearestMipmapNearestFilter ) { + + tex.minFilter = tex.generateMipmaps !== false ? THREE.LinearMipmapLinearFilter : THREE.LinearFilter; + + } + return tex; + + }; + + // A robust world-space tangent. MaterialX BSDFs orthogonalize the tangent against + // the normal (`normalize(T - dot(T,N)*N)`), which yields NaN if T is zero or parallel + // to N — and most geometries (e.g. TorusKnotGeometry) carry no tangent attribute, so + // a raw `tangentWorld` accessor would be degenerate. Derive an arbitrary orthonormal + // tangent from the normal instead; for isotropic GGX the exact direction is irrelevant. + function derivedTangentWorld() { + + const n = normalWorld.normalize(); + // Reference axis least aligned with the normal, to avoid a parallel cross product. + const ref = select( n.y.abs().lessThan( float( 0.99 ) ), vec3( 0, 1, 0 ), vec3( 1, 0, 0 ) ); + return ref.cross( n ).normalize(); + + } + // MaterialXView computes tangents for indexed geometry; use them when available for + // anisotropic parity with the WebGL path. Fall back to a derived tangent otherwise. + const tangentWorld = useGeometryTangent ? tangentWorldBuiltin : derivedTangentWorld(); + + const converted = convertToTslPortable( wgsl, manifest ); + const wgslForConsts = converted.includes + '\n' + converted.entry; + + const includesNode = converted.includes.length ? [ wgslCode( converted.includes ) ] : []; + const entryFn = wgslFn( converted.entry, includesNode ); + + const builtin = { + 'varying:uv': () => ( uvNode || uv() ), + 'varying:normalWorld': () => normalWorld, + 'varying:positionWorld': () => positionWorld, + 'varying:tangentWorld': () => tangentWorld, + 'camera:viewPosition': () => cameraPosition + }; + + const args = {}; + // One TSL texture() node per manifest texture key. wgslFn passes texture and sampler as + // separate WGSL parameters; calling texture() twice (even with the same THREE.Texture) + // creates two binding slots (nodeUniformN + nodeUniformN+1_sampler) and breaks pairing. + const textureNodes = {}; + const textureNodeFor = ( key, threeTex ) => { + + if ( ! textureNodes[ key ] ) textureNodes[ key ] = texture( ensureFilterableTexture( threeTex ) ); + return textureNodes[ key ]; + + }; + + for ( const p of converted.params ) { + + if ( p.semantic === 'uniform' ) { + + args[ p.name ] = makeUniformNode( uniform, toUniformValue( THREE, p ), p.type ); + + } else if ( p.semantic === 'light:direction' ) { + + args[ p.name ] = uniform( ( light && light.direction ) || toUniformValue( THREE, p ) ); + + } else if ( p.semantic === 'light:color' ) { + + args[ p.name ] = uniform( ( light && light.color ) || toUniformValue( THREE, p ) ); + + } else if ( p.semantic === 'light:count' ) { + + args[ p.name ] = uniform( ( numLights != null ? numLights : p.value ) | 0, 'int' ); + + } else if ( p.semantic === 'light:data' ) { + + const layout = parseLightDataStruct( wgslForConsts ); + const { count, flatCount } = parseStructArrayType( p.type, wgslForConsts, null, lightData, layout ); + const packed = layout + ? packLightDataToVec4Array( lightData || p.value, count, layout ) + : new Array( flatCount ).fill( { x: 0, y: 0, z: 0, w: 0 } ); + args[ p.name ] = uniformArray( packed, 'vec4' ); + + } else if ( p.semantic === 'host' ) { + + args[ p.name ] = makeUniformNode( uniform, toUniformValue( THREE, p ), p.type ); + + } else if ( p.semantic === 'texture' ) { + + args[ p.name ] = textureNodeFor( p.key, textures[ p.key ] || fallbackMatTex() ); + + } else if ( p.semantic === 'sampler' ) { + + args[ p.name ] = sampler( textureNodeFor( p.key, textures[ p.key ] || fallbackMatTex() ) ); + + } else if ( p.semantic === 'env:radiance' || p.semantic === 'env:irradiance' ) { + + args[ p.name ] = textureNodeFor( p.key, envTexFor( p.semantic ) ); + + } else if ( p.semantic === 'env:radiance:sampler' || p.semantic === 'env:irradiance:sampler' ) { + + const envSemantic = p.semantic.replace( ':sampler', '' ); + args[ p.name ] = sampler( textureNodeFor( p.key, envTexFor( envSemantic ) ) ); + + } else if ( p.semantic === 'env:matrix' ) { + + args[ p.name ] = uniform( env.matrix || matrixFromArray( THREE, p.value ) ); + + } else if ( p.semantic === 'env:radianceSamples' ) { + + args[ p.name ] = uniform( ( env.samples != null ? env.samples : p.value ) | 0, 'int' ); + + } else if ( p.semantic === 'env:radianceMips' ) { + + args[ p.name ] = uniform( ( env.mips != null ? env.mips : p.value ) | 0, 'int' ); + + } else if ( p.semantic === 'env:lightIntensity' ) { + + args[ p.name ] = uniform( env.intensity != null ? env.intensity : p.value ); + + } else if ( p.semantic && ( p.semantic.startsWith( 'varying:' ) || p.semantic.startsWith( 'camera:' ) ) ) { + + const make = builtin[ p.semantic ]; + if ( ! make ) throw new Error( `mxtsladapter: unknown builtin semantic '${ p.semantic }'` ); + args[ p.name ] = make(); + + } else { + + throw new Error( `mxtsladapter: unhandled parameter semantic '${ p.semantic }' for '${ p.name }'` ); + + } + + } + + const material = new THREE.MeshBasicNodeMaterial(); + // MaterialX WGSL already includes direct lights + IBL; do not run Three.js scene lighting. + material.lights = false; + material.fog = false; + material.colorNode = entryFn( args ); + // Bound argument nodes, keyed by parameter name. `createMxWgslGUI` reads these to + // drive live uniform edits, so this is a functional binding handle, not debug state. + material.userData.mxArgs = args; + return material; + +} + +function toUniformValue( THREE, p ) { + + const v = p.value; + if ( v === undefined || v === null ) { + + // Sensible zero/identity defaults by type. + if ( p.type === 'f32' || p.type === 'i32' || p.type === 'u32' ) return 0; + if ( p.type === 'vec2f' ) return new THREE.Vector2(); + if ( p.type === 'vec4f' ) return new THREE.Vector4(); + return new THREE.Vector3(); + + } + + if ( typeof v === 'boolean' && ( p.type === 'i32' || p.type === 'u32' ) ) return v ? 1 : 0; + + if ( Array.isArray( v ) ) { + + if ( v.length === 2 ) return new THREE.Vector2( v[ 0 ], v[ 1 ] ); + if ( v.length === 3 ) return new THREE.Vector3( v[ 0 ], v[ 1 ], v[ 2 ] ); + if ( v.length === 4 ) return new THREE.Vector4( v[ 0 ], v[ 1 ], v[ 2 ], v[ 3 ] ); + if ( v.length === 16 ) return matrixFromArray( THREE, v ); + + } + + return v; + +} + +/** Bind a TSL uniform node with the correct scalar type for WGSL i32/u32 host inputs. */ +function makeUniformNode( uniformFn, value, wgslType ) { + + if ( wgslType === 'i32' || wgslType === 'u32' ) return uniformFn( value | 0, 'int' ); + return uniformFn( value ); + +} + +/** Build a THREE.Matrix4 from a 16-element column-major array (WGSL mat4x4f layout). */ +function matrixFromArray( THREE, v ) { + + const m = new THREE.Matrix4(); + if ( Array.isArray( v ) && v.length === 16 ) { + + // Three stores column-major internally; WGSL mat4x4f is also column-major, so the + // array maps directly into Matrix4.elements. + m.elements = v.slice(); + + } + return m; + +} + +// ── Live parameter editor ───────────────────────────────────────────────────── + +// Slider range heuristics for scalar surface inputs, keyed by name substring. +function scalarRange( name ) { + + if ( /IOR$/.test( name ) ) return [ 1, 3 ]; + if ( /thickness/.test( name ) ) return [ 0, 2000 ]; // thin-film, nanometres + if ( /roughness|metalness|anisotropy|rotation|affect|walled/.test( name ) ) return [ 0, 1 ]; + if ( /emission|scale|depth|dispersion/.test( name ) ) return [ 0, 5 ]; + return [ 0, 1 ]; + +} + +const GUI_CATEGORIES = [ 'base', 'diffuse', 'metalness', 'specular', 'transmission', + 'subsurface', 'sheen', 'coat', 'thin_film', 'thin_walled', 'emission', 'opacity' ]; + +function guiCategory( stripped ) { + + for ( const c of GUI_CATEGORIES ) if ( stripped === c || stripped.startsWith( c + '_' ) ) return c; + return 'other'; + +} + +/** + * Build a live lil-gui editor for a material created by `createMxWgslMaterial`. + * + * Drives the bound TSL uniform nodes (`material.userData.mxArgs`) directly, so edits take + * effect on the next render with no rebuild. Surface inputs are read from the manifest and + * grouped into folders by closure (base / specular / coat / …); the directional light and + * environment intensity get their own folder. + * + * @param {Object} options + * @param {Function} options.GUI - The lil-gui constructor. + * @param {Object} options.THREE - The `three/webgpu` namespace. + * @param {NodeMaterial} options.material - Material from `createMxWgslMaterial`. + * @param {Object} options.manifest - The generator manifest used to build the material. + * @param {GUI} [options.gui] - Existing gui to populate (otherwise a new one is created). + * @return {GUI} + */ +export function createMxWgslGUI( { GUI, THREE, material, manifest, gui = null } ) { + + manifest = normalizeReflection( manifest ); + gui = gui || new GUI(); + const args = material.userData.mxArgs || {}; + + const folders = {}; + const folderFor = ( parent, key, label ) => { + + const id = parent + '/' + key; + if ( ! folders[ id ] ) { folders[ id ] = ( parent === '' ? gui : folders[ parent ] ).addFolder( label ); folders[ id ].close(); } + return folders[ id ]; + + }; + + // One control bound to a uniform node's `.value`. + const addControl = ( folder, node, type, name, value, label, range ) => { + + if ( type === 'vec3f' && /color|scatter/.test( name ) ) { + + const proxy = { c: Array.isArray( value ) ? value.slice() : [ value.x, value.y, value.z ] }; + folder.addColor( proxy, 'c' ).name( label ).onChange( ( v ) => node.value.fromArray( v ) ); + + } else if ( type === 'vec3f' ) { + + const sub = folder.addFolder( label ); sub.close(); + const proxy = Array.isArray( value ) ? { x: value[ 0 ], y: value[ 1 ], z: value[ 2 ] } : { x: value.x, y: value.y, z: value.z }; + const lo = range ? range[ 0 ] : 0, hi = range ? range[ 1 ] : 2; + for ( const k of [ 'x', 'y', 'z' ] ) + sub.add( proxy, k, lo, hi ).onChange( () => node.value.set( proxy.x, proxy.y, proxy.z ) ); + + } else if ( type === 'bool' ) { + + const proxy = { v: !! value }; + folder.add( proxy, 'v' ).name( label ).onChange( ( v ) => { node.value = v; } ); + + } else if ( type === 'vec2f' ) { + + const sub = folder.addFolder( label ); sub.close(); + const proxy = Array.isArray( value ) ? { x: value[ 0 ], y: value[ 1 ] } : { x: value.x, y: value.y }; + const lo = range ? range[ 0 ] : 0, hi = range ? range[ 1 ] : 2; + for ( const k of [ 'x', 'y' ] ) + sub.add( proxy, k, lo, hi ).onChange( () => node.value.set( proxy.x, proxy.y ) ); + + } else if ( type === 'vec4f' ) { + + const sub = folder.addFolder( label ); sub.close(); + const proxy = Array.isArray( value ) + ? { x: value[ 0 ], y: value[ 1 ], z: value[ 2 ], w: value[ 3 ] } + : { x: value.x, y: value.y, z: value.z, w: value.w }; + const lo = range ? range[ 0 ] : 0, hi = range ? range[ 1 ] : 2; + for ( const k of [ 'x', 'y', 'z', 'w' ] ) + sub.add( proxy, k, lo, hi ).onChange( () => node.value.set( proxy.x, proxy.y, proxy.z, proxy.w ) ); + + } else if ( type === 'f32' || type === 'i32' || type === 'u32' ) { + + const [ lo, hi ] = range || scalarRange( name ); + const proxy = { v: Number( value ) || 0 }; + const ctrl = folder.add( proxy, 'v', lo, hi ).name( label ).onChange( ( v ) => { node.value = v; } ); + if ( type === 'i32' || type === 'u32' ) ctrl.step( 1 ); + + } + + }; + + // Surface inputs (semantic 'uniform'), grouped by closure category. + const surface = folderFor( '', 'surface', 'MaterialX surface' ); + surface.open(); + for ( const u of manifest.uniforms || [] ) { + + if ( u.semantic !== 'uniform' ) continue; + const node = args[ u.name ]; + if ( ! node || u.type === 'mat4x4f' ) continue; + const stripped = u.name.replace( /^[A-Za-z][A-Za-z0-9]*_/, '' ); // drop node prefix (e.g. 'ss_') + const cat = guiCategory( stripped ); + const folder = cat === 'other' ? surface : folderFor( '/surface', cat, cat.replace( /_/g, ' ' ) ); + addControl( folder, node, u.type, u.name, u.value, stripped, null ); + + } + + // Lighting + environment. + const lighting = folderFor( '', 'lighting', 'Lighting & environment' ); + for ( const u of manifest.uniforms || [] ) { + + const node = args[ u.name ]; + if ( ! node ) continue; + if ( u.semantic === 'light:direction' ) addControl( lighting, node, 'vec3f', 'dir_vec', u.value, 'light direction', [ - 1, 1 ] ); + else if ( u.semantic === 'light:color' ) addControl( lighting, node, 'vec3f', 'light_rgb', u.value, 'light color', [ 0, 10 ] ); + else if ( u.semantic === 'env:lightIntensity' ) addControl( lighting, node, 'f32', 'env_intensity', u.value, 'env intensity', [ 0, 3 ] ); + + } + + return gui; + +} diff --git a/javascript/MaterialXView/source/viewer.js b/javascript/MaterialXView/source/viewer.js index 5cfce0bcb3..2590043316 100644 --- a/javascript/MaterialXView/source/viewer.js +++ b/javascript/MaterialXView/source/viewer.js @@ -10,6 +10,8 @@ import { HDRLoader } from 'three/examples/jsm/loaders/HDRLoader.js'; import { prepareEnvTexture, getLightRotation, findLights, registerLights, getUniformValues } from './helper.js' import { Group } from 'three'; import GUI from 'lil-gui'; +import { createMxWgslMaterial, createMxWgslGUI, normalizeReflection } from './mxtsladapter.js'; +import { buildWgslManifest } from './wgslmanifest.js'; const ALL_GEOMETRY_SPECIFIER = "*"; const NO_GEOMETRY_SPECIFIER = ""; @@ -18,6 +20,96 @@ const DAG_PATH_SEPERATOR = "/"; // Logging toggle var logDetailedTime = false; +/** + * Configure genContext for WebGPU material generation. + * @returns {boolean} Whether the surface is transparent. + */ +function configureWebGPUGenContext(mx, gen, genContext, elem) +{ + // WebGPU: leave surface color linear; WebGPURenderer converts working space → sRGB for the canvas. + // WebGL RawShaderMaterial writes shader output directly, so that path keeps hwSrgbEncodeOutput=true. + genContext.getOptions().hwSrgbEncodeOutput = false; + const isTransparent = mx.isTransparentSurface(elem, gen.getTarget()); + genContext.getOptions().hwTransparency = isTransparent; + genContext.getOptions().shaderInterfaceType = mx.ShaderInterfaceType.SHADER_INTERFACE_COMPLETE; + return isTransparent; +} + +/** + * Generate WGSL pixel shader and manifest for a renderable element. + */ +function generateWebGPUShader(mx, gen, genContext, elem) +{ + const shader = gen.generate(elem.getNamePath(), elem, genContext); + const wgsl = shader.getSourceCode('pixel'); + // The in-repo WgslShaderGenerator does not emit a manifest; reconstruct it in JS from + // the generated WGSL text + the Shader's uniform ports (see wgslmanifest.js). + const manifest = buildWgslManifest(shader, wgsl); + return { shader, wgsl, manifest }; +} + +/** + * Build the first directional light payload for the TSL bridge. + */ +function buildDirectionalLight(lightData) +{ + if (!lightData || !lightData.length) return null; + const l = lightData[0]; + return { + direction: l.direction.clone(), + color: l.color.clone().multiplyScalar(l.intensity) + }; +} + +/** + * Build IBL environment payload for the TSL bridge. + */ +function buildEnvironment(radianceTexture, irradianceTexture, matrix, THREE) +{ + // HDR env maps are linear radiance data; disable Three.js sRGB decode on the TSL path + // (same rationale as buildTextureMap for material textures). + radianceTexture.colorSpace = THREE.NoColorSpace; + irradianceTexture.colorSpace = THREE.NoColorSpace; + const mips = Math.trunc(Math.log2(Math.max(radianceTexture.image.width, radianceTexture.image.height))) + 1; + return { + radiance: radianceTexture, + irradiance: irradianceTexture, + samples: 16, + mips, + intensity: 1.0, + matrix + }; +} + +/** + * Map manifest texture keys to loaded THREE.Texture objects. + * + * MaterialXView registers no color-management system, so the generated shader performs no + * sRGB->linear input transform, and the WebGL RawShaderMaterial samples textures raw (the + * stored sRGB bytes are used directly). A TSL `texture()` node, by contrast, decodes to + * linear working space when `texture.colorSpace` is sRGB. To stay pixel-identical with the + * WebGL reference we therefore force `NoColorSpace` so WebGPU samples raw as well. (Proper + * color management would mean registering a CMS so BOTH backends convert in-shader; that is + * a separate change that would alter the existing WebGL appearance.) + */ +function buildTextureMap(manifest, uniforms, THREE) +{ + // C++ reflection uses { bindings: [...] }; normalize before reading textures[]. + manifest = normalizeReflection(manifest); + const textures = {}; + for (const tex of (manifest.textures || [])) + { + if (tex.semantic !== 'texture') continue; + const u = uniforms[tex.key]; + if (u && u.value) + { + u.value.colorSpace = THREE.NoColorSpace; + textures[tex.key] = u.value; + } + } + return textures; +} + /* Scene management */ @@ -243,11 +335,18 @@ export class Scene updateObjectUniforms(child, material, camera) { if (!child || !material || !camera) return; + + // The WebGPU NodeMaterial has no `material.uniforms` block: its per-object world + // transforms come from TSL accessors (positionWorld / normalWorld / cameraPosition) + // and editor edits drive the TSL uniform nodes directly, so this path is a no-op for + // it. The guards below keep the WebGL (RawShaderMaterial) updates working unchanged. const uniforms = material.uniforms; if (!uniforms) return; - uniforms.u_worldMatrix.value = child.matrixWorld; - uniforms.u_viewProjectionMatrix.value = this.#_viewProjMat.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); + if (uniforms.u_worldMatrix) + uniforms.u_worldMatrix.value = child.matrixWorld; + if (uniforms.u_viewProjectionMatrix) + uniforms.u_viewProjectionMatrix.value = this.#_viewProjMat.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); if (uniforms.u_viewPosition) uniforms.u_viewPosition.value = camera.getWorldPosition(this.#_worldViewPos); @@ -900,6 +999,9 @@ export class Material // generateMaterial(matassign, viewer, searchPath, closeUI) { + if (viewer.getBackend() === 'webgpu') + return this.generateMaterialWebGPU(matassign, viewer, searchPath, closeUI); + var elem = matassign.getMaterial(); var startGenerateMat = performance.now(); @@ -985,6 +1087,68 @@ export class Material return newMaterial; } + // + // WebGPU material generation: emit WGSL with the upstream WgslShaderGenerator, reshape it + // into a TSL NodeMaterial via the bridge, and reuse the existing property Editor by + // exposing a RawShaderMaterial-style `uniforms` map plus a per-frame sync into the TSL + // nodes. Lighting reuses the rig's first directional light + the prepared IBL textures. + // + generateMaterialWebGPU(matassign, viewer, searchPath, closeUI) + { + const mx = viewer.getMx(); + const TSL = viewer.getTSL(); + const elem = matassign.getMaterial(); + const gen = viewer.getGenerator(); + const genContext = viewer.getGenContext(); + const textureLoader = new THREE.TextureLoader(); + + try + { + const isTransparent = configureWebGPUGenContext(mx, gen, genContext, elem); + const { shader, wgsl, manifest } = generateWebGPUShader(mx, gen, genContext, elem); + + const flipV = viewer.getScene().getFlipGeometryV(); + const uniforms = getUniformValues(shader.getStage('pixel'), textureLoader, searchPath, flipV); + const textures = buildTextureMap(manifest, uniforms, THREE); + + const environment = buildEnvironment( + viewer.getRadianceTexture(), + viewer.getIrradianceTexture(), + getLightRotation(), + THREE + ); + const light = buildDirectionalLight(viewer.getLightData()); + const lightData = viewer.getLightData(); + const numLights = viewer.getLights()?.length ?? 0; + + // Indexed geometry gets computed tangents in updateScene(); use them for anisotropic parity. + const material = createMxWgslMaterial({ + THREE, TSL, wgsl, manifest, light, lightData, numLights, environment, textures, + useGeometryTangent: true + }); + material.side = THREE.DoubleSide; + material.transparent = isTransparent; + material.name = elem.getName(); + + const gui = viewer.getEditor().getGUI(); + this.updateEditorWebGPU(matassign, material, manifest, gui, closeUI, viewer); + return material; + } + catch (error) + { + // WGSL generation or TSL assembly failed (e.g. an unsupported node). Surface it and + // return a visible magenta fallback so the viewer stays usable instead of crashing. + const name = elem ? elem.getName() : ''; + console.error(`WebGPU material generation failed for '${name}':`, + Number.isInteger(error) ? mx.getExceptionMessage(error) : error); + const fallback = new THREE.MeshBasicNodeMaterial(); + fallback.colorNode = TSL.vec3(1.0, 0.0, 1.0); + fallback.side = THREE.DoubleSide; + fallback.name = name; + return fallback; + } + } + clearSoloMaterialUI() { for (let i = 0; i < this._materials.length; ++i) @@ -1043,31 +1207,18 @@ export class Material // Update property editor for a given MaterialX element, it's shader, and // Three material // - updateEditor(matassign, shader, material, gui, closeUI, viewer) + setupMaterialFolder(matassign, elem, gui, closeUI, viewer) { - var elem = matassign.getMaterial(); - var materials = this._materials; - - const DEFAULT_MIN = 0; - const DEFAULT_MAX = 100; - - var startTime = performance.now(); - + const materials = this._materials; const elemPath = elem.getNamePath(); - - // Create and cache associated UI - var matUI = gui.addFolder(elemPath); + const matUI = gui.addFolder(elemPath); matassign.setMaterialUI(matUI); - let matTitle = matUI.domElement.getElementsByClassName('title')[0]; - // Add a icon to the title to allow for assigning the material to geometry - // Clicking on the icon will "solo" the material to the geometry. - // Clicking on the title will open/close the material folder. + const matTitle = matUI.domElement.getElementsByClassName('title')[0]; matTitle.innerHTML = "" + elem.getNamePath(); - let img = matTitle.getElementsByTagName('img')[0]; + const img = matTitle.getElementsByTagName('img')[0]; if (img) { - // Add event listener to icon to call updateSoloMaterial function img.addEventListener('click', function (event) { Material.updateSoloMaterial(viewer, elemPath, materials, event); @@ -1075,9 +1226,33 @@ export class Material } if (closeUI) - { matUI.close(); - } + + return matUI; + } + + // + // WebGPU property editor: manifest-driven TSL uniform nodes via createMxWgslGUI. + // + updateEditorWebGPU(matassign, material, manifest, gui, closeUI, viewer) + { + const elem = matassign.getMaterial(); + const matUI = this.setupMaterialFolder(matassign, elem, gui, closeUI, viewer); + createMxWgslGUI({ GUI, THREE, material, manifest, gui: matUI }); + } + + updateEditor(matassign, shader, material, gui, closeUI, viewer) + { + var elem = matassign.getMaterial(); + + const DEFAULT_MIN = 0; + const DEFAULT_MAX = 100; + + var startTime = performance.now(); + + var matUI = this.setupMaterialFolder(matassign, elem, gui, closeUI, viewer); + const elemPath = elem.getNamePath(); + const uniformBlocks = Object.values(shader.getStage('pixel').getUniformBlocks()); var uniformToUpdate; const ignoreList = ['u_envRadianceMips', 'u_envRadianceSamples', 'u_alphaThreshold']; @@ -1539,19 +1714,24 @@ export class Viewer // Create shader generator, generation context and "base" document which // contains the standard definition libraries and lighting elements. // - async initialize(mtlxIn, renderer, radianceTexture, irradianceTexture, lightRigXml) + async initialize(mtlxIn, renderer, radianceTexture, irradianceTexture, lightRigXml, opts = {}) { this.mx = mtlxIn; - - // Initialize base document - this.generator = this.mx.EsslShaderGenerator.create(); + this.backend = opts.backend || 'webgl'; + this.tsl = opts.TSL || null; + + // Initialize base document. The WebGPU backend uses the upstream WgslShaderGenerator + // (WGSL, consumed by the TSL bridge); WebGL uses the ESSL generator (GLSL). + this.generator = (this.backend === 'webgpu') + ? this.mx.WgslShaderGenerator.create() + : this.mx.EsslShaderGenerator.create(); this.genContext = new this.mx.GenContext(this.generator); this.document = this.mx.createDocument(); this.stdlib = this.mx.loadStandardLibraries(this.genContext); this.document.setDataLibrary(this.stdlib); - this.initializeLighting(renderer, radianceTexture, irradianceTexture, lightRigXml); + await this.initializeLighting(renderer, radianceTexture, irradianceTexture, lightRigXml); radianceTexture.mapping = THREE.EquirectangularReflectionMapping; this.getScene().setBackgroundTexture(radianceTexture); @@ -1626,6 +1806,16 @@ export class Viewer return this.mx; } + getBackend() + { + return this.backend; + } + + getTSL() + { + return this.tsl; + } + getGenerator() { return this.generator; diff --git a/javascript/MaterialXView/source/wgslmanifest.js b/javascript/MaterialXView/source/wgslmanifest.js new file mode 100644 index 0000000000..055cbaf924 --- /dev/null +++ b/javascript/MaterialXView/source/wgslmanifest.js @@ -0,0 +1,218 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +/** + * WGSL reflection manifest builder. + * + * The in-repo `WgslShaderGenerator` emits WGSL but does not emit the JSON reflection + * manifest that the TSL bridge (`mxtsladapter.js`) consumes. Rather than reintroduce + * manifest emission into the C++ generator, we reconstruct the manifest here in JS from + * two sources that are already available after `generate()`: + * + * 1. The generated WGSL text — supplies the `@group/@binding` layout, the final WGSL + * type spellings, the entry-function names, and the vertex-input struct (varyings). + * 2. The `Shader` object — supplies what is NOT present in the WGSL text: each uniform's + * default value (`ShaderPort.getValue()`) and the public-vs-host role (the uniform + * block name: `PublicUniforms` => editable "uniform", everything else => "host"). + * + * The output matches the "bindings" reflection shape accepted by + * `normalizeReflection()` in mxtsladapter.js, so no adapter changes are required. + */ + +// Uniform block names emitted by HwShaderGenerator (see source/MaterialXGenHw/HwConstants.cpp). +// The role split mirrors the original C++ manifest: PublicUniforms members are the +// user-editable surface inputs; all other blocks are engine/host-set. +const PUBLIC_UNIFORMS = 'PublicUniforms'; +const LIGHT_DATA = 'LightData'; + +// Entry-function names emitted by WgslShaderGenerator::emitPixelStage / emitVertexStage +// (see source/MaterialXGenWgsl/WgslShaderGenerator.cpp). The adapter is entry-name +// agnostic (it reads manifest.entry), so emitting the in-repo names is sufficient. +const PIXEL_ENTRY = 'fragmentMain'; +const VERTEX_ENTRY = 'vertexMain'; + +/** + * Format a MaterialX port value as a JS scalar / array / boolean. + * Mirrors the C++ `valueToJson` in WgslShaderGenerator.cpp so the produced object graph + * is identical to what `JSON.parse(getWgslGeneratedManifest(...))` used to yield. + * + * @param {Object} port - A bound mx.ShaderPort, or null. + * @return {(number|boolean|number[]|null)} The default value, or null when unset. + */ +function portValueToJson( port ) { + + const value = port && port.getValue && port.getValue(); + if ( ! value ) return null; + + const s = value.getValueString(); + if ( s.indexOf( ',' ) !== - 1 ) { + + // Vector / color / matrix: comma-separated numbers. + return s.split( ',' ) + .map( ( item ) => item.trim() ) + .filter( ( item ) => item.length > 0 ) + .map( ( item ) => parseFloat( item ) ); + + } + if ( s === 'true' ) return true; + if ( s === 'false' ) return false; + if ( s === '' ) return 0; + const n = Number( s ); + return Number.isNaN( n ) ? s : n; + +} + +/** + * Build a name -> { role, port } map from the pixel stage's uniform blocks. + * The block name determines the role; LightData members are not enumerated individually + * (the light-data array is exposed as a single `var` binding instead). + */ +function collectUniformPorts( shader ) { + + const map = new Map(); + let stage; + try { + + stage = shader.getStage( 'pixel' ); + + } catch ( e ) { + + return map; + + } + if ( ! stage ) return map; + + const blocks = stage.getUniformBlocks(); // JS object keyed by block name + for ( const blockName of Object.keys( blocks ) ) { + + if ( blockName === LIGHT_DATA ) continue; + const block = blocks[ blockName ]; + if ( ! block ) continue; + const role = blockName === PUBLIC_UNIFORMS ? 'uniform' : 'host'; + const count = block.size(); + for ( let i = 0; i < count; ++ i ) { + + const port = block.get( i ); + if ( ! port ) continue; + map.set( port.getVariable(), { role, port } ); + + } + + } + + return map; + +} + +// Matches `@group(N) @binding(M) var[] name: type;` lines emitted by +// WgslResourceBindingContext. `var` covers scalar/vector/matrix/array uniforms; +// bare `var` covers texture_2d and sampler bindings. +const BINDING_RE = /@group\(\s*(\d+)\s*\)\s*@binding\(\s*(\d+)\s*\)\s*var(?:<[^>]*>)?\s+([A-Za-z_]\w*)\s*:\s*([^;]+);/g; + +/** + * Parse the `@group/@binding` resource declarations out of the WGSL text into manifest + * bindings, attaching role + default value from the uniform-port map. + */ +function parseBindings( wgsl, portMap ) { + + const bindings = []; + BINDING_RE.lastIndex = 0; + for ( let m = BINDING_RE.exec( wgsl ); m !== null; m = BINDING_RE.exec( wgsl ) ) { + + const group = Number( m[ 1 ] ); + const binding = Number( m[ 2 ] ); + const name = m[ 3 ]; + const type = m[ 4 ].trim(); + + if ( name.endsWith( '_texture' ) || type.startsWith( 'texture_' ) ) { + + bindings.push( { stage: 'pixel', group, binding, name, type, role: 'texture', key: name.replace( /_texture$/, '' ) } ); + + } else if ( name.endsWith( '_sampler' ) || type === 'sampler' ) { + + bindings.push( { stage: 'pixel', group, binding, name, type, role: 'sampler', key: name.replace( /_sampler$/, '' ) } ); + + } else if ( type.startsWith( 'array<' ) ) { + + // Light-data array (struct array). The adapter re-parses the struct itself, + // so no per-member value is needed here. + bindings.push( { stage: 'pixel', group, binding, name, type, role: 'lightData' } ); + + } else { + + const info = portMap.get( name ); + const role = info ? info.role : 'host'; + const value = info ? portValueToJson( info.port ) : null; + bindings.push( { stage: 'pixel', group, binding, name, type, role, value } ); + + } + + } + + return bindings; + +} + +/** + * Parse the pixel entry signature `fn FragmentMain(: ) -> vec4f` plus the + * referenced `struct { name: type, ... }` into a single entryParam carrying the + * varying members. normalizeReflection() flattens this into the varying input list. + */ +function parseEntryParams( wgsl ) { + + const sig = new RegExp( `\\bfn\\s+${ PIXEL_ENTRY }\\s*\\(\\s*([A-Za-z_]\\w*)\\s*:\\s*([A-Za-z_]\\w*)\\s*\\)` ).exec( wgsl ); + if ( ! sig ) return []; + + const instName = sig[ 1 ]; + const structName = sig[ 2 ]; + + const structRe = new RegExp( `\\bstruct\\s+${ structName }\\s*\\{([^}]*)\\}` ); + const structMatch = structRe.exec( wgsl ); + const members = []; + if ( structMatch ) { + + for ( const rawLine of structMatch[ 1 ].split( '\n' ) ) { + + const line = rawLine.trim().replace( /,$/, '' ); + if ( ! line ) continue; + const colon = line.indexOf( ':' ); + if ( colon < 0 ) continue; + const decl = line.slice( 0, colon ); + const type = line.slice( colon + 1 ).trim(); + // Skip the @builtin(position) member: it is the fragment-position builtin, not a + // MaterialX surface varying — the library/material code never reads it, and binding + // it would mis-map to a varying semantic with a vec4/vec3 type mismatch. + if ( /@builtin\b/.test( decl ) ) continue; + // Drop @location(...) / @interpolate(flat) attribute prefixes; keep the bare + // member identifier (the generator emits attributes inline in the struct). + const name = decl.replace( /@\w+\s*\([^)]*\)/g, '' ).trim(); + if ( name && type ) members.push( { name, type } ); + + } + + } + + return [ { name: instName, type: structName, members } ]; + +} + +/** + * Build the WGSL reflection manifest (bindings format) from a generated Shader + its WGSL. + * + * @param {Object} shader - The mx.Shader returned by WgslShaderGenerator.generate(). + * @param {string} wgsl - The generated pixel-stage WGSL source. + * @return {Object} Manifest consumable by normalizeReflection() / convertToTslPortable(). + */ +export function buildWgslManifest( shader, wgsl ) { + + const portMap = collectUniformPorts( shader ); + return { + entry: { vertex: VERTEX_ENTRY, pixel: PIXEL_ENTRY }, + output: 'vec4f', + entryParams: parseEntryParams( wgsl ), + bindings: parseBindings( wgsl, portMap ) + }; + +} diff --git a/javascript/MaterialXView/webpack.config.js b/javascript/MaterialXView/webpack.config.js index b3f0e33f79..b00c0aef53 100644 --- a/javascript/MaterialXView/webpack.config.js +++ b/javascript/MaterialXView/webpack.config.js @@ -1,5 +1,6 @@ const path = require('path'); const fs = require('fs'); +const webpack = require('webpack'); const CopyPlugin = require("copy-webpack-plugin"); const HtmlWebpackPlugin = require('html-webpack-plugin') @@ -11,9 +12,9 @@ function processMaterialPath(materialPath, baseURL) { const dirent = fs.readdirSync(materialPath).filter( function (file) { if (file.lastIndexOf(".mtlx") > -1) return file; } ); - return dirent.map((fileName) => ({ - name: fileName, - value: `${baseURL}/${fileName}` + return dirent.map((fileName) => ({ + name: fileName, + value: `${baseURL}/${fileName}` })); } @@ -32,47 +33,58 @@ dirent = fs.readdirSync(geometryFiles).filter( let geometry = dirent .map((fileName) => ({ name: fileName, value: `${geometryFilesURL}/${fileName}` })); -module.exports = { - entry: './source/index.js', - output: { - filename: 'main.js', - path: path.resolve(__dirname, 'dist') - }, - mode: "development", - plugins: [ - new HtmlWebpackPlugin({ - templateParameters: { - materials, - geometry - }, - template: 'index.ejs' - }), - new CopyPlugin({ - patterns: [ - { - context: "../../resources/Images", - from: "*.*", - to: "Images", - }, - { - context: "../../resources/Geometry/", - from: "*.glb", - to: "Geometry", - }, - { from: "./public", to: 'public' }, - { context: "../../resources/Lights", from: "*.*", to: "Lights" }, - { context: "../../resources/Lights/irradiance", from: "*.*", to: "Lights/irradiance" }, - // Dynamically generate material copy patterns from configuration - ...materialConfig.materials.map(materialType => ({ - from: materialType.path, - to: materialType.baseURL - })), - { from: "../build/bin/JsMaterialXCore.wasm" }, - { from: "../build/bin/JsMaterialXCore.js" }, - { from: "../build/bin/JsMaterialXGenShader.wasm" }, - { from: "../build/bin/JsMaterialXGenShader.js" }, - { from: "../build/bin/JsMaterialXGenShader.data" }, - ], - }), - ] -}; +// Shared asset copy (WASM, libraries, textures, materials, geometry, lights). Applied to +// both backend configs so index-webgpu.html deploys standalone with all assets. +const copyPlugin = new CopyPlugin({ + patterns: [ + { context: "../../resources/Images", from: "*.*", to: "Images" }, + { context: "../../resources/Geometry/", from: "*.glb", to: "Geometry" }, + { from: "./public", to: 'public' }, + { context: "../../resources/Lights", from: "*.*", to: "Lights" }, + { context: "../../resources/Lights/irradiance", from: "*.*", to: "Lights/irradiance" }, + ...materialConfig.materials.map(materialType => ({ + from: materialType.path, + to: materialType.baseURL + })), + { from: "../build/bin/JsMaterialXCore.wasm" }, + { from: "../build/bin/JsMaterialXCore.js" }, + { from: "../build/bin/JsMaterialXGenShader.wasm" }, + { from: "../build/bin/JsMaterialXGenShader.js" }, + { from: "../build/bin/JsMaterialXGenShader.data" }, + ], +}); + +// Build one config per rendering backend. Both compile the SAME source; a `__BACKEND__` +// define + a per-backend `three` alias select the WebGL (classic THREE.WebGLRenderer + +// RawShaderMaterial / ESSL) or WebGPU (WebGPURenderer + NodeMaterial / WGSL) path. The two +// HTML pages let a toggle switch backends by navigation (avoids loading both three builds). +function makeConfig(backend) { + const isWebGPU = backend === 'webgpu'; + return { + name: backend, + entry: './source/index.js', + output: { + filename: isWebGPU ? 'main.webgpu.js' : 'main.js', + path: path.resolve(__dirname, 'dist'), + }, + mode: "development", + // Each bundle references only its backend's renderer at runtime, but both code paths + // exist in the shared source, so the unused one's renderer isn't in this build's three + // export set. That's expected — silence the "export not found in 'three'" warning. + ignoreWarnings: [ (w) => /was not found in 'three'/.test((w && w.message) || '') ], + // For WebGPU, resolve the bare `three` specifier to the WebGPU build (exact match + // via `three$`, so `three/tsl` and `three/webgpu` still resolve normally). + resolve: isWebGPU ? { alias: { 'three$': require.resolve('three/webgpu') } } : {}, + plugins: [ + new webpack.DefinePlugin({ __BACKEND__: JSON.stringify(backend) }), + new HtmlWebpackPlugin({ + filename: isWebGPU ? 'index-webgpu.html' : 'index.html', + template: 'index.ejs', + templateParameters: { materials, geometry, backend }, + }), + copyPlugin, + ], + }; +} + +module.exports = [makeConfig('webgl'), makeConfig('webgpu')]; From dc547d0d9abd6889395ade50b386c3b210c2864e Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Thu, 2 Jul 2026 19:16:29 -0700 Subject: [PATCH 04/19] Add Python binding for WgslShaderGenerator --- python/Scripts/generateshader.py | 5 ++-- source/PyMaterialX/CMakeLists.txt | 8 +++-- .../PyMaterialXGenWgsl/CMakeLists.txt | 26 ++++++++++++++++ .../PyMaterialXGenWgsl/PyModule.cpp | 19 ++++++++++++ .../PyWgslShaderGenerator.cpp | 30 +++++++++++++++++++ 5 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 source/PyMaterialX/PyMaterialXGenWgsl/CMakeLists.txt create mode 100644 source/PyMaterialX/PyMaterialXGenWgsl/PyModule.cpp create mode 100644 source/PyMaterialX/PyMaterialXGenWgsl/PyWgslShaderGenerator.cpp diff --git a/python/Scripts/generateshader.py b/python/Scripts/generateshader.py index 46ff359043..ad6d5e09bf 100644 --- a/python/Scripts/generateshader.py +++ b/python/Scripts/generateshader.py @@ -1,7 +1,7 @@ #!/usr/bin/env python ''' Generate shader code for each renderable element in a MaterialX document or folder. -The currently supported target languages are GLSL, ESSL, MSL, OSL, and MDL. +The currently supported target languages are GLSL, ESSL, MSL, OSL, MDL, Slang, and WGSL. ''' import sys, os, argparse, subprocess @@ -12,6 +12,7 @@ import MaterialX.PyMaterialXGenMsl as mx_gen_msl import MaterialX.PyMaterialXGenOsl as mx_gen_osl import MaterialX.PyMaterialXGenSlang as mx_gen_slang +import MaterialX.PyMaterialXGenWgsl as mx_gen_wgsl import MaterialX.PyMaterialXGenShader as mx_gen_shader def validateCode(sourceCodeFile, codevalidator, codevalidatorArgs): @@ -100,7 +101,7 @@ def main(): elif gentarget == 'vulkan': shadergen = mx_gen_glsl.VkShaderGenerator.create() elif gentarget == 'wgsl': - shadergen = mx_gen_glsl.WgslShaderGenerator.create() + shadergen = mx_gen_wgsl.WgslShaderGenerator.create() elif gentarget == 'msl': shadergen = mx_gen_msl.MslShaderGenerator.create() elif gentarget == 'slang': diff --git a/source/PyMaterialX/CMakeLists.txt b/source/PyMaterialX/CMakeLists.txt index cafbb79245..bd07db8d42 100644 --- a/source/PyMaterialX/CMakeLists.txt +++ b/source/PyMaterialX/CMakeLists.txt @@ -37,7 +37,7 @@ endif() add_subdirectory(PyMaterialXCore) add_subdirectory(PyMaterialXFormat) -if (MATERIALX_BUILD_GEN_GLSL OR MATERIALX_BUILD_GEN_OSL OR MATERIALX_BUILD_GEN_MDL OR MATERIALX_BUILD_GEN_MSL OR MATERIALX_BUILD_GEN_SLANG) +if (MATERIALX_BUILD_GEN_GLSL OR MATERIALX_BUILD_GEN_OSL OR MATERIALX_BUILD_GEN_MDL OR MATERIALX_BUILD_GEN_MSL OR MATERIALX_BUILD_GEN_SLANG OR MATERIALX_BUILD_GEN_WGSL) add_subdirectory(PyMaterialXGenShader) if (MATERIALX_BUILD_GEN_GLSL) add_subdirectory(PyMaterialXGenGlsl) @@ -54,6 +54,9 @@ if (MATERIALX_BUILD_GEN_GLSL OR MATERIALX_BUILD_GEN_OSL OR MATERIALX_BUILD_GEN_M if (MATERIALX_BUILD_GEN_SLANG) add_subdirectory(PyMaterialXGenSlang) endif() + if (MATERIALX_BUILD_GEN_WGSL) + add_subdirectory(PyMaterialXGenWgsl) + endif() endif() if (MATERIALX_BUILD_RENDER) add_subdirectory(PyMaterialXRender) @@ -87,7 +90,8 @@ if (MATERIALX_BUILD_DOCS) # are integrated prior to building the Python extension modules. foreach(_py_mod IN ITEMS PyMaterialXCore PyMaterialXFormat PyMaterialXGenShader PyMaterialXGenGlsl PyMaterialXGenMsl PyMaterialXGenMdl - PyMaterialXGenOsl PyMaterialXRender PyMaterialXRenderGlsl + PyMaterialXGenOsl PyMaterialXGenSlang PyMaterialXGenWgsl + PyMaterialXRender PyMaterialXRenderGlsl PyMaterialXRenderOsl PyMaterialXRenderMsl) if(TARGET ${_py_mod}) add_dependencies(${_py_mod} PyBindDocs) diff --git a/source/PyMaterialX/PyMaterialXGenWgsl/CMakeLists.txt b/source/PyMaterialX/PyMaterialXGenWgsl/CMakeLists.txt new file mode 100644 index 0000000000..c3e1d007db --- /dev/null +++ b/source/PyMaterialX/PyMaterialXGenWgsl/CMakeLists.txt @@ -0,0 +1,26 @@ +file(GLOB pymaterialxgenwgsl_source "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") +file(GLOB pymaterialxgenwgsl_headers "${CMAKE_CURRENT_SOURCE_DIR}/*.h") + +pybind11_add_module(PyMaterialXGenWgsl SHARED ${PYBIND11_MODULE_FLAGS} ${pymaterialxgenwgsl_source} ${pymaterialxgenwgsl_headers}) + +if(APPLE) + set_target_properties(PyMaterialXGenWgsl PROPERTIES CXX_VISIBILITY_PRESET "default") +endif() + +set_target_properties( + PyMaterialXGenWgsl + PROPERTIES + OUTPUT_NAME PyMaterialXGenWgsl + COMPILE_FLAGS "${EXTERNAL_COMPILE_FLAGS}" + LINK_FLAGS "${EXTERNAL_LINK_FLAGS}" + INSTALL_RPATH "${MATERIALX_UP_TWO_RPATH}" + DEBUG_POSTFIX "${MATERIALX_PYTHON_DEBUG_POSTFIX}") + +target_link_libraries( + PyMaterialXGenWgsl + PUBLIC PyMaterialXGenShader + MaterialXGenWgsl + PRIVATE ${CMAKE_DL_LIBS}) + +install(TARGETS PyMaterialXGenWgsl + DESTINATION "${MATERIALX_PYTHON_FOLDER_NAME}") diff --git a/source/PyMaterialX/PyMaterialXGenWgsl/PyModule.cpp b/source/PyMaterialX/PyMaterialXGenWgsl/PyModule.cpp new file mode 100644 index 0000000000..3963bb7a8e --- /dev/null +++ b/source/PyMaterialX/PyMaterialXGenWgsl/PyModule.cpp @@ -0,0 +1,19 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +namespace py = pybind11; + +void bindPyWgslShaderGenerator(py::module& mod); + +PYBIND11_MODULE(PyMaterialXGenWgsl, mod) +{ + mod.doc() = "Shader generation using the WebGPU Shading Language (WGSL)."; + + PYMATERIALX_IMPORT_MODULE(PyMaterialXGenShader); + + bindPyWgslShaderGenerator(mod); +} diff --git a/source/PyMaterialX/PyMaterialXGenWgsl/PyWgslShaderGenerator.cpp b/source/PyMaterialX/PyMaterialXGenWgsl/PyWgslShaderGenerator.cpp new file mode 100644 index 0000000000..47db7c5147 --- /dev/null +++ b/source/PyMaterialX/PyMaterialXGenWgsl/PyWgslShaderGenerator.cpp @@ -0,0 +1,30 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include +#include + +namespace py = pybind11; +namespace mx = MaterialX; + +namespace +{ + mx::ShaderGeneratorPtr WgslShaderGenerator_create() + { + return mx::WgslShaderGenerator::create(); + } +} + +void bindPyWgslShaderGenerator(py::module& mod) +{ + py::class_(mod, "WgslShaderGenerator") + .def_static("create", &WgslShaderGenerator_create) + .def("generate", &mx::WgslShaderGenerator::generate) + .def("getTarget", &mx::WgslShaderGenerator::getTarget) + .def("getVersion", &mx::WgslShaderGenerator::getVersion); +} From 402e59998f4e820246afd71bbf766444e755bcae Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Thu, 2 Jul 2026 20:15:41 -0700 Subject: [PATCH 05/19] fixup: disable wgsl SPIRV validation We might look into naga-cli --- .github/workflows/main.yml | 2 +- libraries/CMakeLists.txt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 90c096f336..ba0bb41051 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -289,7 +289,7 @@ jobs: python python/Scripts/generateshader.py resources/Materials/Examples --target glsl --validator C:/vcpkg/installed/x64-windows-release/tools/glslang/glslangValidator.exe python python/Scripts/generateshader.py resources/Materials/Examples/StandardSurface --target essl --validator C:/vcpkg/installed/x64-windows-release/tools/glslang/glslangValidator.exe python python/Scripts/generateshader.py resources/Materials/Examples/StandardSurface --target vulkan --validator C:/vcpkg/installed/x64-windows-release/tools/glslang/glslangValidator.exe - python python/Scripts/generateshader.py resources/Materials/Examples/StandardSurface --target wgsl --validator "C:/vcpkg/installed/x64-windows-release/tools/glslang/glslangValidator.exe --target-env vulkan1.3 --quiet" + # Native genwgsl emits WGSL; glslangValidator is GLSL/SPIR-V only — re-enable with naga when wired in CI. - name: Shader Validation Tests (MacOS) if: matrix.test_shaders == 'ON' && runner.os == 'macOS' diff --git a/libraries/CMakeLists.txt b/libraries/CMakeLists.txt index 432df18bea..cf5d6b4c7b 100644 --- a/libraries/CMakeLists.txt +++ b/libraries/CMakeLists.txt @@ -14,7 +14,8 @@ if(MATERIALX_BUILD_DATA_LIBRARY) *.osl *.h *.metal - *.slang) + *.slang + *.wgsl) foreach(SOURCE_FILE IN LISTS MATERIALX_DATA_LIBRARY_SOURCE_FILES) set(SOURCE_FILEPATH ${CMAKE_CURRENT_SOURCE_DIR}/${SOURCE_FILE}) From fae361f5caeef588cc456b77f9ae8cef17fea944 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Tue, 7 Jul 2026 09:36:46 -0700 Subject: [PATCH 06/19] Delete auto-generated wgsl shaders fragments These will be generated as part of the CI. Note: there are still a few wgsl files that are hand written since their glsl counterparts use overloads, defines and storage qualifiers --- libraries/pbrlib/genwgsl/mx_add_bsdf.wgsl | 17 ---- libraries/pbrlib/genwgsl/mx_add_edf.wgsl | 16 ---- .../pbrlib/genwgsl/mx_anisotropic_vdf.wgsl | 23 ----- libraries/pbrlib/genwgsl/mx_artistic_ior.wgsl | 24 ------ libraries/pbrlib/genwgsl/mx_blackbody.wgsl | 56 ------------- .../genwgsl/mx_burley_diffuse_bsdf.wgsl | 54 ------------ .../pbrlib/genwgsl/mx_displacement_float.wgsl | 13 --- .../genwgsl/mx_displacement_vector3.wgsl | 13 --- .../genwgsl/mx_generalized_schlick_edf.wgsl | 33 -------- libraries/pbrlib/genwgsl/mx_layer_bsdf.wgsl | 17 ---- libraries/pbrlib/genwgsl/mx_layer_vdf.wgsl | 17 ---- libraries/pbrlib/genwgsl/mx_mix_bsdf.wgsl | 19 ----- libraries/pbrlib/genwgsl/mx_mix_edf.wgsl | 18 ---- .../genwgsl/mx_multiply_bsdf_color3.wgsl | 19 ----- .../genwgsl/mx_multiply_bsdf_float.wgsl | 19 ----- .../genwgsl/mx_multiply_edf_color3.wgsl | 16 ---- .../pbrlib/genwgsl/mx_multiply_edf_float.wgsl | 16 ---- .../genwgsl/mx_oren_nayar_diffuse_bsdf.wgsl | 69 --------------- .../genwgsl/mx_roughness_anisotropy.wgsl | 27 ------ .../pbrlib/genwgsl/mx_roughness_dual.wgsl | 16 ---- libraries/pbrlib/genwgsl/mx_sheen_bsdf.wgsl | 83 ------------------- .../pbrlib/genwgsl/mx_translucent_bsdf.wgsl | 46 ---------- libraries/pbrlib/genwgsl/mx_uniform_edf.wgsl | 20 ----- libraries/stdlib/genwgsl/mx_aastep.wgsl | 11 --- libraries/stdlib/genwgsl/mx_burn_color3.wgsl | 22 ----- libraries/stdlib/genwgsl/mx_burn_color4.wgsl | 24 ------ libraries/stdlib/genwgsl/mx_burn_float.wgsl | 20 ----- .../stdlib/genwgsl/mx_cellnoise2d_float.wgsl | 12 --- .../stdlib/genwgsl/mx_cellnoise3d_float.wgsl | 12 --- .../mx_creatematrix_vector3_matrix33.wgsl | 14 ---- .../mx_creatematrix_vector3_matrix44.wgsl | 16 ---- .../mx_creatematrix_vector4_matrix44.wgsl | 16 ---- .../genwgsl/mx_disjointover_color4.wgsl | 52 ------------ libraries/stdlib/genwgsl/mx_dodge_color3.wgsl | 22 ----- libraries/stdlib/genwgsl/mx_dodge_color4.wgsl | 24 ------ libraries/stdlib/genwgsl/mx_dodge_float.wgsl | 20 ----- libraries/stdlib/genwgsl/mx_flake2d.wgsl | 26 ------ libraries/stdlib/genwgsl/mx_flake3d.wgsl | 24 ------ .../stdlib/genwgsl/mx_fractal2d_float.wgsl | 22 ----- .../stdlib/genwgsl/mx_fractal2d_vector2.wgsl | 22 ----- .../stdlib/genwgsl/mx_fractal2d_vector3.wgsl | 22 ----- .../stdlib/genwgsl/mx_fractal2d_vector4.wgsl | 22 ----- .../stdlib/genwgsl/mx_fractal3d_float.wgsl | 22 ----- .../stdlib/genwgsl/mx_fractal3d_vector2.wgsl | 22 ----- .../stdlib/genwgsl/mx_fractal3d_vector3.wgsl | 22 ----- .../stdlib/genwgsl/mx_fractal3d_vector4.wgsl | 22 ----- .../genwgsl/mx_heighttonormal_vector3.wgsl | 36 -------- .../stdlib/genwgsl/mx_hsvtorgb_color3.wgsl | 12 --- .../stdlib/genwgsl/mx_hsvtorgb_color4.wgsl | 13 --- .../stdlib/genwgsl/mx_luminance_color3.wgsl | 12 --- .../stdlib/genwgsl/mx_luminance_color4.wgsl | 13 --- .../stdlib/genwgsl/mx_mix_surfaceshader.wgsl | 13 --- .../stdlib/genwgsl/mx_noise2d_float.wgsl | 18 ---- .../stdlib/genwgsl/mx_noise2d_vector2.wgsl | 18 ---- .../stdlib/genwgsl/mx_noise2d_vector3.wgsl | 18 ---- .../stdlib/genwgsl/mx_noise2d_vector4.wgsl | 21 ----- .../stdlib/genwgsl/mx_noise3d_float.wgsl | 18 ---- .../stdlib/genwgsl/mx_noise3d_vector2.wgsl | 18 ---- .../stdlib/genwgsl/mx_noise3d_vector3.wgsl | 18 ---- .../stdlib/genwgsl/mx_noise3d_vector4.wgsl | 21 ----- libraries/stdlib/genwgsl/mx_normalmap.wgsl | 42 ---------- .../stdlib/genwgsl/mx_premult_color4.wgsl | 11 --- libraries/stdlib/genwgsl/mx_ramplr_float.wgsl | 14 ---- .../stdlib/genwgsl/mx_ramplr_vector2.wgsl | 14 ---- .../stdlib/genwgsl/mx_ramplr_vector3.wgsl | 14 ---- .../stdlib/genwgsl/mx_ramplr_vector4.wgsl | 14 ---- libraries/stdlib/genwgsl/mx_ramptb_float.wgsl | 14 ---- .../stdlib/genwgsl/mx_ramptb_vector2.wgsl | 14 ---- .../stdlib/genwgsl/mx_ramptb_vector3.wgsl | 14 ---- .../stdlib/genwgsl/mx_ramptb_vector4.wgsl | 14 ---- .../stdlib/genwgsl/mx_rgbtohsv_color3.wgsl | 12 --- .../stdlib/genwgsl/mx_rgbtohsv_color4.wgsl | 13 --- .../stdlib/genwgsl/mx_rotate_vector2.wgsl | 18 ---- .../stdlib/genwgsl/mx_rotate_vector3.wgsl | 23 ----- .../stdlib/genwgsl/mx_smoothstep_float.wgsl | 20 ----- .../stdlib/genwgsl/mx_splitlr_float.wgsl | 18 ---- .../stdlib/genwgsl/mx_splitlr_vector2.wgsl | 18 ---- .../stdlib/genwgsl/mx_splitlr_vector3.wgsl | 18 ---- .../stdlib/genwgsl/mx_splitlr_vector4.wgsl | 18 ---- .../stdlib/genwgsl/mx_splittb_float.wgsl | 18 ---- .../stdlib/genwgsl/mx_splittb_vector2.wgsl | 18 ---- .../stdlib/genwgsl/mx_splittb_vector3.wgsl | 18 ---- .../stdlib/genwgsl/mx_splittb_vector4.wgsl | 18 ---- .../stdlib/genwgsl/mx_surface_unlit.wgsl | 9 -- .../genwgsl/mx_transformmatrix_vector2M3.wgsl | 15 ---- .../genwgsl/mx_transformmatrix_vector3M4.wgsl | 15 ---- .../stdlib/genwgsl/mx_unpremult_color4.wgsl | 11 --- .../genwgsl/mx_worleynoise2d_float.wgsl | 16 ---- .../genwgsl/mx_worleynoise2d_vector2.wgsl | 16 ---- .../genwgsl/mx_worleynoise2d_vector3.wgsl | 16 ---- .../genwgsl/mx_worleynoise3d_float.wgsl | 16 ---- .../genwgsl/mx_worleynoise3d_vector2.wgsl | 16 ---- .../genwgsl/mx_worleynoise3d_vector3.wgsl | 16 ---- 93 files changed, 1932 deletions(-) delete mode 100644 libraries/pbrlib/genwgsl/mx_add_bsdf.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_add_edf.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_anisotropic_vdf.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_artistic_ior.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_blackbody.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_burley_diffuse_bsdf.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_displacement_float.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_displacement_vector3.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_generalized_schlick_edf.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_layer_bsdf.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_layer_vdf.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_mix_bsdf.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_mix_edf.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_multiply_bsdf_color3.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_multiply_bsdf_float.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_multiply_edf_color3.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_multiply_edf_float.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_oren_nayar_diffuse_bsdf.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_roughness_anisotropy.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_roughness_dual.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_sheen_bsdf.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_translucent_bsdf.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_uniform_edf.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_aastep.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_burn_color3.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_burn_color4.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_burn_float.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_cellnoise2d_float.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_cellnoise3d_float.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_creatematrix_vector3_matrix33.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_creatematrix_vector3_matrix44.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_creatematrix_vector4_matrix44.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_disjointover_color4.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_dodge_color3.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_dodge_color4.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_dodge_float.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_flake2d.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_flake3d.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_fractal2d_float.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_fractal2d_vector2.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_fractal2d_vector3.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_fractal2d_vector4.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_fractal3d_float.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_fractal3d_vector2.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_fractal3d_vector3.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_fractal3d_vector4.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_heighttonormal_vector3.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_hsvtorgb_color3.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_hsvtorgb_color4.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_luminance_color3.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_luminance_color4.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_mix_surfaceshader.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_noise2d_float.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_noise2d_vector2.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_noise2d_vector3.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_noise2d_vector4.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_noise3d_float.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_noise3d_vector2.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_noise3d_vector3.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_noise3d_vector4.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_normalmap.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_premult_color4.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_ramplr_float.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_ramplr_vector2.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_ramplr_vector3.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_ramplr_vector4.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_ramptb_float.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_ramptb_vector2.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_ramptb_vector3.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_ramptb_vector4.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_rgbtohsv_color3.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_rgbtohsv_color4.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_rotate_vector2.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_rotate_vector3.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_smoothstep_float.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_splitlr_float.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_splitlr_vector2.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_splitlr_vector3.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_splitlr_vector4.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_splittb_float.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_splittb_vector2.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_splittb_vector3.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_splittb_vector4.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_surface_unlit.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_transformmatrix_vector2M3.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_transformmatrix_vector3M4.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_unpremult_color4.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_worleynoise2d_float.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_worleynoise2d_vector2.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_worleynoise2d_vector3.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_worleynoise3d_float.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_worleynoise3d_vector2.wgsl delete mode 100644 libraries/stdlib/genwgsl/mx_worleynoise3d_vector3.wgsl diff --git a/libraries/pbrlib/genwgsl/mx_add_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_add_bsdf.wgsl deleted file mode 100644 index f954d627eb..0000000000 --- a/libraries/pbrlib/genwgsl/mx_add_bsdf.wgsl +++ /dev/null @@ -1,17 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_add_bsdf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_closure_type.wgsl" - -fn mx_add_bsdf(closureData_21: ClosureData, in1_8: BSDF, in2_8: BSDF, result_82: ptr) { - var closureData_22: ClosureData; - var in1_9: BSDF; - var in2_9: BSDF; - - closureData_22 = closureData_21; - in1_9 = in1_8; - in2_9 = in2_8; - (*result_82).response = (in1_9.response + in2_9.response); - (*result_82).throughput = max(((in1_9.throughput + in2_9.throughput) - vec3(1.0)), vec3(0.0)); - return; -} diff --git a/libraries/pbrlib/genwgsl/mx_add_edf.wgsl b/libraries/pbrlib/genwgsl/mx_add_edf.wgsl deleted file mode 100644 index 604bcc00d2..0000000000 --- a/libraries/pbrlib/genwgsl/mx_add_edf.wgsl +++ /dev/null @@ -1,16 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_add_edf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_closure_type.wgsl" - -fn mx_add_edf(closureData_21: ClosureData, in1_8: vec3f, in2_8: vec3f, result_82: ptr) { - var closureData_22: ClosureData; - var in1_9: vec3f; - var in2_9: vec3f; - - closureData_22 = closureData_21; - in1_9 = in1_8; - in2_9 = in2_8; - (*result_82) = (in1_9 + in2_9); - return; -} diff --git a/libraries/pbrlib/genwgsl/mx_anisotropic_vdf.wgsl b/libraries/pbrlib/genwgsl/mx_anisotropic_vdf.wgsl deleted file mode 100644 index 63e7f5edd3..0000000000 --- a/libraries/pbrlib/genwgsl/mx_anisotropic_vdf.wgsl +++ /dev/null @@ -1,23 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_anisotropic_vdf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_closure_type.wgsl" - -fn mx_anisotropic_vdf(closureData_21: ClosureData, absorption_2: vec3f, scattering: vec3f, anisotropy_2: f32, vdf: ptr) { - var closureData_22: ClosureData; - var absorption_3: vec3f; - var anisotropy_3: f32; - - closureData_22 = closureData_21; - absorption_3 = absorption_2; - anisotropy_3 = anisotropy_2; - if (closureData_22.closureType == 2i) { - { - (*vdf).response = vec3(0.0); - (*vdf).throughput = exp(-(absorption_3)); - return; - } - } else { - return; - } -} diff --git a/libraries/pbrlib/genwgsl/mx_artistic_ior.wgsl b/libraries/pbrlib/genwgsl/mx_artistic_ior.wgsl deleted file mode 100644 index c67a1a6591..0000000000 --- a/libraries/pbrlib/genwgsl/mx_artistic_ior.wgsl +++ /dev/null @@ -1,24 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_artistic_ior.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_artistic_ior(reflectivity: vec3f, edge_color: vec3f, ior_8: ptr, extinction_1: ptr) { - var r_4: vec3f; - var r_sqrt: vec3f; - var n_min: vec3f; - var n_max: vec3f; - var np1_: vec3f; - var nm1_: vec3f; - var k2_: vec3f; - - r_4 = clamp(reflectivity, vec3(0.0), vec3(0.99)); - r_sqrt = sqrt(r_4); - n_min = ((vec3(1.0) - r_4) / (vec3(1.0) + r_4)); - n_max = ((vec3(1.0) + r_sqrt) / (vec3(1.0) - r_sqrt)); - (*ior_8) = mix(n_max, n_min, edge_color); - np1_ = (((*ior_8)) + vec3(1.0)); - nm1_ = (((*ior_8)) - vec3(1.0)); - k2_ = ((((np1_ * np1_) * r_4) - (nm1_ * nm1_)) / (vec3(1.0) - r_4)); - k2_ = max(k2_, vec3(0.0)); - (*extinction_1) = sqrt(k2_); - return; -} diff --git a/libraries/pbrlib/genwgsl/mx_blackbody.wgsl b/libraries/pbrlib/genwgsl/mx_blackbody.wgsl deleted file mode 100644 index 27f58b5742..0000000000 --- a/libraries/pbrlib/genwgsl/mx_blackbody.wgsl +++ /dev/null @@ -1,56 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_blackbody.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_blackbody(temperatureKelvin: f32, colorValue: ptr) { - var temperatureKelvin_1: f32; - var xc: f32; - var yc: f32; - var t_6: f32; - var t2_: f32; - var t3_: f32; - var xc2_: f32; - var xc3_: f32; - var XYZ: vec3f; - - temperatureKelvin_1 = temperatureKelvin; - temperatureKelvin_1 = clamp(temperatureKelvin_1, 800.0, 25000.0); - t_6 = (1000.0 / temperatureKelvin_1); - t2_ = (t_6 * t_6); - t3_ = ((t_6 * t_6) * t_6); - if (temperatureKelvin_1 < 4000.0) { - { - xc = ((((-0.2661239 * t3_) - (0.234358 * t2_)) + (0.8776956 * t_6)) + 0.17991); - } - } else { - { - xc = ((((-3.025847 * t3_) + (2.1070378 * t2_)) + (0.2226347 * t_6)) + 0.24039); - } - } - xc2_ = (xc * xc); - xc3_ = ((xc * xc) * xc); - if (temperatureKelvin_1 < 2222.0) { - { - yc = ((((-1.1063814 * xc3_) - (1.3481102 * xc2_)) + (2.1855583 * xc)) - 0.20219684); - } - } else { - if (temperatureKelvin_1 < 4000.0) { - { - yc = ((((-0.9549476 * xc3_) - (1.3741859 * xc2_)) + (2.09137 * xc)) - 0.16748866); - } - } else { - { - yc = ((((3.081758 * xc3_) - (5.873387 * xc2_)) + (3.7511299 * xc)) - 0.37001482); - } - } - } - if (yc <= 0.0) { - { - (*colorValue) = vec3(1.0); - return; - } - } - XYZ = vec3f((xc / yc), 1.0, (((1.0 - xc) - yc) / yc)); - (*colorValue) = (mx_matrix_mul_mat3_vec3(XYZ_to_RGB, XYZ)); - (*colorValue) = max(((*colorValue)), vec3(0.0)); - return; -} diff --git a/libraries/pbrlib/genwgsl/mx_burley_diffuse_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_burley_diffuse_bsdf.wgsl deleted file mode 100644 index f6b7d15d0f..0000000000 --- a/libraries/pbrlib/genwgsl/mx_burley_diffuse_bsdf.wgsl +++ /dev/null @@ -1,54 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_burley_diffuse_bsdf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_closure_type.wgsl" -#include "lib/mx_microfacet_diffuse.wgsl" - -fn mx_burley_diffuse_bsdf(closureData_21: ClosureData, weight_7: f32, color_9: vec3f, roughness_29: f32, N_24: vec3f, bsdf_8: ptr) { - var closureData_22: ClosureData; - var weight_8: f32; - var color_10: vec3f; - var roughness_30: f32; - var N_25: vec3f; - var V_8: vec3f; - var L_4: vec3f; - var NdotV_24: f32; - var NdotL_5: f32; - var LdotH_1: f32; - var Li: vec3f; - - closureData_22 = closureData_21; - weight_8 = weight_7; - color_10 = color_9; - roughness_30 = roughness_29; - N_25 = N_24; - (*bsdf_8).throughput = vec3(0.0); - if (weight_8 < 0.00000001) { - { - return; - } - } - V_8 = closureData_22.V; - L_4 = closureData_22.L; - N_25 = (mx_forward_facing_normal(N_25, V_8)); - NdotV_24 = clamp(dot(N_25, V_8), 0.00000001, 1.0); - if (closureData_22.closureType == 1i) { - { - NdotL_5 = clamp(dot(N_25, L_4), 0.00000001, 1.0); - LdotH_1 = clamp(dot(L_4, normalize((L_4 + V_8))), 0.00000001, 1.0); - (*bsdf_8).response = ((((color_10 * closureData_22.occlusion) * weight_8) * NdotL_5) * 0.31830987); - (*bsdf_8).response = (((*bsdf_8)).response * (mx_burley_diffuse(NdotV_24, NdotL_5, LdotH_1, roughness_30))); - return; - } - } else { - if (closureData_22.closureType == 3i) { - { - Li = (mx_environment_irradiance(N_25) * (mx_burley_diffuse_dir_albedo(NdotV_24, roughness_30))); - (*bsdf_8).response = ((Li * color_10) * weight_8); - return; - } - } else { - return; - } - } -} diff --git a/libraries/pbrlib/genwgsl/mx_displacement_float.wgsl b/libraries/pbrlib/genwgsl/mx_displacement_float.wgsl deleted file mode 100644 index 791eccc4bd..0000000000 --- a/libraries/pbrlib/genwgsl/mx_displacement_float.wgsl +++ /dev/null @@ -1,13 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_displacement_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_displacement_float(disp_1: f32, scale_3: f32, result_82: ptr) { - var disp_2: f32; - var scale_4: f32; - - disp_2 = disp_1; - scale_4 = scale_3; - (*result_82).offset = vec3(disp_2); - (*result_82).scale = scale_4; - return; -} diff --git a/libraries/pbrlib/genwgsl/mx_displacement_vector3.wgsl b/libraries/pbrlib/genwgsl/mx_displacement_vector3.wgsl deleted file mode 100644 index 328da6a7cd..0000000000 --- a/libraries/pbrlib/genwgsl/mx_displacement_vector3.wgsl +++ /dev/null @@ -1,13 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_displacement_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_displacement_vector3(disp_1: vec3f, scale_3: f32, result_82: ptr) { - var disp_2: vec3f; - var scale_4: f32; - - disp_2 = disp_1; - scale_4 = scale_3; - (*result_82).offset = disp_2; - (*result_82).scale = scale_4; - return; -} diff --git a/libraries/pbrlib/genwgsl/mx_generalized_schlick_edf.wgsl b/libraries/pbrlib/genwgsl/mx_generalized_schlick_edf.wgsl deleted file mode 100644 index 3eae1d27f2..0000000000 --- a/libraries/pbrlib/genwgsl/mx_generalized_schlick_edf.wgsl +++ /dev/null @@ -1,33 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_generalized_schlick_edf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_closure_type.wgsl" -#include "lib/mx_microfacet.wgsl" - -fn mx_generalized_schlick_edf(closureData_21: ClosureData, color0_1: vec3f, color90_1: vec3f, exponent_4: f32, base_2: vec3f, result_82: ptr) { - var closureData_22: ClosureData; - var color0_2: vec3f; - var color90_2: vec3f; - var exponent_5: f32; - var base_3: vec3f; - var N_25: vec3f; - var NdotV_24: f32; - var f_1: vec3f; - - closureData_22 = closureData_21; - color0_2 = color0_1; - color90_2 = color90_1; - exponent_5 = exponent_4; - base_3 = base_2; - if (closureData_22.closureType == 4i) { - { - N_25 = (mx_forward_facing_normal(closureData_22.N, closureData_22.V)); - NdotV_24 = clamp(dot(N_25, closureData_22.V), 0.00000001, 1.0); - f_1 = (mx_fresnel_schlick_vec3_exp(NdotV_24, color0_2, color90_2, exponent_5)); - (*result_82) = (base_3 * f_1); - return; - } - } else { - return; - } -} diff --git a/libraries/pbrlib/genwgsl/mx_layer_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_layer_bsdf.wgsl deleted file mode 100644 index 335e625e68..0000000000 --- a/libraries/pbrlib/genwgsl/mx_layer_bsdf.wgsl +++ /dev/null @@ -1,17 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_layer_bsdf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_closure_type.wgsl" - -fn mx_layer_bsdf(closureData_21: ClosureData, top_1: BSDF, base_2: BSDF, result_82: ptr) { - var closureData_22: ClosureData; - var top_2: BSDF; - var base_3: BSDF; - - closureData_22 = closureData_21; - top_2 = top_1; - base_3 = base_2; - (*result_82).response = (top_2.response + (base_3.response * top_2.throughput)); - (*result_82).throughput = (top_2.throughput * base_3.throughput); - return; -} diff --git a/libraries/pbrlib/genwgsl/mx_layer_vdf.wgsl b/libraries/pbrlib/genwgsl/mx_layer_vdf.wgsl deleted file mode 100644 index 166a03b5b0..0000000000 --- a/libraries/pbrlib/genwgsl/mx_layer_vdf.wgsl +++ /dev/null @@ -1,17 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_layer_vdf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_closure_type.wgsl" - -fn mx_layer_vdf(closureData_21: ClosureData, top_1: BSDF, base_2: VDF, result_82: ptr) { - var closureData_22: ClosureData; - var top_2: BSDF; - var base_3: VDF; - - closureData_22 = closureData_21; - top_2 = top_1; - base_3 = base_2; - (*result_82).response = (top_2.response * base_3.throughput); - (*result_82).throughput = (top_2.throughput * base_3.throughput); - return; -} diff --git a/libraries/pbrlib/genwgsl/mx_mix_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_mix_bsdf.wgsl deleted file mode 100644 index 4db2230dcf..0000000000 --- a/libraries/pbrlib/genwgsl/mx_mix_bsdf.wgsl +++ /dev/null @@ -1,19 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_mix_bsdf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_closure_type.wgsl" - -fn mx_mix_bsdf(closureData_21: ClosureData, fg_9: BSDF, bg_9: BSDF, mixValue_1: f32, result_82: ptr) { - var closureData_22: ClosureData; - var fg_10: BSDF; - var bg_10: BSDF; - var mixValue_2: f32; - - closureData_22 = closureData_21; - fg_10 = fg_9; - bg_10 = bg_9; - mixValue_2 = mixValue_1; - (*result_82).response = mix(bg_10.response, fg_10.response, vec3(mixValue_2)); - (*result_82).throughput = mix(bg_10.throughput, fg_10.throughput, vec3(mixValue_2)); - return; -} diff --git a/libraries/pbrlib/genwgsl/mx_mix_edf.wgsl b/libraries/pbrlib/genwgsl/mx_mix_edf.wgsl deleted file mode 100644 index 8a749c995b..0000000000 --- a/libraries/pbrlib/genwgsl/mx_mix_edf.wgsl +++ /dev/null @@ -1,18 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_mix_edf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_closure_type.wgsl" - -fn mx_mix_edf(closureData_21: ClosureData, fg_9: vec3f, bg_9: vec3f, mixValue_1: f32, result_82: ptr) { - var closureData_22: ClosureData; - var fg_10: vec3f; - var bg_10: vec3f; - var mixValue_2: f32; - - closureData_22 = closureData_21; - fg_10 = fg_9; - bg_10 = bg_9; - mixValue_2 = mixValue_1; - (*result_82) = mix(bg_10, fg_10, vec3(mixValue_2)); - return; -} diff --git a/libraries/pbrlib/genwgsl/mx_multiply_bsdf_color3.wgsl b/libraries/pbrlib/genwgsl/mx_multiply_bsdf_color3.wgsl deleted file mode 100644 index 14d2a9db8d..0000000000 --- a/libraries/pbrlib/genwgsl/mx_multiply_bsdf_color3.wgsl +++ /dev/null @@ -1,19 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_multiply_bsdf_color3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_closure_type.wgsl" - -fn mx_multiply_bsdf_color3(closureData_21: ClosureData, in1_8: BSDF, in2_8: vec3f, result_82: ptr) { - var closureData_22: ClosureData; - var in1_9: BSDF; - var in2_9: vec3f; - var tint_2: vec3f; - - closureData_22 = closureData_21; - in1_9 = in1_8; - in2_9 = in2_8; - tint_2 = clamp(in2_9, vec3(0.0), vec3(1.0)); - (*result_82).response = (in1_9.response * tint_2); - (*result_82).throughput = in1_9.throughput; - return; -} diff --git a/libraries/pbrlib/genwgsl/mx_multiply_bsdf_float.wgsl b/libraries/pbrlib/genwgsl/mx_multiply_bsdf_float.wgsl deleted file mode 100644 index c0e84e82bb..0000000000 --- a/libraries/pbrlib/genwgsl/mx_multiply_bsdf_float.wgsl +++ /dev/null @@ -1,19 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_multiply_bsdf_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_closure_type.wgsl" - -fn mx_multiply_bsdf_float(closureData_21: ClosureData, in1_8: BSDF, in2_8: f32, result_82: ptr) { - var closureData_22: ClosureData; - var in1_9: BSDF; - var in2_9: f32; - var weight_8: f32; - - closureData_22 = closureData_21; - in1_9 = in1_8; - in2_9 = in2_8; - weight_8 = clamp(in2_9, 0.0, 1.0); - (*result_82).response = (in1_9.response * weight_8); - (*result_82).throughput = in1_9.throughput; - return; -} diff --git a/libraries/pbrlib/genwgsl/mx_multiply_edf_color3.wgsl b/libraries/pbrlib/genwgsl/mx_multiply_edf_color3.wgsl deleted file mode 100644 index af5b33dd81..0000000000 --- a/libraries/pbrlib/genwgsl/mx_multiply_edf_color3.wgsl +++ /dev/null @@ -1,16 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_multiply_edf_color3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_closure_type.wgsl" - -fn mx_multiply_edf_color3(closureData_21: ClosureData, in1_8: vec3f, in2_8: vec3f, result_82: ptr) { - var closureData_22: ClosureData; - var in1_9: vec3f; - var in2_9: vec3f; - - closureData_22 = closureData_21; - in1_9 = in1_8; - in2_9 = in2_8; - (*result_82) = (in1_9 * in2_9); - return; -} diff --git a/libraries/pbrlib/genwgsl/mx_multiply_edf_float.wgsl b/libraries/pbrlib/genwgsl/mx_multiply_edf_float.wgsl deleted file mode 100644 index 39f7087dab..0000000000 --- a/libraries/pbrlib/genwgsl/mx_multiply_edf_float.wgsl +++ /dev/null @@ -1,16 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_multiply_edf_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_closure_type.wgsl" - -fn mx_multiply_edf_float(closureData_21: ClosureData, in1_8: vec3f, in2_8: f32, result_82: ptr) { - var closureData_22: ClosureData; - var in1_9: vec3f; - var in2_9: f32; - - closureData_22 = closureData_21; - in1_9 = in1_8; - in2_9 = in2_8; - (*result_82) = (in1_9 * in2_9); - return; -} diff --git a/libraries/pbrlib/genwgsl/mx_oren_nayar_diffuse_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_oren_nayar_diffuse_bsdf.wgsl deleted file mode 100644 index cfb9ba5f4d..0000000000 --- a/libraries/pbrlib/genwgsl/mx_oren_nayar_diffuse_bsdf.wgsl +++ /dev/null @@ -1,69 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_oren_nayar_diffuse_bsdf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_closure_type.wgsl" -#include "lib/mx_microfacet_diffuse.wgsl" - -fn mx_oren_nayar_diffuse_bsdf(closureData_21: ClosureData, weight_7: f32, color_9: vec3f, roughness_29: f32, N_24: vec3f, energy_compensation: bool, bsdf_8: ptr) { - var closureData_22: ClosureData; - var weight_8: f32; - var color_10: vec3f; - var roughness_30: f32; - var N_25: vec3f; - var V_8: vec3f; - var L_4: vec3f; - var NdotV_24: f32; - var NdotL_5: f32; - var LdotV_2: f32; - var local: vec3f; - var diffuse: vec3f; - var local_1: vec3f; - var diffuse_1: vec3f; - var Li: vec3f; - - closureData_22 = closureData_21; - weight_8 = weight_7; - color_10 = color_9; - roughness_30 = roughness_29; - N_25 = N_24; - (*bsdf_8).throughput = vec3(0.0); - if (weight_8 < 0.00000001) { - { - return; - } - } - V_8 = closureData_22.V; - L_4 = closureData_22.L; - N_25 = (mx_forward_facing_normal(N_25, V_8)); - NdotV_24 = clamp(dot(N_25, V_8), 0.00000001, 1.0); - if (closureData_22.closureType == 1i) { - { - NdotL_5 = clamp(dot(N_25, L_4), 0.00000001, 1.0); - LdotV_2 = clamp(dot(L_4, V_8), 0.00000001, 1.0); - if energy_compensation { - local = (mx_oren_nayar_compensated_diffuse(NdotV_24, NdotL_5, LdotV_2, roughness_30, color_10)); - } else { - local = ((mx_oren_nayar_diffuse(NdotV_24, NdotL_5, LdotV_2, roughness_30)) * color_10); - } - diffuse = local; - (*bsdf_8).response = ((((diffuse * closureData_22.occlusion) * weight_8) * NdotL_5) * 0.31830987); - return; - } - } else { - if (closureData_22.closureType == 3i) { - { - if energy_compensation { - local_1 = (mx_oren_nayar_compensated_diffuse_dir_albedo(NdotV_24, roughness_30, color_10)); - } else { - local_1 = ((mx_oren_nayar_diffuse_dir_albedo(NdotV_24, roughness_30)) * color_10); - } - diffuse_1 = local_1; - Li = mx_environment_irradiance(N_25); - (*bsdf_8).response = ((Li * diffuse_1) * weight_8); - return; - } - } else { - return; - } - } -} diff --git a/libraries/pbrlib/genwgsl/mx_roughness_anisotropy.wgsl b/libraries/pbrlib/genwgsl/mx_roughness_anisotropy.wgsl deleted file mode 100644 index 085895817c..0000000000 --- a/libraries/pbrlib/genwgsl/mx_roughness_anisotropy.wgsl +++ /dev/null @@ -1,27 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_roughness_anisotropy.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_roughness_anisotropy(roughness_29: f32, anisotropy_2: f32, result_82: ptr) { - var roughness_30: f32; - var anisotropy_3: f32; - var roughness_sqr: f32; - var aspect: f32; - - roughness_30 = roughness_29; - anisotropy_3 = anisotropy_2; - roughness_sqr = clamp((roughness_30 * roughness_30), 0.00000001, 1.0); - if (anisotropy_3 > 0.0) { - { - aspect = sqrt((1.0 - clamp(anisotropy_3, 0.0, 0.98))); - (*result_82).x = min((roughness_sqr / aspect), 1.0); - (*result_82).y = (roughness_sqr * aspect); - return; - } - } else { - { - (*result_82).x = roughness_sqr; - (*result_82).y = roughness_sqr; - return; - } - } -} diff --git a/libraries/pbrlib/genwgsl/mx_roughness_dual.wgsl b/libraries/pbrlib/genwgsl/mx_roughness_dual.wgsl deleted file mode 100644 index a45cacaa7f..0000000000 --- a/libraries/pbrlib/genwgsl/mx_roughness_dual.wgsl +++ /dev/null @@ -1,16 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_roughness_dual.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_roughness_dual(roughness_29: vec2f, result_82: ptr) { - var roughness_30: vec2f; - - roughness_30 = roughness_29; - if (roughness_30.y < 0.0) { - { - roughness_30.y = roughness_30.x; - } - } - (*result_82).x = clamp((roughness_30.x * roughness_30.x), 0.00000001, 1.0); - (*result_82).y = clamp((roughness_30.y * roughness_30.y), 0.00000001, 1.0); - return; -} diff --git a/libraries/pbrlib/genwgsl/mx_sheen_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_sheen_bsdf.wgsl deleted file mode 100644 index 67fba4257a..0000000000 --- a/libraries/pbrlib/genwgsl/mx_sheen_bsdf.wgsl +++ /dev/null @@ -1,83 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_sheen_bsdf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_closure_type.wgsl" -#include "lib/mx_microfacet_sheen.wgsl" - -fn mx_sheen_bsdf(closureData_21: ClosureData, weight_7: f32, color_9: vec3f, roughness_29: f32, N_24: vec3f, mode: i32, bsdf_8: ptr) { - var closureData_22: ClosureData; - var weight_8: f32; - var color_10: vec3f; - var roughness_30: f32; - var N_25: vec3f; - var V_8: vec3f; - var L_4: vec3f; - var NdotV_24: f32; - var dirAlbedo: f32; - var H_2: vec3f; - var NdotL_5: f32; - var NdotH_2: f32; - var fr: vec3f; - var fr_1: vec3f; - var dirAlbedo_1: f32; - var Li: vec3f; - - closureData_22 = closureData_21; - weight_8 = weight_7; - color_10 = color_9; - roughness_30 = roughness_29; - N_25 = N_24; - if (weight_8 < 0.00000001) { - { - return; - } - } - V_8 = closureData_22.V; - L_4 = closureData_22.L; - N_25 = (mx_forward_facing_normal(N_25, V_8)); - NdotV_24 = clamp(dot(N_25, V_8), 0.00000001, 1.0); - if (closureData_22.closureType == 1i) { - { - if (mode == 0i) { - { - H_2 = normalize((L_4 + V_8)); - NdotL_5 = clamp(dot(N_25, L_4), 0.00000001, 1.0); - NdotH_2 = clamp(dot(N_25, H_2), 0.00000001, 1.0); - fr = (color_10 * (mx_imageworks_sheen_brdf(NdotL_5, NdotV_24, NdotH_2, roughness_30))); - dirAlbedo = (mx_imageworks_sheen_dir_albedo(NdotV_24, roughness_30)); - (*bsdf_8).response = (((fr * NdotL_5) * closureData_22.occlusion) * weight_8); - } - } else { - { - roughness_30 = clamp(roughness_30, 0.01, 1.0); - fr_1 = (color_10 * (mx_zeltner_sheen_brdf(L_4, V_8, N_25, NdotV_24, roughness_30))); - dirAlbedo = (mx_zeltner_sheen_dir_albedo(NdotV_24, roughness_30)); - (*bsdf_8).response = (((dirAlbedo * fr_1) * closureData_22.occlusion) * weight_8); - } - } - (*bsdf_8).throughput = vec3((1.0 - (dirAlbedo * weight_8))); - return; - } - } else { - if (closureData_22.closureType == 3i) { - { - if (mode == 0i) { - { - dirAlbedo_1 = (mx_imageworks_sheen_dir_albedo(NdotV_24, roughness_30)); - } - } else { - { - roughness_30 = clamp(roughness_30, 0.01, 1.0); - dirAlbedo_1 = (mx_zeltner_sheen_dir_albedo(NdotV_24, roughness_30)); - } - } - Li = mx_environment_irradiance(N_25); - (*bsdf_8).response = (((Li * color_10) * dirAlbedo_1) * weight_8); - (*bsdf_8).throughput = vec3((1.0 - (dirAlbedo_1 * weight_8))); - return; - } - } else { - return; - } - } -} diff --git a/libraries/pbrlib/genwgsl/mx_translucent_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_translucent_bsdf.wgsl deleted file mode 100644 index c58ca23141..0000000000 --- a/libraries/pbrlib/genwgsl/mx_translucent_bsdf.wgsl +++ /dev/null @@ -1,46 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_translucent_bsdf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_closure_type.wgsl" - -fn mx_translucent_bsdf(closureData_21: ClosureData, weight_7: f32, color_9: vec3f, N_24: vec3f, bsdf_8: ptr) { - var closureData_22: ClosureData; - var weight_8: f32; - var color_10: vec3f; - var N_25: vec3f; - var V_8: vec3f; - var L_4: vec3f; - var NdotL_5: f32; - var Li: vec3f; - - closureData_22 = closureData_21; - weight_8 = weight_7; - color_10 = color_9; - N_25 = N_24; - (*bsdf_8).throughput = vec3(0.0); - if (weight_8 < 0.00000001) { - { - return; - } - } - V_8 = closureData_22.V; - L_4 = closureData_22.L; - N_25 = -(N_25); - if (closureData_22.closureType == 1i) { - { - NdotL_5 = clamp(dot(N_25, L_4), 0.0, 1.0); - (*bsdf_8).response = (((color_10 * weight_8) * NdotL_5) * 0.31830987); - return; - } - } else { - if (closureData_22.closureType == 3i) { - { - Li = mx_environment_irradiance(N_25); - (*bsdf_8).response = ((Li * color_10) * weight_8); - return; - } - } else { - return; - } - } -} diff --git a/libraries/pbrlib/genwgsl/mx_uniform_edf.wgsl b/libraries/pbrlib/genwgsl/mx_uniform_edf.wgsl deleted file mode 100644 index 64965b44b0..0000000000 --- a/libraries/pbrlib/genwgsl/mx_uniform_edf.wgsl +++ /dev/null @@ -1,20 +0,0 @@ -// Generated from libraries/pbrlib/genglsl/mx_uniform_edf.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_closure_type.wgsl" - -fn mx_uniform_edf(closureData_21: ClosureData, color_9: vec3f, result_82: ptr) { - var closureData_22: ClosureData; - var color_10: vec3f; - - closureData_22 = closureData_21; - color_10 = color_9; - if (closureData_22.closureType == 4i) { - { - (*result_82) = color_10; - return; - } - } else { - return; - } -} diff --git a/libraries/stdlib/genwgsl/mx_aastep.wgsl b/libraries/stdlib/genwgsl/mx_aastep.wgsl deleted file mode 100644 index e37477e62d..0000000000 --- a/libraries/stdlib/genwgsl/mx_aastep.wgsl +++ /dev/null @@ -1,11 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_aastep.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_aastep(threshold: f32, value_2: f32) -> f32 { - var value_3: f32; - var afwidth: f32; - - value_3 = value_2; - afwidth = (length(vec2f(dpdx(value_3), dpdy(value_3))) * 0.70710677); - return smoothstep((threshold - afwidth), (threshold + afwidth), value_3); -} diff --git a/libraries/stdlib/genwgsl/mx_burn_color3.wgsl b/libraries/stdlib/genwgsl/mx_burn_color3.wgsl deleted file mode 100644 index 77bbff968b..0000000000 --- a/libraries/stdlib/genwgsl/mx_burn_color3.wgsl +++ /dev/null @@ -1,22 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_burn_color3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "mx_burn_float.wgsl" - -fn mx_burn_color3(fg_9: vec3f, bg_9: vec3f, mixval_6: f32, result_82: ptr) { - var fg_10: vec3f; - var bg_10: vec3f; - var mixval_7: f32; - var f_1: f32; - - fg_10 = fg_9; - bg_10 = bg_9; - mixval_7 = mixval_6; - mx_burn_float(fg_10.x, bg_10.x, mixval_7, (&f_1)); - (*result_82).x = f_1; - mx_burn_float(fg_10.y, bg_10.y, mixval_7, (&f_1)); - (*result_82).y = f_1; - mx_burn_float(fg_10.z, bg_10.z, mixval_7, (&f_1)); - (*result_82).z = f_1; - return; -} diff --git a/libraries/stdlib/genwgsl/mx_burn_color4.wgsl b/libraries/stdlib/genwgsl/mx_burn_color4.wgsl deleted file mode 100644 index bff6caef96..0000000000 --- a/libraries/stdlib/genwgsl/mx_burn_color4.wgsl +++ /dev/null @@ -1,24 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_burn_color4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "mx_burn_float.wgsl" - -fn mx_burn_color4(fg_9: vec4f, bg_9: vec4f, mixval_6: f32, result_82: ptr) { - var fg_10: vec4f; - var bg_10: vec4f; - var mixval_7: f32; - var f_1: f32; - - fg_10 = fg_9; - bg_10 = bg_9; - mixval_7 = mixval_6; - mx_burn_float(fg_10.x, bg_10.x, mixval_7, (&f_1)); - (*result_82).x = f_1; - mx_burn_float(fg_10.y, bg_10.y, mixval_7, (&f_1)); - (*result_82).y = f_1; - mx_burn_float(fg_10.z, bg_10.z, mixval_7, (&f_1)); - (*result_82).z = f_1; - mx_burn_float(fg_10.w, bg_10.w, mixval_7, (&f_1)); - (*result_82).w = f_1; - return; -} diff --git a/libraries/stdlib/genwgsl/mx_burn_float.wgsl b/libraries/stdlib/genwgsl/mx_burn_float.wgsl deleted file mode 100644 index f7f56f1af0..0000000000 --- a/libraries/stdlib/genwgsl/mx_burn_float.wgsl +++ /dev/null @@ -1,20 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_burn_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_burn_float(fg_9: f32, bg_9: f32, mixval_6: f32, result_82: ptr) { - var fg_10: f32; - var bg_10: f32; - var mixval_7: f32; - - fg_10 = fg_9; - bg_10 = bg_9; - mixval_7 = mixval_6; - if (abs(fg_10) < 0.00000001) { - { - (*result_82) = 0.0; - return; - } - } - (*result_82) = ((mixval_7 * (1.0 - ((1.0 - bg_10) / fg_10))) + ((1.0 - mixval_7) * bg_10)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_cellnoise2d_float.wgsl b/libraries/stdlib/genwgsl/mx_cellnoise2d_float.wgsl deleted file mode 100644 index 07ad01d3d9..0000000000 --- a/libraries/stdlib/genwgsl/mx_cellnoise2d_float.wgsl +++ /dev/null @@ -1,12 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_cellnoise2d_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_cellnoise2d_float(texcoord_29: vec2f, result_82: ptr) { - var texcoord_30: vec2f; - - texcoord_30 = texcoord_29; - (*result_82) = mx_cell_noise_float_vec2(texcoord_30); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_cellnoise3d_float.wgsl b/libraries/stdlib/genwgsl/mx_cellnoise3d_float.wgsl deleted file mode 100644 index 5155c7cafb..0000000000 --- a/libraries/stdlib/genwgsl/mx_cellnoise3d_float.wgsl +++ /dev/null @@ -1,12 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_cellnoise3d_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_cellnoise3d_float(position_13: vec3f, result_82: ptr) { - var position_14: vec3f; - - position_14 = position_13; - (*result_82) = mx_cell_noise_float_vec3(position_14); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_creatematrix_vector3_matrix33.wgsl b/libraries/stdlib/genwgsl/mx_creatematrix_vector3_matrix33.wgsl deleted file mode 100644 index 399cad8414..0000000000 --- a/libraries/stdlib/genwgsl/mx_creatematrix_vector3_matrix33.wgsl +++ /dev/null @@ -1,14 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_creatematrix_vector3_matrix33.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_creatematrix_vector3_matrix33(in1_8: vec3f, in2_8: vec3f, in3_2: vec3f, result_82: ptr) { - var in1_9: vec3f; - var in2_9: vec3f; - var in3_3: vec3f; - - in1_9 = in1_8; - in2_9 = in2_8; - in3_3 = in3_2; - (*result_82) = mat3x3f(vec3f(in1_9.x, in1_9.y, in1_9.z), vec3f(in2_9.x, in2_9.y, in2_9.z), vec3f(in3_3.x, in3_3.y, in3_3.z)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_creatematrix_vector3_matrix44.wgsl b/libraries/stdlib/genwgsl/mx_creatematrix_vector3_matrix44.wgsl deleted file mode 100644 index 93c12d72c1..0000000000 --- a/libraries/stdlib/genwgsl/mx_creatematrix_vector3_matrix44.wgsl +++ /dev/null @@ -1,16 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_creatematrix_vector3_matrix44.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_creatematrix_vector3_matrix44(in1_8: vec3f, in2_8: vec3f, in3_2: vec3f, in4_1: vec3f, result_82: ptr) { - var in1_9: vec3f; - var in2_9: vec3f; - var in3_3: vec3f; - var in4_2: vec3f; - - in1_9 = in1_8; - in2_9 = in2_8; - in3_3 = in3_2; - in4_2 = in4_1; - (*result_82) = mat4x4f(vec4f(in1_9.x, in1_9.y, in1_9.z, 0.0), vec4f(in2_9.x, in2_9.y, in2_9.z, 0.0), vec4f(in3_3.x, in3_3.y, in3_3.z, 0.0), vec4f(in4_2.x, in4_2.y, in4_2.z, 1.0)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_creatematrix_vector4_matrix44.wgsl b/libraries/stdlib/genwgsl/mx_creatematrix_vector4_matrix44.wgsl deleted file mode 100644 index 90b73ed8e5..0000000000 --- a/libraries/stdlib/genwgsl/mx_creatematrix_vector4_matrix44.wgsl +++ /dev/null @@ -1,16 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_creatematrix_vector4_matrix44.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_creatematrix_vector4_matrix44(in1_8: vec4f, in2_8: vec4f, in3_2: vec4f, in4_1: vec4f, result_82: ptr) { - var in1_9: vec4f; - var in2_9: vec4f; - var in3_3: vec4f; - var in4_2: vec4f; - - in1_9 = in1_8; - in2_9 = in2_8; - in3_3 = in3_2; - in4_2 = in4_1; - (*result_82) = mat4x4f(vec4f(in1_9.x, in1_9.y, in1_9.z, in1_9.w), vec4f(in2_9.x, in2_9.y, in2_9.z, in2_9.w), vec4f(in3_3.x, in3_3.y, in3_3.z, in3_3.w), vec4f(in4_2.x, in4_2.y, in4_2.z, in4_2.w)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_disjointover_color4.wgsl b/libraries/stdlib/genwgsl/mx_disjointover_color4.wgsl deleted file mode 100644 index 3e78aff978..0000000000 --- a/libraries/stdlib/genwgsl/mx_disjointover_color4.wgsl +++ /dev/null @@ -1,52 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_disjointover_color4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_disjointover_color4(fg_9: vec4f, bg_9: vec4f, mixval_6: f32, result_82: ptr) { - var fg_10: vec4f; - var bg_10: vec4f; - var mixval_7: f32; - var summedAlpha: f32; - var x_34: f32; - - fg_10 = fg_9; - bg_10 = bg_9; - mixval_7 = mixval_6; - summedAlpha = (fg_10.w + bg_10.w); - if (summedAlpha <= 1.0) { - { - let _e34 = (*result_82); - let _e40 = (fg_10.xyz + bg_10.xyz); - (*result_82).x = _e40.x; - (*result_82).y = _e40.y; - (*result_82).z = _e40.z; - } - } else { - { - if (abs(bg_10.w) < 0.00000001) { - { - let _e52 = (*result_82); - (*result_82).x = 0.0; - (*result_82).y = 0.0; - (*result_82).z = 0.0; - } - } else { - { - x_34 = ((1.0 - fg_10.w) / bg_10.w); - let _e67 = (*result_82); - let _e75 = (fg_10.xyz + (bg_10.xyz * x_34)); - (*result_82).x = _e75.x; - (*result_82).y = _e75.y; - (*result_82).z = _e75.z; - } - } - } - } - (*result_82).w = min(summedAlpha, 1.0); - let _e86 = (*result_82); - let _e98 = ((((*result_82)).xyz * mixval_7) + ((1.0 - mixval_7) * bg_10.xyz)); - (*result_82).x = _e98.x; - (*result_82).y = _e98.y; - (*result_82).z = _e98.z; - (*result_82).w = ((((*result_82)).w * mixval_7) + ((1.0 - mixval_7) * bg_10.w)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_dodge_color3.wgsl b/libraries/stdlib/genwgsl/mx_dodge_color3.wgsl deleted file mode 100644 index 0470383f9e..0000000000 --- a/libraries/stdlib/genwgsl/mx_dodge_color3.wgsl +++ /dev/null @@ -1,22 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_dodge_color3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "mx_dodge_float.wgsl" - -fn mx_dodge_color3(fg_9: vec3f, bg_9: vec3f, mixval_6: f32, result_82: ptr) { - var fg_10: vec3f; - var bg_10: vec3f; - var mixval_7: f32; - var f_1: f32; - - fg_10 = fg_9; - bg_10 = bg_9; - mixval_7 = mixval_6; - mx_dodge_float(fg_10.x, bg_10.x, mixval_7, (&f_1)); - (*result_82).x = f_1; - mx_dodge_float(fg_10.y, bg_10.y, mixval_7, (&f_1)); - (*result_82).y = f_1; - mx_dodge_float(fg_10.z, bg_10.z, mixval_7, (&f_1)); - (*result_82).z = f_1; - return; -} diff --git a/libraries/stdlib/genwgsl/mx_dodge_color4.wgsl b/libraries/stdlib/genwgsl/mx_dodge_color4.wgsl deleted file mode 100644 index 04491d3c55..0000000000 --- a/libraries/stdlib/genwgsl/mx_dodge_color4.wgsl +++ /dev/null @@ -1,24 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_dodge_color4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "mx_dodge_float.wgsl" - -fn mx_dodge_color4(fg_9: vec4f, bg_9: vec4f, mixval_6: f32, result_82: ptr) { - var fg_10: vec4f; - var bg_10: vec4f; - var mixval_7: f32; - var f_1: f32; - - fg_10 = fg_9; - bg_10 = bg_9; - mixval_7 = mixval_6; - mx_dodge_float(fg_10.x, bg_10.x, mixval_7, (&f_1)); - (*result_82).x = f_1; - mx_dodge_float(fg_10.y, bg_10.y, mixval_7, (&f_1)); - (*result_82).y = f_1; - mx_dodge_float(fg_10.z, bg_10.z, mixval_7, (&f_1)); - (*result_82).z = f_1; - mx_dodge_float(fg_10.w, bg_10.w, mixval_7, (&f_1)); - (*result_82).w = f_1; - return; -} diff --git a/libraries/stdlib/genwgsl/mx_dodge_float.wgsl b/libraries/stdlib/genwgsl/mx_dodge_float.wgsl deleted file mode 100644 index a137e171c0..0000000000 --- a/libraries/stdlib/genwgsl/mx_dodge_float.wgsl +++ /dev/null @@ -1,20 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_dodge_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_dodge_float(fg_9: f32, bg_9: f32, mixval_6: f32, result_82: ptr) { - var fg_10: f32; - var bg_10: f32; - var mixval_7: f32; - - fg_10 = fg_9; - bg_10 = bg_9; - mixval_7 = mixval_6; - if (abs((1.0 - fg_10)) < 0.00000001) { - { - (*result_82) = 0.0; - return; - } - } - (*result_82) = ((mixval_7 * (bg_10 / (1.0 - fg_10))) + ((1.0 - mixval_7) * bg_10)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_flake2d.wgsl b/libraries/stdlib/genwgsl/mx_flake2d.wgsl deleted file mode 100644 index 98905e1fdf..0000000000 --- a/libraries/stdlib/genwgsl/mx_flake2d.wgsl +++ /dev/null @@ -1,26 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_flake2d.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_flake.wgsl" - -fn mx_flake2d(size_2: f32, roughness_29: f32, coverage_2: f32, texcoord_29: vec2f, normal_2: vec3f, tangent_2: vec3f, bitangent_2: vec3f, id_2: ptr, rand_2: ptr, presence_2: ptr, flakenormal_2: ptr) { - var size_3: f32; - var roughness_30: f32; - var coverage_3: f32; - var texcoord_30: vec2f; - var normal_3: vec3f; - var tangent_3: vec3f; - var bitangent_3: vec3f; - var position_14: vec3f; - - size_3 = size_2; - roughness_30 = roughness_29; - coverage_3 = coverage_2; - texcoord_30 = texcoord_29; - normal_3 = normal_2; - tangent_3 = tangent_2; - bitangent_3 = bitangent_2; - position_14 = vec3f(texcoord_30.x, texcoord_30.y, 0.0); - mx_flake(size_3, roughness_30, coverage_3, position_14, normal_3, tangent_3, bitangent_3, id_2, rand_2, presence_2, flakenormal_2); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_flake3d.wgsl b/libraries/stdlib/genwgsl/mx_flake3d.wgsl deleted file mode 100644 index b0cc93fa1a..0000000000 --- a/libraries/stdlib/genwgsl/mx_flake3d.wgsl +++ /dev/null @@ -1,24 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_flake3d.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_flake.wgsl" - -fn mx_flake3d(size_2: f32, roughness_29: f32, coverage_2: f32, position_13: vec3f, normal_2: vec3f, tangent_2: vec3f, bitangent_2: vec3f, id_2: ptr, rand_2: ptr, presence_2: ptr, flakenormal_2: ptr) { - var size_3: f32; - var roughness_30: f32; - var coverage_3: f32; - var position_14: vec3f; - var normal_3: vec3f; - var tangent_3: vec3f; - var bitangent_3: vec3f; - - size_3 = size_2; - roughness_30 = roughness_29; - coverage_3 = coverage_2; - position_14 = position_13; - normal_3 = normal_2; - tangent_3 = tangent_2; - bitangent_3 = bitangent_2; - mx_flake(size_3, roughness_30, coverage_3, position_14, normal_3, tangent_3, bitangent_3, id_2, rand_2, presence_2, flakenormal_2); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_fractal2d_float.wgsl b/libraries/stdlib/genwgsl/mx_fractal2d_float.wgsl deleted file mode 100644 index 603a4f564d..0000000000 --- a/libraries/stdlib/genwgsl/mx_fractal2d_float.wgsl +++ /dev/null @@ -1,22 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_fractal2d_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_fractal2d_float(amplitude_15: f32, octaves_15: i32, lacunarity_15: f32, diminish_15: f32, texcoord_29: vec2f, result_82: ptr) { - var amplitude_16: f32; - var octaves_16: i32; - var lacunarity_16: f32; - var diminish_16: f32; - var texcoord_30: vec2f; - var value_3: f32; - - amplitude_16 = amplitude_15; - octaves_16 = octaves_15; - lacunarity_16 = lacunarity_15; - diminish_16 = diminish_15; - texcoord_30 = texcoord_29; - value_3 = (mx_fractal2d_noise_float(texcoord_30, octaves_16, lacunarity_16, diminish_16)); - (*result_82) = (value_3 * amplitude_16); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_fractal2d_vector2.wgsl b/libraries/stdlib/genwgsl/mx_fractal2d_vector2.wgsl deleted file mode 100644 index 0f3741d6ff..0000000000 --- a/libraries/stdlib/genwgsl/mx_fractal2d_vector2.wgsl +++ /dev/null @@ -1,22 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_fractal2d_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_fractal2d_vector2(amplitude_15: vec2f, octaves_15: i32, lacunarity_15: f32, diminish_15: f32, texcoord_29: vec2f, result_82: ptr) { - var amplitude_16: vec2f; - var octaves_16: i32; - var lacunarity_16: f32; - var diminish_16: f32; - var texcoord_30: vec2f; - var value_3: vec2f; - - amplitude_16 = amplitude_15; - octaves_16 = octaves_15; - lacunarity_16 = lacunarity_15; - diminish_16 = diminish_15; - texcoord_30 = texcoord_29; - value_3 = (mx_fractal2d_noise_vec2(texcoord_30, octaves_16, lacunarity_16, diminish_16)); - (*result_82) = (value_3 * amplitude_16); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_fractal2d_vector3.wgsl b/libraries/stdlib/genwgsl/mx_fractal2d_vector3.wgsl deleted file mode 100644 index 553224e099..0000000000 --- a/libraries/stdlib/genwgsl/mx_fractal2d_vector3.wgsl +++ /dev/null @@ -1,22 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_fractal2d_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_fractal2d_vector3(amplitude_15: vec3f, octaves_15: i32, lacunarity_15: f32, diminish_15: f32, texcoord_29: vec2f, result_82: ptr) { - var amplitude_16: vec3f; - var octaves_16: i32; - var lacunarity_16: f32; - var diminish_16: f32; - var texcoord_30: vec2f; - var value_3: vec3f; - - amplitude_16 = amplitude_15; - octaves_16 = octaves_15; - lacunarity_16 = lacunarity_15; - diminish_16 = diminish_15; - texcoord_30 = texcoord_29; - value_3 = (mx_fractal2d_noise_vec3(texcoord_30, octaves_16, lacunarity_16, diminish_16)); - (*result_82) = (value_3 * amplitude_16); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_fractal2d_vector4.wgsl b/libraries/stdlib/genwgsl/mx_fractal2d_vector4.wgsl deleted file mode 100644 index 72c3c93d21..0000000000 --- a/libraries/stdlib/genwgsl/mx_fractal2d_vector4.wgsl +++ /dev/null @@ -1,22 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_fractal2d_vector4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_fractal2d_vector4(amplitude_15: vec4f, octaves_15: i32, lacunarity_15: f32, diminish_15: f32, texcoord_29: vec2f, result_82: ptr) { - var amplitude_16: vec4f; - var octaves_16: i32; - var lacunarity_16: f32; - var diminish_16: f32; - var texcoord_30: vec2f; - var value_3: vec4f; - - amplitude_16 = amplitude_15; - octaves_16 = octaves_15; - lacunarity_16 = lacunarity_15; - diminish_16 = diminish_15; - texcoord_30 = texcoord_29; - value_3 = (mx_fractal2d_noise_vec4(texcoord_30, octaves_16, lacunarity_16, diminish_16)); - (*result_82) = (value_3 * amplitude_16); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_fractal3d_float.wgsl b/libraries/stdlib/genwgsl/mx_fractal3d_float.wgsl deleted file mode 100644 index d1fe4fba3e..0000000000 --- a/libraries/stdlib/genwgsl/mx_fractal3d_float.wgsl +++ /dev/null @@ -1,22 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_fractal3d_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_fractal3d_float(amplitude_15: f32, octaves_15: i32, lacunarity_15: f32, diminish_15: f32, position_13: vec3f, result_82: ptr) { - var amplitude_16: f32; - var octaves_16: i32; - var lacunarity_16: f32; - var diminish_16: f32; - var position_14: vec3f; - var value_3: f32; - - amplitude_16 = amplitude_15; - octaves_16 = octaves_15; - lacunarity_16 = lacunarity_15; - diminish_16 = diminish_15; - position_14 = position_13; - value_3 = (mx_fractal3d_noise_float(position_14, octaves_16, lacunarity_16, diminish_16)); - (*result_82) = (value_3 * amplitude_16); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_fractal3d_vector2.wgsl b/libraries/stdlib/genwgsl/mx_fractal3d_vector2.wgsl deleted file mode 100644 index 4c775f30b6..0000000000 --- a/libraries/stdlib/genwgsl/mx_fractal3d_vector2.wgsl +++ /dev/null @@ -1,22 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_fractal3d_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_fractal3d_vector2(amplitude_15: vec2f, octaves_15: i32, lacunarity_15: f32, diminish_15: f32, position_13: vec3f, result_82: ptr) { - var amplitude_16: vec2f; - var octaves_16: i32; - var lacunarity_16: f32; - var diminish_16: f32; - var position_14: vec3f; - var value_3: vec2f; - - amplitude_16 = amplitude_15; - octaves_16 = octaves_15; - lacunarity_16 = lacunarity_15; - diminish_16 = diminish_15; - position_14 = position_13; - value_3 = (mx_fractal3d_noise_vec2(position_14, octaves_16, lacunarity_16, diminish_16)); - (*result_82) = (value_3 * amplitude_16); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_fractal3d_vector3.wgsl b/libraries/stdlib/genwgsl/mx_fractal3d_vector3.wgsl deleted file mode 100644 index cc9ad60d7d..0000000000 --- a/libraries/stdlib/genwgsl/mx_fractal3d_vector3.wgsl +++ /dev/null @@ -1,22 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_fractal3d_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_fractal3d_vector3(amplitude_15: vec3f, octaves_15: i32, lacunarity_15: f32, diminish_15: f32, position_13: vec3f, result_82: ptr) { - var amplitude_16: vec3f; - var octaves_16: i32; - var lacunarity_16: f32; - var diminish_16: f32; - var position_14: vec3f; - var value_3: vec3f; - - amplitude_16 = amplitude_15; - octaves_16 = octaves_15; - lacunarity_16 = lacunarity_15; - diminish_16 = diminish_15; - position_14 = position_13; - value_3 = (mx_fractal3d_noise_vec3(position_14, octaves_16, lacunarity_16, diminish_16)); - (*result_82) = (value_3 * amplitude_16); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_fractal3d_vector4.wgsl b/libraries/stdlib/genwgsl/mx_fractal3d_vector4.wgsl deleted file mode 100644 index d54256c823..0000000000 --- a/libraries/stdlib/genwgsl/mx_fractal3d_vector4.wgsl +++ /dev/null @@ -1,22 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_fractal3d_vector4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_fractal3d_vector4(amplitude_15: vec4f, octaves_15: i32, lacunarity_15: f32, diminish_15: f32, position_13: vec3f, result_82: ptr) { - var amplitude_16: vec4f; - var octaves_16: i32; - var lacunarity_16: f32; - var diminish_16: f32; - var position_14: vec3f; - var value_3: vec4f; - - amplitude_16 = amplitude_15; - octaves_16 = octaves_15; - lacunarity_16 = lacunarity_15; - diminish_16 = diminish_15; - position_14 = position_13; - value_3 = (mx_fractal3d_noise_vec4(position_14, octaves_16, lacunarity_16, diminish_16)); - (*result_82) = (value_3 * amplitude_16); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_heighttonormal_vector3.wgsl b/libraries/stdlib/genwgsl/mx_heighttonormal_vector3.wgsl deleted file mode 100644 index f0f9d43e3c..0000000000 --- a/libraries/stdlib/genwgsl/mx_heighttonormal_vector3.wgsl +++ /dev/null @@ -1,36 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_heighttonormal_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_heighttonormal_vector3(height: f32, scale_3: f32, texcoord_29: vec2f, result_82: ptr) { - var scale_4: f32; - var texcoord_30: vec2f; - var SOBEL_SCALE_FACTOR: f32 = 0.0625; - var dHdS: vec2f; - var dUdS: vec2f; - var dVdS: vec2f; - var tangent_3: vec3f; - var bitangent_3: vec3f; - var n_2: vec3f; - - scale_4 = scale_3; - texcoord_30 = texcoord_29; - dHdS = ((vec2f(dpdx(height), dpdy(height)) * scale_4) * SOBEL_SCALE_FACTOR); - dUdS = vec2f(dpdx(texcoord_30.x), dpdy(texcoord_30.x)); - dVdS = vec2f(dpdx(texcoord_30.y), dpdy(texcoord_30.y)); - tangent_3 = vec3f(dUdS.x, dVdS.x, dHdS.x); - bitangent_3 = vec3f(dUdS.y, dVdS.y, dHdS.y); - n_2 = cross(tangent_3, bitangent_3); - if (dot(n_2, n_2) < 0.0000000000000001) { - { - n_2 = vec3f(0.0, 0.0, 1.0); - } - } else { - if (n_2.z < 0.0) { - { - n_2 = (n_2 * -1.0); - } - } - } - (*result_82) = ((normalize(n_2) * 0.5) + vec3(0.5)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_hsvtorgb_color3.wgsl b/libraries/stdlib/genwgsl/mx_hsvtorgb_color3.wgsl deleted file mode 100644 index f6688b1260..0000000000 --- a/libraries/stdlib/genwgsl/mx_hsvtorgb_color3.wgsl +++ /dev/null @@ -1,12 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_hsvtorgb_color3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_hsv.wgsl" - -fn mx_hsvtorgb_color3(_in_9: vec3f, result_82: ptr) { - var _in_10: vec3f; - - _in_10 = _in_9; - (*result_82) = mx_hsvtorgb(_in_10); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_hsvtorgb_color4.wgsl b/libraries/stdlib/genwgsl/mx_hsvtorgb_color4.wgsl deleted file mode 100644 index fe29caf1e6..0000000000 --- a/libraries/stdlib/genwgsl/mx_hsvtorgb_color4.wgsl +++ /dev/null @@ -1,13 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_hsvtorgb_color4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_hsv.wgsl" - -fn mx_hsvtorgb_color4(_in_9: vec4f, result_82: ptr) { - var _in_10: vec4f; - - _in_10 = _in_9; - let _e23 = mx_hsvtorgb(_in_10.xyz); - (*result_82) = vec4f(_e23.x, _e23.y, _e23.z, _in_10.w); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_luminance_color3.wgsl b/libraries/stdlib/genwgsl/mx_luminance_color3.wgsl deleted file mode 100644 index eaa7ae9019..0000000000 --- a/libraries/stdlib/genwgsl/mx_luminance_color3.wgsl +++ /dev/null @@ -1,12 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_luminance_color3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_luminance_color3(_in_9: vec3f, lumacoeffs_1: vec3f, result_82: ptr) { - var _in_10: vec3f; - var lumacoeffs_2: vec3f; - - _in_10 = _in_9; - lumacoeffs_2 = lumacoeffs_1; - (*result_82) = vec3(dot(_in_10, lumacoeffs_2)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_luminance_color4.wgsl b/libraries/stdlib/genwgsl/mx_luminance_color4.wgsl deleted file mode 100644 index 887bf86291..0000000000 --- a/libraries/stdlib/genwgsl/mx_luminance_color4.wgsl +++ /dev/null @@ -1,13 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_luminance_color4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_luminance_color4(_in_9: vec4f, lumacoeffs_1: vec3f, result_82: ptr) { - var _in_10: vec4f; - var lumacoeffs_2: vec3f; - - _in_10 = _in_9; - lumacoeffs_2 = lumacoeffs_1; - let _e27 = vec3(dot(_in_10.xyz, lumacoeffs_2)); - (*result_82) = vec4f(_e27.x, _e27.y, _e27.z, _in_10.w); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_mix_surfaceshader.wgsl b/libraries/stdlib/genwgsl/mx_mix_surfaceshader.wgsl deleted file mode 100644 index aeecfd342c..0000000000 --- a/libraries/stdlib/genwgsl/mx_mix_surfaceshader.wgsl +++ /dev/null @@ -1,13 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_mix_surfaceshader.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_mix_surfaceshader(fg_9: surfaceshader, bg_9: surfaceshader, w: f32, returnshader: ptr) { - var fg_10: surfaceshader; - var bg_10: surfaceshader; - - fg_10 = fg_9; - bg_10 = bg_9; - (*returnshader).color = mix(bg_10.color, fg_10.color, vec3(w)); - (*returnshader).transparency = mix(bg_10.transparency, fg_10.transparency, vec3(w)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_noise2d_float.wgsl b/libraries/stdlib/genwgsl/mx_noise2d_float.wgsl deleted file mode 100644 index 0be9584b9d..0000000000 --- a/libraries/stdlib/genwgsl/mx_noise2d_float.wgsl +++ /dev/null @@ -1,18 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_noise2d_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_noise2d_float(amplitude_15: f32, pivot_7: f32, texcoord_29: vec2f, result_82: ptr) { - var amplitude_16: f32; - var pivot_8: f32; - var texcoord_30: vec2f; - var value_3: f32; - - amplitude_16 = amplitude_15; - pivot_8 = pivot_7; - texcoord_30 = texcoord_29; - value_3 = mx_perlin_noise_float_2d(texcoord_30); - (*result_82) = ((value_3 * amplitude_16) + pivot_8); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_noise2d_vector2.wgsl b/libraries/stdlib/genwgsl/mx_noise2d_vector2.wgsl deleted file mode 100644 index 620e389426..0000000000 --- a/libraries/stdlib/genwgsl/mx_noise2d_vector2.wgsl +++ /dev/null @@ -1,18 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_noise2d_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_noise2d_vector2(amplitude_15: vec2f, pivot_7: f32, texcoord_29: vec2f, result_82: ptr) { - var amplitude_16: vec2f; - var pivot_8: f32; - var texcoord_30: vec2f; - var value_3: vec3f; - - amplitude_16 = amplitude_15; - pivot_8 = pivot_7; - texcoord_30 = texcoord_29; - value_3 = mx_perlin_noise_vec3_2d(texcoord_30); - (*result_82) = ((value_3.xy * amplitude_16) + vec2(pivot_8)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_noise2d_vector3.wgsl b/libraries/stdlib/genwgsl/mx_noise2d_vector3.wgsl deleted file mode 100644 index 561929ea07..0000000000 --- a/libraries/stdlib/genwgsl/mx_noise2d_vector3.wgsl +++ /dev/null @@ -1,18 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_noise2d_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_noise2d_vector3(amplitude_15: vec3f, pivot_7: f32, texcoord_29: vec2f, result_82: ptr) { - var amplitude_16: vec3f; - var pivot_8: f32; - var texcoord_30: vec2f; - var value_3: vec3f; - - amplitude_16 = amplitude_15; - pivot_8 = pivot_7; - texcoord_30 = texcoord_29; - value_3 = mx_perlin_noise_vec3_2d(texcoord_30); - (*result_82) = ((value_3 * amplitude_16) + vec3(pivot_8)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_noise2d_vector4.wgsl b/libraries/stdlib/genwgsl/mx_noise2d_vector4.wgsl deleted file mode 100644 index 1abb915618..0000000000 --- a/libraries/stdlib/genwgsl/mx_noise2d_vector4.wgsl +++ /dev/null @@ -1,21 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_noise2d_vector4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_noise2d_vector4(amplitude_15: vec4f, pivot_7: f32, texcoord_29: vec2f, result_82: ptr) { - var amplitude_16: vec4f; - var pivot_8: f32; - var texcoord_30: vec2f; - var xyz: vec3f; - var w_1: f32; - - amplitude_16 = amplitude_15; - pivot_8 = pivot_7; - texcoord_30 = texcoord_29; - xyz = mx_perlin_noise_vec3_2d(texcoord_30); - w_1 = (mx_perlin_noise_float_2d((texcoord_30 + vec2f(19.0, 73.0)))); - let _e37 = xyz; - (*result_82) = ((vec4f(_e37.x, _e37.y, _e37.z, w_1) * amplitude_16) + vec4(pivot_8)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_noise3d_float.wgsl b/libraries/stdlib/genwgsl/mx_noise3d_float.wgsl deleted file mode 100644 index f1673c296c..0000000000 --- a/libraries/stdlib/genwgsl/mx_noise3d_float.wgsl +++ /dev/null @@ -1,18 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_noise3d_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_noise3d_float(amplitude_15: f32, pivot_7: f32, position_13: vec3f, result_82: ptr) { - var amplitude_16: f32; - var pivot_8: f32; - var position_14: vec3f; - var value_3: f32; - - amplitude_16 = amplitude_15; - pivot_8 = pivot_7; - position_14 = position_13; - value_3 = mx_perlin_noise_float_3d(position_14); - (*result_82) = ((value_3 * amplitude_16) + pivot_8); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_noise3d_vector2.wgsl b/libraries/stdlib/genwgsl/mx_noise3d_vector2.wgsl deleted file mode 100644 index ac959d4bfc..0000000000 --- a/libraries/stdlib/genwgsl/mx_noise3d_vector2.wgsl +++ /dev/null @@ -1,18 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_noise3d_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_noise3d_vector2(amplitude_15: vec2f, pivot_7: f32, position_13: vec3f, result_82: ptr) { - var amplitude_16: vec2f; - var pivot_8: f32; - var position_14: vec3f; - var value_3: vec3f; - - amplitude_16 = amplitude_15; - pivot_8 = pivot_7; - position_14 = position_13; - value_3 = mx_perlin_noise_vec3_3d(position_14); - (*result_82) = ((value_3.xy * amplitude_16) + vec2(pivot_8)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_noise3d_vector3.wgsl b/libraries/stdlib/genwgsl/mx_noise3d_vector3.wgsl deleted file mode 100644 index 3da976aacb..0000000000 --- a/libraries/stdlib/genwgsl/mx_noise3d_vector3.wgsl +++ /dev/null @@ -1,18 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_noise3d_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_noise3d_vector3(amplitude_15: vec3f, pivot_7: f32, position_13: vec3f, result_82: ptr) { - var amplitude_16: vec3f; - var pivot_8: f32; - var position_14: vec3f; - var value_3: vec3f; - - amplitude_16 = amplitude_15; - pivot_8 = pivot_7; - position_14 = position_13; - value_3 = mx_perlin_noise_vec3_3d(position_14); - (*result_82) = ((value_3 * amplitude_16) + vec3(pivot_8)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_noise3d_vector4.wgsl b/libraries/stdlib/genwgsl/mx_noise3d_vector4.wgsl deleted file mode 100644 index b9e428531f..0000000000 --- a/libraries/stdlib/genwgsl/mx_noise3d_vector4.wgsl +++ /dev/null @@ -1,21 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_noise3d_vector4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_noise3d_vector4(amplitude_15: vec4f, pivot_7: f32, position_13: vec3f, result_82: ptr) { - var amplitude_16: vec4f; - var pivot_8: f32; - var position_14: vec3f; - var xyz: vec3f; - var w_1: f32; - - amplitude_16 = amplitude_15; - pivot_8 = pivot_7; - position_14 = position_13; - xyz = mx_perlin_noise_vec3_3d(position_14); - w_1 = (mx_perlin_noise_float_3d((position_14 + vec3f(19.0, 73.0, 29.0)))); - let _e39 = xyz; - (*result_82) = ((vec4f(_e39.x, _e39.y, _e39.z, w_1) * amplitude_16) + vec4(pivot_8)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_normalmap.wgsl b/libraries/stdlib/genwgsl/mx_normalmap.wgsl deleted file mode 100644 index c3d6ff922b..0000000000 --- a/libraries/stdlib/genwgsl/mx_normalmap.wgsl +++ /dev/null @@ -1,42 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_normalmap.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_normalmap_vector2(value_2: vec3f, normal_scale_1: vec2f, N_24: vec3f, T_1: vec3f, B_1: vec3f, result_82: ptr) { - var value_3: vec3f; - var normal_scale_2: vec2f; - var N_25: vec3f; - var T_2: vec3f; - var B_2: vec3f; - var local: vec3f; - - value_3 = value_2; - normal_scale_2 = normal_scale_1; - N_25 = N_24; - T_2 = T_1; - B_2 = B_1; - if (dot(value_3, value_3) == 0.0) { - local = vec3f(0.0, 0.0, 1.0); - } else { - local = ((value_3 * 2.0) - vec3(1.0)); - } - value_3 = local; - value_3 = ((((T_2 * value_3.x) * normal_scale_2.x) + ((B_2 * value_3.y) * normal_scale_2.y)) + (N_25 * value_3.z)); - (*result_82) = normalize(value_3); - return; -} - -fn mx_normalmap_float(value_2: vec3f, normal_scale_1: f32, N_24: vec3f, T_1: vec3f, B_1: vec3f, result_82: ptr) { - var value_3: vec3f; - var normal_scale_2: f32; - var N_25: vec3f; - var T_2: vec3f; - var B_2: vec3f; - - value_3 = value_2; - normal_scale_2 = normal_scale_1; - N_25 = N_24; - T_2 = T_1; - B_2 = B_1; - mx_normalmap_vector2(value_3, vec2(normal_scale_2), N_25, T_2, B_2, result_82); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_premult_color4.wgsl b/libraries/stdlib/genwgsl/mx_premult_color4.wgsl deleted file mode 100644 index fdf898606f..0000000000 --- a/libraries/stdlib/genwgsl/mx_premult_color4.wgsl +++ /dev/null @@ -1,11 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_premult_color4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_premult_color4(_in_9: vec4f, result_82: ptr) { - var _in_10: vec4f; - - _in_10 = _in_9; - let _e25 = (_in_10.xyz * _in_10.w); - (*result_82) = vec4f(_e25.x, _e25.y, _e25.z, _in_10.w); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_ramplr_float.wgsl b/libraries/stdlib/genwgsl/mx_ramplr_float.wgsl deleted file mode 100644 index 337cf8b660..0000000000 --- a/libraries/stdlib/genwgsl/mx_ramplr_float.wgsl +++ /dev/null @@ -1,14 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_ramplr_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_ramplr_float(valuel_7: f32, valuer_7: f32, texcoord_29: vec2f, result_82: ptr) { - var valuel_8: f32; - var valuer_8: f32; - var texcoord_30: vec2f; - - valuel_8 = valuel_7; - valuer_8 = valuer_7; - texcoord_30 = texcoord_29; - (*result_82) = mix(valuel_8, valuer_8, clamp(texcoord_30.x, 0.0, 1.0)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_ramplr_vector2.wgsl b/libraries/stdlib/genwgsl/mx_ramplr_vector2.wgsl deleted file mode 100644 index 00a3f356de..0000000000 --- a/libraries/stdlib/genwgsl/mx_ramplr_vector2.wgsl +++ /dev/null @@ -1,14 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_ramplr_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_ramplr_vector2(valuel_7: vec2f, valuer_7: vec2f, texcoord_29: vec2f, result_82: ptr) { - var valuel_8: vec2f; - var valuer_8: vec2f; - var texcoord_30: vec2f; - - valuel_8 = valuel_7; - valuer_8 = valuer_7; - texcoord_30 = texcoord_29; - (*result_82) = mix(valuel_8, valuer_8, vec2(clamp(texcoord_30.x, 0.0, 1.0))); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_ramplr_vector3.wgsl b/libraries/stdlib/genwgsl/mx_ramplr_vector3.wgsl deleted file mode 100644 index f82d090533..0000000000 --- a/libraries/stdlib/genwgsl/mx_ramplr_vector3.wgsl +++ /dev/null @@ -1,14 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_ramplr_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_ramplr_vector3(valuel_7: vec3f, valuer_7: vec3f, texcoord_29: vec2f, result_82: ptr) { - var valuel_8: vec3f; - var valuer_8: vec3f; - var texcoord_30: vec2f; - - valuel_8 = valuel_7; - valuer_8 = valuer_7; - texcoord_30 = texcoord_29; - (*result_82) = mix(valuel_8, valuer_8, vec3(clamp(texcoord_30.x, 0.0, 1.0))); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_ramplr_vector4.wgsl b/libraries/stdlib/genwgsl/mx_ramplr_vector4.wgsl deleted file mode 100644 index 9f3fec7625..0000000000 --- a/libraries/stdlib/genwgsl/mx_ramplr_vector4.wgsl +++ /dev/null @@ -1,14 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_ramplr_vector4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_ramplr_vector4(valuel_7: vec4f, valuer_7: vec4f, texcoord_29: vec2f, result_82: ptr) { - var valuel_8: vec4f; - var valuer_8: vec4f; - var texcoord_30: vec2f; - - valuel_8 = valuel_7; - valuer_8 = valuer_7; - texcoord_30 = texcoord_29; - (*result_82) = mix(valuel_8, valuer_8, vec4(clamp(texcoord_30.x, 0.0, 1.0))); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_ramptb_float.wgsl b/libraries/stdlib/genwgsl/mx_ramptb_float.wgsl deleted file mode 100644 index e8d6d8c77b..0000000000 --- a/libraries/stdlib/genwgsl/mx_ramptb_float.wgsl +++ /dev/null @@ -1,14 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_ramptb_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_ramptb_float(valuet_7: f32, valueb_7: f32, texcoord_29: vec2f, result_82: ptr) { - var valuet_8: f32; - var valueb_8: f32; - var texcoord_30: vec2f; - - valuet_8 = valuet_7; - valueb_8 = valueb_7; - texcoord_30 = texcoord_29; - (*result_82) = mix(valueb_8, valuet_8, clamp(texcoord_30.y, 0.0, 1.0)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_ramptb_vector2.wgsl b/libraries/stdlib/genwgsl/mx_ramptb_vector2.wgsl deleted file mode 100644 index f5dc86276f..0000000000 --- a/libraries/stdlib/genwgsl/mx_ramptb_vector2.wgsl +++ /dev/null @@ -1,14 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_ramptb_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_ramptb_vector2(valuet_7: vec2f, valueb_7: vec2f, texcoord_29: vec2f, result_82: ptr) { - var valuet_8: vec2f; - var valueb_8: vec2f; - var texcoord_30: vec2f; - - valuet_8 = valuet_7; - valueb_8 = valueb_7; - texcoord_30 = texcoord_29; - (*result_82) = mix(valueb_8, valuet_8, vec2(clamp(texcoord_30.y, 0.0, 1.0))); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_ramptb_vector3.wgsl b/libraries/stdlib/genwgsl/mx_ramptb_vector3.wgsl deleted file mode 100644 index b80cf895bd..0000000000 --- a/libraries/stdlib/genwgsl/mx_ramptb_vector3.wgsl +++ /dev/null @@ -1,14 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_ramptb_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_ramptb_vector3(valuet_7: vec3f, valueb_7: vec3f, texcoord_29: vec2f, result_82: ptr) { - var valuet_8: vec3f; - var valueb_8: vec3f; - var texcoord_30: vec2f; - - valuet_8 = valuet_7; - valueb_8 = valueb_7; - texcoord_30 = texcoord_29; - (*result_82) = mix(valueb_8, valuet_8, vec3(clamp(texcoord_30.y, 0.0, 1.0))); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_ramptb_vector4.wgsl b/libraries/stdlib/genwgsl/mx_ramptb_vector4.wgsl deleted file mode 100644 index 3f6da3a6a4..0000000000 --- a/libraries/stdlib/genwgsl/mx_ramptb_vector4.wgsl +++ /dev/null @@ -1,14 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_ramptb_vector4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_ramptb_vector4(valuet_7: vec4f, valueb_7: vec4f, texcoord_29: vec2f, result_82: ptr) { - var valuet_8: vec4f; - var valueb_8: vec4f; - var texcoord_30: vec2f; - - valuet_8 = valuet_7; - valueb_8 = valueb_7; - texcoord_30 = texcoord_29; - (*result_82) = mix(valueb_8, valuet_8, vec4(clamp(texcoord_30.y, 0.0, 1.0))); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_rgbtohsv_color3.wgsl b/libraries/stdlib/genwgsl/mx_rgbtohsv_color3.wgsl deleted file mode 100644 index 18b7f9aaa7..0000000000 --- a/libraries/stdlib/genwgsl/mx_rgbtohsv_color3.wgsl +++ /dev/null @@ -1,12 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_rgbtohsv_color3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_hsv.wgsl" - -fn mx_rgbtohsv_color3(_in_9: vec3f, result_82: ptr) { - var _in_10: vec3f; - - _in_10 = _in_9; - (*result_82) = mx_rgbtohsv(_in_10); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_rgbtohsv_color4.wgsl b/libraries/stdlib/genwgsl/mx_rgbtohsv_color4.wgsl deleted file mode 100644 index ee63e187f3..0000000000 --- a/libraries/stdlib/genwgsl/mx_rgbtohsv_color4.wgsl +++ /dev/null @@ -1,13 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_rgbtohsv_color4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_hsv.wgsl" - -fn mx_rgbtohsv_color4(_in_9: vec4f, result_82: ptr) { - var _in_10: vec4f; - - _in_10 = _in_9; - let _e23 = mx_rgbtohsv(_in_10.xyz); - (*result_82) = vec4f(_e23.x, _e23.y, _e23.z, _in_10.w); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_rotate_vector2.wgsl b/libraries/stdlib/genwgsl/mx_rotate_vector2.wgsl deleted file mode 100644 index b4cd3c1f42..0000000000 --- a/libraries/stdlib/genwgsl/mx_rotate_vector2.wgsl +++ /dev/null @@ -1,18 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_rotate_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_rotate_vector2(_in_9: vec2f, amount_1: f32, result_82: ptr) { - var _in_10: vec2f; - var amount_2: f32; - var rotationRadians: f32; - var sa: f32; - var ca: f32; - - _in_10 = _in_9; - amount_2 = amount_1; - rotationRadians = radians(amount_2); - sa = sin(rotationRadians); - ca = cos(rotationRadians); - (*result_82) = vec2f(((ca * _in_10.x) + (sa * _in_10.y)), ((-(sa) * _in_10.x) + (ca * _in_10.y))); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_rotate_vector3.wgsl b/libraries/stdlib/genwgsl/mx_rotate_vector3.wgsl deleted file mode 100644 index 5b48b639c6..0000000000 --- a/libraries/stdlib/genwgsl/mx_rotate_vector3.wgsl +++ /dev/null @@ -1,23 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_rotate_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_rotate_vector3(_in_9: vec3f, amount_1: f32, axis: vec3f, result_82: ptr) { - var _in_10: vec3f; - var amount_2: f32; - var axis_1: vec3f; - var rotationRadians: f32; - var s_8: f32; - var c_3: f32; - var oc: f32; - - _in_10 = _in_9; - amount_2 = amount_1; - axis_1 = axis; - axis_1 = normalize(axis_1); - rotationRadians = radians(amount_2); - s_8 = sin(rotationRadians); - c_3 = cos(rotationRadians); - oc = (1.0 - c_3); - (*result_82) = (((_in_10 * c_3) + (cross(_in_10, axis_1) * s_8)) + ((axis_1 * dot(axis_1, _in_10)) * oc)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_smoothstep_float.wgsl b/libraries/stdlib/genwgsl/mx_smoothstep_float.wgsl deleted file mode 100644 index 41b3690ead..0000000000 --- a/libraries/stdlib/genwgsl/mx_smoothstep_float.wgsl +++ /dev/null @@ -1,20 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_smoothstep_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_smoothstep_float(val_3: f32, low: f32, high: f32, result_82: ptr) { - var val_4: f32; - - val_4 = val_3; - if (val_4 >= high) { - (*result_82) = 1.0; - return; - } else { - if (val_4 <= low) { - (*result_82) = 0.0; - return; - } else { - (*result_82) = smoothstep(low, high, val_4); - return; - } - } -} diff --git a/libraries/stdlib/genwgsl/mx_splitlr_float.wgsl b/libraries/stdlib/genwgsl/mx_splitlr_float.wgsl deleted file mode 100644 index 9c43beb399..0000000000 --- a/libraries/stdlib/genwgsl/mx_splitlr_float.wgsl +++ /dev/null @@ -1,18 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_splitlr_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "mx_aastep.wgsl" - -fn mx_splitlr_float(valuel_7: f32, valuer_7: f32, center_7: f32, texcoord_29: vec2f, result_82: ptr) { - var valuel_8: f32; - var valuer_8: f32; - var center_8: f32; - var texcoord_30: vec2f; - - valuel_8 = valuel_7; - valuer_8 = valuer_7; - center_8 = center_7; - texcoord_30 = texcoord_29; - (*result_82) = mix(valuel_8, valuer_8, (mx_aastep(center_8, texcoord_30.x))); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_splitlr_vector2.wgsl b/libraries/stdlib/genwgsl/mx_splitlr_vector2.wgsl deleted file mode 100644 index 4c79fad614..0000000000 --- a/libraries/stdlib/genwgsl/mx_splitlr_vector2.wgsl +++ /dev/null @@ -1,18 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_splitlr_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "mx_aastep.wgsl" - -fn mx_splitlr_vector2(valuel_7: vec2f, valuer_7: vec2f, center_7: f32, texcoord_29: vec2f, result_82: ptr) { - var valuel_8: vec2f; - var valuer_8: vec2f; - var center_8: f32; - var texcoord_30: vec2f; - - valuel_8 = valuel_7; - valuer_8 = valuer_7; - center_8 = center_7; - texcoord_30 = texcoord_29; - (*result_82) = mix(valuel_8, valuer_8, vec2((mx_aastep(center_8, texcoord_30.x)))); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_splitlr_vector3.wgsl b/libraries/stdlib/genwgsl/mx_splitlr_vector3.wgsl deleted file mode 100644 index b2edeb19ad..0000000000 --- a/libraries/stdlib/genwgsl/mx_splitlr_vector3.wgsl +++ /dev/null @@ -1,18 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_splitlr_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "mx_aastep.wgsl" - -fn mx_splitlr_vector3(valuel_7: vec3f, valuer_7: vec3f, center_7: f32, texcoord_29: vec2f, result_82: ptr) { - var valuel_8: vec3f; - var valuer_8: vec3f; - var center_8: f32; - var texcoord_30: vec2f; - - valuel_8 = valuel_7; - valuer_8 = valuer_7; - center_8 = center_7; - texcoord_30 = texcoord_29; - (*result_82) = mix(valuel_8, valuer_8, vec3((mx_aastep(center_8, texcoord_30.x)))); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_splitlr_vector4.wgsl b/libraries/stdlib/genwgsl/mx_splitlr_vector4.wgsl deleted file mode 100644 index b8a02f437a..0000000000 --- a/libraries/stdlib/genwgsl/mx_splitlr_vector4.wgsl +++ /dev/null @@ -1,18 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_splitlr_vector4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "mx_aastep.wgsl" - -fn mx_splitlr_vector4(valuel_7: vec4f, valuer_7: vec4f, center_7: f32, texcoord_29: vec2f, result_82: ptr) { - var valuel_8: vec4f; - var valuer_8: vec4f; - var center_8: f32; - var texcoord_30: vec2f; - - valuel_8 = valuel_7; - valuer_8 = valuer_7; - center_8 = center_7; - texcoord_30 = texcoord_29; - (*result_82) = mix(valuel_8, valuer_8, vec4((mx_aastep(center_8, texcoord_30.x)))); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_splittb_float.wgsl b/libraries/stdlib/genwgsl/mx_splittb_float.wgsl deleted file mode 100644 index 2522215d32..0000000000 --- a/libraries/stdlib/genwgsl/mx_splittb_float.wgsl +++ /dev/null @@ -1,18 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_splittb_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "mx_aastep.wgsl" - -fn mx_splittb_float(valuet_7: f32, valueb_7: f32, center_7: f32, texcoord_29: vec2f, result_82: ptr) { - var valuet_8: f32; - var valueb_8: f32; - var center_8: f32; - var texcoord_30: vec2f; - - valuet_8 = valuet_7; - valueb_8 = valueb_7; - center_8 = center_7; - texcoord_30 = texcoord_29; - (*result_82) = mix(valueb_8, valuet_8, (mx_aastep(center_8, texcoord_30.y))); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_splittb_vector2.wgsl b/libraries/stdlib/genwgsl/mx_splittb_vector2.wgsl deleted file mode 100644 index e52470a29a..0000000000 --- a/libraries/stdlib/genwgsl/mx_splittb_vector2.wgsl +++ /dev/null @@ -1,18 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_splittb_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "mx_aastep.wgsl" - -fn mx_splittb_vector2(valuet_7: vec2f, valueb_7: vec2f, center_7: f32, texcoord_29: vec2f, result_82: ptr) { - var valuet_8: vec2f; - var valueb_8: vec2f; - var center_8: f32; - var texcoord_30: vec2f; - - valuet_8 = valuet_7; - valueb_8 = valueb_7; - center_8 = center_7; - texcoord_30 = texcoord_29; - (*result_82) = mix(valueb_8, valuet_8, vec2((mx_aastep(center_8, texcoord_30.y)))); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_splittb_vector3.wgsl b/libraries/stdlib/genwgsl/mx_splittb_vector3.wgsl deleted file mode 100644 index 23ff900855..0000000000 --- a/libraries/stdlib/genwgsl/mx_splittb_vector3.wgsl +++ /dev/null @@ -1,18 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_splittb_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "mx_aastep.wgsl" - -fn mx_splittb_vector3(valuet_7: vec3f, valueb_7: vec3f, center_7: f32, texcoord_29: vec2f, result_82: ptr) { - var valuet_8: vec3f; - var valueb_8: vec3f; - var center_8: f32; - var texcoord_30: vec2f; - - valuet_8 = valuet_7; - valueb_8 = valueb_7; - center_8 = center_7; - texcoord_30 = texcoord_29; - (*result_82) = mix(valueb_8, valuet_8, vec3((mx_aastep(center_8, texcoord_30.y)))); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_splittb_vector4.wgsl b/libraries/stdlib/genwgsl/mx_splittb_vector4.wgsl deleted file mode 100644 index bf7cf37c26..0000000000 --- a/libraries/stdlib/genwgsl/mx_splittb_vector4.wgsl +++ /dev/null @@ -1,18 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_splittb_vector4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "mx_aastep.wgsl" - -fn mx_splittb_vector4(valuet_7: vec4f, valueb_7: vec4f, center_7: f32, texcoord_29: vec2f, result_82: ptr) { - var valuet_8: vec4f; - var valueb_8: vec4f; - var center_8: f32; - var texcoord_30: vec2f; - - valuet_8 = valuet_7; - valueb_8 = valueb_7; - center_8 = center_7; - texcoord_30 = texcoord_29; - (*result_82) = mix(valueb_8, valuet_8, vec4((mx_aastep(center_8, texcoord_30.y)))); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_surface_unlit.wgsl b/libraries/stdlib/genwgsl/mx_surface_unlit.wgsl deleted file mode 100644 index cb8ca75c32..0000000000 --- a/libraries/stdlib/genwgsl/mx_surface_unlit.wgsl +++ /dev/null @@ -1,9 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_surface_unlit.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_surface_unlit(emission: f32, emission_color: vec3f, transmission: f32, transmission_color: vec3f, opacity: f32, result_82: ptr) { - - (*result_82).color = ((emission * emission_color) * opacity); - (*result_82).transparency = mix(vec3(1.0), (transmission * transmission_color), vec3(opacity)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_transformmatrix_vector2M3.wgsl b/libraries/stdlib/genwgsl/mx_transformmatrix_vector2M3.wgsl deleted file mode 100644 index 560d102e15..0000000000 --- a/libraries/stdlib/genwgsl/mx_transformmatrix_vector2M3.wgsl +++ /dev/null @@ -1,15 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_transformmatrix_vector2M3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_transformmatrix_vector2M3(val_3: vec2f, transform_1: mat3x3f, result_82: ptr) { - var val_4: vec2f; - var transform_2: mat3x3f; - var res: vec3f; - - val_4 = val_3; - transform_2 = transform_1; - let _e24 = val_4; - res = (mx_matrix_mul_mat3_vec3(transform_2, vec3f(_e24.x, _e24.y, 1.0))); - (*result_82) = res.xy; - return; -} diff --git a/libraries/stdlib/genwgsl/mx_transformmatrix_vector3M4.wgsl b/libraries/stdlib/genwgsl/mx_transformmatrix_vector3M4.wgsl deleted file mode 100644 index 9ce61a9972..0000000000 --- a/libraries/stdlib/genwgsl/mx_transformmatrix_vector3M4.wgsl +++ /dev/null @@ -1,15 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_transformmatrix_vector3M4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_transformmatrix_vector3M4(val_3: vec3f, transform_1: mat4x4f, result_82: ptr) { - var val_4: vec3f; - var transform_2: mat4x4f; - var res: vec4f; - - val_4 = val_3; - transform_2 = transform_1; - let _e24 = val_4; - res = (mx_matrix_mul_mat4_vec4(transform_2, vec4f(_e24.x, _e24.y, _e24.z, 1.0))); - (*result_82) = res.xyz; - return; -} diff --git a/libraries/stdlib/genwgsl/mx_unpremult_color4.wgsl b/libraries/stdlib/genwgsl/mx_unpremult_color4.wgsl deleted file mode 100644 index 38562e7371..0000000000 --- a/libraries/stdlib/genwgsl/mx_unpremult_color4.wgsl +++ /dev/null @@ -1,11 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_unpremult_color4.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -fn mx_unpremult_color4(_in_9: vec4f, result_82: ptr) { - var _in_10: vec4f; - - _in_10 = _in_9; - let _e26 = (_in_10.xyz / vec3(_in_10.w)); - (*result_82) = vec4f(_e26.x, _e26.y, _e26.z, _in_10.w); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_worleynoise2d_float.wgsl b/libraries/stdlib/genwgsl/mx_worleynoise2d_float.wgsl deleted file mode 100644 index f0e609d959..0000000000 --- a/libraries/stdlib/genwgsl/mx_worleynoise2d_float.wgsl +++ /dev/null @@ -1,16 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_worleynoise2d_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_worleynoise2d_float(texcoord_29: vec2f, jitter_15: f32, style_11: i32, result_82: ptr) { - var texcoord_30: vec2f; - var jitter_16: f32; - var style_12: i32; - - texcoord_30 = texcoord_29; - jitter_16 = jitter_15; - style_12 = style_11; - (*result_82) = (mx_worley_noise_float_2d(texcoord_30, jitter_16, style_12, 0i)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_worleynoise2d_vector2.wgsl b/libraries/stdlib/genwgsl/mx_worleynoise2d_vector2.wgsl deleted file mode 100644 index aac4b61b68..0000000000 --- a/libraries/stdlib/genwgsl/mx_worleynoise2d_vector2.wgsl +++ /dev/null @@ -1,16 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_worleynoise2d_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_worleynoise2d_vector2(texcoord_29: vec2f, jitter_15: f32, style_11: i32, result_82: ptr) { - var texcoord_30: vec2f; - var jitter_16: f32; - var style_12: i32; - - texcoord_30 = texcoord_29; - jitter_16 = jitter_15; - style_12 = style_11; - (*result_82) = (mx_worley_noise_vec2_2d(texcoord_30, jitter_16, style_12, 0i)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_worleynoise2d_vector3.wgsl b/libraries/stdlib/genwgsl/mx_worleynoise2d_vector3.wgsl deleted file mode 100644 index 60459bc30b..0000000000 --- a/libraries/stdlib/genwgsl/mx_worleynoise2d_vector3.wgsl +++ /dev/null @@ -1,16 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_worleynoise2d_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_worleynoise2d_vector3(texcoord_29: vec2f, jitter_15: f32, style_11: i32, result_82: ptr) { - var texcoord_30: vec2f; - var jitter_16: f32; - var style_12: i32; - - texcoord_30 = texcoord_29; - jitter_16 = jitter_15; - style_12 = style_11; - (*result_82) = (mx_worley_noise_vec3_2d(texcoord_30, jitter_16, style_12, 0i)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_worleynoise3d_float.wgsl b/libraries/stdlib/genwgsl/mx_worleynoise3d_float.wgsl deleted file mode 100644 index d357127090..0000000000 --- a/libraries/stdlib/genwgsl/mx_worleynoise3d_float.wgsl +++ /dev/null @@ -1,16 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_worleynoise3d_float.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_worleynoise3d_float(position_13: vec3f, jitter_15: f32, style_11: i32, result_82: ptr) { - var position_14: vec3f; - var jitter_16: f32; - var style_12: i32; - - position_14 = position_13; - jitter_16 = jitter_15; - style_12 = style_11; - (*result_82) = (mx_worley_noise_float_3d(position_14, jitter_16, style_12, 0i)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_worleynoise3d_vector2.wgsl b/libraries/stdlib/genwgsl/mx_worleynoise3d_vector2.wgsl deleted file mode 100644 index 8de97aea46..0000000000 --- a/libraries/stdlib/genwgsl/mx_worleynoise3d_vector2.wgsl +++ /dev/null @@ -1,16 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_worleynoise3d_vector2.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_worleynoise3d_vector2(position_13: vec3f, jitter_15: f32, style_11: i32, result_82: ptr) { - var position_14: vec3f; - var jitter_16: f32; - var style_12: i32; - - position_14 = position_13; - jitter_16 = jitter_15; - style_12 = style_11; - (*result_82) = (mx_worley_noise_vec2_3d(position_14, jitter_16, style_12, 0i)); - return; -} diff --git a/libraries/stdlib/genwgsl/mx_worleynoise3d_vector3.wgsl b/libraries/stdlib/genwgsl/mx_worleynoise3d_vector3.wgsl deleted file mode 100644 index 3ec74a4d10..0000000000 --- a/libraries/stdlib/genwgsl/mx_worleynoise3d_vector3.wgsl +++ /dev/null @@ -1,16 +0,0 @@ -// Generated from libraries/stdlib/genglsl/mx_worleynoise3d_vector3.glsl by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py. -// Do not edit -- re-run the transpiler to regenerate (see source/MaterialXGenWgsl/README.md). - -#include "lib/mx_noise.wgsl" - -fn mx_worleynoise3d_vector3(position_13: vec3f, jitter_15: f32, style_11: i32, result_82: ptr) { - var position_14: vec3f; - var jitter_16: f32; - var style_12: i32; - - position_14 = position_13; - jitter_16 = jitter_15; - style_12 = style_11; - (*result_82) = (mx_worley_noise_vec3_3d(position_14, jitter_16, style_12, 0i)); - return; -} From bbd7fb2bd89959953a26a00f01f6d10295b56d68 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Tue, 7 Jul 2026 11:26:02 -0700 Subject: [PATCH 07/19] Enable wgsl data library generation during CI - Add generate_wgsl rule that installs naga-cli and updates data library with wgsl TODO: check if cargo is availble on CI runners --- .github/workflows/main.yml | 37 +++++++++++++++++++++-- .github/workflows/release.yml | 12 ++++++++ CMakeLists.txt | 2 +- pyproject.toml | 1 + source/MaterialXGenWgsl/CMakeLists.txt | 4 ++- source/MaterialXGenWgsl/README.md | 39 ++++++++++++++++--------- source/MaterialXGenWgsl/tools/README.md | 29 +++++++++++------- 7 files changed, 95 insertions(+), 29 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ba0bb41051..607b838aa5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,6 +14,7 @@ permissions: env: CMAKE_BUILD_PARALLEL_LEVEL: 4 CTEST_PARALLEL_LEVEL: 4 + NAGA_VERSION: "29.0.0" jobs: @@ -38,6 +39,7 @@ jobs: compiler: gcc compiler_version: "14" python: 3.13 + generate_wgsl: ON extended_build_perfetto: ON extended_cmake_config: -DMATERIALX_BUILD_PERFETTO_TRACING=ON @@ -94,6 +96,7 @@ jobs: python: 3.14 test_shaders: ON static_analysis: ON + generate_wgsl: ON cmake_config: -DCMAKE_EXPORT_COMPILE_COMMANDS=ON - name: MacOS_Xcode_DynamicAnalysis @@ -131,6 +134,7 @@ jobs: python: 3.14 cmake_config: -G "Visual Studio 17 2022" -A "x64" test_shaders: ON + generate_wgsl: ON extended_build_oiio: ON extended_cmake_config: -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows -DMATERIALX_BUILD_OIIO=ON @@ -239,6 +243,14 @@ jobs: if: matrix.clang_format == 'ON' run: find source \( -name *.h -o -name *.cpp -o -name *.mm -o -name *.inl \) ! -path "*/External/*" ! -path "*/NanoGUI/*" | xargs clang-format -i --verbose + - name: Install naga + if: matrix.generate_wgsl == 'ON' + run: cargo install naga-cli --locked --version ${{ env.NAGA_VERSION }} + + - name: Generate WGSL library + if: matrix.generate_wgsl == 'ON' + run: python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries + - name: CMake Generate shell: bash run: | @@ -247,6 +259,7 @@ jobs: -DMATERIALX_BUILD_PYTHON=ON \ -DMATERIALX_BUILD_TESTS=ON \ -DMATERIALX_TEST_RENDER=${{ matrix.extended_build_osl == 'ON' && 'ON' || 'OFF' }} \ + -DMATERIALX_BUILD_GEN_WGSL=${{ matrix.generate_wgsl == 'ON' && 'ON' || 'OFF' }} \ -DMATERIALX_WARNINGS_AS_ERRORS=ON \ ${{ env.IS_EXTENDED_BUILD == 'true' && matrix.extended_cmake_config || '' }} \ ${{ matrix.cmake_config }} \ @@ -274,7 +287,9 @@ jobs: python Scripts/mxdoc.py --docType html ../libraries/bxdf/standard_surface.mtlx python Scripts/generateshader.py ../resources/Materials/Examples/StandardSurface --target glsl python Scripts/generateshader.py ../resources/Materials/Examples/StandardSurface --target vulkan - python Scripts/generateshader.py ../resources/Materials/Examples/StandardSurface --target wgsl + if: matrix.generate_wgsl == 'ON' + run: | + python Scripts/generateshader.py ../resources/Materials/Examples/StandardSurface --target wgsl python Scripts/generateshader.py ../resources/Materials/Examples/StandardSurface --target osl python Scripts/generateshader.py ../resources/Materials/Examples/StandardSurface --target mdl python Scripts/generateshader.py ../resources/Materials/Examples/StandardSurface --target msl @@ -289,7 +304,6 @@ jobs: python python/Scripts/generateshader.py resources/Materials/Examples --target glsl --validator C:/vcpkg/installed/x64-windows-release/tools/glslang/glslangValidator.exe python python/Scripts/generateshader.py resources/Materials/Examples/StandardSurface --target essl --validator C:/vcpkg/installed/x64-windows-release/tools/glslang/glslangValidator.exe python python/Scripts/generateshader.py resources/Materials/Examples/StandardSurface --target vulkan --validator C:/vcpkg/installed/x64-windows-release/tools/glslang/glslangValidator.exe - # Native genwgsl emits WGSL; glslangValidator is GLSL/SPIR-V only — re-enable with naga when wired in CI. - name: Shader Validation Tests (MacOS) if: matrix.test_shaders == 'ON' && runner.os == 'macOS' @@ -458,8 +472,19 @@ jobs: with: node-version: '22.16.0' + - name: Install naga + run: cargo install naga-cli --locked --version ${{ env.NAGA_VERSION }} + + - name: Install Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + with: + python-version: 3.13 + + - name: Generate WGSL library + run: python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries + - name: JavaScript CMake Generate - run: cmake -S . -B javascript/build -DMATERIALX_BUILD_JS=ON -DMATERIALX_EMSDK_PATH=${{ env.EMSDK }} + run: cmake -S . -B javascript/build -DMATERIALX_BUILD_JS=ON -DMATERIALX_BUILD_GEN_WGSL=ON -DMATERIALX_EMSDK_PATH=${{ env.EMSDK }} - name: JavaScript CMake Build run: cmake --build javascript/build --target install --config Release @@ -510,6 +535,12 @@ jobs: with: python-version: 3.11 + - name: Install naga + run: cargo install naga-cli --locked --version ${{ env.NAGA_VERSION }} + + - name: Generate WGSL library + run: python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries + - name: Build SDist id: generate run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cd4b23323c..e968aaf364 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,6 +14,7 @@ jobs: runs-on: ubuntu-latest env: RELEASE_TAG: ${{ github.ref_name }} + NAGA_VERSION: "29.0.0" permissions: contents: write id-token: write @@ -28,6 +29,17 @@ jobs: - name: Create Archive Name run: echo "MATERIALX_ARCHIVE=MaterialX-${RELEASE_TAG//v}" >> $GITHUB_ENV + - name: Install Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + with: + python-version: 3.11 + + - name: Install naga + run: cargo install naga-cli --locked --version ${NAGA_VERSION} + + - name: Generate WGSL library + run: python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries + - name: Generate Archives run: | git archive --prefix ${MATERIALX_ARCHIVE}/ --output ${MATERIALX_ARCHIVE}.zip ${RELEASE_TAG} diff --git a/CMakeLists.txt b/CMakeLists.txt index f7a196e2a9..599d1fdd35 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,7 +43,7 @@ option(MATERIALX_BUILD_GEN_OSL "Build the OSL shader generator back-ends." 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_GEN_SLANG "Build the Slang shader generator back-end." ON) -option(MATERIALX_BUILD_GEN_WGSL "Build the WGSL (WebGPU) shader generator back-end." ON) +option(MATERIALX_BUILD_GEN_WGSL "Build the WGSL (WebGPU) shader generator back-end." OFF) option(MATERIALX_GENERATE_WGSL_LIBRARY "Regenerate the genwgsl shader-node library from genglsl at build time (requires Python and naga); surfaces transpile/validation failures as build errors." OFF) option(MATERIALX_BUILD_RENDER "Build the MaterialX Render modules." ON) option(MATERIALX_BUILD_RENDER_PLATFORMS "Build platform-specific render modules for each shader generator." ON) diff --git a/pyproject.toml b/pyproject.toml index 12aba95f51..17b20aad56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,7 @@ result = "{major}.{minor}.{build}" [tool.scikit-build.cmake.define] MATERIALX_BUILD_SHARED_LIBS = 'OFF' # Be explicit MATERIALX_BUILD_PYTHON = 'ON' +MATERIALX_BUILD_GEN_WGSL = 'ON' MATERIALX_BUILD_DOCS = 'ON' MATERIALX_PYTHON_FORCE_REPLACE_DOCS = 'ON' MATERIALX_TEST_RENDER = 'OFF' diff --git a/source/MaterialXGenWgsl/CMakeLists.txt b/source/MaterialXGenWgsl/CMakeLists.txt index 32d7a39158..07ab716bca 100644 --- a/source/MaterialXGenWgsl/CMakeLists.txt +++ b/source/MaterialXGenWgsl/CMakeLists.txt @@ -15,7 +15,9 @@ mx_add_library(MaterialXGenWgsl # Optional build-time regeneration of the genwgsl shader-node library from genglsl. This runs the # offline transpiler (tools/glsl_to_wgsl.py) over libraries/ into the build tree purely for # validation: transpile or naga errors -- and any previously-generable node that regresses -- make -# the build fail. The committed genwgsl .wgsl files remain the source of truth (the output goes to +# the build fail. The generated genwgsl .wgsl node files are NOT committed; they are produced from +# genglsl (the single source of truth) by CI, which runs the transpiler with `--out libraries` to +# populate the runtime library. This target is a decoupled local validation aid only (output goes to # the build dir, not the source tree). Requires a Python interpreter and the `naga` CLI. if(MATERIALX_GENERATE_WGSL_LIBRARY) if(MATERIALX_PYTHON_EXECUTABLE) diff --git a/source/MaterialXGenWgsl/README.md b/source/MaterialXGenWgsl/README.md index 779edb8961..bf11ed6604 100644 --- a/source/MaterialXGenWgsl/README.md +++ b/source/MaterialXGenWgsl/README.md @@ -4,7 +4,9 @@ A native [WGSL](https://www.w3.org/TR/WGSL/) (WebGPU Shading Language) shader ge MaterialX, registered under the `genwgsl` target. `WgslShaderGenerator` derives directly from `HwShaderGenerator` (not the GLSL hierarchy) and emits standalone WGSL vertex + fragment shaders, mirroring the structure of the `MaterialXGenMsl` / `MaterialXGenSlang` back-ends. It is gated behind -the `MATERIALX_BUILD_GEN_WGSL` CMake option (ON by default). +the `MATERIALX_BUILD_GEN_WGSL` CMake option (**OFF by default**: its node library is transpiled from +genglsl and is not committed, so enabling the target requires that library to be generated first — +see below). ## What makes this back-end unusual @@ -12,9 +14,10 @@ Every other shader-gen back-end ships a fully hand-written node library. `genwgs its node library is a *hybrid*, and that is the main thing to understand before editing it: * **Most node `.wgsl` files are machine-generated** from their `genglsl` originals by the offline - transpiler in [`tools/`](tools/README.md). They are committed (the generator does not run at - build time by default) but carry a `// Generated from … do not edit` banner to distinguish them - from the hand-written files alongside — do not hand-edit them; regenerate. + transpiler in [`tools/`](tools/README.md). They are **not committed** — they are a derived build + artifact, transpiled from genglsl (the single source of truth) by CI, so drift between the GLSL and + WGSL libraries is impossible by construction. The generated files carry a + `// Generated from … do not edit` banner; do not hand-edit them, and do not commit them. * **A small number of nodes are hand-written** and intentionally *not* generated, because WGSL has no function overloading and a few `genwgsl/lib/` helpers were hand-adapted away from their GLSL signatures. These are listed as `EXPECTED_FALLBACK` in `tools/glsl_to_wgsl.py` (currently the @@ -26,18 +29,26 @@ its node library is a *hybrid*, and that is the main thing to understand before The library lives in `libraries/{stdlib,pbrlib,lights}/genwgsl/` with the target defined in `libraries/targets/genwgsl.mtlx`; node implementations are wired up via `*_genwgsl_impl.mtlx`. -## Keeping the generated library in sync with genglsl +## Generating the library -The generated `.wgsl` files can drift as `genglsl` improves. The transpiler regenerates them and a -diff surfaces the drift — see [`tools/README.md`](tools/README.md) for the tool, its overload -mapping table, and its lib-arity self-validation. +Because the generated node files are not committed, you must produce them before building the WGSL +target. CI does this automatically (installing the [`naga`](https://github.com/gfx-rs/wgpu/tree/trunk/naga) +CLI and running the transpiler) on the jobs that build or ship WGSL — the web viewer, the Python +sdist/wheels, and the tagged-release archives. Locally, populate the library in place with: -For CI / local validation, configure with `-DMATERIALX_GENERATE_WGSL_LIBRARY=ON` (requires Python -and the [`naga`](https://github.com/gfx-rs/wgpu/tree/trunk/naga) CLI). This adds a -`MaterialXGenWgslLibrary` build target that re-transpiles the library into the build tree on every -build and **fails the build** if a node that should generate cleanly does not (a regression or a new -naga validation error), while tolerating the known `EXPECTED_FALLBACK` nodes. The committed `.wgsl` -files remain the source of truth; the build-tree output is for validation only. +``` +python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries +``` + +then configure with `-DMATERIALX_BUILD_GEN_WGSL=ON`. The transpiler exits non-zero on an unexpected +failure (a regression or a new naga validation error), while tolerating the known `EXPECTED_FALLBACK` +nodes — so CI running it doubles as validation that a change hasn't broken the WGSL target. See +[`tools/README.md`](tools/README.md) for the tool, its overload mapping table, and its lib-arity +self-validation. + +The separate `-DMATERIALX_GENERATE_WGSL_LIBRARY=ON` option adds a `MaterialXGenWgslLibrary` target +that re-transpiles into the *build tree* (not the source tree) as a local validation aid; it is +decoupled from `MATERIALX_BUILD_GEN_WGSL`. ## Layout diff --git a/source/MaterialXGenWgsl/tools/README.md b/source/MaterialXGenWgsl/tools/README.md index 8ac0539f16..73716ac653 100644 --- a/source/MaterialXGenWgsl/tools/README.md +++ b/source/MaterialXGenWgsl/tools/README.md @@ -7,13 +7,16 @@ originally hand-ported and repeatedly drifted as genglsl improved, so most of th generated, and re-running the tool (then diffing) surfaces drift. ``` -python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out build/genwgsl_generated +python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --only mx_noise3d_float mx_sheen_bsdf ``` Requires [`naga`](https://github.com/gfx-rs/wgpu/tree/trunk/naga) (`naga-cli`, v29+) on `PATH` or at -`%NAGA%`. Output is written to a staging dir (default `build/genwgsl_generated/...`); diff it against -the committed `libraries/.../genwgsl/*.wgsl` before copying over. +`%NAGA%`. The generated node files are **not committed** — they are a derived artifact produced from +genglsl (the single source of truth). Passing `--out libraries` writes each generated file straight +into `libraries//genwgsl/`, populating the runtime library in place alongside the hand-written +files (the tool never touches `lib/` or the fallback nodes). CI runs exactly this to build/ship WGSL; +run it locally to work on the WGSL target. Use `--out ` to emit to a staging dir instead. ## Scope @@ -102,11 +105,17 @@ so configure stays fast) and is cached — it recompiles only if the binary is m already be installed; MaterialX does not bootstrap the Rust toolchain. If cargo cannot be found, generation is skipped with a warning. -## Sync workflow +## Local workflow -1. Regenerate to staging: `python glsl_to_wgsl.py --libraries libraries --out build/genwgsl_generated`. -2. Diff `build/genwgsl_generated/**/*.wgsl` against `libraries/**/genwgsl/*.wgsl`. -3. Copy the generated files over the committed ones (the hand-written fallbacks and `lib/` are not - produced by the tool, so they are preserved). -4. Rebuild + restage test data, run the `[genwgsl]` tests, and `naga`-validate the emitted - `Default.{vertex,pixel}.wgsl`. +Since the generated files are not committed, there is no baseline to diff against — you simply +regenerate the library in place whenever genglsl changes: + +1. Regenerate in place: `python glsl_to_wgsl.py --libraries libraries --out libraries`. This overwrites + the generated node files under `libraries/{stdlib,pbrlib}/genwgsl/` and leaves the hand-written + fallbacks and `lib/` helpers untouched. A non-zero exit means an *unexpected* node failed (a + regression) — the known `EXPECTED_FALLBACK` nodes failing is normal. +2. Configure with `-DMATERIALX_BUILD_GEN_WGSL=ON`, rebuild + restage test data, run the `[genwgsl]` + tests, and `naga`-validate the emitted `Default.{vertex,pixel}.wgsl`. + +CI performs step 1 on every job that builds or ships WGSL (the web viewer, sdist/wheels, and the +tagged-release archives), so a change that breaks the WGSL target fails CI. From a2703abde1e1fff140f254ee2ea1d9fb38f45403 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Tue, 7 Jul 2026 13:14:27 -0700 Subject: [PATCH 08/19] fix (build): Enable WGSL on python enabled runners --- .github/workflows/main.yml | 13 ++++--------- python/Scripts/generateshader.py | 11 ++++++++++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 607b838aa5..57ff839f3b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,7 +39,6 @@ jobs: compiler: gcc compiler_version: "14" python: 3.13 - generate_wgsl: ON extended_build_perfetto: ON extended_cmake_config: -DMATERIALX_BUILD_PERFETTO_TRACING=ON @@ -96,7 +95,6 @@ jobs: python: 3.14 test_shaders: ON static_analysis: ON - generate_wgsl: ON cmake_config: -DCMAKE_EXPORT_COMPILE_COMMANDS=ON - name: MacOS_Xcode_DynamicAnalysis @@ -134,7 +132,6 @@ jobs: python: 3.14 cmake_config: -G "Visual Studio 17 2022" -A "x64" test_shaders: ON - generate_wgsl: ON extended_build_oiio: ON extended_cmake_config: -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows -DMATERIALX_BUILD_OIIO=ON @@ -244,11 +241,11 @@ jobs: run: find source \( -name *.h -o -name *.cpp -o -name *.mm -o -name *.inl \) ! -path "*/External/*" ! -path "*/NanoGUI/*" | xargs clang-format -i --verbose - name: Install naga - if: matrix.generate_wgsl == 'ON' + if: matrix.python != 'None' run: cargo install naga-cli --locked --version ${{ env.NAGA_VERSION }} - name: Generate WGSL library - if: matrix.generate_wgsl == 'ON' + if: matrix.python != 'None' run: python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries - name: CMake Generate @@ -259,7 +256,7 @@ jobs: -DMATERIALX_BUILD_PYTHON=ON \ -DMATERIALX_BUILD_TESTS=ON \ -DMATERIALX_TEST_RENDER=${{ matrix.extended_build_osl == 'ON' && 'ON' || 'OFF' }} \ - -DMATERIALX_BUILD_GEN_WGSL=${{ matrix.generate_wgsl == 'ON' && 'ON' || 'OFF' }} \ + -DMATERIALX_BUILD_GEN_WGSL=${{ matrix.python != 'None' && 'ON' || 'OFF' }} \ -DMATERIALX_WARNINGS_AS_ERRORS=ON \ ${{ env.IS_EXTENDED_BUILD == 'true' && matrix.extended_cmake_config || '' }} \ ${{ matrix.cmake_config }} \ @@ -287,9 +284,7 @@ jobs: python Scripts/mxdoc.py --docType html ../libraries/bxdf/standard_surface.mtlx python Scripts/generateshader.py ../resources/Materials/Examples/StandardSurface --target glsl python Scripts/generateshader.py ../resources/Materials/Examples/StandardSurface --target vulkan - if: matrix.generate_wgsl == 'ON' - run: | - python Scripts/generateshader.py ../resources/Materials/Examples/StandardSurface --target wgsl + python Scripts/generateshader.py ../resources/Materials/Examples/StandardSurface --target wgsl python Scripts/generateshader.py ../resources/Materials/Examples/StandardSurface --target osl python Scripts/generateshader.py ../resources/Materials/Examples/StandardSurface --target mdl python Scripts/generateshader.py ../resources/Materials/Examples/StandardSurface --target msl diff --git a/python/Scripts/generateshader.py b/python/Scripts/generateshader.py index ad6d5e09bf..72f97344c7 100644 --- a/python/Scripts/generateshader.py +++ b/python/Scripts/generateshader.py @@ -12,9 +12,14 @@ import MaterialX.PyMaterialXGenMsl as mx_gen_msl import MaterialX.PyMaterialXGenOsl as mx_gen_osl import MaterialX.PyMaterialXGenSlang as mx_gen_slang -import MaterialX.PyMaterialXGenWgsl as mx_gen_wgsl import MaterialX.PyMaterialXGenShader as mx_gen_shader +# The WGSL generator is an optional +try: + import MaterialX.PyMaterialXGenWgsl as mx_gen_wgsl +except ImportError: + mx_gen_wgsl = None + def validateCode(sourceCodeFile, codevalidator, codevalidatorArgs): if codevalidator: cmd = codevalidator.split() @@ -101,6 +106,10 @@ def main(): elif gentarget == 'vulkan': shadergen = mx_gen_glsl.VkShaderGenerator.create() elif gentarget == 'wgsl': + if mx_gen_wgsl is None: + print('WGSL shader generation is not available in this build ' + '(MATERIALX_BUILD_GEN_WGSL was disabled).') + sys.exit(1) shadergen = mx_gen_wgsl.WgslShaderGenerator.create() elif gentarget == 'msl': shadergen = mx_gen_msl.MslShaderGenerator.create() From 8b0b0633283044fa54bba86f953da62994ffe5d8 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Thu, 9 Jul 2026 14:25:46 -0700 Subject: [PATCH 09/19] Add developer documentation for WGSL Shader Generation --- .../DeveloperGuide/WGSLShaderGeneration.md | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 documents/DeveloperGuide/WGSLShaderGeneration.md diff --git a/documents/DeveloperGuide/WGSLShaderGeneration.md b/documents/DeveloperGuide/WGSLShaderGeneration.md new file mode 100644 index 0000000000..56df0aee2b --- /dev/null +++ b/documents/DeveloperGuide/WGSLShaderGeneration.md @@ -0,0 +1,247 @@ +# WGSL Shader Generation + +MaterialX includes a native [WGSL](https://www.w3.org/TR/WGSL/) (WebGPU Shading Language) shader generator back-end, registered under the `genwgsl` target. `WgslShaderGenerator` derives from `HwShaderGenerator` and emits standalone WGSL vertex and fragment shaders, similar in structure to the MSL and Slang back-ends. + +This guide covers the tooling, CMake configuration, and local workflows for working with the WGSL target. For general shader generation concepts, see [Shader Generation](ShaderGeneration.md). For implementation details of the transpiler itself, see [`source/MaterialXGenWgsl/tools/README.md`](../../source/MaterialXGenWgsl/tools/README.md). + +## Overview + +The `genwgsl` uses a **hybrid node library**: + +| Library content | Source | Committed to git? | +| --- | --- | --- | +| Most node `.wgsl` files | Transpiled from `genglsl` by `glsl_to_wgsl.py` | No — derived artifact | +| Core `lib/` math and closure helpers | Hand-maintained | Yes | +| Texture, image, and light nodes | Hand-maintained | Yes | +| A small set of BSDF nodes (`EXPECTED_FALLBACK`) | Hand-maintained | Yes | + +The GLSL node library (`genglsl`) is the **single source of truth** for generated nodes. CI runs the transpiler on every job that generates WGSL to prevent drift between GLSL and WGSL libraries. + +The library lives under `libraries/{stdlib,pbrlib,lights}/genwgsl/`, with the target defined in `libraries/targets/genwgsl.mtlx`. + +## Prerequisites + +| Requirement | Notes | +| --- | --- | +| **Python 3.9+** | Required to run the transpiler | +| **naga-cli** (v29+) | `cargo install naga-cli`, or set the `NAGA` environment variable to the binary path | +| **Rust cargo** (optional) | Only needed if naga is not already installed; CMake can build naga into the build tree | +| **Emscripten 4.0.8** | Required only for JavaScript / WebGPU viewer testing | + +## CMake Options + +| Option | Default | Description | +| --- | --- | --- | +| `MATERIALX_BUILD_GEN_WGSL` | `OFF` | Build the `MaterialXGenWgsl` library and enable the `genwgsl` shader target | +| `MATERIALX_GENERATE_WGSL_LIBRARY` | `OFF` | Add a `MaterialXGenWgslLibrary` build target that re-transpiles into the build tree as a validation aid | +| `MATERIALX_NAGA_EXECUTABLE` | (auto-detect) | Path to the `naga` CLI | +| `MATERIALX_CARGO_PATH` | (auto-detect) | Path to a Rust cargo home (used to install naga if not found) | + +`MATERIALX_BUILD_GEN_WGSL` is **off by default** so ordinary builds do not require Python or naga. Enable it when working on the WGSL target. + +`MATERIALX_GENERATE_WGSL_LIBRARY` is independent of `MATERIALX_BUILD_GEN_WGSL` and writes generated files to the build tree only (not the source tree). + +## Tooling + +### `glsl_to_wgsl.py` + +The transpiler at `source/MaterialXGenWgsl/tools/glsl_to_wgsl.py` converts `genglsl` node fragments into `genwgsl` equivalents using [naga](https://github.com/gfx-rs/wgpu/tree/trunk/naga). + +**NOTE:*** It is **not** a general-purpose GLSL-to-WGSL converter. It is scoped to MaterialX shader-node fragments. + +**Regenerate the full library in place:** + +```sh +python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries +``` + +**Regenerate specific nodes only:** + +```sh +python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries --only mx_noise3d_float mx_sheen_bsdf +``` + +A non-zero exit code means an *unexpected* node failed (a regression). Known fallback nodes listed in `EXPECTED_FALLBACK` are tolerated. + +### What the transpiler does + +`glsl_to_wgsl.py` is **not** a general-purpose GLSL-to-WGSL converter. It transpiles MaterialX **shader-node fragments** — one function per file, referenced by `file=` implementations in `stdlib`, `pbrlib`, and `lights`. naga performs the actual translation; the script wraps it with a pre-processor and post-processor so incomplete node fragments become valid input and the output matches genwgsl library conventions. + +**Pipeline (per node function):** + +1. **Pre-process** — wrap the fragment in a complete GLSL shader naga can parse +2. **Transpile** — `naga --input-kind glsl --shader-stage frag` +3. **Post-process** — clean up naga output and remap helper calls to genwgsl names + +#### Input: an incomplete node fragment + +A typical genglsl node is not valid standalone GLSL. It has `#include`s, no `main`, and sometimes MaterialX `$`-tokens: + +```glsl +#include "lib/mx_noise.glsl" + +void mx_noise3d_float(float amplitude, float pivot, vec3 position, out float result) +{ + float value = mx_perlin_noise_float(position); + result = value * amplitude + pivot; +} +``` + +The tool cannot feed this directly to naga. Instead it synthesizes a complete translation unit: + +- Shared `#define` / `const` / `struct` context from included libs (prototypes only, not full bodies) +- Closure structs (`BSDF`, `surfaceshader`, etc.) +- `$`-token placeholders swapped for legal identifiers (e.g. `$texSamplerSampler2D` → `MTLXTOK_texSamplerSampler2D`, restored after transpile) +- The one node function body being transpiled +- A dummy `main()` entry point (required by naga's GLSL frontend) + +#### Post-process: genwgsl conventions + +naga's output is correct but verbose (SSA-style parameter shadows, `vec3` syntax, etc.). The post-processor: + +- Collapses single-use temporaries and parameter-copy shadows +- Normalizes types (`vec3` → `vec3f`, `2f` → `2.0`) +- Remaps overloaded GLSL helper calls via `CALL_MAP` to type-suffixed genwgsl names + +WGSL has no function overloading, so the hand-written genwgsl `lib/` gives each GLSL overload a distinct name. For the noise example above, `mx_perlin_noise_float(position)` with a `vec3` argument is rewritten to `mx_perlin_noise_float_3d(position)`. + +Similarly, GLSL `mx_square` overloads map to `mx_square_f32`, `mx_square_vec2`, or `mx_square_vec3` depending on the argument type. + +#### What it handles + +| Category | Example | Notes | +| --- | --- | --- | +| Standalone math / utility nodes | `mx_noise3d_float`, `mx_mix_surfaceshader` | `#include` lib helpers; calls remapped via `CALL_MAP` | +| PBR nodes with standard lib signatures | Most BSDF combiners, EDF nodes | Generated when all helper calls resolve | +| Closure / `inout` parameters | `inout BSDF bsdf` | Closure preamble supplies `BSDF` struct; `inout` becomes `ptr` | +| Cross-node helper calls | One node calling another node's function | Prototypes collected from all genglsl files | + +**Illustrative output** (post-processed fragment for `mx_noise3d_float`): + +```wgsl +#include "lib/mx_noise.wgsl" + +fn mx_noise3d_float(amplitude: f32, pivot: f32, position: vec3f, result: ptr) { + let value = mx_perlin_noise_float_3d(position); + (*result) = value * amplitude + pivot; +} +``` + +Generated files carry a `// Generated from … do not edit` banner. + +#### What it does not handle + +| Category | Example | Reason | +| --- | --- | --- | +| Core `lib/` files | `lib/mx_math.glsl` | Hand-maintained; tool never touches `lib/` | +| Texture / image nodes | `mx_image_color3` | naga's GLSL frontend has no sampler support — auto-skipped by filename pattern (`image`, `hextiled`) | +| Light shaders | `mx_point_light` | Use dynamically generated `LightData` — auto-skipped (`_light$` pattern) | +| Adapted BSDF helpers | `mx_dielectric_bsdf`, `mx_conductor_bsdf` | Call lib helpers whose genwgsl signatures diverge from GLSL (e.g. `mx_ggx_energy_compensation` takes a precomputed `vec3f` instead of `FresnelData`) — listed in `EXPECTED_FALLBACK`, kept hand-written | +| Unmapped overloads | Any call not in `CALL_MAP` | Node stays hand-written; logged as unsupported | + +**Texture node** (auto-skipped — uses samplers naga cannot parse): + +```glsl +#include "lib/$fileTransformUv" + +void mx_image_color3($texSamplerSignature, int layer, vec3 defaultval, vec2 texcoord, ...) +{ + vec2 uv = mx_transform_uv(texcoord, uv_scale, uv_offset); + result = texture($texSamplerSampler2D, uv).rgb; +} +``` + +The result is a **reduced library**: most nodes are generated from genglsl; texture, light, and a small set of adapted BSDF nodes remain hand-written. The tool exits non-zero only when a node *outside* `EXPECTED_FALLBACK` fails unexpectedly (a regression). If a fallback node starts transpiling cleanly, the tool prints a warning so it can be removed from `EXPECTED_FALLBACK`. + +For full transpiler internals see [`source/MaterialXGenWgsl/tools/README.md`](../../source/MaterialXGenWgsl/tools/README.md). + +### CI + +GitHub Actions runs `glsl_to_wgsl.py` on Python-enabled build jobs, the JavaScript job, the Python sdist job, and release archives. A GLSL change that breaks WGSL generation will fail CI even without a local naga install. + +## Local Developer Workflows + +### After modifying a GLSL node + +1. Regenerate the WGSL library: + ```sh + python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries + ``` +2. Configure with `-DMATERIALX_BUILD_GEN_WGSL=ON` and rebuild. +3. Run the `[genwgsl]` unit tests: + ```sh + ctest -R genwgsl + ``` + +### C++ shader-generation testing + +This is the fastest path for validating WGSL output without Emscripten: + +```sh +python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries + +cmake -S . -B build -DMATERIALX_BUILD_GEN_WGSL=ON +cmake --build build --config Release +ctest -R genwgsl --test-dir build +``` + +The `[genwgsl]` tests in `source/MaterialXTest/MaterialXGenWgsl/GenWgsl.cpp` cover syntax, target registration, and shader generation from example materials. + +### JavaScript / WebGPU viewer testing + +For end-to-end testing in the browser (Three.js WebGPU renderer, TSL bridge), build with both JavaScript and WGSL enabled: + +```sh +python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries + +cmake -S . -B javascript/build \ + -DMATERIALX_BUILD_JS=ON \ + -DMATERIALX_BUILD_GEN_WGSL=ON \ + -DMATERIALX_EMSDK_PATH= \ + -G Ninja +cmake --build javascript/build --target install --config Release +``` + +On Windows, `javascript/build_javascript_win.bat` automates this flow: Emscripten build, npm install, Playwright tests, and a local dev server at `http://localhost:8080`. + +The viewer produces two webpack bundles from the same source: + +| Page | Backend | Renderer | +| --- | --- | --- | +| `index.html` | WebGL | `THREE.WebGLRenderer` + ESSL (`RawShaderMaterial`) | +| `index-webgpu.html` | WebGPU | `WebGPURenderer` + WGSL (via TSL / `NodeMaterial`) | + +Separate bundles are used because the WebGL and WebGPU Three.js entry points are incompatible. A toggle link switches between the two pages. + +To build the viewer bundle after WASM is ready: + +```sh +cd javascript/MaterialXView +npm install +npm run build +npm run start # dev server at http://localhost:8080 +``` + +### Build-time validation (optional) + +To re-transpile into the build tree on every build (without modifying the source tree): + +```sh +cmake -S . -B build \ + -DMATERIALX_BUILD_GEN_WGSL=ON \ + -DMATERIALX_GENERATE_WGSL_LIBRARY=ON +cmake --build build +``` + +This adds the `MaterialXGenWgslLibrary` target, which surfaces transpile and naga validation failures as build errors. + +## Release Artifacts + +Generated WGSL node files are not committed to git, but they are included in release archives. The release workflow runs `glsl_to_wgsl.py` before packaging, so `libraries/*/genwgsl/*.wgsl` files ship alongside the hand-written WGSL helpers. + +## Related Documentation + +- [Shader Generation](ShaderGeneration.md) — general shader generation framework +- [`source/MaterialXGenWgsl/README.md`](../../source/MaterialXGenWgsl/README.md) — back-end layout and design +- [`source/MaterialXGenWgsl/tools/README.md`](../../source/MaterialXGenWgsl/tools/README.md) — transpiler internals, `CALL_MAP`, and `EXPECTED_FALLBACK` +- [`javascript/README.md`](../../javascript/README.md) — JavaScript bindings and viewer setup From f035fb09a7046cc2d0d3d8ab5cb7d91d2507e40b Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Mon, 13 Jul 2026 18:05:10 -0700 Subject: [PATCH 10/19] Remove hand-rolled wgsl microfacet library --- .../pbrlib/genwgsl/lib/mx_closure_type.wgsl | 49 --- .../pbrlib/genwgsl/lib/mx_microfacet.wgsl | 98 ----- .../genwgsl/lib/mx_microfacet_diffuse.wgsl | 128 ------ .../genwgsl/lib/mx_microfacet_sheen.wgsl | 72 ---- .../genwgsl/lib/mx_microfacet_specular.wgsl | 388 ------------------ .../genwgsl/lib/mx_transmission_opacity.wgsl | 6 - .../genwgsl/lib/mx_transmission_refract.wgsl | 19 - .../pbrlib/genwgsl/mx_chiang_hair_bsdf.wgsl | 288 ------------- .../pbrlib/genwgsl/mx_conductor_bsdf.wgsl | 54 --- .../pbrlib/genwgsl/mx_dielectric_bsdf.wgsl | 61 --- .../genwgsl/mx_generalized_schlick_bsdf.wgsl | 87 ---- .../pbrlib/genwgsl/mx_subsurface_bsdf.wgsl | 39 -- 12 files changed, 1289 deletions(-) delete mode 100644 libraries/pbrlib/genwgsl/lib/mx_closure_type.wgsl delete mode 100644 libraries/pbrlib/genwgsl/lib/mx_microfacet.wgsl delete mode 100644 libraries/pbrlib/genwgsl/lib/mx_microfacet_diffuse.wgsl delete mode 100644 libraries/pbrlib/genwgsl/lib/mx_microfacet_sheen.wgsl delete mode 100644 libraries/pbrlib/genwgsl/lib/mx_microfacet_specular.wgsl delete mode 100644 libraries/pbrlib/genwgsl/lib/mx_transmission_opacity.wgsl delete mode 100644 libraries/pbrlib/genwgsl/lib/mx_transmission_refract.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_chiang_hair_bsdf.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_conductor_bsdf.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_dielectric_bsdf.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_generalized_schlick_bsdf.wgsl delete mode 100644 libraries/pbrlib/genwgsl/mx_subsurface_bsdf.wgsl diff --git a/libraries/pbrlib/genwgsl/lib/mx_closure_type.wgsl b/libraries/pbrlib/genwgsl/lib/mx_closure_type.wgsl deleted file mode 100644 index 02075bdf5e..0000000000 --- a/libraries/pbrlib/genwgsl/lib/mx_closure_type.wgsl +++ /dev/null @@ -1,49 +0,0 @@ -struct ClosureData { - closureType: i32, - L: vec3f, - V: vec3f, - N: vec3f, - P: vec3f, - occlusion: f32, -} - -struct BSDF { - response: vec3f, - throughput: vec3f, -} - -struct VDF { - response: vec3f, - throughput: vec3f, -} - -// EDF (Emission Distribution Function) is represented as a simple vec3 color value. -// In GLSL this was `#define EDF vec3`; in WGSL we use a type alias. -alias EDF = vec3f; - -// In GLSL this was `#define material surfaceshader`; in WGSL we use a type alias. -// The surfaceshader struct must be defined before this file is included. -alias material = surfaceshader; - -struct FresnelData { - // Fresnel model (FRESNEL_MODEL_DIELECTRIC / _CONDUCTOR / _SCHLICK) and thin-film flag. - model: i32, - airy: bool, - // Physical Fresnel. - ior: vec3f, - extinction: vec3f, - // Generalized Schlick Fresnel. - F0: vec3f, - F82: vec3f, - F90: vec3f, - exponent: f32, - // Thin film. - thinfilm_thickness: f32, - thinfilm_ior: f32, - // Selects the refracted environment lookup over the reflected one (surface transmission). - refraction: bool, -} - -fn makeClosureData(closureType: i32, L: vec3f, V: vec3f, N: vec3f, P: vec3f, occlusion: f32) -> ClosureData { - return ClosureData(closureType, L, V, N, P, occlusion); -} diff --git a/libraries/pbrlib/genwgsl/lib/mx_microfacet.wgsl b/libraries/pbrlib/genwgsl/lib/mx_microfacet.wgsl deleted file mode 100644 index abe67b7ceb..0000000000 --- a/libraries/pbrlib/genwgsl/lib/mx_microfacet.wgsl +++ /dev/null @@ -1,98 +0,0 @@ -// Microfacet utility functions (WGSL port of genglsl/lib/mx_microfacet.glsl) - -const M_PI_INV: f32 = 1.0 / 3.1415926535897932; - -fn mx_pow5(x: f32) -> f32 { - let x2 = x * x; - return x2 * x2 * x; -} - -fn mx_pow6(x: f32) -> f32 { - let x2 = x * x; - return x2 * x2 * x2; -} - -// Fresnel-Schlick variants — suffixed by return type and parameter set. - -// F0 only (scalar) -fn mx_fresnel_schlick_f32(cosTheta: f32, F0: f32) -> f32 { - let x = clamp(1.0 - cosTheta, 0.0, 1.0); - return F0 + (1.0 - F0) * mx_pow5(x); -} - -// F0 only (vec3) -fn mx_fresnel_schlick_vec3(cosTheta: f32, F0: vec3f) -> vec3f { - let x = clamp(1.0 - cosTheta, 0.0, 1.0); - return F0 + (vec3f(1.0) - F0) * mx_pow5(x); -} - -// F0 + F90 (scalar) -fn mx_fresnel_schlick_f32_f90(cosTheta: f32, F0: f32, F90: f32) -> f32 { - let x = clamp(1.0 - cosTheta, 0.0, 1.0); - return mix(F0, F90, mx_pow5(x)); -} - -// F0 + F90 (vec3) -fn mx_fresnel_schlick_vec3_f90(cosTheta: f32, F0: vec3f, F90: vec3f) -> vec3f { - let x = clamp(1.0 - cosTheta, 0.0, 1.0); - return mix(F0, F90, vec3f(mx_pow5(x))); -} - -// F0 + F90 + exponent (scalar) -fn mx_fresnel_schlick_f32_exp(cosTheta: f32, F0: f32, F90: f32, exponent: f32) -> f32 { - let x = clamp(1.0 - cosTheta, 0.0, 1.0); - return mix(F0, F90, pow(x, exponent)); -} - -// F0 + F90 + exponent (vec3) -fn mx_fresnel_schlick_vec3_exp(cosTheta: f32, F0: vec3f, F90: vec3f, exponent: f32) -> vec3f { - let x = clamp(1.0 - cosTheta, 0.0, 1.0); - return mix(F0, F90, vec3f(pow(x, exponent))); -} - -fn mx_forward_facing_normal(N: vec3f, V: vec3f) -> vec3f { - if (dot(N, V) < 0.0) { - return -N; - } - return N; -} - -// Spherical Fibonacci sampling utilities. -fn mx_golden_ratio_sequence(i: i32) -> f32 { - let GOLDEN_RATIO = 1.618034; - return fract((f32(i) + 1.0) * GOLDEN_RATIO); -} - -fn mx_spherical_fibonacci(i: i32, numSamples: i32) -> vec2f { - return vec2f((f32(i) + 0.5) / f32(numSamples), mx_golden_ratio_sequence(i)); -} - -fn mx_uniform_sample_hemisphere(Xi: vec2f) -> vec3f { - let phi = 2.0 * 3.1415926535897932 * Xi.x; - let cosTheta = 1.0 - Xi.y; - let sinTheta = sqrt(1.0 - mx_square_f32(cosTheta)); - return vec3f(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta); -} - -fn mx_cosine_sample_hemisphere(Xi: vec2f) -> vec3f { - let phi = 2.0 * 3.1415926535897932 * Xi.x; - let cosTheta = sqrt(Xi.y); - let sinTheta = sqrt(1.0 - Xi.y); - return vec3f(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta); -} - -// Constructs an orthonormal basis (T, B, N) from a unit normal N. -// Uses the Duff et al. (JCGT 2017) / Frisvad construction with the sign branch -// at N.z = -1 instead of N.z = 0. The original Naga-transpiled version branched -// at N.z < 0, which placed a tangent-frame discontinuity at the equator of any -// Z-up sphere -- causing visible sawtooth artifacts on glossy/metallic materials. -// Moving the branch to N.z < -0.9999999 pushes the singularity to a tiny region -// near the south pole where it is virtually invisible. -fn mx_orthonormal_basis(N: vec3f) -> mat3x3f { - let s = select(1.0, -1.0, N.z < -0.9999999); - let a = -1.0 / (s + N.z); - let b = N.x * N.y * a; - let X = vec3f(1.0 + s * N.x * N.x * a, s * b, -s * N.x); - let Y = vec3f(b, s + N.y * N.y * a, -N.y); - return mat3x3f(X, Y, N); -} diff --git a/libraries/pbrlib/genwgsl/lib/mx_microfacet_diffuse.wgsl b/libraries/pbrlib/genwgsl/lib/mx_microfacet_diffuse.wgsl deleted file mode 100644 index 113aad9518..0000000000 --- a/libraries/pbrlib/genwgsl/lib/mx_microfacet_diffuse.wgsl +++ /dev/null @@ -1,128 +0,0 @@ -#include "mx_microfacet.wgsl" - -// Qualitative Oren-Nayar diffuse with simplified math: -// https://www1.cs.columbia.edu/CAVE/publications/pdfs/Oren_SIGGRAPH94.pdf -fn mx_oren_nayar_diffuse(NdotV: f32, NdotL: f32, LdotV: f32, roughness: f32) -> f32 { - let s = LdotV - NdotL * NdotV; - var stinv: f32; - if (s > 0.0) { - stinv = s / max(NdotL, NdotV); - } else { - stinv = 0.0; - } - - let sigma2 = mx_square_f32(roughness); - let A = 1.0 - 0.5 * (sigma2 / (sigma2 + 0.33)); - let B = 0.45 * sigma2 / (sigma2 + 0.09); - - return A + B * stinv; -} - -// Missing MaterialX functions - Oren-Nayar compensated diffuse -const FUJII_CONSTANT_1: f32 = 0.5 - 2.0 / (3.0 * 3.1415926535897932); -const FUJII_CONSTANT_2: f32 = 2.0 / 3.0 - 28.0 / (15.0 * 3.1415926535897932); - -fn mx_oren_nayar_fujii_diffuse_dir_albedo(cosTheta: f32, roughness: f32) -> f32 { - let A = 1.0 / (1.0 + FUJII_CONSTANT_1 * roughness); - let B = roughness * A; - let Si = sqrt(max(0.0, 1.0 - mx_square_f32(cosTheta))); - let cosThetaClamped = clamp(cosTheta, -1.0, 1.0); - let G = Si * (acos(cosThetaClamped) - Si * cosTheta) + 2.0 * ((Si / max(cosTheta, 0.0001)) * (1.0 - Si * Si * Si) - Si) / 3.0; - return A + (B * G * (1.0 / 3.1415926535897932)); -} - -fn mx_oren_nayar_fujii_diffuse_avg_albedo(roughness: f32) -> f32 { - let A = 1.0 / (1.0 + FUJII_CONSTANT_1 * roughness); - return A * (1.0 + FUJII_CONSTANT_2 * roughness); -} - -fn mx_oren_nayar_compensated_diffuse(NdotV: f32, NdotL: f32, LdotV: f32, roughness: f32, color: vec3f) -> vec3f { - let s = LdotV - NdotL * NdotV; - var stinv: f32; - if (s > 0.0) { - stinv = s / max(NdotL, NdotV); - } else { - stinv = s; - } - - let A = 1.0 / (1.0 + FUJII_CONSTANT_1 * roughness); - let lobeSingleScatter = color * A * (1.0 + roughness * stinv); - - let dirAlbedoV = mx_oren_nayar_fujii_diffuse_dir_albedo(NdotV, roughness); - let dirAlbedoL = mx_oren_nayar_fujii_diffuse_dir_albedo(NdotL, roughness); - let avgAlbedo = mx_oren_nayar_fujii_diffuse_avg_albedo(roughness); - let colorMultiScatter = mx_square_vec3(color) * avgAlbedo / (vec3(1.0) - color * max(0.0, 1.0 - avgAlbedo)); - let lobeMultiScatter = colorMultiScatter * max(0.00000001, 1.0 - dirAlbedoV) * max(0.00000001, 1.0 - dirAlbedoL) / max(0.00000001, 1.0 - avgAlbedo); - - return lobeSingleScatter + lobeMultiScatter; -} - -fn mx_oren_nayar_compensated_diffuse_dir_albedo(cosTheta: f32, roughness: f32, color: vec3f) -> vec3f { - let dirAlbedo = mx_oren_nayar_fujii_diffuse_dir_albedo(cosTheta, roughness); - let avgAlbedo = mx_oren_nayar_fujii_diffuse_avg_albedo(roughness); - let colorMultiScatter = mx_square_vec3(color) * avgAlbedo / (vec3(1.0) - color * max(0.0, 1.0 - avgAlbedo)); - return mix(colorMultiScatter, color, dirAlbedo); -} - -fn mx_oren_nayar_diffuse_dir_albedo(NdotV: f32, roughness: f32) -> f32 { - let x = NdotV; - let y = roughness; - let r = vec2(1.0, 1.0) + vec2(-0.4297, -0.6076) * y + vec2(-0.7632, -0.4993) * x * y + vec2(1.4385, 2.0315) * mx_square_f32(y); - return clamp(r.x / r.y, 0.0, 1.0); -} - -// Burley diffusion profile for subsurface scattering approximation. -// Ported from genglsl/lib/mx_microfacet_diffuse.glsl. -fn mx_burley_diffusion_profile(dist: f32, shape: vec3f) -> vec3f { - let num1 = exp(-shape * dist); - let num2 = exp(-shape * dist / 3.0); - let denom = max(dist, M_FLOAT_EPS); - return (num1 + num2) / denom; -} - -// Integrate the Burley diffusion profile over a sphere of the given radius. -// Inspired by Eric Penner's presentation in http://advances.realtimerendering.com/s2011/ -fn mx_integrate_burley_diffusion(N: vec3f, L: vec3f, radius: f32, mfp: vec3f) -> vec3f { - let theta = acos(clamp(dot(N, L), -1.0, 1.0)); - - // Estimate the Burley diffusion shape from mean free path. - let shape = vec3f(1.0) / max(mfp, vec3f(0.1)); - - // Integrate the profile over the sphere. - var sumD = vec3f(0.0); - var sumR = vec3f(0.0); - let SAMPLE_COUNT: i32 = 32; - let SAMPLE_WIDTH: f32 = (2.0 * M_PI) / f32(SAMPLE_COUNT); - for (var i: i32 = 0; i < SAMPLE_COUNT; i++) { - let x = -M_PI + (f32(i) + 0.5) * SAMPLE_WIDTH; - let dist = radius * abs(2.0 * sin(x * 0.5)); - let R = mx_burley_diffusion_profile(dist, shape); - sumD += R * max(cos(theta + x), 0.0); - sumR += R; - } - - return sumD / sumR; -} - -fn mx_subsurface_scattering_approx(N: vec3f, L: vec3f, P: vec3f, albedo: vec3f, mfp: vec3f, curvature: f32) -> vec3f { - let radius = 1.0 / max(curvature, 0.01); - return albedo * mx_integrate_burley_diffusion(N, L, radius, mfp) / vec3f(M_PI); -} - -// Disney Burley diffuse BRDF. -// https://media.disneyanimation.com/uploads/production/publication_asset/48/asset/s2012_pbs_disney_brdf_notes_v3.pdf -// Section 5.3 -fn mx_burley_diffuse(NdotV: f32, NdotL: f32, LdotH: f32, roughness: f32) -> f32 { - let F90 = 0.5 + 2.0 * roughness * mx_square_f32(LdotH); - let refL = mx_fresnel_schlick_f32_f90(NdotL, 1.0, F90); - let refV = mx_fresnel_schlick_f32_f90(NdotV, 1.0, F90); - return refL * refV; -} - -// Directional albedo for Burley diffuse. Curve fit by Stephen Hill. -fn mx_burley_diffuse_dir_albedo(NdotV: f32, roughness: f32) -> f32 { - let x = NdotV; - let fit0 = 0.97619 - 0.488095 * mx_pow5(1.0 - x); - let fit1 = 1.55754 + (-2.02221 + (2.56283 - 1.06244 * x) * x) * x; - return mix(fit0, fit1, roughness); -} diff --git a/libraries/pbrlib/genwgsl/lib/mx_microfacet_sheen.wgsl b/libraries/pbrlib/genwgsl/lib/mx_microfacet_sheen.wgsl deleted file mode 100644 index 92c907c2ca..0000000000 --- a/libraries/pbrlib/genwgsl/lib/mx_microfacet_sheen.wgsl +++ /dev/null @@ -1,72 +0,0 @@ -#include "mx_microfacet.wgsl" - -// Imageworks sheen NDF -fn mx_imageworks_sheen_NDF(NdotH: f32, roughness: f32) -> f32 { - let invRoughness = 1.0 / max(roughness, 0.005); - let cos2 = NdotH * NdotH; - let sin2 = 1.0 - cos2; - return (2.0 + invRoughness) * pow(sin2, invRoughness * 0.5) / (2.0 * 3.1415926535897932); -} - -// Missing MaterialX functions - Sheen BRDF and directional albedo -fn mx_imageworks_sheen_brdf(NdotL: f32, NdotV: f32, NdotH: f32, roughness: f32) -> f32 { - let D = mx_imageworks_sheen_NDF(NdotH, roughness); - let F = 1.0; - let G = 1.0; - return D * F * G / (4.0 * (NdotL + NdotV - NdotL * NdotV)); -} - -fn mx_imageworks_sheen_dir_albedo(NdotV: f32, roughness: f32) -> f32 { - let x = NdotV; - let y = roughness; - let r = vec2(13.67300, 1.0) + vec2(-68.78018, 61.57746) * x + vec2(799.08825, 442.78211) * y + vec2(-905.00061, 2597.49308) * x * y + vec2(60.28956, 121.81241) * mx_square_f32(x) + vec2(1086.96473, 3045.55075) * mx_square_f32(y); - return clamp(r.x / r.y, 0.0, 1.0); -} - -fn mx_zeltner_sheen_dir_albedo(x: f32, y: f32) -> f32 { - let s = y * (0.0206607 + 1.58491 * y) / (0.0379424 + y * (1.32227 + y)); - let m = y * (-0.193854 + y * (-1.14885 + y * (1.7932 - 0.95943 * y * y))) / (0.046391 + y); - let o = y * (0.000654023 + (-0.0207818 + 0.119681 * y) * y) / (1.26264 + y * (-1.92021 + y)); - return exp(-0.5 * mx_square_f32((x - m) / s)) / (s * sqrt(2.0 * 3.1415926535897932)) + o; -} - -fn mx_zeltner_sheen_ltc_aInv(x: f32, y: f32) -> f32 { - return (2.58126 * x + 0.813703 * y) * y / (1.0 + 0.310327 * x * x + 2.60994 * x * y); -} - -fn mx_zeltner_sheen_ltc_bInv(x: f32, y: f32) -> f32 { - return sqrt(1.0 - x) * (y - 1.0) * y * y * y / (0.0000254053 + 1.71228 * x - 1.71506 * x * y + 1.34174 * y * y); -} - -fn mx_transpose_mat3(m: mat3x3f) -> mat3x3f { - return mat3x3f( - vec3f(m[0].x, m[1].x, m[2].x), - vec3f(m[0].y, m[1].y, m[2].y), - vec3f(m[0].z, m[1].z, m[2].z) - ); -} - -fn mx_orthonormal_basis_ltc(V: vec3f, N: vec3f, NdotV: f32) -> mat3x3f { - var X = V - N * NdotV; - let lenSqr = dot(X, X); - if (lenSqr > 0.0) { - X = X * (1.0 / sqrt(lenSqr)); - let Y = cross(N, X); - return mat3x3f(vec3f(X.x, Y.x, N.x), vec3f(X.y, Y.y, N.y), vec3f(X.z, Y.z, N.z)); - } - return mx_orthonormal_basis(N); -} - -fn mx_zeltner_sheen_brdf(L: vec3f, V: vec3f, N: vec3f, NdotV: f32, roughness: f32) -> f32 { - let basis = mx_orthonormal_basis_ltc(V, N, NdotV); - let toLTC = mx_transpose_mat3(basis); - let w = (toLTC * L); - - let aInv = mx_zeltner_sheen_ltc_aInv(NdotV, roughness); - let bInv = mx_zeltner_sheen_ltc_bInv(NdotV, roughness); - - let wo = vec3f(aInv * w.x + bInv * w.z, aInv * w.y, w.z); - let lenSqr = dot(wo, wo); - - return max(wo.z, 0.0) * (1.0 / 3.1415926535897932) * mx_square_f32(aInv / lenSqr); -} diff --git a/libraries/pbrlib/genwgsl/lib/mx_microfacet_specular.wgsl b/libraries/pbrlib/genwgsl/lib/mx_microfacet_specular.wgsl deleted file mode 100644 index 9e74a7a2ca..0000000000 --- a/libraries/pbrlib/genwgsl/lib/mx_microfacet_specular.wgsl +++ /dev/null @@ -1,388 +0,0 @@ -#include "mx_microfacet.wgsl" - -fn mx_average_alpha(alpha: vec2f) -> f32 { - return sqrt(alpha.x * alpha.y); -} - -// Convert IOR to F0 (normal-incidence reflectivity) -fn mx_ior_to_f0(ior: f32) -> f32 { - return mx_square_f32((ior - 1f) / (ior + 1f)); -} - -// Convert F0 to IOR (for transmission) -fn mx_f0_to_ior(F0: f32) -> f32 { - let sqrtF0 = sqrt(clamp(F0, 0.01, 0.99)); - return (1.0 + sqrtF0) / (1.0 - sqrtF0); -} - -fn mx_f0_to_ior_vec3(F0: vec3f) -> vec3f { - let sqrtF0 = sqrt(clamp(F0, vec3f(0.01), vec3f(0.99))); - return (vec3f(1.0) + sqrtF0) / (vec3f(1.0) - sqrtF0); -} - -// Fresnel models, mirroring mx_microfacet_specular.glsl. -const FRESNEL_MODEL_DIELECTRIC: i32 = 0; -const FRESNEL_MODEL_CONDUCTOR: i32 = 1; -const FRESNEL_MODEL_SCHLICK: i32 = 2; - -// Number of terms in the thin-film Airy series (generator option hwAiryFresnelIterations; default 2). -const AIRY_FRESNEL_ITERATIONS: i32 = 2; - -fn mx_init_fresnel_dielectric(ior: f32, thinfilm_thickness: f32, thinfilm_ior: f32) -> FresnelData { - var fd: FresnelData; - fd.model = FRESNEL_MODEL_DIELECTRIC; - fd.airy = thinfilm_thickness > 0.0; - let F0_scalar = mx_ior_to_f0(ior); - fd.F0 = vec3f(F0_scalar); - fd.F82 = vec3f(F0_scalar); - fd.F90 = vec3f(1f); - fd.exponent = 5f; - fd.thinfilm_thickness = thinfilm_thickness; - fd.thinfilm_ior = thinfilm_ior; - fd.ior = vec3f(ior); - fd.extinction = vec3f(0.0); - return fd; -} - -// Initialize FresnelData for a conductor material. -// Computes F0 from complex IOR (n, k) using the exact Fresnel equation at normal incidence: -// F0 = ((n - 1)^2 + k^2) / ((n + 1)^2 + k^2) -fn mx_init_fresnel_conductor(ior: vec3f, extinction: vec3f, thinfilm_thickness: f32, thinfilm_ior: f32) -> FresnelData { - var fd: FresnelData; - fd.model = FRESNEL_MODEL_CONDUCTOR; - fd.airy = thinfilm_thickness > 0.0; - fd.ior = ior; - fd.extinction = extinction; - let n_minus_1 = ior - vec3f(1.0); - let n_plus_1 = ior + vec3f(1.0); - let k2 = extinction * extinction; - fd.F0 = (n_minus_1 * n_minus_1 + k2) / (n_plus_1 * n_plus_1 + k2); - fd.F82 = fd.F0; - fd.F90 = vec3f(1.0); - fd.exponent = 5.0; - fd.thinfilm_thickness = thinfilm_thickness; - fd.thinfilm_ior = thinfilm_ior; - return fd; -} - -fn mx_saturate(x: f32) -> f32 { - return clamp(x, 0.0, 1.0); -} - -fn mx_saturate_v3(x: vec3f) -> vec3f { - return clamp(x, vec3f(0.0), vec3f(1.0)); -} - -// Initialize Fresnel data for generalized Schlick model. -fn mx_init_fresnel_schlick( - color0: vec3f, - color82: vec3f, - color90: vec3f, - exponent: f32, - thinfilm_thickness: f32, - thinfilm_ior: f32 -) -> FresnelData { - var fd: FresnelData; - fd.model = FRESNEL_MODEL_SCHLICK; - fd.airy = thinfilm_thickness > 0.0; - fd.F0 = mx_saturate_v3(color0); - fd.F82 = mx_saturate_v3(color82); - fd.F90 = mx_saturate_v3(color90); - fd.exponent = exponent; - fd.thinfilm_thickness = thinfilm_thickness; - fd.thinfilm_ior = thinfilm_ior; - return fd; -} - -// https://renderwonk.com/publications/wp-generalization-adobe/gen-adobe.pdf -fn mx_fresnel_hoffman_schlick(cosTheta: f32, fd: FresnelData) -> vec3f { - let COS_THETA_MAX: f32 = 1.0 / 7.0; - let COS_THETA_FACTOR: f32 = 1.0 / (COS_THETA_MAX * pow(1.0 - COS_THETA_MAX, 6.0)); - - let x = clamp(cosTheta, 0.0, 1.0); - let a = mix(fd.F0, fd.F90, pow(1.0 - COS_THETA_MAX, fd.exponent)) * (vec3f(1.0) - fd.F82) * COS_THETA_FACTOR; - return mix(fd.F0, fd.F90, pow(1.0 - x, fd.exponent)) - a * x * mx_pow6(1.0 - x); -} - -// https://seblagarde.wordpress.com/2013/04/29/memo-on-fresnel-equations/ -fn mx_fresnel_dielectric(cosTheta: f32, ior: f32) -> f32 { - let c = cosTheta; - let g2 = ior * ior + c * c - 1.0; - if (g2 < 0.0) { - // Total internal reflection - return 1.0; - } - let g = sqrt(g2); - return 0.5 * mx_square_f32((g - c) / (g + c)) * - (1.0 + mx_square_f32(((g + c) * c - 1.0) / ((g - c) * c + 1.0))); -} - -// https://seblagarde.wordpress.com/2013/04/29/memo-on-fresnel-equations/ -fn mx_fresnel_dielectric_polarized(cosTheta: f32, ior: f32) -> vec2f { - let cosTheta2 = mx_square_f32(clamp(cosTheta, 0.0, 1.0)); - let sinTheta2 = 1.0 - cosTheta2; - - let t0 = max(ior * ior - sinTheta2, 0.0); - let t1 = t0 + cosTheta2; - let t2 = 2.0 * sqrt(t0) * cosTheta; - let Rs = (t1 - t2) / (t1 + t2); - - let t3 = cosTheta2 * t0 + sinTheta2 * sinTheta2; - let t4 = t2 * sinTheta2; - let Rp = Rs * (t3 - t4) / (t3 + t4); - - return vec2f(Rp, Rs); -} - -// https://seblagarde.wordpress.com/2013/04/29/memo-on-fresnel-equations/ -fn mx_fresnel_conductor_polarized(cosTheta: f32, n: vec3f, k: vec3f, Rp: ptr, Rs: ptr) { - let cosTheta2 = mx_square_f32(clamp(cosTheta, 0.0, 1.0)); - let sinTheta2 = 1.0 - cosTheta2; - let n2 = n * n; - let k2 = k * k; - - let t0 = n2 - k2 - vec3f(sinTheta2); - let a2plusb2 = sqrt(t0 * t0 + 4.0 * n2 * k2); - let t1 = a2plusb2 + vec3f(cosTheta2); - let a = sqrt(max(0.5 * (a2plusb2 + t0), vec3f(0.0))); - let t2 = 2.0 * a * cosTheta; - (*Rs) = (t1 - t2) / (t1 + t2); - - let t3 = cosTheta2 * a2plusb2 + vec3f(sinTheta2 * sinTheta2); - let t4 = t2 * sinTheta2; - (*Rp) = (*Rs) * (t3 - t4) / (t3 + t4); -} - -fn mx_fresnel_conductor(cosTheta: f32, n: vec3f, k: vec3f) -> vec3f { - var Rp: vec3f; - var Rs: vec3f; - mx_fresnel_conductor_polarized(cosTheta, n, k, &Rp, &Rs); - return 0.5 * (Rp + Rs); -} - -// https://belcour.github.io/blog/research/publication/2017/05/01/brdf-thin-film.html -fn mx_fresnel_conductor_phase_polarized(cosTheta: f32, eta1: f32, eta2: vec3f, kappa2: vec3f, phiP: ptr, phiS: ptr) { - let k2 = kappa2 / eta2; - let sinThetaSqr = vec3f(1.0 - cosTheta * cosTheta); - let A = eta2 * eta2 * (vec3f(1.0) - k2 * k2) - eta1 * eta1 * sinThetaSqr; - let B = sqrt(A * A + mx_square_vec3(2.0 * eta2 * eta2 * k2)); - let U = sqrt((A + B) / 2.0); - let V = max(vec3f(0.0), sqrt((B - A) / 2.0)); - - (*phiS) = atan2(2.0 * eta1 * V * cosTheta, U * U + V * V - vec3f(mx_square_f32(eta1 * cosTheta))); - (*phiP) = atan2(2.0 * eta1 * eta2 * eta2 * cosTheta * (2.0 * k2 * U - (vec3f(1.0) - k2 * k2) * V), - mx_square_vec3(eta2 * eta2 * (vec3f(1.0) + k2 * k2) * cosTheta) - eta1 * eta1 * (U * U + V * V)); -} - -// https://belcour.github.io/blog/research/publication/2017/05/01/brdf-thin-film.html -fn mx_eval_sensitivity(opd: f32, shift: vec3f) -> vec3f { - // Use Gaussian fits, given by 3 parameters: val, pos and var. - let phase = 2.0 * M_PI * opd; - let val = vec3f(5.4856e-13, 4.4201e-13, 5.2481e-13); - let pos = vec3f(1.6810e+06, 1.7953e+06, 2.2084e+06); - let vari = vec3f(4.3278e+09, 9.3046e+09, 6.6121e+09); - var xyz = val * sqrt(2.0 * M_PI * vari) * cos(pos * phase + shift) * exp(-vari * phase * phase); - xyz.x = xyz.x + 9.7470e-14 * sqrt(2.0 * M_PI * 4.5282e+09) * cos(2.2399e+06 * phase + shift.x) * exp(-4.5282e+09 * phase * phase); - return xyz / 1.0685e-7; -} - -// A Practical Extension to Microfacet Theory for the Modeling of Varying Iridescence -// https://belcour.github.io/blog/research/publication/2017/05/01/brdf-thin-film.html -fn mx_fresnel_airy(cosTheta: f32, fd: FresnelData) -> vec3f { - // XYZ to CIE 1931 RGB color space (using neutral E illuminant). - let XYZ_TO_RGB = mat3x3f(2.3706743, -0.5138850, 0.0052982, -0.9000405, 1.4253036, -0.0146949, -0.4706338, 0.0885814, 1.0093968); - - // Assume vacuum on the outside. - let eta1 = 1.0; - let eta2 = max(fd.thinfilm_ior, eta1); - let eta3 = select(fd.ior, mx_f0_to_ior_vec3(fd.F0), fd.model == FRESNEL_MODEL_SCHLICK); - let kappa3 = select(fd.extinction, vec3f(0.0), fd.model == FRESNEL_MODEL_SCHLICK); - let cosThetaT = sqrt(1.0 - (1.0 - mx_square_f32(cosTheta)) * mx_square_f32(eta1 / eta2)); - - // First interface. - var R12 = mx_fresnel_dielectric_polarized(cosTheta, eta2 / eta1); - if (cosThetaT <= 0.0) { - // Total internal reflection - R12 = vec2f(1.0); - } - let T121 = vec2f(1.0) - R12; - - // Second interface. - var R23p: vec3f; - var R23s: vec3f; - if (fd.model == FRESNEL_MODEL_SCHLICK) { - let f = mx_fresnel_hoffman_schlick(cosThetaT, fd); - R23p = 0.5 * f; - R23s = 0.5 * f; - } else { - mx_fresnel_conductor_polarized(cosThetaT, eta3 / eta2, kappa3 / eta2, &R23p, &R23s); - } - - // Phase shift. - let cosB = cos(atan(eta2 / eta1)); - let phi21 = vec2f(select(M_PI, 0.0, cosTheta < cosB), M_PI); - var phi23p: vec3f; - var phi23s: vec3f; - if (fd.model == FRESNEL_MODEL_SCHLICK) { - phi23p = vec3f(select(0.0, M_PI, eta3.x < eta2), - select(0.0, M_PI, eta3.y < eta2), - select(0.0, M_PI, eta3.z < eta2)); - phi23s = phi23p; - } else { - mx_fresnel_conductor_phase_polarized(cosThetaT, eta2, eta3, kappa3, &phi23p, &phi23s); - } - let r123p = max(sqrt(R12.x * R23p), vec3f(0.0)); - let r123s = max(sqrt(R12.y * R23s), vec3f(0.0)); - - // Iridescence term. - var I = vec3f(0.0); - var Cm: vec3f; - var Sm: vec3f; - - // Optical path difference. - let distMeters = fd.thinfilm_thickness * 1.0e-9; - let opd = 2.0 * eta2 * cosThetaT * distMeters; - - // Iridescence term using spectral antialiasing for parallel polarization. - let Rsp = (mx_square_f32(T121.x) * R23p) / (vec3f(1.0) - R12.x * R23p); - I += vec3f(R12.x) + Rsp; - Cm = Rsp - vec3f(T121.x); - for (var m: i32 = 1; m <= AIRY_FRESNEL_ITERATIONS; m++) { - Cm *= r123p; - Sm = 2.0 * mx_eval_sensitivity(f32(m) * opd, f32(m) * (phi23p + vec3f(phi21.x))); - I += Cm * Sm; - } - - // Iridescence term using spectral antialiasing for perpendicular polarization. - let Rpp = (mx_square_f32(T121.y) * R23s) / (vec3f(1.0) - R12.y * R23s); - I += vec3f(R12.y) + Rpp; - Cm = Rpp - vec3f(T121.y); - for (var m: i32 = 1; m <= AIRY_FRESNEL_ITERATIONS; m++) { - Cm *= r123s; - Sm = 2.0 * mx_eval_sensitivity(f32(m) * opd, f32(m) * (phi23s + vec3f(phi21.y))); - I += Cm * Sm; - } - - // Average parallel and perpendicular polarization. - I *= 0.5; - - // Convert back to RGB reflectance. - I = clamp(XYZ_TO_RGB * I, vec3f(0.0), vec3f(1.0)); - - return I; -} - -// Fresnel computation dispatching on the Fresnel model, with thin-film (Airy) iridescence. -fn mx_compute_fresnel(cosTheta: f32, fd: FresnelData) -> vec3f { - if (fd.airy) { - return mx_fresnel_airy(cosTheta, fd); - } else if (fd.model == FRESNEL_MODEL_DIELECTRIC) { - return vec3f(mx_fresnel_dielectric(cosTheta, fd.ior.x)); - } else if (fd.model == FRESNEL_MODEL_CONDUCTOR) { - return mx_fresnel_conductor(cosTheta, fd.ior, fd.extinction); - } - return mx_fresnel_hoffman_schlick(cosTheta, fd); -} - -// https://media.disneyanimation.com/uploads/production/publication_asset/48/asset/s2012_pbs_disney_brdf_notes_v3.pdf -// Appendix B.2 Equation 13 -fn mx_ggx_NDF(H: vec3f, alpha: vec2f) -> f32 { - let He = H.xy / alpha; - let denom = dot(He, He) + mx_square_f32(H.z); - return 1.0 / (M_PI * alpha.x * alpha.y * mx_square_f32(denom)); -} - -// Height-correlated Smith G2 for GGX. -fn mx_ggx_smith_G2(NdotL: f32, NdotV: f32, alpha: f32) -> f32 { - let alpha2 = mx_square_f32(alpha); - let lambdaL = sqrt(alpha2 + (1.0 - alpha2) * mx_square_f32(NdotL)); - let lambdaV = sqrt(alpha2 + (1.0 - alpha2) * mx_square_f32(NdotV)); - return (2.0 * NdotL * NdotV) / (lambdaL * NdotV + lambdaV * NdotL); -} - -// Rational quadratic fit to Monte Carlo data for GGX directional albedo. -fn mx_ggx_dir_albedo_analytic(NdotV: f32, alpha: f32, F0: vec3f, F90: vec3f) -> vec3f { - let x = NdotV; - let y = alpha; - let x2 = mx_square_f32(x); - let y2 = mx_square_f32(y); - - // Rational polynomial fit — accumulate terms incrementally to avoid - // deeply nested parenthesized expressions that exceed WGSL parser limits. - var r = vec4f(0.1003, 0.9345, 1.0, 1.0); - r = r + vec4f(-0.6303, -2.323, -1.765, 0.2281) * x; - r = r + vec4f(9.748, 2.229, 8.263, 15.94) * y; - r = r + vec4f(-2.038, -3.748, 11.53, -55.83) * (x * y); - r = r + vec4f(29.34, 1.424, 28.96, 13.08) * x2; - r = r + vec4f(-8.245, -0.7684, -7.507, 41.26) * y2; - r = r + vec4f(-26.44, 1.436, -36.11, 54.9) * (x2 * y); - r = r + vec4f(19.99, 0.2913, 15.86, 300.2) * (x * y2); - r = r + vec4f(-5.448, 0.6286, 33.37, -285.1) * (x2 * y2); - - let AB = clamp(r.xy / r.zw, vec2f(0.0), vec2f(1.0)); - return F0 * AB.x + F90 * AB.y; -} - -fn mx_ggx_dir_albedo(NdotV: f32, alpha: f32, F0: vec3f, F90: vec3f) -> vec3f { - return mx_ggx_dir_albedo_analytic(NdotV, alpha, F0, F90); -} - -// GGX energy compensation: accounts for energy lost to multiple scattering. -fn mx_ggx_energy_compensation(NdotV: f32, alpha: f32, Fss: vec3f) -> vec3f { - let Ess = mx_ggx_dir_albedo(NdotV, alpha, vec3f(1.0), vec3f(1.0)); - return vec3f(1.0) + Fss * (vec3f(1.0) - Ess) / max(Ess, vec3f(M_FLOAT_EPS)); -} - -// https://ggx-research.github.io/publication/2023/06/09/publication-ggx.html -fn mx_ggx_importance_sample_VNDF(Xi: vec2f, V: vec3f, alpha: vec2f) -> vec3f { - let V_h = normalize(vec3f(V.xy * alpha, V.z)); - - let phi = 2.0 * M_PI * Xi.x; - let z = (1.0 - Xi.y) * (1.0 + V_h.z) - V_h.z; - let sinTheta = sqrt(clamp(1.0 - z * z, 0.0, 1.0)); - let x = sinTheta * cos(phi); - let y = sinTheta * sin(phi); - let c = vec3f(x, y, z); - - let H = c + V_h; - return normalize(vec3f(H.xy * alpha, max(H.z, 0.0))); -} - -// https://www.cs.cornell.edu/~srm/publications/EGSR07-btdf.pdf -// Equation 34 -fn mx_ggx_smith_G1(cosTheta: f32, alpha: f32) -> f32 { - let cosTheta2 = mx_square_f32(cosTheta); - let tanTheta2 = (1.0 - cosTheta2) / cosTheta2; - return 2.0 / (1.0 + sqrt(1.0 + mx_square_f32(alpha) * tanTheta2)); -} - -// Compute the refraction of a ray through a solid sphere. -fn mx_refraction_solid_sphere(R_in: vec3f, N: vec3f, ior: f32) -> vec3f { - let R = refract(R_in, N, 1.0 / ior); - let N1 = normalize(R * dot(R, N) - N * 0.5); - return refract(R, N1, ior); -} - -fn mx_latlong_projection(dir: vec3f) -> vec2f { - let latitude = -asin(dir.y) * M_PI_INV + 0.5; - let longitude = atan2(dir.x, -dir.z) * M_PI_INV * 0.5 + 0.5; - return vec2f(longitude, latitude); -} - -// Sample a lat-long environment map with the given direction, transform, and mip level. -// The texture and sampler are provided by the generator ($texSamplerSignature). -fn mx_latlong_map_lookup(dir: vec3f, transform: mat4x4f, lod: f32, envTex: texture_2d, envSampler: sampler) -> vec3f { - let envDir = normalize((transform * vec4f(dir, 0.0)).xyz); - let uv = mx_latlong_projection(envDir); - return textureSampleLevel(envTex, envSampler, uv, lod).rgb; -} - -// Return the mip level with appropriate coverage for a filtered importance sample. -// https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch20.html -// Section 20.4 Equation 13 -fn mx_latlong_compute_lod(dir: vec3f, pdf: f32, maxMipLevel: f32, envSamples: i32) -> f32 { - let MIP_LEVEL_OFFSET = 1.5; - let effectiveMaxMipLevel = maxMipLevel - MIP_LEVEL_OFFSET; - let distortion = sqrt(1.0 - mx_square_f32(dir.y)); - return max(effectiveMaxMipLevel - 0.5 * log2(f32(envSamples) * pdf * distortion), 0.0); -} diff --git a/libraries/pbrlib/genwgsl/lib/mx_transmission_opacity.wgsl b/libraries/pbrlib/genwgsl/lib/mx_transmission_opacity.wgsl deleted file mode 100644 index 235429b624..0000000000 --- a/libraries/pbrlib/genwgsl/lib/mx_transmission_opacity.wgsl +++ /dev/null @@ -1,6 +0,0 @@ -#include "mx_microfacet_specular.wgsl" - -// Opacity-only surface transmission: no refraction, just tint. -fn mx_surface_transmission(N: vec3f, V: vec3f, X: vec3f, alpha: vec2f, distribution: i32, fd: FresnelData, tint: vec3f) -> vec3f { - return tint; -} diff --git a/libraries/pbrlib/genwgsl/lib/mx_transmission_refract.wgsl b/libraries/pbrlib/genwgsl/lib/mx_transmission_refract.wgsl deleted file mode 100644 index 8515cb709e..0000000000 --- a/libraries/pbrlib/genwgsl/lib/mx_transmission_refract.wgsl +++ /dev/null @@ -1,19 +0,0 @@ -#include "mx_microfacet_specular.wgsl" - -fn mx_surface_transmission(N: vec3f, V: vec3f, X: vec3f, alpha: vec2f, distribution: i32, fd: FresnelData, tint_in: vec3f) -> vec3f -{ - // Approximate the appearance of surface transmission as glossy environment map - // refraction, ignoring any scene geometry that might be visible through the surface. - // Setting fd.refraction makes mx_environment_radiance sample along the refracted ray - // (mx_refraction_solid_sphere) with an inverted Fresnel weight, rather than reflecting. - var fd_refract: FresnelData = fd; - fd_refract.refraction = true; - - var tint: vec3f = tint_in; - - if (bool($refractionTwoSided)) { - tint = mx_square_vec3(tint); - } - - return mx_environment_radiance(N, V, X, alpha, distribution, fd_refract) * tint; -} diff --git a/libraries/pbrlib/genwgsl/mx_chiang_hair_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_chiang_hair_bsdf.wgsl deleted file mode 100644 index 2ead9a244a..0000000000 --- a/libraries/pbrlib/genwgsl/mx_chiang_hair_bsdf.wgsl +++ /dev/null @@ -1,288 +0,0 @@ -#include "lib/mx_closure_type.wgsl" -#include "lib/mx_microfacet_specular.wgsl" - -// https://eugenedeon.com/pdfs/egsrhair.pdf -fn mx_deon_hair_absorption_from_melanin( - melanin_concentration: f32, - melanin_redness: f32, - // constants converted to color via exp(-c). the defaults are lin_rec709 colors, they may be - // transformed to scene-linear rendering color space. - eumelanin_color: vec3f, // default: (0.657704, 0.498077, 0.254106) == exp(-(0.419, 0.697, 1.37)) - pheomelanin_color: vec3f, // default: (0.829443, 0.670320, 0.349937) == exp(-(0.187, 0.4, 1.05)) - absorption: ptr) -{ - var melanin: f32 = -log(max(1.0 - melanin_concentration, 0.0001)); - var eumelanin: f32 = melanin * (1.0 - melanin_redness); - var pheomelanin: f32 = melanin * melanin_redness; - *absorption = max( - eumelanin * -log(eumelanin_color) + pheomelanin * -log(pheomelanin_color), - vec3f(0.0) - ); -} - -// https://media.disneyanimation.com/uploads/production/publication_asset/152/asset/eurographics2016Fur_Smaller.pdf -fn mx_chiang_hair_absorption_from_color(color: vec3f, betaN: f32, absorption: ptr) -{ - var b2: f32 = betaN* betaN; - var b4: f32 = b2 * b2; - var b_fac: f32 = - 5.969 - - (0.215 * betaN) + - (2.532 * b2) - - (10.73 * b2 * betaN) + - (5.574 * b4) + - (0.245 * b4 * betaN); - var sigma: vec3f = log(min(max(color, 0.001), vec3f(1.0))) / b_fac; - *absorption = sigma * sigma; -} - -fn mx_chiang_hair_roughness( - longitudinal: f32, - azimuthal: f32, - scale_TT: f32, // empirical roughness scale from Marschner et al. (2003). - scale_TRT: f32, // default: scale_TT = 0.5, scale_TRT = 2.0 - roughness_R: ptr, - roughness_TT: ptr, - roughness_TRT: ptr) -{ - var lr: f32 = clamp(longitudinal, 0.001, 1.0); - var ar: f32 = clamp(azimuthal, 0.001, 1.0); - - // longitudinal variance - var v: f32 = 0.726 * lr + 0.812 * lr * lr + 3.7 * pow(lr, 20.0); - v = v * v; - - var s: f32 = 0.265 * ar + 1.194 * ar * ar + 5.372 * pow(ar, 22.0); - - *roughness_R = vec2f(v, s); - *roughness_TT = vec2f(v * scale_TT * scale_TT, s); - *roughness_TRT = vec2f(v * scale_TRT * scale_TRT, s); -} - -fn mx_hair_transform_sin_cos(x: f32) -> f32 -{ - return sqrt(max(1.0 - x * x, 0.0)); -} - -fn mx_hair_I0(x: f32) -> f32 -{ - var v: f32 = 1.0; - var n: f32 = 1.0; - var d: f32 = 1.0; - var f: f32 = 1.0; - var x2: f32 = x * x; - for (var i: i32 = 0; i < 9 ; i++) - { - d *= 4.0 * (f * f); - n *= x2; - v += n / d; - f += 1.0; - } - return v; -} - -fn mx_hair_log_I0(x: f32) -> f32 -{ - if (x > 12.0) - return x + 0.5 * (-log(2.0 * M_PI) + log(1.0 / x) + 1.0 / (8.0 * x)); - else - return log(mx_hair_I0(x)); -} - -fn mx_hair_logistic(x: f32, s: f32) -> f32 -{ - var x_val: f32 = x; - if (x_val > 0.0) - x_val = -x_val; - var f: f32 = exp(x_val / s); - return f / (s * (1.0 + f) * (1.0 + f)); -} - -fn mx_hair_logistic_cdf(x: f32, s: f32) -> f32 -{ - return 1.0 / (1.0 + exp(-x / s)); -} - -fn mx_hair_trimmed_logistic(x: f32, s: f32, a: f32, b: f32) -> f32 -{ - // the constant can be found in Chiang et al. (2016) Appendix A, eq. (12) - var s_val: f32 = s * 0.626657; // sqrt(M_PI/8) - return mx_hair_logistic(x, s_val) / (mx_hair_logistic_cdf(b, s_val) - mx_hair_logistic_cdf(a, s_val)); -} - -fn mx_hair_phi(p: i32, gammaO: f32, gammaT: f32) -> f32 -{ - var fP: f32 = f32(p); - return 2.0 * fP * gammaT - 2.0 * gammaO + fP * M_PI; -} - -fn mx_hair_longitudinal_scattering( // Mp - sinThetaI: f32, - cosThetaI: f32, - sinThetaO: f32, - cosThetaO: f32, - v: f32) -> f32 -{ - var inv_v: f32 = 1.0 / v; - var a: f32 = cosThetaO * cosThetaI * inv_v; - var b: f32 = sinThetaO * sinThetaI * inv_v; - if (v < 0.1) - return exp(mx_hair_log_I0(a) - b - inv_v + 0.6931 + log(0.5 * inv_v)); - else - return ((exp(-b) * mx_hair_I0(a)) / (2.0 * v * sinh(inv_v))); -} - -fn mx_hair_azimuthal_scattering( // Np - phi: f32, - p: i32, - s: f32, - gammaO: f32, - gammaT: f32) -> f32 -{ - if (p >= 3) - return 0.5 / M_PI; - - var dphi: f32 = phi - mx_hair_phi(p, gammaO, gammaT); - if (isinf(dphi)) - return 0.5 / M_PI; - - while (dphi > M_PI) dphi -= (2.0 * M_PI); - while (dphi < (-M_PI)) dphi += (2.0 * M_PI); - - return mx_hair_trimmed_logistic(dphi, s, -M_PI, M_PI); -} - -fn mx_hair_alpha_angles( - alpha: f32, - sinThetaI: f32, - cosThetaI: f32, - angles: ptr>) -{ - // 0:R, 1:TT, 2:TRT, 3:TRRT+ - for (var i: i32 = 0; i <= 3; i++) - { - if (alpha == 0.0 || i == 3) - (*angles)[i] = vec2f(sinThetaI, cosThetaI); - else - { - var m: f32 = 2.0 - f32(i) * 3.0; - var sa: f32 = sin(m * alpha); - var ca: f32 = cos(m * alpha); - (*angles)[i].x = sinThetaI * ca + cosThetaI * sa; - (*angles)[i].y = cosThetaI * ca - sinThetaI * sa; - } - } -} - -fn mx_hair_attenuation(f: f32, T: vec3f, Ap: ptr>) // Ap -{ - // 0:R, 1:TT, 2:TRT, 3:TRRT+ - (*Ap)[0] = vec3f(f); - (*Ap)[1] = (1.0 - f) * (1.0 - f) * T; - (*Ap)[2] = (*Ap)[1] * T * f; - (*Ap)[3] = (*Ap)[2] * T * f / (vec3f(1.0) - T * f); -} - -fn mx_chiang_hair_bsdf(closureData: ClosureData, tint_R: vec3f, tint_TT: vec3f, tint_TRT: vec3f, ior: f32, - roughness_R: vec2f, roughness_TT: vec2f, roughness_TRT: vec2f, cuticle_angle: f32, - absorption_coefficient: vec3f, N: vec3f, X: vec3f, bsdf: ptr) -{ - var V: vec3f = closureData.V; - var L: vec3f = closureData.L; - var N_: vec3f = mx_forward_facing_normal(N, V); // mutable copy (WGSL params are immutable) - var X_: vec3f = X; - - (*bsdf).throughput = vec3f(0.0); - - if (closureData.closureType == CLOSURE_TYPE_REFLECTION) - { - X_ = normalize(X_ - dot(X_, N_) * N_); - var Y: vec3f = cross(N_, X_); - - var sinThetaO: f32 = dot(V, X_); - var sinThetaI: f32 = dot(L, X_); - var cosThetaO: f32 = mx_hair_transform_sin_cos(sinThetaO); - var cosThetaI: f32 = mx_hair_transform_sin_cos(sinThetaI); - - var y1: f32 = dot(L, N_); - var x1: f32 = dot(L, Y); - var y2: f32 = dot(V, N_); - var x2: f32 = dot(V, Y); - var phi: f32 = atan2(y1 * x2 - y2 * x1, x1 * x2 + y1 * y2); - - var k1_p: vec3f = normalize(V - X_ * dot(V, X_)); - var cosGammaO: f32 = dot(N_, k1_p); - var sinGammaO: f32 = mx_hair_transform_sin_cos(cosGammaO); - if (dot(k1_p, Y) > 0.0) - sinGammaO = -sinGammaO; - var gammaO: f32 = asin(sinGammaO); - - var sinThetaT: f32 = sinThetaO / ior; - var cosThetaT: f32 = mx_hair_transform_sin_cos(sinThetaT); - var etaP: f32 = sqrt(max(ior * ior - sinThetaO * sinThetaO, 0.0)) / max(cosThetaO, M_FLOAT_EPS); - var sinGammaT: f32 = max(min(sinGammaO / etaP, 1.0), -1.0); - var cosGammaT: f32 = sqrt(1.0 - sinGammaT * sinGammaT); - var gammaT: f32 = asin(sinGammaT); - - // attenuation - var Ap: array; - var fresnel: f32 = mx_fresnel_dielectric(cosThetaO * cosGammaO, ior); - var T: vec3f = exp(-absorption_coefficient * (2.0 * cosGammaT / cosThetaT)); - mx_hair_attenuation(fresnel, T, &(Ap)); - - // parameters for each lobe - var angles: array; - var alpha: f32 = cuticle_angle * M_PI - (M_PI / 2.0); // remap [0, 1] to [-PI/2, PI/2] - mx_hair_alpha_angles(alpha, sinThetaI, cosThetaI, &(angles)); - - var tint: array; - tint[0] = tint_R; - tint[1] = tint_TT; - tint[2] = tint_TRT; - tint[3] = tint_TRT; - - var roughness_R_clamped: vec2f = clamp(roughness_R, 0.001, 1.0); - var roughness_TT_clamped: vec2f = clamp(roughness_TT, 0.001, 1.0); - var roughness_TRT_clamped: vec2f = clamp(roughness_TRT, 0.001, 1.0); - - var vs: array; - vs[0] = roughness_R_clamped; - vs[1] = roughness_TT_clamped; - vs[2] = roughness_TRT_clamped; - vs[3] = roughness_TRT_clamped; - - // R, TT, TRT, TRRT+ - var F: vec3f = vec3f(0.0); - for (var i: i32 = 0; i <= 3; i++) - { - tint[i] = max(tint[i], vec3f(0.0)); - var Mp: f32 = mx_hair_longitudinal_scattering(angles[i].x, angles[i].y, sinThetaO, cosThetaO, vs[i].x); - var Np: f32 = select(mx_hair_azimuthal_scattering(phi, i, vs[i].y, gammaO, gammaT), (1.0 / 2.0 * M_PI), (i == 3)); - F += Mp * Np * tint[i] * Ap[i]; - } - - (*bsdf).response = F * closureData.occlusion * (1.0 / M_PI); - } - else if (closureData.closureType == CLOSURE_TYPE_INDIRECT) - { - // This indirect term is a *very* rough approximation. - - var NdotV: f32 = clamp(dot(N_, V), M_FLOAT_EPS, 1.0); - var fd: FresnelData = mx_init_fresnel_dielectric(ior, 0.0, 1.0); - var F: vec3f = mx_compute_fresnel(NdotV, fd); - - var roughness: vec2f = (roughness_R + roughness_TT + roughness_TRT) / vec2f(3.0); // ? - var safeAlpha: vec2f = clamp(roughness, vec2f(M_FLOAT_EPS), vec2f(1.0)); - var avgAlpha: f32 = mx_average_alpha(safeAlpha); - - // Use GGX to match the behavior of mx_environment_radiance. - var F0: f32 = mx_ior_to_f0(ior); - var comp: vec3f = mx_ggx_energy_compensation(NdotV, avgAlpha, F); - var dirAlbedo: vec3f = mx_ggx_dir_albedo(NdotV, avgAlpha, F0, 1.0) * comp; - - var Li: vec3f = mx_environment_radiance(N_, V, X_, safeAlpha, 0, fd); - var tint: vec3f = (tint_R + tint_TT + tint_TRT) / vec3f(3.0); // ? - - (*bsdf).response = Li * comp * tint; - } -} diff --git a/libraries/pbrlib/genwgsl/mx_conductor_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_conductor_bsdf.wgsl deleted file mode 100644 index af5f2deec0..0000000000 --- a/libraries/pbrlib/genwgsl/mx_conductor_bsdf.wgsl +++ /dev/null @@ -1,54 +0,0 @@ -#include "lib/mx_closure_type.wgsl" -#include "lib/mx_microfacet_specular.wgsl" - -fn mx_conductor_bsdf(closureData: ClosureData, weight: f32, ior_n: vec3f, ior_k: vec3f, roughness: vec2f, retroreflective: bool, thinfilm_thickness: f32, thinfilm_ior: f32, N: vec3f, X: vec3f, distribution: i32, bsdf: ptr) -{ - (*bsdf).throughput = vec3f(0.0); - - if (weight < M_FLOAT_EPS) - { - return; - } - - var V: vec3f = closureData.V; - var L: vec3f = closureData.L; - var N_: vec3f = mx_forward_facing_normal(N, V); // mutable copy (WGSL params are immutable) - var X_: vec3f = X; - - var NdotV: f32 = clamp(dot(N_, V), M_FLOAT_EPS, 1.0); - - var fd: FresnelData = mx_init_fresnel_conductor(ior_n, ior_k, thinfilm_thickness, thinfilm_ior); - - var safeAlpha: vec2f = clamp(roughness, vec2f(M_FLOAT_EPS), vec2f(1.0)); - var avgAlpha: f32 = mx_average_alpha(safeAlpha); - - if (closureData.closureType == CLOSURE_TYPE_REFLECTION) - { - X_ = normalize(X_ - dot(X_, N_) * N_); - var Y: vec3f = cross(N_, X_); - var H: vec3f = normalize(L + V); - - var NdotL: f32 = clamp(dot(N_, L), M_FLOAT_EPS, 1.0); - var VdotH: f32 = clamp(dot(V, H), M_FLOAT_EPS, 1.0); - - var Ht: vec3f = vec3f(dot(H, X_), dot(H, Y), dot(H, N_)); - - var F: vec3f = mx_compute_fresnel(VdotH, fd); - var D: f32 = mx_ggx_NDF(Ht, safeAlpha); - var G: f32 = mx_ggx_smith_G2(NdotL, NdotV, avgAlpha); - - var comp: vec3f = mx_ggx_energy_compensation(NdotV, avgAlpha, F); - - // Note: NdotL is cancelled out - (*bsdf).response = D * F * G * comp * closureData.occlusion * weight / (4.0 * NdotV); - } - else if (closureData.closureType == CLOSURE_TYPE_INDIRECT) - { - var F: vec3f = mx_compute_fresnel(NdotV, fd); - var comp: vec3f = mx_ggx_energy_compensation(NdotV, avgAlpha, F); - // FIS bakes the Fresnel term into Li, so weight only by the energy-compensation term - // (matching mx_conductor_bsdf.glsl); multiplying by dirAlbedo would double-count Fresnel. - var Li: vec3f = mx_environment_radiance(N_, V, X_, safeAlpha, distribution, fd); - (*bsdf).response = Li * comp * weight; - } -} diff --git a/libraries/pbrlib/genwgsl/mx_dielectric_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_dielectric_bsdf.wgsl deleted file mode 100644 index cfb49af486..0000000000 --- a/libraries/pbrlib/genwgsl/mx_dielectric_bsdf.wgsl +++ /dev/null @@ -1,61 +0,0 @@ -#include "lib/mx_closure_type.wgsl" -#include "lib/mx_microfacet_specular.wgsl" - -fn mx_dielectric_bsdf(closureData: ClosureData, weight: f32, tint: vec3f, ior: f32, roughness: vec2f, retroreflective: bool, thinfilm_thickness: f32, thinfilm_ior: f32, N_in: vec3f, X_in: vec3f, distribution: i32, scatter_mode: i32, bsdf: ptr) { - if (weight < M_FLOAT_EPS) { return; } - if (closureData.closureType != CLOSURE_TYPE_TRANSMISSION && scatter_mode == 1) { return; } - - let V = closureData.V; - let L = closureData.L; - let N = mx_forward_facing_normal(N_in, V); - let NdotV = clamp(dot(N, V), 1e-8, 1.0); - - let fd = mx_init_fresnel_dielectric(ior, thinfilm_thickness, thinfilm_ior); - let F0 = mx_ior_to_f0(ior); - - let safeAlpha = clamp(roughness, vec2f(1e-8), vec2f(1.0)); - let avgAlpha = mx_average_alpha(safeAlpha); - let safeTint = max(tint, vec3f(0.0)); - - if (closureData.closureType == CLOSURE_TYPE_REFLECTION) { - var X = normalize(X_in - dot(X_in, N) * N); - let Y = cross(N, X); - let H = normalize(L + V); - - let NdotL = clamp(dot(N, L), 1e-8, 1.0); - let VdotH = clamp(dot(V, H), 1e-8, 1.0); - - let Ht = vec3f(dot(H, X), dot(H, Y), dot(H, N)); - - let F = mx_compute_fresnel(VdotH, fd); - let D = mx_ggx_NDF(Ht, safeAlpha); - let G = mx_ggx_smith_G2(NdotL, NdotV, avgAlpha); - - let comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F); - let dirAlbedo = mx_ggx_dir_albedo(NdotV, avgAlpha, vec3f(F0), vec3f(1.0)) * comp; - (*bsdf).throughput = vec3f(1.0) - dirAlbedo * weight; - - (*bsdf).response = D * F * G * comp * safeTint * closureData.occlusion * weight / (4.0 * NdotV); - } else if (closureData.closureType == CLOSURE_TYPE_TRANSMISSION) { - let F = mx_compute_fresnel(NdotV, fd); - - let comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F); - let dirAlbedo = mx_ggx_dir_albedo(NdotV, avgAlpha, vec3f(F0), vec3f(1.0)) * comp; - (*bsdf).throughput = vec3f(1.0) - dirAlbedo * weight; - - if (scatter_mode != 0) { - (*bsdf).response = mx_surface_transmission(N, V, X_in, safeAlpha, distribution, fd, safeTint) * weight; - } - } else if (closureData.closureType == CLOSURE_TYPE_INDIRECT) { - let F = mx_compute_fresnel(NdotV, fd); - - let comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F); - let dirAlbedo = mx_ggx_dir_albedo(NdotV, avgAlpha, vec3f(F0), vec3f(1.0)) * comp; - (*bsdf).throughput = vec3f(1.0) - dirAlbedo * weight; - - // FIS bakes the Fresnel term into Li, so weight only by the energy-compensation term - // (matching mx_dielectric_bsdf.glsl); dirAlbedo would double-count Fresnel. - let Li = mx_environment_radiance(N, V, X_in, safeAlpha, distribution, fd); - (*bsdf).response = Li * safeTint * comp * weight; - } -} diff --git a/libraries/pbrlib/genwgsl/mx_generalized_schlick_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_generalized_schlick_bsdf.wgsl deleted file mode 100644 index 30b0e8e0b9..0000000000 --- a/libraries/pbrlib/genwgsl/mx_generalized_schlick_bsdf.wgsl +++ /dev/null @@ -1,87 +0,0 @@ -#include "lib/mx_closure_type.wgsl" -#include "lib/mx_microfacet_specular.wgsl" - -fn mx_generalized_schlick_bsdf( - closureData: ClosureData, - weight: f32, - color0: vec3f, - color82: vec3f, - color90: vec3f, - exponent: f32, - roughness: vec2f, - retroreflective: bool, - thinfilm_thickness: f32, - thinfilm_ior: f32, - N: vec3f, - X: vec3f, - distribution: i32, - scatter_mode: i32, - bsdf: ptr -) { - if (weight < M_FLOAT_EPS) { - return; - } - - if (closureData.closureType != CLOSURE_TYPE_TRANSMISSION && scatter_mode == 1) { - return; - } - - let V = closureData.V; - let L = closureData.L; - - var N_facing = mx_forward_facing_normal(N, V); - let NdotV = mx_saturate(dot(N_facing, V)); - - let safeColor0 = max(color0, vec3f(0.0)); - let safeColor82 = max(color82, vec3f(0.0)); - let safeColor90 = max(color90, vec3f(0.0)); - let fd = mx_init_fresnel_schlick(safeColor0, safeColor82, safeColor90, exponent, thinfilm_thickness, thinfilm_ior); - - let safeAlpha = clamp(roughness, vec2f(M_FLOAT_EPS), vec2f(1.0)); - let avgAlpha = mx_average_alpha(safeAlpha); - - if (closureData.closureType == CLOSURE_TYPE_REFLECTION) { - var X_ortho = normalize(X - dot(X, N_facing) * N_facing); - let Y = cross(N_facing, X_ortho); - let H = normalize(L + V); - - let NdotL = mx_saturate(dot(N_facing, L)); - let VdotH = mx_saturate(dot(V, H)); - - let Ht = vec3f(dot(H, X_ortho), dot(H, Y), dot(H, N_facing)); - - let F = mx_compute_fresnel(VdotH, fd); - let D = mx_ggx_NDF(Ht, safeAlpha); - let G = mx_ggx_smith_G2(NdotL, NdotV, avgAlpha); - - let comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F); - let dirAlbedo = mx_ggx_dir_albedo(NdotV, avgAlpha, safeColor0, safeColor90) * comp; - let avgDirAlbedo = dot(dirAlbedo, vec3f(1.0 / 3.0)); - (*bsdf).throughput = vec3f(1.0 - avgDirAlbedo * weight); - - (*bsdf).response = D * F * G * comp * closureData.occlusion * weight / (4.0 * NdotV); - } else if (closureData.closureType == CLOSURE_TYPE_TRANSMISSION) { - let F = mx_compute_fresnel(NdotV, fd); - - let comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F); - let dirAlbedo = mx_ggx_dir_albedo(NdotV, avgAlpha, safeColor0, safeColor90) * comp; - let avgDirAlbedo = dot(dirAlbedo, vec3f(1.0 / 3.0)); - (*bsdf).throughput = vec3f(1.0 - avgDirAlbedo * weight); - - if (scatter_mode != 0) { - (*bsdf).response = mx_surface_transmission(N_facing, V, X, safeAlpha, distribution, fd, vec3f(1.0)) * weight; - } - } else if (closureData.closureType == CLOSURE_TYPE_INDIRECT) { - let F = mx_compute_fresnel(NdotV, fd); - - let comp = mx_ggx_energy_compensation(NdotV, avgAlpha, F); - let dirAlbedo = mx_ggx_dir_albedo(NdotV, avgAlpha, safeColor0, safeColor90) * comp; - let avgDirAlbedo = dot(dirAlbedo, vec3f(1.0 / 3.0)); - (*bsdf).throughput = vec3f(1.0 - avgDirAlbedo * weight); - - // FIS bakes the Fresnel term into Li, so weight only by the energy-compensation term - // (matching mx_generalized_schlick_bsdf.glsl); dirAlbedo would double-count Fresnel. - let Li = mx_environment_radiance(N_facing, V, X, safeAlpha, distribution, fd); - (*bsdf).response = Li * comp * weight; - } -} diff --git a/libraries/pbrlib/genwgsl/mx_subsurface_bsdf.wgsl b/libraries/pbrlib/genwgsl/mx_subsurface_bsdf.wgsl deleted file mode 100644 index dcaeffd4e4..0000000000 --- a/libraries/pbrlib/genwgsl/mx_subsurface_bsdf.wgsl +++ /dev/null @@ -1,39 +0,0 @@ -#include "lib/mx_closure_type.wgsl" -#include "lib/mx_microfacet_diffuse.wgsl" - -// NOTE: The GLSL version computes curvature via fwidth(N)/fwidth(P), but WGSL -// forbids fwidth inside non-uniform control flow (e.g. the light loop that -// calls this function). We use a flat curvature approximation (0.0) for now. -// TODO: wire proper curvature when the renderer can pre-compute it per-vertex. - -fn mx_subsurface_bsdf(closureData: ClosureData, weight: f32, color: vec3f, radius: vec3f, anisotropy: f32, N: vec3f, bsdf: ptr) -{ - (*bsdf).throughput = vec3f(0.0); - - if (weight < M_FLOAT_EPS) - { - return; - } - - var V: vec3f = closureData.V; - var L: vec3f = closureData.L; - var P: vec3f = closureData.P; - var occlusion: f32 = closureData.occlusion; - var N_: vec3f = mx_forward_facing_normal(N, V); - - if (closureData.closureType == CLOSURE_TYPE_REFLECTION) - { - // Flat curvature approximation — see note above. - var curvature: f32 = 0.0; - var sss: vec3f = mx_subsurface_scattering_approx(N_, L, P, color, radius, curvature); - var NdotL: f32 = clamp(dot(N_, L), M_FLOAT_EPS, 1.0); - var visibleOcclusion: f32 = 1.0 - NdotL * (1.0 - occlusion); - (*bsdf).response = sss * visibleOcclusion * weight; - } - else if (closureData.closureType == CLOSURE_TYPE_INDIRECT) - { - // For now, we render indirect subsurface as simple indirect diffuse. - var Li: vec3f = mx_environment_irradiance(N_); - (*bsdf).response = Li * color * weight; - } -} From 0fe2cc0fcf6bfc1ede637b3649a4ee01a2a1fd55 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Tue, 14 Jul 2026 08:39:51 -0700 Subject: [PATCH 11/19] Update `glsl_to_wgsl.py` to generate the **entire** `genwgsl` node library --- .gitignore | 6 +- .../DeveloperGuide/WGSLShaderGeneration.md | 31 +- .../DeveloperGuide/WGSLTranspilerFixes.md | 83 ++ .../pbrlib/genglsl/lib/mx_microfacet.glsl | 4 +- source/MaterialXGenWgsl/README.md | 16 +- .../MaterialXGenWgsl/WgslShaderGenerator.cpp | 9 +- source/MaterialXGenWgsl/tools/README.md | 65 +- source/MaterialXGenWgsl/tools/glsl_to_wgsl.py | 1095 +++++++++++++++-- 8 files changed, 1185 insertions(+), 124 deletions(-) create mode 100644 documents/DeveloperGuide/WGSLTranspilerFixes.md diff --git a/.gitignore b/.gitignore index 7444050b1c..5722236269 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ build dist .DS_Store -CMakeUserPresets.json \ No newline at end of file +CMakeUserPresets.json + +/libraries/stdlib/genwgsl/*.wgsl +/libraries/pbrlib/genwgsl/*.wgsl +/libraries/pbrlib/genwgsl/lib/*.wgsl \ No newline at end of file diff --git a/documents/DeveloperGuide/WGSLShaderGeneration.md b/documents/DeveloperGuide/WGSLShaderGeneration.md index 56df0aee2b..015eb27d63 100644 --- a/documents/DeveloperGuide/WGSLShaderGeneration.md +++ b/documents/DeveloperGuide/WGSLShaderGeneration.md @@ -11,11 +11,11 @@ The `genwgsl` uses a **hybrid node library**: | Library content | Source | Committed to git? | | --- | --- | --- | | Most node `.wgsl` files | Transpiled from `genglsl` by `glsl_to_wgsl.py` | No — derived artifact | -| Core `lib/` math and closure helpers | Hand-maintained | Yes | +| Core `lib/` math and closure helpers | Transpiled from `genglsl/lib/` by `glsl_to_wgsl.py` | No — derived artifact | | Texture, image, and light nodes | Hand-maintained | Yes | -| A small set of BSDF nodes (`EXPECTED_FALLBACK`) | Hand-maintained | Yes | +| `mx_chiang_hair_bsdf` (`EXPECTED_FALLBACK`) | Hand-maintained | Yes | -The GLSL node library (`genglsl`) is the **single source of truth** for generated nodes. CI runs the transpiler on every job that generates WGSL to prevent drift between GLSL and WGSL libraries. +The GLSL node and lib libraries (`genglsl`, `genglsl/lib/`) are the **single source of truth** for generated WGSL. CI runs the transpiler on every job that generates WGSL to prevent drift between GLSL and WGSL libraries. The library lives under `libraries/{stdlib,pbrlib,lights}/genwgsl/`, with the target defined in `libraries/targets/genwgsl.mtlx`. @@ -69,9 +69,10 @@ A non-zero exit code means an *unexpected* node failed (a regression). Known fal **Pipeline (per node function):** -1. **Pre-process** — wrap the fragment in a complete GLSL shader naga can parse -2. **Transpile** — `naga --input-kind glsl --shader-stage frag` -3. **Post-process** — clean up naga output and remap helper calls to genwgsl names +1. **Lib helpers** — transpile `genglsl/lib/*.glsl` first (topological include order, `LIB_PREAMBLE`, overload renaming via inverted `CALL_MAP`) +2. **Pre-process** — wrap the node fragment in a complete GLSL shader naga can parse +3. **Transpile** — `naga --input-kind glsl --shader-stage frag` +4. **Post-process** — clean up naga output and remap helper calls to genwgsl names #### Input: an incomplete node fragment @@ -103,7 +104,7 @@ naga's output is correct but verbose (SSA-style parameter shadows, `vec3` s - Normalizes types (`vec3` → `vec3f`, `2f` → `2.0`) - Remaps overloaded GLSL helper calls via `CALL_MAP` to type-suffixed genwgsl names -WGSL has no function overloading, so the hand-written genwgsl `lib/` gives each GLSL overload a distinct name. For the noise example above, `mx_perlin_noise_float(position)` with a `vec3` argument is rewritten to `mx_perlin_noise_float_3d(position)`. +WGSL has no function overloading, so the genwgsl `lib/` gives each GLSL overload a distinct name. For the noise example above, `mx_perlin_noise_float(position)` with a `vec3` argument is rewritten to `mx_perlin_noise_float_3d(position)`. Similarly, GLSL `mx_square` overloads map to `mx_square_f32`, `mx_square_vec2`, or `mx_square_vec3` depending on the argument type. @@ -133,12 +134,16 @@ Generated files carry a `// Generated from … do not edit` banner. | Category | Example | Reason | | --- | --- | --- | -| Core `lib/` files | `lib/mx_math.glsl` | Hand-maintained; tool never touches `lib/` | | Texture / image nodes | `mx_image_color3` | naga's GLSL frontend has no sampler support — auto-skipped by filename pattern (`image`, `hextiled`) | | Light shaders | `mx_point_light` | Use dynamically generated `LightData` — auto-skipped (`_light$` pattern) | -| Adapted BSDF helpers | `mx_dielectric_bsdf`, `mx_conductor_bsdf` | Call lib helpers whose genwgsl signatures diverge from GLSL (e.g. `mx_ggx_energy_compensation` takes a precomputed `vec3f` instead of `FresnelData`) — listed in `EXPECTED_FALLBACK`, kept hand-written | +| Chiang hair BSDF | `mx_chiang_hair_bsdf` | naga limitation on hair scattering helpers — listed in `EXPECTED_FALLBACK`, kept hand-written | | Unmapped overloads | Any call not in `CALL_MAP` | Node stays hand-written; logged as unsupported | +**Specular environment IBL:** `WgslShaderGenerator` supports FIS, prefilter, and none methods +(`mx_environment_fis.wgsl`, `mx_environment_prefilter.wgsl`, `mx_environment_none.wgsl`). The +prefilter environment bake pass (`hwWriteEnvPrefilter` / `mx_generate_prefilter_env`) remains +GLSL-only today. + **Texture node** (auto-skipped — uses samplers naga cannot parse): ```glsl @@ -151,7 +156,10 @@ void mx_image_color3($texSamplerSignature, int layer, vec3 defaultval, vec2 texc } ``` -The result is a **reduced library**: most nodes are generated from genglsl; texture, light, and a small set of adapted BSDF nodes remain hand-written. The tool exits non-zero only when a node *outside* `EXPECTED_FALLBACK` fails unexpectedly (a regression). If a fallback node starts transpiling cleanly, the tool prints a warning so it can be removed from `EXPECTED_FALLBACK`. +The result is a **reduced library**: most nodes and all 22 `lib/` helpers are generated from +genglsl; texture, light, and `mx_chiang_hair_bsdf` remain hand-written. The tool exits non-zero +only when a file *outside* `EXPECTED_FALLBACK` fails unexpectedly (a regression). If a fallback node +starts transpiling cleanly, the tool prints a warning so it can be removed from `EXPECTED_FALLBACK`. For full transpiler internals see [`source/MaterialXGenWgsl/tools/README.md`](../../source/MaterialXGenWgsl/tools/README.md). @@ -237,10 +245,11 @@ This adds the `MaterialXGenWgslLibrary` target, which surfaces transpile and nag ## Release Artifacts -Generated WGSL node files are not committed to git, but they are included in release archives. The release workflow runs `glsl_to_wgsl.py` before packaging, so `libraries/*/genwgsl/*.wgsl` files ship alongside the hand-written WGSL helpers. +Generated WGSL node and `lib/` files are not committed to git, but they are included in release archives. The release workflow runs `glsl_to_wgsl.py` before packaging, so `libraries/*/genwgsl/**/*.wgsl` files ship in the archive. ## Related Documentation +- [WGSL Transpiler Fixes](WGSLTranspilerFixes.md) — generation policy and the naga-reconciliation fixes for full-library generation - [Shader Generation](ShaderGeneration.md) — general shader generation framework - [`source/MaterialXGenWgsl/README.md`](../../source/MaterialXGenWgsl/README.md) — back-end layout and design - [`source/MaterialXGenWgsl/tools/README.md`](../../source/MaterialXGenWgsl/tools/README.md) — transpiler internals, `CALL_MAP`, and `EXPECTED_FALLBACK` diff --git a/documents/DeveloperGuide/WGSLTranspilerFixes.md b/documents/DeveloperGuide/WGSLTranspilerFixes.md new file mode 100644 index 0000000000..eb44a44964 --- /dev/null +++ b/documents/DeveloperGuide/WGSLTranspilerFixes.md @@ -0,0 +1,83 @@ +# WGSL Transpiler: Full-Library Generation Fixes + +This note summarizes the changes that make `glsl_to_wgsl.py` generate the **entire** `genwgsl` node +and lib library from `genglsl` (previously many files were hand-written). It covers (1) the +generation policy — what is generated vs. kept hand-written — and (2) the transpiler correctness +fixes needed to reconcile naga's WGSL output with the genwgsl library conventions. + +For the general workflow and tooling see [WGSLShaderGeneration.md](WGSLShaderGeneration.md) and +[`source/MaterialXGenWgsl/tools/README.md`](../../source/MaterialXGenWgsl/tools/README.md). + +## Generation policy + +| Scope | Handling | Mechanism | +| --- | --- | --- | +| `pbrlib/genwgsl/` nodes + most `pbrlib/genwgsl/lib` helpers | **Generated** | transpiled from `genglsl` | +| `stdlib/genwgsl/lib` helpers | **Hand-written** (project choice) | `HANDWRITTEN_LIB_DIRS` | +| Environment / shadow / bake lib helpers | **Hand-written** | `LIB_KEEP_HANDWRITTEN` | +| Image / hextile / light nodes | **Hand-written** (auto-skipped) | `SKIP_PATTERNS` | +| `mx_chiang_hair_bsdf` | **Hand-written** | `EXPECTED_FALLBACK` | + +**Why some lib helpers stay hand-written.** The environment (`mx_environment_fis/none/prefilter`), +shadow (`mx_shadow`, `mx_shadow_platform`), and bake (`mx_generate_albedo_table`, +`mx_generate_prefilter_env`) helpers sample textures through generator-substituted `$`-token +**texture + sampler** bindings. naga's GLSL frontend cannot parse sampler types, so the transpiler +can only stub those calls — the stubs would leak into the output as unresolved references. These +files are therefore listed in `LIB_KEEP_HANDWRITTEN` and maintained by hand (the same limitation +already skips image/light *nodes*). `mx_environment_prefilter.wgsl` and +`mx_generate_prefilter_env.wgsl` were hand-authored to match the `mx_environment_fis.wgsl` +conventions. + +The four BSDF nodes `mx_conductor/dielectric/generalized_schlick/subsurface_bsdf` now transpile +cleanly and are generated; only `mx_chiang_hair_bsdf` remains an `EXPECTED_FALLBACK`. + +## Transpiler correctness fixes + +naga produces valid but SSA-style WGSL and deterministically rewrites identifiers. The following +post-processing was added/fixed so the generated output matches the genwgsl library and the +`WgslShaderGenerator` runtime template. All changes are in +`source/MaterialXGenWgsl/tools/glsl_to_wgsl.py`. + +| Area | Symptom before fix | Fix | +| --- | --- | --- | +| **Struct parsing** (`transpile_glsl_structs`) | GLSL comments inside a struct (e.g. `// Fresnel model`) were parsed as fields (`Fresnel: //,`) and array fields (`vec2 c[3]`) became `c[3]: vec2f` | Strip line/block comments; rewrite array fields to `array`; skip types provided centrally by `LIB_STRUCT_PREAMBLE` (`LIB_PREAMBLE_STRUCTS`) so `FresnelData`/`ClosureData` aren't redeclared | +| **`surfaceshader`** | Redeclared — emitted both by `WgslShaderGenerator::emitTypeDefinitions` and by the lib preamble | Removed `surfaceshader` from `LIB_STRUCT_PREAMBLE` (the generator owns it; the `material = surfaceshader` alias still resolves) | +| **File-scope consts** (`transpile_glsl_consts`) | Function-local `const int SAMPLE_COUNT` was hoisted to module scope → duplicate / redeclared consts across microfacet libs | Blank out function bodies before matching, so only true file-scope consts are emitted | +| **Const references** (`resolve_const_refs`) | naga suffixes references to a digit-ending const (`FUJII_CONSTANT_1_`) or a name it sees twice in its input (`FRESNEL_MODEL_SCHLICK_1`), leaving them unresolved | Rewrite `NAME_` / `NAME_` references back to the declared const name (keyed only on known const names, so numbered locals are untouched) | +| **`FresnelData` fields** (`LIB_FIELD_RENAMES`) | naga renders digit-ending fields `F0`/`F82`/`F90` as `F0_`/`F82_`/`F90_`, mismatching the struct | Strip the trailing `_` from those field accesses | +| **`mx_noise` overloads** (`CALL_MAP`) | WGSL has no overloading, so `mx_bilerp`, `mx_hash_int`, `mx_cell_noise_vec3`, etc. collided | Added type-suffixed `CALL_MAP` entries (e.g. `mx_hash_int_i2`, `mx_cell_noise_vec3_vec2`) matching the hand-written naming | +| **Overload resolution** (`extract_transpiled_fn`, `remap_calls_by_naga_sig`) | For overloaded siblings (`mx_ggx_dir_albedo` vec3/scalar/FresnelData) the wrong body was extracted and calls resolved to the wrong overload | Extract by parameter **signature** when a name is ambiguous; remap calls using the **actual per-module signatures** naga assigned, instead of assuming a sorted index order | +| **`out`/`inout` params** (`naga_proto_params`) | Stripping `out`/`inout` from prototypes made callers pass a dereferenced value (`mx_normalmap_vector2(..., (*result))`) instead of the pointer | Keep `out`/`inout` in prototypes (only strip the neutral `const`/`in`) so naga passes `ptr` | + +### The recurring theme + +naga is deterministic but rewrites identifiers to keep its output valid: it appends `_` to any name +ending in a digit, `_N` to colliding names, and renders `out`/`inout` params as `ptr`. +Wherever the transpiler re-emits a symbol itself (a hand-written struct/const, or a prototype), the +generated references must be reconciled back to that symbol. The fixes above are all instances of +that reconciliation. + +## Build / repo workflow + +Generated `.wgsl` files carry a `// Generated from … glsl_to_wgsl.py` banner and are **not +committed** — they are regenerated from `genglsl` (the single source of truth) in CI and locally. +Because `MATERIALX_BUILD_GEN_WGSL` now defaults **OFF**, a clean local build of the web viewer is: + +```sh +# 1. regenerate the WGSL library (genglsl -> genwgsl) +python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries + +# 2. configure with the WGSL backend enabled +cmake -S . -B javascript/build \ + -DMATERIALX_BUILD_JS=ON -DMATERIALX_BUILD_GEN_WGSL=ON \ + -DMATERIALX_EMSDK_PATH= -G Ninja + +# 3. build the WASM modules (libraries are embedded into JsMaterialXGenShader.data) +cmake --build javascript/build --target JsMaterialXCore JsMaterialXGenShader + +# 4. run the viewer +cd javascript/MaterialXView && npm install && npm run start # http://localhost:8080 +``` + +Re-running the transpiler must produce byte-identical output, so a `git diff` after regeneration +cleanly surfaces any GLSL/WGSL drift. diff --git a/libraries/pbrlib/genglsl/lib/mx_microfacet.glsl b/libraries/pbrlib/genglsl/lib/mx_microfacet.glsl index 3f50799513..a0a7106509 100644 --- a/libraries/pbrlib/genglsl/lib/mx_microfacet.glsl +++ b/libraries/pbrlib/genglsl/lib/mx_microfacet.glsl @@ -107,9 +107,11 @@ float mx_uniform_hemisphere_PDF() // Construct an orthonormal basis from a unit vector. // https://graphics.pixar.com/library/OrthonormalB/paper.pdf +// Branch at N.z = -1 instead of N.z = 0: a branch at the equator places a tangent-frame +// discontinuity on Z-up spheres, causing sawtooth artifacts on glossy/metallic materials. mat3 mx_orthonormal_basis(vec3 N) { - float sign = (N.z < 0.0) ? -1.0 : 1.0; + float sign = (N.z < -0.9999999) ? -1.0 : 1.0; float a = -1.0 / (sign + N.z); float b = N.x * N.y * a; vec3 X = vec3(1.0 + sign * N.x * N.x * a, sign * b, -sign * N.x); diff --git a/source/MaterialXGenWgsl/README.md b/source/MaterialXGenWgsl/README.md index bf11ed6604..7140cfc5c8 100644 --- a/source/MaterialXGenWgsl/README.md +++ b/source/MaterialXGenWgsl/README.md @@ -18,13 +18,13 @@ its node library is a *hybrid*, and that is the main thing to understand before artifact, transpiled from genglsl (the single source of truth) by CI, so drift between the GLSL and WGSL libraries is impossible by construction. The generated files carry a `// Generated from … do not edit` banner; do not hand-edit them, and do not commit them. -* **A small number of nodes are hand-written** and intentionally *not* generated, because WGSL has - no function overloading and a few `genwgsl/lib/` helpers were hand-adapted away from their GLSL - signatures. These are listed as `EXPECTED_FALLBACK` in `tools/glsl_to_wgsl.py` (currently the - `conductor` / `dielectric` / `generalized_schlick` BSDFs, `subsurface`, and `chiang_hair`). +* **`lib/` helper files are machine-generated** from `genglsl/lib/` by the same transpiler (run + before node transpilation). All 22 `genglsl/lib` files transpile via naga (including prefilter + environment helpers). They are also **not committed**. +* **`mx_chiang_hair_bsdf` is hand-written** and intentionally *not* generated (naga limitation on + hair scattering helpers). It is listed in `EXPECTED_FALLBACK` in `tools/glsl_to_wgsl.py`. * **Texture / image and light nodes are hand-written** too — the transpiler skips them (naga's GLSL front-end has no sampler support, and light shaders use the dynamically generated `LightData`). -* **Core `lib/` math and closure helpers are hand-maintained** (out of scope for the transpiler). The library lives in `libraries/{stdlib,pbrlib,lights}/genwgsl/` with the target defined in `libraries/targets/genwgsl.mtlx`; node implementations are wired up via `*_genwgsl_impl.mtlx`. @@ -40,9 +40,9 @@ sdist/wheels, and the tagged-release archives. Locally, populate the library in python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries ``` -then configure with `-DMATERIALX_BUILD_GEN_WGSL=ON`. The transpiler exits non-zero on an unexpected -failure (a regression or a new naga validation error), while tolerating the known `EXPECTED_FALLBACK` -nodes — so CI running it doubles as validation that a change hasn't broken the WGSL target. See +then configure with `-DMATERIALX_BUILD_GEN_WGSL=ON`. The transpiler exits non-zero on any lib or +node failure outside `EXPECTED_FALLBACK` — so CI running it doubles as validation that a change +hasn't broken the WGSL target. See [`tools/README.md`](tools/README.md) for the tool, its overload mapping table, and its lib-arity self-validation. diff --git a/source/MaterialXGenWgsl/WgslShaderGenerator.cpp b/source/MaterialXGenWgsl/WgslShaderGenerator.cpp index 5f9f6fa05c..d1bc286a4e 100644 --- a/source/MaterialXGenWgsl/WgslShaderGenerator.cpp +++ b/source/MaterialXGenWgsl/WgslShaderGenerator.cpp @@ -333,11 +333,18 @@ void WgslShaderGenerator::emitSpecularEnvironment(GenContext& context, ShaderSta { emitLibraryInclude("pbrlib/genwgsl/lib/mx_environment_fis.wgsl", context, stage); } + else if (specularMethod == SPECULAR_ENVIRONMENT_PREFILTER) + { + emitLibraryInclude("pbrlib/genwgsl/lib/mx_environment_prefilter.wgsl", context, stage); + } else if (specularMethod == SPECULAR_ENVIRONMENT_NONE) { emitLibraryInclude("pbrlib/genwgsl/lib/mx_environment_none.wgsl", context, stage); } - // SPECULAR_ENVIRONMENT_PREFILTER is not supported for standalone WGSL shaders. + else + { + throw ExceptionShaderGenError("Invalid hardware specular environment method specified: '" + std::to_string(specularMethod) + "'"); + } emitLineBreak(stage); } diff --git a/source/MaterialXGenWgsl/tools/README.md b/source/MaterialXGenWgsl/tools/README.md index 73716ac653..9af59a31af 100644 --- a/source/MaterialXGenWgsl/tools/README.md +++ b/source/MaterialXGenWgsl/tools/README.md @@ -1,7 +1,8 @@ # genwgsl library transpiler `glsl_to_wgsl.py` transpiles the MaterialX **genglsl shader-node** fragments -(`libraries/{stdlib,pbrlib,lights}/genglsl/mx_*.glsl`) into their **genwgsl** WGSL equivalents. +(`libraries/{stdlib,pbrlib,lights}/genglsl/mx_*.glsl`) and **genglsl/lib/** helpers +(`libraries/{stdlib,pbrlib}/genglsl/lib/*.glsl`) into their **genwgsl** WGSL equivalents. It exists to keep the WGSL node library in sync with the GLSL originals: the genwgsl nodes were originally hand-ported and repeatedly drifted as genglsl improved, so most of them are now generated, and re-running the tool (then diffing) surfaces drift. @@ -12,21 +13,21 @@ python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --onl ``` Requires [`naga`](https://github.com/gfx-rs/wgpu/tree/trunk/naga) (`naga-cli`, v29+) on `PATH` or at -`%NAGA%`. The generated node files are **not committed** — they are a derived artifact produced from -genglsl (the single source of truth). Passing `--out libraries` writes each generated file straight -into `libraries//genwgsl/`, populating the runtime library in place alongside the hand-written -files (the tool never touches `lib/` or the fallback nodes). CI runs exactly this to build/ship WGSL; -run it locally to work on the WGSL target. Use `--out ` to emit to a staging dir instead. +`%NAGA%`. The generated node and `lib/` files are **not committed** — they are derived artifacts +produced from genglsl (the single source of truth). Passing `--out libraries` writes each generated +file straight into `libraries//genwgsl/`, populating the runtime library in place alongside the +hand-written texture/light/`EXPECTED_FALLBACK` nodes. CI runs exactly this to build/ship WGSL; run it locally +to work on the WGSL target. Use `--out ` to emit to a staging dir instead. ## Scope -* **In:** standalone node `.glsl` files referenced by `file=` impls in stdlib/pbrlib/lights. -* **Out:** core `lib/` math/helper files (hand-maintained), texture/image and light nodes (naga's - GLSL frontend has no sampler / dynamic-LightData support — auto-skipped), and a handful of nodes - that depend on **hand-adapted** lib helpers (see below) — those stay hand-written. +* **In:** standalone node `.glsl` files referenced by `file=` impls in stdlib/pbrlib/lights, and + `genglsl/lib/*.glsl` helper files referenced by those nodes. +* **Out:** texture/image and light nodes (naga's GLSL frontend has no sampler / dynamic-LightData + support — auto-skipped), and `mx_chiang_hair_bsdf` (naga limitation — `EXPECTED_FALLBACK`). -The result is a *reduced* library: generated nodes for everything that resolves cleanly against the -genwgsl lib, hand-written nodes for the rest. +The result is a *reduced* library: generated nodes and `lib/` helpers for everything that resolves +cleanly against genglsl, hand-written nodes for the rest. ## How it works @@ -45,8 +46,18 @@ deterministic (no timestamps; overload stubs indexed by declaration order) so re byte-identical and `git diff` = drift. **What a reviewer should focus on:** the two hand-maintained tables — `CALL_MAP` (GLSL overload → -genwgsl name) and `EXPECTED_FALLBACK` (nodes intentionally not generated). They encode the genwgsl -library's divergences from genglsl and must be kept in step with it; everything else is mechanical. +genwgsl name) and `EXPECTED_FALLBACK` (nodes intentionally not generated). They encode overload +renaming and known naga limitations; everything else is mechanical. + +**Execution order in `main()`:** + +1. `transpile_libs()` — topological sort of `genglsl/lib/*.glsl` (22 files), per-function naga + transpile (or sibling-body path for heavy intra-file deps), `LIB_PREAMBLE` + (`DIRECTIONAL_ALBEDO_METHOD 0`, etc.), inverted `CALL_MAP` for overload output names, + `LIB_FIELD_RENAMES` (`tf_*` → `thinfilm_*`), texture stubs (`$envPrefilterMip`, etc.), and a + special handler for `mx_closure_type` (`LIB_STRUCT_PREAMBLE` aggregates closure structs). +2. `build_wgsl_lib_symbols()` — reads generated `lib/*.wgsl` from `--out` for arity validation. +3. `transpile_nodes()` — existing per-node loop. The pipeline, per node function: @@ -60,21 +71,19 @@ The pipeline, per node function: 3. **Post-process** — clean up naga's SSA-style output into a readable fragment (collapse param-copy shadows, inline single-use temps, `vec3`→`vec3f`, drop float-literal `f` suffixes, ...). -WGSL has no function overloading, and the hand-written genwgsl `lib/` diverges from genglsl in two -further ways a naive transpile can't satisfy. Two post-process passes bridge this: +WGSL has no function overloading. Two post-process passes bridge this: 1. **`remap_calls` / `CALL_MAP`** — overloaded GLSL helpers get a distinct type-suffixed name in the genwgsl lib (`mx_square_f32`, `mx_perlin_noise_float_3d`, `mx_matrix_mul_mat4_vec4`, ...). - `CALL_MAP` maps `(glsl_helper, parameter_types) → genwgsl name`. naga numbers overload stubs - `_N` by **prototype-declaration order** (kept and indexed deterministically even when unused), - and prototypes are emitted sorted, so each call is rewritten to the right lib function. - A `None` entry marks an intentionally-unsupported (adapted) overload. + `CALL_MAP` maps `(glsl_helper, parameter_types) → genwgsl name`. For lib transpilation the map + is inverted when emitting function definitions. naga numbers overload stubs `_N` by + **prototype-declaration order** (kept and indexed deterministically even when unused), and + prototypes are emitted sorted, so each call is rewritten to the right lib function. + A `None` entry marks an intentionally-unsupported overload. -2. **`check_lib_arity`** — some genwgsl helpers were hand-adapted beyond overloading - (e.g. `mx_ggx_energy_compensation` takes a precomputed `vec3f`, not `FresnelData`; - `mx_subsurface_scattering_approx` grew a `curvature` parameter). This pass parses the real - genwgsl `lib/*.wgsl` signatures and fails any node whose helper-call arity disagrees, so it - falls back to hand-written instead of producing a shader that fails to link. +2. **`check_lib_arity`** — parses the generated `lib/*.wgsl` signatures and fails any node whose + helper-call arity disagrees, so it falls back to hand-written instead of producing a shader that + fails to link. A node that hits either case is reported as `FAIL ... (kept hand-written)` and left out of the generated set — that is expected, not an error in the tool. The set of such nodes is listed in @@ -111,9 +120,9 @@ Since the generated files are not committed, there is no baseline to diff agains regenerate the library in place whenever genglsl changes: 1. Regenerate in place: `python glsl_to_wgsl.py --libraries libraries --out libraries`. This overwrites - the generated node files under `libraries/{stdlib,pbrlib}/genwgsl/` and leaves the hand-written - fallbacks and `lib/` helpers untouched. A non-zero exit means an *unexpected* node failed (a - regression) — the known `EXPECTED_FALLBACK` nodes failing is normal. + generated node and `lib/` files under `libraries/{stdlib,pbrlib}/genwgsl/` and leaves the + hand-written texture/light/`EXPECTED_FALLBACK` nodes untouched. A non-zero exit means an + *unexpected* failure (a regression) — the known `EXPECTED_FALLBACK` nodes failing is normal. 2. Configure with `-DMATERIALX_BUILD_GEN_WGSL=ON`, rebuild + restage test data, run the `[genwgsl]` tests, and `naga`-validate the emitted `Default.{vertex,pixel}.wgsl`. diff --git a/source/MaterialXGenWgsl/tools/glsl_to_wgsl.py b/source/MaterialXGenWgsl/tools/glsl_to_wgsl.py index 73db0ad8fc..a68da74f25 100644 --- a/source/MaterialXGenWgsl/tools/glsl_to_wgsl.py +++ b/source/MaterialXGenWgsl/tools/glsl_to_wgsl.py @@ -4,29 +4,34 @@ # SPDX-License-Identifier: Apache-2.0 # """ -Transpile MaterialX genglsl *shader-node* library fragments to genwgsl (WGSL). - -The genglsl node fragments (e.g. libraries/pbrlib/genglsl/mx_dielectric_bsdf.glsl) are not -complete shaders: they use `#include`, `$`-tokens, and define functions with no entry point. -This tool turns each node function into a complete GLSL translation unit, runs `naga` -(GLSL -> WGSL), then strips scaffolding and cleans up naga's SSA-style output into a readable -genwgsl fragment that mirrors the hand-written convention. - -Strategy (key to readable output): transpile ONE node function at a time, supplying the -included lib context as *struct definitions + function prototypes + #defines* rather than full -bodies. With a single real function body in the module, naga keeps clean parameter names and we -avoid both name-collision suffixes and `$`-tokens that live inside lib bodies. - -Re-running keeps genwgsl in sync with genglsl; diffing the output against committed files -surfaces drift. - -Scope: standalone node `.glsl` files referenced by `file=` impls in stdlib/pbrlib/lights. -Out of scope: core `lib/` helpers (hand-maintained) and image/texture nodes (naga's GLSL -frontend has no sampler support) -- the latter are auto-skipped. +Transpile MaterialX genglsl node fragments and genglsl/lib helpers to genwgsl (WGSL). + +genglsl sources are not complete shaders: they use `#include`, `$`-tokens, and define functions +with no entry point. This tool wraps each target in a naga-parseable GLSL fragment shader, runs +`naga` (GLSL -> WGSL), then post-processes naga's SSA-style output into readable genwgsl fragments. + +Generated node and lib `.wgsl` files are derived artifacts (not committed; CI regenerates them +before build). Re-running keeps genwgsl in sync with genglsl. + +Node transpilation (per mx_*.glsl function): + - ONE function body per naga call; lib context is *prototypes + #defines + structs*, not full + lib bodies — keeps parameter names clean and avoids `$`-tokens inside lib implementations. + - Skipped: image/hextiled texture nodes and light shaders (naga sampler / LightData limits). + - Expected fallback: mx_chiang_hair_bsdf (naga limitation on hair scattering helpers). + +Lib transpilation (`transpile_libs`, runs before nodes): + - Each genglsl/lib/*.glsl becomes genwgsl/lib/*.wgsl in topological #include order (22 files). + - Default: per-function transpile with full bodies of #included lib deps inlined, plus + LIB_PREAMBLE (generator-option pins), texture stubs, and CALL_MAP overload renaming. + - Sibling-body path for mx_microfacet_specular, mx_microfacet_sheen, mx_flake, mx_noise + (heavy intra-file call graphs: transitive in-file callees + overload-aware topo sort). + - Special case: mx_closure_type (static struct preamble + transpiled makeClosureData). + - Lib transpile failures are hard errors (no hand-port fallback). Usage: + python glsl_to_wgsl.py --libraries libraries --out libraries python glsl_to_wgsl.py --libraries libraries --out build/genwgsl_generated - python glsl_to_wgsl.py --libraries libraries --only mx_conductor_bsdf mx_normalmap + python glsl_to_wgsl.py --libraries libraries --only mx_conductor_bsdf mx_math """ import argparse @@ -74,18 +79,26 @@ def discover_libs(libroot): r"_light$", # light shaders take the dynamically-generated LightData struct )] -# Nodes that are intentionally NOT generated and remain hand-written, by file stem. These call lib -# helpers the genwgsl lib hand-adapted (FresnelData-taking ggx helpers, mx_subsurface_scattering_approx's -# extra `curvature` param) or hit a naga limitation (chiang hair). They are EXPECTED to fail transpile, -# so they don't constitute a build error -- only an *unexpected* failure (a previously-generable node -# that broke, e.g. after a genglsl change) sets a non-zero exit. Keep this in sync with the committed -# hand-written .wgsl files: if one starts transpiling cleanly, the tool warns so it can be removed here. +# Nodes that are intentionally NOT generated and remain hand-written, by file stem. These hit a +# naga limitation (chiang hair). They are EXPECTED to fail transpile, so they don't constitute a +# build error -- only an *unexpected* failure (a previously-generable node that broke, e.g. after a +# genglsl change) sets a non-zero exit. Keep this in sync with the committed hand-written .wgsl +# files: if one starts transpiling cleanly, the tool warns so it can be removed here. EXPECTED_FALLBACK = { "mx_chiang_hair_bsdf", - "mx_conductor_bsdf", - "mx_dielectric_bsdf", - "mx_generalized_schlick_bsdf", - "mx_subsurface_bsdf", +} + +# genwgsl/lib helpers kept hand-written rather than transpiled, for two reasons: +# * whole library dirs the project maintains by hand (HANDWRITTEN_LIB_DIRS) -- their lib .wgsl are +# left untouched; nodes in those dirs are still generated; +# * individual helpers whose bindings naga's GLSL frontend cannot express: environment +# radiance/prefilter and shadow-map lookups sample generator-substituted texture+sampler +# `$`-tokens, which naga rejects (same sampler limitation that skips image/light nodes). The +# transpiler stubs the texture ops to parse, but the stub calls would leak into the output. +HANDWRITTEN_LIB_DIRS = {"stdlib"} +LIB_KEEP_HANDWRITTEN = { + "mx_environment_fis", "mx_environment_none", "mx_environment_prefilter", + "mx_generate_albedo_table", "mx_generate_prefilter_env", "mx_shadow", "mx_shadow_platform", } TYPE_FIXUPS = [ @@ -136,6 +149,40 @@ def discover_libs(libroot): ("mx_worley_noise_vec3", ("vec2", "float", "int", "int")): "mx_worley_noise_vec3_2d", ("mx_worley_noise_vec3", ("vec3", "float", "int", "int")): "mx_worley_noise_vec3_3d", + # Remaining mx_noise.glsl overloaded helpers -> type-suffixed genwgsl names (WGSL has no + # overloading). Names mirror the hand-written genwgsl lib so cross-file callers still resolve. + ("mx_bilerp", ("float", "float", "float", "float", "float", "float")): "mx_bilerp_f32", + ("mx_bilerp", ("vec3", "vec3", "vec3", "vec3", "float", "float")): "mx_bilerp_vec3", + ("mx_trilerp", ("float",) * 11): "mx_trilerp_f32", + ("mx_trilerp", ("vec3",) * 8 + ("float", "float", "float")): "mx_trilerp_vec3", + + ("mx_gradient_float", ("uint", "float", "float")): "mx_gradient_float_2d", + ("mx_gradient_float", ("uint", "float", "float", "float")): "mx_gradient_float_3d", + ("mx_gradient_vec3", ("uvec3", "float", "float")): "mx_gradient_vec3_2d", + ("mx_gradient_vec3", ("uvec3", "float", "float", "float")): "mx_gradient_vec3_3d", + ("mx_gradient_scale2d", ("float",)): "mx_gradient_scale2d_f32", + ("mx_gradient_scale2d", ("vec3",)): "mx_gradient_scale2d_vec3", + ("mx_gradient_scale3d", ("float",)): "mx_gradient_scale3d_f32", + ("mx_gradient_scale3d", ("vec3",)): "mx_gradient_scale3d_vec3", + + ("mx_hash_int", ("int",)): "mx_hash_int_i1", + ("mx_hash_int", ("int", "int")): "mx_hash_int_i2", + ("mx_hash_int", ("int", "int", "int")): "mx_hash_int_i3", + ("mx_hash_int", ("int", "int", "int", "int")): "mx_hash_int_i4", + ("mx_hash_int", ("int", "int", "int", "int", "int")): "mx_hash_int_i5", + ("mx_hash_vec3", ("int", "int")): "mx_hash_vec3_i2", + ("mx_hash_vec3", ("int", "int", "int")): "mx_hash_vec3_i3", + + ("mx_cell_noise_vec3", ("float",)): "mx_cell_noise_vec3_f32", + ("mx_cell_noise_vec3", ("vec2",)): "mx_cell_noise_vec3_vec2", + ("mx_cell_noise_vec3", ("vec3",)): "mx_cell_noise_vec3_vec3", + ("mx_cell_noise_vec3", ("vec4",)): "mx_cell_noise_vec3_vec4", + + ("mx_worley_cell_position", ("int", "int", "int", "int", "float")): "mx_worley_cell_position_2d", + ("mx_worley_cell_position", ("int", "int", "int", "int", "int", "int", "float")): "mx_worley_cell_position_3d", + ("mx_worley_distance", ("vec2", "int", "int", "int", "int", "float", "int")): "mx_worley_distance_2d", + ("mx_worley_distance", ("vec3", "int", "int", "int", "int", "int", "int", "float", "int")): "mx_worley_distance_3d", + ("mx_matrix_mul", ("vec2", "mat2")): "mx_matrix_mul_vec2_mat2", ("mx_matrix_mul", ("vec3", "mat3")): "mx_matrix_mul_vec3_mat3", ("mx_matrix_mul", ("vec4", "mat4")): "mx_matrix_mul_vec4_mat4", @@ -146,13 +193,125 @@ def discover_libs(libroot): ("mx_matrix_mul", ("mat3", "mat3")): "mx_matrix_mul_mat3_mat3", ("mx_matrix_mul", ("mat4", "mat4")): "mx_matrix_mul_mat4_mat4", - # vec3/vec3-F90 directional albedo maps cleanly; the float and FresnelData overloads were - # adapted in the genwgsl lib (caller precomputes), so nodes using them stay hand-written. + # vec3/vec3-F90 directional albedo maps cleanly; float overload delegates to vec3. ("mx_ggx_dir_albedo", ("float", "float", "vec3", "vec3")): "mx_ggx_dir_albedo", - ("mx_ggx_dir_albedo", ("float", "float", "float", "float")): None, - ("mx_ggx_dir_albedo", ("float", "float", "FresnelData")): None, - # Single GLSL signature, but the genwgsl lib takes a precomputed Fss (vec3f), not FresnelData. - ("mx_ggx_energy_compensation", ("float", "float", "FresnelData")): None, + ("mx_ggx_dir_albedo", ("float", "float", "float", "float")): "mx_ggx_dir_albedo_scalar", + ("mx_ggx_dir_albedo", ("float", "float", "FresnelData")): "mx_ggx_dir_albedo_fresnel", + ("mx_ggx_energy_compensation", ("float", "float", "FresnelData")): "mx_ggx_energy_compensation", +} + +# ---------------------------------------------------------------------------- lib transpilation tables + +# Lib files where per-function transpile without in-file siblings fails because helpers call +# each other in the same file. These use transpile_lib_file_siblings (transitive in-file +# callee bodies + overload-aware topo sort) instead of one-function-at-a-time. +LIB_USE_SIBLING_BODIES = { + "mx_microfacet_specular", "mx_microfacet_sheen", "mx_flake", "mx_noise", +} + +# Pin preprocessor branches to match current genwgsl behavior. Method 0 = analytic directional +# albedo (table/MC paths in microfacet libs are excluded from the WGSL port). +LIB_PREAMBLE = """ +#define DIRECTIONAL_ALBEDO_METHOD 0 +#define AIRY_FRESNEL_ITERATIONS 2 +""" + +# naga's GLSL frontend rejects `uniform sampler2D` and real texture ops in lib fragments. +# These stubs let the shader parse; only the function bodies we extract matter for output. +TEXTURE_STUB_PREAMBLE = """ +vec3 mtlx_tex_lookup_rgb(vec2 uv, float lod) { return vec3(0.0); } +vec2 mtlx_tex_lookup_rg(vec2 uv) { return vec2(0.0); } +float mtlx_tex_lookup_b(vec2 uv) { return 0.0; } +float mtlx_tex_size_x() { return 256.0; } +""" + +# MaterialX $-tokens are not valid GLSL identifiers. Lib files use them for sampler uniforms, +# environment tables, and the closure constructor macro — expand to literals/stubs before naga. +LIB_TOKEN_FIXUPS = [ + (re.compile(r"\$texSamplerSignature\b"), "sampler2D mtlx_tex_sampler"), + (re.compile(r"\$texSamplerSampler2D\b"), "mtlx_tex_sampler"), + (re.compile(r"\$albedoTable\b"), "mtlx_albedo_table"), + (re.compile(r"\$albedoTableSize\b"), "vec2(256.0)"), + (re.compile(r"\$envRadianceSamples\b"), "16"), + (re.compile(r"\$envRadianceMips\b"), "8"), + (re.compile(r"\$envMatrix\b"), "mat4(1.0)"), + (re.compile(r"\$envRadiance\b"), "mtlx_env_radiance_stub"), + (re.compile(r"\$envIrradiance\b"), "mtlx_env_irradiance_stub"), + (re.compile(r"\$envLightIntensity\b"), "vec3(1.0)"), + (re.compile(r"\$envPrefilterMip\b"), "0.0"), + (re.compile(r"\$envRadianceSampler2D\b"), "mtlx_tex_sampler"), + (re.compile(r"\$closureDataConstructor\b"), + "ClosureData(closureType, L, V, N, P, occlusion)"), + (re.compile(r"\$refractionTwoSided\b"), "false"), +] + +# GLSL FresnelData uses tf_* field names; genwgsl lib uses thinfilm_* (hand-port convention). +LIB_FIELD_RENAMES = [ + (re.compile(r"\btf_thickness\b"), "thinfilm_thickness"), + (re.compile(r"\btf_ior\b"), "thinfilm_ior"), + # naga's WGSL backend appends `_` to any identifier ending in a digit (F0 -> F0_). The genwgsl + # FresnelData (LIB_STRUCT_PREAMBLE) keeps the digit-suffixed field names, so strip naga's + # trailing underscore back off these field accesses to match the struct definition. + (re.compile(r"\bF0_\b"), "F0"), + (re.compile(r"\bF82_\b"), "F82"), + (re.compile(r"\bF90_\b"), "F90"), +] + +# mx_closure_type.glsl only defines ClosureData + makeClosureData; the genwgsl file must also +# expose BSDF/VDF/FresnelData/EDF/material that nodes include. Prepended verbatim (not from naga). +# NOTE: surfaceshader is intentionally NOT here -- WgslShaderGenerator::emitTypeDefinitions emits it +# (and displacement/lightshader) *before* including this file, so the `material = surfaceshader` +# alias below resolves. Re-declaring it here would redeclare the struct in the assembled shader. +LIB_STRUCT_PREAMBLE = """ +struct ClosureData { + closureType: i32, + L: vec3f, + V: vec3f, + N: vec3f, + P: vec3f, + occlusion: f32, +} + +struct BSDF { + response: vec3f, + throughput: vec3f, +} + +struct VDF { + response: vec3f, + throughput: vec3f, +} + +alias EDF = vec3f; +alias material = surfaceshader; + +struct FresnelData { + model: i32, + airy: bool, + ior: vec3f, + extinction: vec3f, + F0: vec3f, + F82: vec3f, + F90: vec3f, + exponent: f32, + thinfilm_thickness: f32, + thinfilm_ior: f32, + refraction: bool, +} +""" + +# Struct types emitted centrally by LIB_STRUCT_PREAMBLE (into mx_closure_type.wgsl). Other lib +# files that also declare them in GLSL (e.g. FresnelData in mx_microfacet_specular.glsl) must NOT +# re-emit a definition, or the assembled shader has duplicate/conflicting structs -- and the +# preamble's hand-port field names (thinfilm_* vs. the GLSL tf_*) would diverge from the bodies. +LIB_PREAMBLE_STRUCTS = {"surfaceshader", "ClosureData", "BSDF", "VDF", "FresnelData"} + +GLSL_TO_WGSL_TYPE = { + # Used by transpile_glsl_structs/consts when emitting WGSL struct/const preamble for lib files. + "float": "f32", "int": "i32", "bool": "bool", + "vec2": "vec2f", "vec3": "vec3f", "vec4": "vec4f", + "mat2": "mat2x2f", "mat3": "mat3x3f", "mat4": "mat4x4f", + "mat3x3": "mat3x3f", "mat2x2": "mat2x2f", "mat4x4": "mat4x4f", } @@ -224,6 +383,272 @@ def parse_functions(text): return fns +def fn_param_types(params_str): + """Extract bare GLSL parameter types from a parameter list string.""" + types = [] + for p in params_str.split(","): + p = re.sub(r"^\s*(const|in|out|inout)\s+", "", p.strip()) + if p: + types.append(re.sub(r"\s+", " ", + re.sub(r"\s+[A-Za-z_]\w*\s*(\[\d*\])?$", "", p)).strip()) + return tuple(types) + + +def expand_lib_tokens(text): + """Replace MaterialX $-tokens and texture/sampler calls with naga-parseable GLSL. + + Node transpile uses per-function MTLXTOK_* sentinels; lib transpile uses fixed substitutions + from LIB_TOKEN_FIXUPS plus regex rewrites below so included lib bodies compile as a unit.""" + for rx, repl in LIB_TOKEN_FIXUPS: + text = rx.sub(repl, text) + # Replace sampler types and texture ops with stubs naga accepts. + text = re.sub(r"\bsampler2D\s+mtlx_\w+\b", "int mtlx_sampler_stub", text) + text = re.sub(r"textureLod\s*\(\s*mtlx_tex_sampler\s*,\s*([^,]+),\s*([^)]+)\)", + r"mtlx_tex_lookup_rgb(\1, \2)", text) + text = re.sub(r"texture\s*\(\s*mtlx_tex_sampler\s*,\s*([^)]+)\)\.xy", + r"mtlx_tex_lookup_rg(\1)", text) + text = re.sub(r"texture\s*\(\s*mtlx_albedo_table\s*,\s*([^)]+)\)\.rg", + r"mtlx_tex_lookup_rg(\1)", text) + text = re.sub(r"texture\s*\(\s*mtlx_albedo_table\s*,\s*([^)]+)\)\.b", + r"mtlx_tex_lookup_b(\1)", text) + text = re.sub(r"textureSize\s*\(\s*mtlx_tex_sampler\s*,\s*0\s*\)", + r"vec2(256.0)", text) + text = re.sub(r"textureSize\s*\(\s*mtlx_albedo_table\s*,\s*0\s*\)\.x", + r"mtlx_tex_size_x()", text) + text = re.sub(r"textureSize\s*\(\s*mtlx_env_radiance_stub\s*,\s*0\s*\)\.x", + r"mtlx_tex_size_x()", text) + text = re.sub(r"texture\s*\(\s*mtlx_env_radiance_stub\s*,\s*([^)]+)\)\.rgb", + r"mtlx_tex_lookup_rgb(\1, 0.0)", text) + text = re.sub(r"texture\s*\(\s*mtlx_env_irradiance_stub\s*,\s*([^)]+)\)\.rgb", + r"mtlx_tex_lookup_rgb(\1, 0.0)", text) + text = re.sub(r"mx_latlong_map_lookup\s*\(([^,]+),\s*([^,]+),\s*([^,]+),\s*mtlx_env_radiance_stub\s*\)", + r"mtlx_tex_lookup_rgb(vec2(0.0), \3)", text) + text = re.sub(r"mx_latlong_map_lookup\s*\(([^,]+),\s*([^,]+),\s*([^,]+),\s*mtlx_env_irradiance_stub\s*\)", + r"mtlx_tex_lookup_rgb(vec2(0.0), \3)", text) + return text + + +def wgsl_fn_name(glsl_name, types): + """Return the genwgsl function name for a GLSL overload. + + CALL_MAP is keyed by (name, param_types). For lib *output* we use this to emit type-suffixed + definitions (e.g. mx_square_f32); for node *calls* remap_calls() applies the same map.""" + target = CALL_MAP.get((glsl_name, types)) + if target: + return target + return glsl_name + + +def apply_field_renames(text): + """Apply LIB_FIELD_RENAMES to struct bodies and field accesses in transpiled WGSL.""" + for rx, repl in LIB_FIELD_RENAMES: + text = rx.sub(repl, text) + return text + + +def resolve_const_refs(text, const_names): + """Rewrite naga-suffixed references to module-scope consts back to their declared name. + + naga's WGSL backend suffixes an identifier reference with `_` when the name ends in a digit + (guarding its own `name_N` SSA scheme) and with `_` when the name collides with another + declaration in naga's input. Both bite our file-scope consts: build_context puts every lib + const in naga's parse context, and the sibling-body path *also* emits the file's preamble into + the wrapped fragment, so naga sees `FRESNEL_MODEL_SCHLICK` twice and renames body references to + `FRESNEL_MODEL_SCHLICK_1`; a digit-ending const like FUJII_CONSTANT_1 becomes FUJII_CONSTANT_1_. + The declarations we emit keep the clean GLSL name, so map `NAME_` / `NAME_` back to `NAME`. + Longest name first so a shorter const can't capture a longer one's suffix; a suffixed token that + is itself a declared const (e.g. FUJII_CONSTANT_1) is left untouched. Keyed only on known const + names, so naga's numbered *locals* (e.g. F0_1 = F0) are not affected.""" + names = set(n for n in const_names if n) + for nm in sorted(names, key=len, reverse=True): + text = re.sub(r"\b" + re.escape(nm) + r"_\d*\b", + lambda m: m.group(0) if m.group(0) in names else nm, text) + return text + + +def const_names_from_wgsl(consts): + """Names of `const NAME: T = ...` declarations (WGSL form, from transpile_glsl_consts).""" + return [m.group(1) for c in consts for m in [re.match(r"const (\w+):", c)] if m] + + +def glsl_const_names(glsl_text): + """Names of file-scope `const TYPE NAME = ...` declarations in GLSL context text (e.g. base).""" + return re.findall(r"\bconst\s+[\w<>]+\s+([A-Za-z_]\w*)\s*=", glsl_text) + + +def glsl_type_to_wgsl(typename): + return GLSL_TO_WGSL_TYPE.get(typename, typename) + + +def transpile_glsl_structs(text): + """Convert top-level GLSL struct definitions to WGSL (emitted above lib fn bodies). + + Skips types provided centrally by LIB_STRUCT_PREAMBLE (see LIB_PREAMBLE_STRUCTS). Strips GLSL + comments before splitting fields -- otherwise a comment word is read as a `type name` pair (e.g. + `// Fresnel model` -> `Fresnel: //,`) and the real following field is dropped. Rewrites GLSL + array fields to WGSL (`vec2 coords[3]` -> `coords: array`).""" + out = [] + for m in re.finditer(r"\bstruct\s+(\w+)\s*\{([^}]*)\}\s*;?", text): + name = m.group(1) + if name in LIB_PREAMBLE_STRUCTS: + continue + body_src = re.sub(r"/\*.*?\*/", "", m.group(2), flags=re.S) # block comments + body_src = re.sub(r"//[^\n]*", "", body_src) # line comments + fields = [] + for part in body_src.split(";"): + part = part.strip() + if not part: + continue + tokens = part.split() + if len(tokens) < 2: + continue + ftype, fname = tokens[0], tokens[1] + wtype = glsl_type_to_wgsl(ftype) + arr = re.match(r"(\w+)\[(\d+)\]$", fname) # GLSL array field `name[N]` + if arr: + fname, wtype = arr.group(1), f"array<{wtype}, {arr.group(2)}>" + fields.append(f" {fname}: {wtype},") + if fields: + body = "\n".join(fields).rstrip(",") + out.append(f"struct {name} {{\n{body}\n}}") + return out + + +def transpile_glsl_consts(text): + """Convert file-scope GLSL `const T name = ...` to WGSL `const name: T = ...` syntax. + + Only *top-level* consts are emitted. Function-local consts (e.g. the `const int SAMPLE_COUNT` + inside several directional-albedo helpers) are handled by naga inside the transpiled bodies; + hoisting them here would produce duplicate module-scope consts -- and since the same name + recurs across microfacet libs, redeclaration errors once several are included together. Blank + out function bodies first so their indented consts aren't matched by the file-scope regex.""" + scope = text + for fn in parse_functions(text): + scope = scope.replace(fn["full"], "") + out = [] + for m in re.finditer(r"^\s*const\s+(\w+)\s+(\w+)\s*=\s*([^;]+);", scope, re.M): + out.append(f"const {m.group(2)}: {glsl_type_to_wgsl(m.group(1))} = {m.group(3)};") + return out + + +def lib_includes(text): + """Return #include paths from a lib file (lib/... only).""" + return [inc for inc in re.findall(r'#include\s+"([^"]+)"', text) + if inc.startswith("lib/")] + + +def topo_sort_lib_files(lib_files): + """Topological sort of lib/*.glsl by `#include \"lib/...\"` dependencies. + + Ensures mx_microfacet.glsl is processed before mx_microfacet_specular.glsl, etc., so + generated genwgsl/lib/*.wgsl includes resolve when nodes are transpiled later.""" + stems = {f.stem: f for f in lib_files} + deps = {} + for f in lib_files: + txt = f.read_text(encoding="utf-8") + deps[f.stem] = {Path(inc).stem for inc in lib_includes(txt) if Path(inc).stem in stems} + order, seen = [], set() + + def visit(stem): + if stem in seen: + return + seen.add(stem) + for d in sorted(deps.get(stem, ())): + visit(d) + order.append(stems[stem]) + + for stem in sorted(stems): + visit(stem) + return order + + +def postprocess_wgsl_fn(text, fn_name, protos, lib_symbols=None, remap=True): + """Shared post-processing for one transpiled WGSL function (lib or node). + + Runs cleanup, type normalization, CALL_MAP remapping, and optional lib arity checks. + Returns (text, []) on success or (None, unsupported_calls) when remap/arity fails.""" + text = cleanup_function(text) + for rx, repl in TYPE_FIXUPS: + text = rx.sub(repl, text) + text = re.sub(r"\b(\d+\.\d+(?:[eE][-+]?\d+)?)f\b", r"\1", text) + text = re.sub(r"(?", "vec3": "vec3", "vec4": "vec4", + "ivec2": "vec2", "ivec3": "vec3", "ivec4": "vec4", + "uvec2": "vec2", "uvec3": "vec3", "uvec4": "vec4", + "mat2": "mat2x2", "mat3": "mat3x3", "mat4": "mat4x4", +} + + +def _wgsl_sig_types(fn_text): + """Parameter types from a (raw naga) WGSL fn signature, e.g. ('f32', 'vec3').""" + sig = re.match(r"fn\s+\w+\s*\(([^)]*)\)", fn_text, re.S) + if not sig: + return () + out = [] + for p in sig.group(1).split(","): + m = re.match(r"[A-Za-z_]\w*\s*:\s*(.+)", p.strip(), re.S) + if m: + out.append(re.sub(r"\s+", "", m.group(1))) + return tuple(out) + + +def extract_transpiled_fn(wgsl, glsl_name, wgsl_name, glsl_types=None): + """Pull one non-stub function from naga output and rename to the target genwgsl name. + + Per-function lib/node transpile feeds naga a module with one real body; this extracts that + body and renames it (e.g. mx_square -> mx_square_f32) via wgsl_fn_name. When several same-named + overloads appear in one naga module (sibling path, e.g. mx_ggx_dir_albedo's vec3, scalar, and + FresnelData forms), `glsl_types` disambiguates by matching the parameter signature; with a single + candidate the base-name match is used directly (signature match is skipped, since naga rewrites + `out` params to pointers and would otherwise not compare equal).""" + cands = [(name, t) for name, t in top_level_fns(wgsl) + if fn_base(name) == glsl_name and "{\n}" not in t and t.count("\n") > 1] + if not cands: + return None + chosen = cands[0] + if len(cands) > 1 and glsl_types is not None: + want = tuple(GLSL_TO_NAGA_TYPE.get(t, t) for t in glsl_types) + chosen = next(((n, t) for n, t in cands if _wgsl_sig_types(t) == want), cands[0]) + name, t = chosen + return re.sub(r"(\bfn\s+)" + re.escape(name) + r"\b", r"\1" + wgsl_name, t) + + # Closure/shader types the generator emits (GlslShaderGenerator::emitTypeDefinitions), not the # lib files -- supply them so node BSDF/EDF signatures parse. CLOSURE_PREAMBLE = """ @@ -238,7 +663,27 @@ def parse_functions(text): """ -def build_context(libroot, libs): +def naga_proto_params(params_str): + """Normalize GLSL prototype params for naga: strip the semantically-neutral `const`/`in` + qualifiers, KEEP `out`/`inout`, and drop sampler params naga's prototype parser rejects. + + Keeping `out`/`inout` is essential: naga renders those params as `ptr`, and when it + transpiles a *caller* it must see the callee's prototype as by-pointer so it passes the pointer + (e.g. `mx_normalmap_vector2(..., result)`) rather than dereferencing it (`(*result)`), which + would mismatch the transpiled definition's pointer parameter. naga accepts `out`/`inout` in a + forward declaration.""" + parts = [] + for p in params_str.split(","): + p = p.strip() + p = re.sub(r"^const\s+", "", p) # const: no call-site effect + p = re.sub(r"^in\s+", "", p) # `in` is the default (by value); `\s+` guards `int`/`inout` + if not p or "sampler2D" in p: + continue + parts.append(p) + return ", ".join(parts) + + +def build_context(libroot, libs, nodes=True): """Build the naga parse context. Returns (base, protos, overloaded): base - CLOSURE_PREAMBLE + dedup'd #defines/consts/struct-defs from every genglsl lib. protos - {(name, types): "prototype;"} for every function defined in any genglsl lib OR @@ -246,7 +691,10 @@ def build_context(libroot, libs): and generator-supplied helpers (mx_environment_radiance) all resolve. overloaded - the set of names appearing with more than one signature (informational; the actual overload remapping is driven by CALL_MAP in remap_calls). - Per node we emit `base` + every prototype except the one being defined.""" + Per node we emit `base` + every prototype except the one being defined. + + Pass nodes=False for lib transpilation: skip mx_*.glsl node files (their `inout` protos break + naga) and rely on inlined lib bodies + LIB_PREAMBLE instead of CLOSURE_PREAMBLE duplication.""" defines, consts, structs, protos = {}, {}, {}, {} # Scan lib/ helpers and the node files themselves (the latter for cross-node prototypes). sources = [] @@ -254,10 +702,11 @@ def build_context(libroot, libs): gldir = libroot / lib / "genglsl" if gldir.is_dir(): sources += sorted((gldir / "lib").glob("*.glsl")) - # Node files too (for cross-node prototypes), minus the skipped ones (light shaders - # reference the dynamic LightData struct; texture nodes carry $-tokens). - sources += [f for f in sorted(gldir.glob("mx_*.glsl")) - if not any(p.search(f.stem) for p in SKIP_PATTERNS)] + if nodes: + # Node files too (for cross-node prototypes), minus the skipped ones (light shaders + # reference the dynamic LightData struct; texture nodes carry $-tokens). + sources += [f for f in sorted(gldir.glob("mx_*.glsl")) + if not any(p.search(f.stem) for p in SKIP_PATTERNS)] # Collect #defines, consts, struct definitions, and function prototypes across all sources. # `setdefault` keeps the first occurrence, so a symbol defined in several files is deduped. for f in sources: @@ -271,17 +720,9 @@ def build_context(libroot, libs): for m in re.finditer(r"\bstruct\s+(\w+)\s*\{[^}]*\}\s*;?", txt): structs.setdefault(m.group(1), m.group(0)) for fn in parse_functions(txt): - # Dedupe by name + parameter *types* (GLSL overload identity, ignoring the - # `const`/`in` qualifiers naga also ignores), so the same helper defined in - # several lib files collapses to one prototype while real overloads survive. - types = [] - for p in fn["params"].split(","): - p = re.sub(r"^\s*(const|in)\s+", "", p.strip()) # drop leading qualifier - if p: - # Strip the parameter NAME (and any `[N]` array suffix) off the end, leaving the - # bare type; collapse internal whitespace. e.g. "vec3 F0" -> "vec3". - types.append(re.sub(r"\s+", " ", re.sub(r"\s+[A-Za-z_]\w*\s*(\[\d*\])?$", "", p)).strip()) - protos.setdefault((fn["name"], tuple(types)), f"{fn['ret']} {fn['name']}({fn['params']});") + types = fn_param_types(fn["params"]) + protos.setdefault((fn["name"], types), + f"{fn['ret']} {fn['name']}({naga_proto_params(fn['params'])});") base_items = list(defines.values()) + list(consts.values()) + list(structs.values()) base = CLOSURE_PREAMBLE + "\n".join(s for s in base_items if "$" not in s) protos = {k: v for k, v in protos.items() if "$" not in v} @@ -371,17 +812,15 @@ def _count_items(s, brackets="()[]<>"): return n -def build_wgsl_lib_symbols(libroot, libs): - """Parse the hand-written genwgsl `lib/` files into {function name: parameter count}. +def build_wgsl_lib_symbols(libroot, libs, outroot=None): + """Parse genwgsl `lib/` files into {function name: parameter count}. - The genwgsl lib has been hand-adapted in places that diverge from genglsl beyond overloading - (e.g. mx_subsurface_scattering_approx grew a `curvature` parameter, mx_ggx_energy_compensation - takes a precomputed Fss). A transpiled node faithfully follows the GLSL call, so we validate - every helper call against the real lib arity and fall the node back to hand-written on a - mismatch -- this auto-detects such divergences instead of discovering them at naga link time.""" + Reads from `outroot` when set (post-generation), else `libroot` (legacy hand-written tree). + """ symbols = {} + root = outroot if outroot is not None else libroot for lib in libs: - wdir = libroot / lib / "genwgsl" / "lib" + wdir = root / lib / "genwgsl" / "lib" if not wdir.is_dir(): continue for f in sorted(wdir.glob("*.wgsl")): @@ -441,6 +880,43 @@ def remap_calls(text, node_fn_name, protos): return text, unsupported +# naga's canonical type spelling -> GLSL type used to key CALL_MAP (inverse of GLSL_TO_NAGA_TYPE). +NAGA_TO_GLSL_TYPE = {v: k for k, v in GLSL_TO_NAGA_TYPE.items()} + + +def remap_calls_by_naga_sig(text, wgsl_module, self_base): + """Remap overloaded helper calls using the signatures naga actually assigned in wgsl_module. + + The sibling-body path defines several overloads of a name in one module, and naga numbers the + stubs (`mx_foo`, `mx_foo_1`, ...) in an order that depends on its internal processing -- not the + sorted-proto order remap_calls assumes. Rather than guess the index, read each naga function's + real parameter signature straight from the module, recover its GLSL types, and resolve + (base, types) through CALL_MAP. Longest names first so `mx_foo_1` isn't clobbered by `mx_foo`. + `self_base` is the target's own name (skip, so a recursive call isn't misrouted). Returns + (text, unsupported) where unsupported lists calls whose CALL_MAP entry is None/missing.""" + call_bases = {b for (b, _t) in CALL_MAP} + sigs = {} + for name, t in top_level_fns(wgsl_module): + wt = _wgsl_sig_types(t) + sigs[name] = tuple(NAGA_TO_GLSL_TYPE.get(x, x) for x in wt) + unsupported = [] + for name in sorted(sigs, key=len, reverse=True): + base = fn_base(name) + if base == self_base or base not in call_bases: + continue + if not re.search(r"\b" + re.escape(name) + r"\s*\(", text): + continue + key = (base, sigs[name]) + if key not in CALL_MAP: + continue # this exact overload isn't a CALL_MAP entry (e.g. a same-base local helper) + target = CALL_MAP[key] + if not target: # explicit None -> adapted signature, keep hand-written + unsupported.append(name + "(" + ", ".join(sigs[name]) + ")") + continue + text = re.sub(r"\b" + re.escape(name) + r"\s*\(", target + "(", text) + return text, unsupported + + def top_level_fns(wgsl): """Yield (name, text) for each top-level `fn` in a WGSL module.""" for m in re.finditer(r"\bfn\s+([A-Za-z_]\w*)\s*\(", wgsl): @@ -448,7 +924,476 @@ def top_level_fns(wgsl): yield m.group(1), wgsl[m.start():_match_brace(wgsl, brace)] -# ---------------------------------------------------------------------------- driver +# ---------------------------------------------------------------------------- lib driver +# +# transpile_libs() walks genglsl/lib/*.glsl in include order and writes genwgsl/lib/*.wgsl. +# Strategy mirrors nodes (wrap -> naga -> cleanup) but emits full helper libraries: +# - default: one naga invocation per function, with #included deps inlined as bodies +# - LIB_USE_SIBLING_BODIES: transitive in-file callee bodies (overload-aware topo sort) +# - mx_closure_type: static struct preamble + transpiled makeClosureData only + +def lib_needs_samplers(src): + """True if this lib references samplers or $-token texture uniforms (needs TEXTURE_STUB_PREAMBLE).""" + return bool(re.search(r"\btexture\w*\s*\(|\$texSampler|\$albedoTable|\$envRadiance|\$envIrradiance", + src)) + + +def _extract_mx_calls(fn_body, known_names): + """Return mx_* callees invoked in fn_body (body only, not the signature).""" + if "{" not in fn_body: + return set() + body = fn_body[fn_body.index("{") + 1:fn_body.rfind("}")] + return {m.group(1) for m in re.finditer(r"\b(mx_\w+)\s*\(", body) if m.group(1) in known_names} + + +def sort_functions_topo(fns): + """Reorder function definitions so callees precede callers (naga requirement). + + Overload-aware: each (name, param-types) pair is a distinct node so naga receives every + callee body, not just the first overload sharing a name.""" + if len(fns) <= 1: + return fns + + def fn_key(fn): + return (fn["name"], fn_param_types(fn["params"])) + + by_name = {} + key_to_fn = {} + for fn in fns: + k = fn_key(fn) + by_name.setdefault(fn["name"], []).append(fn) + key_to_fn[k] = fn + + local_names = set(by_name.keys()) + deps = {} + for fn in fns: + k = fn_key(fn) + dep_keys = [] + for callee in _extract_mx_calls(fn["full"], local_names): + for callee_fn in by_name.get(callee, []): + ck = fn_key(callee_fn) + if ck in key_to_fn: + dep_keys.append(ck) + deps[k] = dep_keys + + order_keys, temp, perm = [], set(), set() + + def visit(k): + if k in perm: + return + if k in temp: + return + temp.add(k) + for dk in deps.get(k, ()): + visit(dk) + temp.remove(k) + perm.add(k) + order_keys.append(k) + + for fn in fns: + visit(fn_key(fn)) + + return [key_to_fn[k] for k in order_keys] + + +def rebuild_fn_body(fns): + """Concatenate function definitions in topo order for a single naga translation unit.""" + return "\n\n".join(fn["full"] for fn in fns) + + +def expand_lib_includes(text, gllib_dir, seen=None): + """Inline `#include \"lib/...\"` bodies so naga sees full dependency definitions. + + Recursive with `seen` to break include cycles. Only `lib/` includes are expanded (not + cross-library paths). Used for per-function dep context and whole-file transpile.""" + seen = seen or set() + + def replacer(match): + inc_path = match.group(1) + if not inc_path.startswith("lib/"): + return match.group(0) + stem = Path(inc_path).stem + if stem in seen: + return "" + seen.add(stem) + inc_file = gllib_dir / (stem + ".glsl") + if not inc_file.is_file(): + return "" + inc_text = expand_lib_tokens(inc_file.read_text(encoding="utf-8")) + inc_text = expand_lib_includes(inc_text, gllib_dir, seen) + inc_text = re.sub(r'#include\s+"[^"]+"\s*\n', "", inc_text) + return inc_text + "\n" + + return re.sub(r'#include\s+"([^"]+)"\s*\n', replacer, text) + + +def _strip_wgsl_stubs(wgsl): + """Remove empty stub functions naga emits for unused GLSL prototypes.""" + out = [] + for name, text in top_level_fns(wgsl): + if "{\n}" in text and text.count("\n") <= 2: + continue + out.append(text) + return "\n\n".join(out) + + +def _match_glsl_to_wgsl_fns(glsl_fns, wgsl): + """Pair GLSL function definitions with naga output by name and overload order. + + Whole-file transpile emits every function in one module; this maps each GLSL definition to + its naga counterpart (mx_foo, mx_foo_1, ...) and renames to the genwgsl overload name.""" + wgsl_by_base = {} + for name, text in top_level_fns(wgsl): + if "{\n}" in text and text.count("\n") <= 2: + continue + wgsl_by_base.setdefault(fn_base(name), []).append((name, text)) + + outputs = [] + seen = {} + for fn in glsl_fns: + i = seen.get(fn["name"], 0) + seen[fn["name"]] = i + 1 + wgsl_list = wgsl_by_base.get(fn["name"], []) + if i >= len(wgsl_list): + return None, fn["name"] + naga_name, text = wgsl_list[i] + out_name = wgsl_fn_name(fn["name"], fn_param_types(fn["params"])) + text = re.sub(r"(\bfn\s+)" + re.escape(naga_name) + r"\b", r"\1" + out_name, text) + outputs.append((fn["name"], text)) + return outputs, None + + +def _mx_calls_in_text(text): + """Return mx_* names invoked in text (function bodies and preamble, not signatures).""" + calls = set() + fns = parse_functions(text) + if fns: + preamble = text[:text.index(fns[0]["full"])] + calls |= {m.group(1) for m in re.finditer(r"\b(mx_\w+)\s*\(", preamble)} + for fn in fns: + body = fn["full"][fn["full"].index("{") + 1:fn["full"].rfind("}")] + calls |= {m.group(1) for m in re.finditer(r"\b(mx_\w+)\s*\(", body)} + else: + calls |= {m.group(1) for m in re.finditer(r"\b(mx_\w+)\s*\(", text)} + return calls + + +def proto_block_for_body(body_src, protos, defined_sigs): + """Emit only prototypes for mx_* helpers referenced in body_src and not already defined.""" + called = _mx_calls_in_text(body_src) + lines = [] + for (nm, t), p in sorted(protos.items()): + if (nm, t) in defined_sigs: + continue + if nm in called: + lines.append(p) + return "\n".join(lines) + + +def sibling_callee_closure(target_fn, local_fns): + """Local functions transitively called by target_fn (all overloads of each called name).""" + by_name = {} + for fn in local_fns: + by_name.setdefault(fn["name"], []).append(fn) + local_names = set(by_name.keys()) + + def fn_key(fn): + return (fn["name"], fn_param_types(fn["params"])) + + needed = set() + stack = [target_fn] + while stack: + fn = stack.pop() + key = fn_key(fn) + if key in needed: + continue + needed.add(key) + for callee in _extract_mx_calls(fn["full"], local_names): + for callee_fn in by_name.get(callee, []): + if fn_key(callee_fn) not in needed: + stack.append(callee_fn) + + closure = [fn for fn in local_fns if fn_key(fn) in needed] + return sort_functions_topo(closure) + + +def file_preamble_before_fns(src): + """Struct/const text before the first function definition in a lib fragment.""" + stripped = re.sub(r'#include\s+"[^"]+"\s*\n', "", src) + fns = parse_functions(stripped) + if not fns: + return "" + return stripped[:stripped.index(fns[0]["full"])] + + +def apply_lib_wgsl_patches(stem, text): + """Stem-specific fixes for transpiled lib output (generator/runtime conventions).""" + if stem == "mx_microfacet_specular": + # Generator emits split env texture + sampler; match hand-port latlong signature. + text = re.sub( + r"fn mx_latlong_map_lookup\([^)]+\)[^{]*\{[^}]*\}", + """fn mx_latlong_map_lookup(dir: vec3f, transform: mat4x4f, lod: f32, envTex: texture_2d, envSampler: sampler) -> vec3f { + let envDir = normalize((transform * vec4f(dir, 0.0)).xyz); + let uv = mx_latlong_projection(envDir); + return textureSampleLevel(envTex, envSampler, uv, lod).rgb; +}""", + text, + count=1, + flags=re.S, + ) + return text + + +def transpile_lib_file_siblings(glsl_path, out_path, base, protos, lib_name, src, raw, header, + sampler_preamble, gllib_dir): + """Per-function transpile with transitive in-file callee bodies (naga ordering fix).""" + stem = glsl_path.stem + stripped = re.sub(r'#include\s+"[^"]+"\s*\n', "", src) + local_fns = parse_functions(stripped) + if not local_fns: + return False + + file_preamble = file_preamble_before_fns(src) + inc_lines = "\n".join( + f'#include "{i}"\n' for i in re.findall(r'#include\s+"([^"]+)"', raw) if i.startswith("lib/")) + dep_src = expand_lib_includes(inc_lines, gllib_dir) if inc_lines else "" + dep_src = re.sub(r'#include\s+"[^"]+"\s*\n', "", dep_src) + for f in local_fns: + dep_src = dep_src.replace(f["full"], "") + + dep_fns = parse_functions(dep_src) if dep_src else [] + dep_preamble = "" + if dep_fns: + dep_preamble = dep_src[:dep_src.index(dep_fns[0]["full"])] + dep_names = {fn["name"] for fn in dep_fns} + + def fn_key(fn): + return (fn["name"], fn_param_types(fn["params"])) + + outputs = [] + for target_fn in local_fns: + types = fn_param_types(target_fn["params"]) + out_name = wgsl_fn_name(target_fn["name"], types) + siblings = sibling_callee_closure(target_fn, local_fns) + + # Pull in dep-file helpers transitively called from the sibling closure. + needed_dep = [] + needed_dep_keys = set() + stack = list(siblings) + while stack: + fn = stack.pop() + for callee in _extract_mx_calls(fn["full"], dep_names): + for dep_fn in dep_fns: + if dep_fn["name"] == callee: + k = fn_key(dep_fn) + if k not in needed_dep_keys: + needed_dep_keys.add(k) + needed_dep.append(dep_fn) + stack.append(dep_fn) + + all_body_fns = sort_functions_topo(needed_dep + siblings) + callees = [fn for fn in all_body_fns if fn_key(fn) != fn_key(target_fn)] + callees = sort_functions_topo(callees) + ordered = callees + [target_fn] + sibling_body = rebuild_fn_body(ordered) + sibling_sigs = {fn_key(fn) for fn in ordered} + defined_sigs = sibling_sigs + combined = file_preamble + dep_preamble + sibling_body + proto_block = proto_block_for_body(combined, protos, defined_sigs) + glsl = ("#version 450\n" + LIB_PREAMBLE + sampler_preamble + base + "\n" + + file_preamble + dep_preamble + "\n" + proto_block + "\n" + sibling_body + + "\nlayout(location=0) out vec4 mtlx_o;\nvoid main() { mtlx_o = vec4(0.0); }\n") + wgsl, err = naga_transpile_glsl( + glsl, out_path.parent / f"_debug_lib_{stem}_{target_fn['name']}.frag" + if os.environ.get("MTLX_DEBUG") else None) + if wgsl is None: + print(f" FAIL lib/{glsl_path.name}::{target_fn['name']}: {err}") + return False + text = extract_transpiled_fn(wgsl, target_fn["name"], out_name, glsl_types=types) + if text is None: + print(f" FAIL lib/{glsl_path.name}::{target_fn['name']}: not found in naga output") + return False + # Overloaded calls: resolve by naga's actual per-module signatures (its stub numbering here + # is topological, not the sorted order remap_calls assumes), then run the shared cleanup. + text, unsupported = remap_calls_by_naga_sig(text, wgsl, target_fn["name"]) + if unsupported: + print(f" FAIL lib/{glsl_path.name}::{target_fn['name']}: unmapped call(s) {unsupported}") + return False + text, _ = postprocess_wgsl_fn(text, target_fn["name"], protos, remap=False) + outputs.append(text) + + structs = transpile_glsl_structs(stripped) + consts = transpile_glsl_consts(stripped) + preamble = ("\n\n".join(structs + consts) + "\n\n") if structs or consts else "" + src_rel = f"libraries/{lib_name}/genglsl/lib/{glsl_path.name}" + banner = (f"// Generated from {src_rel} by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py.\n" + f"// Do not edit -- re-run the transpiler to regenerate " + f"(see source/MaterialXGenWgsl/README.md).\n\n") + all_consts = glsl_const_names(base) + const_names_from_wgsl(consts) + body = resolve_const_refs("\n\n".join(outputs), all_consts) + body = apply_lib_wgsl_patches(stem, body) + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(banner + header + preamble + body + "\n", encoding="utf-8") + print(f" OK lib/{glsl_path.name} -> {out_path}") + return True + + +def transpile_lib_file(glsl_path, out_path, base, protos, lib_name): + """Transpile one genglsl/lib/*.glsl file to genwgsl/lib/*.wgsl. + + Dispatches to _transpile_closure_type_lib, transpile_lib_file_siblings, or per-function + transpile (default). Output: generated banner + #include lines + structs/consts + fn bodies.""" + raw = glsl_path.read_text(encoding="utf-8") + src = expand_lib_tokens(raw) + stem = glsl_path.stem + sampler_preamble = TEXTURE_STUB_PREAMBLE if lib_needs_samplers(raw) else "" + + if stem == "mx_closure_type": + return _transpile_closure_type_lib(glsl_path, out_path, base, protos, lib_name) + + includes = [f'#include "{inc.replace(".glsl", ".wgsl")}"' + for inc in re.findall(r'#include\s+"([^"]+)"', raw)] + header = ("\n".join(includes) + "\n\n") if includes else "" + gllib_dir = glsl_path.parent + + if stem in LIB_USE_SIBLING_BODIES: + return transpile_lib_file_siblings(glsl_path, out_path, base, protos, lib_name, src, raw, + header, sampler_preamble, gllib_dir) + + local_fns = parse_functions(re.sub(r'#include\s+"[^"]+"\s*\n', "", src)) + if not local_fns: + print(f" SKIP lib/{glsl_path.name}: no functions found") + return False + + local_sigs = {(fn["name"], fn_param_types(fn["params"])) for fn in local_fns} + local_names = {fn["name"] for fn in local_fns} + inc_lines = "\n".join( + f'#include "{i}"\n' for i in re.findall(r'#include\s+"([^"]+)"', raw) if i.startswith("lib/")) + dep_src = expand_lib_includes(inc_lines, gllib_dir) if inc_lines else "" + dep_src = re.sub(r'#include\s+"[^"]+"\s*\n', "", dep_src) + # Remove local function bodies from dep_src — only the target fn body is transpiled per pass. + for f in local_fns: + dep_src = dep_src.replace(f["full"], "") + + outputs = [] + for fn in local_fns: + types = fn_param_types(fn["params"]) + out_name = wgsl_fn_name(fn["name"], types) + proto_block = "\n".join( + p for (nm, t), p in sorted(protos.items()) + if not (nm == fn["name"] and t == types)) + glsl = ("#version 450\n" + LIB_PREAMBLE + sampler_preamble + base + "\n" + + dep_src + "\n" + proto_block + "\n" + fn["full"] + + "\nlayout(location=0) out vec4 mtlx_o;\nvoid main() { mtlx_o = vec4(0.0); }\n") + wgsl, err = naga_transpile_glsl( + glsl, out_path.parent / f"_debug_lib_{stem}_{fn['name']}.frag" + if os.environ.get("MTLX_DEBUG") else None) + if wgsl is None: + print(f" FAIL lib/{glsl_path.name}::{fn['name']}: {err}") + return False + text = extract_transpiled_fn(wgsl, fn["name"], out_name) + if text is None: + print(f" FAIL lib/{glsl_path.name}::{fn['name']}: not found in naga output") + return False + text, unsupported = postprocess_wgsl_fn(text, fn["name"], protos, remap=True) + if unsupported: + print(f" FAIL lib/{glsl_path.name}::{fn['name']}: unmapped call(s) {unsupported}") + return False + outputs.append(text) + + structs = transpile_glsl_structs(re.sub(r'#include\s+"[^"]+"\s*\n', "", src)) + consts = transpile_glsl_consts(re.sub(r'#include\s+"[^"]+"\s*\n', "", src)) + preamble_parts = structs + consts + preamble = ("\n\n".join(preamble_parts) + "\n\n") if preamble_parts else "" + + src_rel = f"libraries/{lib_name}/genglsl/lib/{glsl_path.name}" + banner = (f"// Generated from {src_rel} by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py.\n" + f"// Do not edit -- re-run the transpiler to regenerate " + f"(see source/MaterialXGenWgsl/README.md).\n\n") + all_consts = glsl_const_names(base) + const_names_from_wgsl(consts) + body = resolve_const_refs("\n\n".join(outputs), all_consts) + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(banner + header + preamble + body + "\n", encoding="utf-8") + print(f" OK lib/{glsl_path.name} -> {out_path}") + return True + + +def _transpile_closure_type_lib(glsl_path, out_path, base, protos, lib_name): + """Special handler for mx_closure_type: prepend shared structs, transpile makeClosureData. + + GLSL only defines ClosureData + constructor macro; genwgsl nodes expect BSDF/VDF/FresnelData + types from this file. Struct block is static (LIB_STRUCT_PREAMBLE); functions come from naga.""" + src = expand_lib_tokens(glsl_path.read_text(encoding="utf-8")) + outputs = [] + for fn in parse_functions(src): + types = fn_param_types(fn["params"]) + out_name = wgsl_fn_name(fn["name"], types) + proto_block = "\n".join( + p for (nm, t), p in sorted(protos.items()) + if not (nm == fn["name"] and t == types)) + glsl = ("#version 450\n" + LIB_PREAMBLE + base + "\n" + proto_block + + "\n" + fn["full"] + + "\nlayout(location=0) out vec4 mtlx_o;\nvoid main() { mtlx_o = vec4(0.0); }\n") + wgsl, err = naga_transpile_glsl(glsl) + if wgsl is None: + print(f" FAIL lib/{glsl_path.name}::{fn['name']}: {err}") + return False + text = extract_transpiled_fn(wgsl, fn["name"], out_name) + if text is None: + print(f" FAIL lib/{glsl_path.name}::{fn['name']}: not found in naga output") + return False + text, unsupported = postprocess_wgsl_fn(text, fn["name"], protos, remap=False) + if unsupported: + print(f" FAIL lib/{glsl_path.name}::{fn['name']}: {unsupported}") + return False + outputs.append(apply_field_renames(text)) + + src_rel = f"libraries/{lib_name}/genglsl/lib/{glsl_path.name}" + banner = (f"// Generated from {src_rel} by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py.\n" + f"// Do not edit -- re-run the transpiler to regenerate " + f"(see source/MaterialXGenWgsl/README.md).\n\n") + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(banner + LIB_STRUCT_PREAMBLE.strip() + "\n\n" + "\n\n".join(outputs) + "\n", + encoding="utf-8") + print(f" OK lib/{glsl_path.name} -> {out_path}") + return True + + +def transpile_libs(libroot, outroot, libs, base, protos, only=None): + """Transpile all genglsl/lib/*.glsl helpers to genwgsl/lib/*.wgsl. + + Runs before node transpilation in main(). Returns a list of stems that failed. + Uses build_context(nodes=False) for lib-only prototypes.""" + ok = failed = 0 + unexpected = [] + # Lib-only context: no node protos (inout) and no duplicate closure preamble. + lib_base, lib_protos, _ = build_context(libroot, libs, nodes=False) + print("\nTranspiling genglsl/lib/ helpers...") + for lib in libs: + if lib in HANDWRITTEN_LIB_DIRS: + print(f" KEEP {lib}/genwgsl/lib (hand-written by project choice; not transpiled)") + continue + gllib = libroot / lib / "genglsl" / "lib" + if not gllib.is_dir(): + continue + lib_files = topo_sort_lib_files(sorted(gllib.glob("*.glsl"))) + for glsl in lib_files: + if glsl.stem in LIB_KEEP_HANDWRITTEN: + print(f" KEEP lib/{glsl.name} (hand-written; naga cannot express its sampler bindings)") + continue + if only and glsl.stem not in only: + continue + out = outroot / lib / "genwgsl" / "lib" / (glsl.stem + ".wgsl") + if transpile_lib_file(glsl, out, lib_base, lib_protos, lib): + ok += 1 + else: + failed += 1 + unexpected.append(glsl.stem) + print(f"Lib done: {ok} transpiled, {failed} failures.") + return unexpected + + +# ---------------------------------------------------------------------------- node driver def transpile(node_path, out_path, base, protos, lib_symbols): node_src = node_path.read_text(encoding="utf-8") @@ -586,13 +1531,15 @@ def main(): libroot, outroot = Path(args.libraries), Path(args.out) libs = discover_libs(libroot) - # Build the two reference tables once, up front: the GLSL parse context (shared by every node) - # and the genwgsl lib's real signatures (for the arity check). `_overloaded` is unused here -- - # remap_calls drives off CALL_MAP -- but build_context returns it for callers that want it. base, protos, _overloaded = build_context(libroot, libs) - lib_symbols = build_wgsl_lib_symbols(libroot, libs) + + # Lib helpers first: nodes #include genwgsl/lib/*.wgsl and check_lib_arity reads fresh output. + lib_unexpected = transpile_libs(libroot, outroot, libs, base, protos, only=args.only) + lib_symbols = build_wgsl_lib_symbols(libroot, libs, outroot=outroot) + ok = skip = 0 - expected, unexpected = [], [] # node stems that failed transpile, by category + node_expected, node_unexpected = [], [] + print("\nTranspiling genglsl node fragments...") for lib in libs: gldir = libroot / lib / "genglsl" if not gldir.is_dir(): @@ -614,15 +1561,15 @@ def main(): print(f" WARN {glsl.name}: now transpiles cleanly; remove it from " f"EXPECTED_FALLBACK and commit the generated version.") elif base_name in EXPECTED_FALLBACK: - expected.append(base_name) # known hand-written fallback -- not a build error + node_expected.append(base_name) else: - unexpected.append(base_name) # regression: should have generated, but didn't + node_unexpected.append(base_name) - print(f"\nDone: {ok} transpiled, {skip} skipped, " - f"{len(expected)} expected fallbacks, {len(unexpected)} unexpected failures.") - if unexpected: - # Surface as an error so a CMake build step fails (see MATERIALX_GENERATE_WGSL_LIBRARY). - print("ERROR: nodes that should transpile failed: " + ", ".join(sorted(unexpected))) + print(f"\nDone: {ok} nodes transpiled, {skip} skipped, " + f"{len(node_expected)} expected fallbacks, {len(node_unexpected)} unexpected failures.") + all_unexpected = lib_unexpected + node_unexpected + if all_unexpected: + print("ERROR: files that should transpile failed: " + ", ".join(sorted(set(all_unexpected)))) return 1 return 0 From 5381863b00748a7739d82cfc1524e1ffff5a95cd Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Mon, 20 Jul 2026 17:14:19 -0700 Subject: [PATCH 12/19] Implement WGSL prefilter env generation and update supporting shader libs --- .../genglsl/lib/mx_microfacet_specular.glsl | 32 ++++----- .../genwgsl/lib/mx_environment_fis.wgsl | 50 +++++++------- .../genwgsl/lib/mx_environment_prefilter.wgsl | 30 +++++++++ .../genwgsl/lib/mx_generate_albedo_table.wgsl | 10 +-- .../lib/mx_generate_prefilter_env.wgsl | 67 +++++++++++++++++++ libraries/pbrlib/genwgsl/lib/mx_shadow.wgsl | 5 +- .../genwgsl/lib/mx_shadow_platform.wgsl | 8 +-- 7 files changed, 148 insertions(+), 54 deletions(-) create mode 100644 libraries/pbrlib/genwgsl/lib/mx_environment_prefilter.wgsl create mode 100644 libraries/pbrlib/genwgsl/lib/mx_generate_prefilter_env.wgsl diff --git a/libraries/pbrlib/genglsl/lib/mx_microfacet_specular.glsl b/libraries/pbrlib/genglsl/lib/mx_microfacet_specular.glsl index d706e11f6d..b239c78ac1 100644 --- a/libraries/pbrlib/genglsl/lib/mx_microfacet_specular.glsl +++ b/libraries/pbrlib/genglsl/lib/mx_microfacet_specular.glsl @@ -22,8 +22,8 @@ struct FresnelData float exponent; // Thin film - float tf_thickness; - float tf_ior; + float thinfilm_thickness; + float thinfilm_ior; // Refraction bool refraction; @@ -306,7 +306,7 @@ vec3 mx_fresnel_airy(float cosTheta, FresnelData fd) // Assume vacuum on the outside float eta1 = 1.0; - float eta2 = max(fd.tf_ior, eta1); + float eta2 = max(fd.thinfilm_ior, eta1); vec3 eta3 = (fd.model == FRESNEL_MODEL_SCHLICK) ? mx_f0_to_ior(fd.F0) : fd.ior; vec3 kappa3 = (fd.model == FRESNEL_MODEL_SCHLICK) ? vec3(0.0) : fd.extinction; float cosThetaT = sqrt(1.0 - (1.0 - mx_square(cosTheta)) * mx_square(eta1 / eta2)); @@ -356,7 +356,7 @@ vec3 mx_fresnel_airy(float cosTheta, FresnelData fd) vec3 Cm, Sm; // Optical path difference - float distMeters = fd.tf_thickness * 1.0e-9; + float distMeters = fd.thinfilm_thickness * 1.0e-9; float opd = 2.0 * eta2 * cosThetaT * distMeters; // Iridescence term using spectral antialiasing for Parallel polarization @@ -398,53 +398,53 @@ vec3 mx_fresnel_airy(float cosTheta, FresnelData fd) return I; } -FresnelData mx_init_fresnel_dielectric(float ior, float tf_thickness, float tf_ior) +FresnelData mx_init_fresnel_dielectric(float ior, float thinfilm_thickness, float thinfilm_ior) { FresnelData fd; fd.model = FRESNEL_MODEL_DIELECTRIC; - fd.airy = tf_thickness > 0.0; + fd.airy = thinfilm_thickness > 0.0; fd.ior = vec3(ior); fd.extinction = vec3(0.0); fd.F0 = vec3(0.0); fd.F82 = vec3(0.0); fd.F90 = vec3(0.0); fd.exponent = 0.0; - fd.tf_thickness = tf_thickness; - fd.tf_ior = tf_ior; + fd.thinfilm_thickness = thinfilm_thickness; + fd.thinfilm_ior = thinfilm_ior; fd.refraction = false; return fd; } -FresnelData mx_init_fresnel_conductor(vec3 ior, vec3 extinction, float tf_thickness, float tf_ior) +FresnelData mx_init_fresnel_conductor(vec3 ior, vec3 extinction, float thinfilm_thickness, float thinfilm_ior) { FresnelData fd; fd.model = FRESNEL_MODEL_CONDUCTOR; - fd.airy = tf_thickness > 0.0; + fd.airy = thinfilm_thickness > 0.0; fd.ior = ior; fd.extinction = extinction; fd.F0 = vec3(0.0); fd.F82 = vec3(0.0); fd.F90 = vec3(0.0); fd.exponent = 0.0; - fd.tf_thickness = tf_thickness; - fd.tf_ior = tf_ior; + fd.thinfilm_thickness = thinfilm_thickness; + fd.thinfilm_ior = thinfilm_ior; fd.refraction = false; return fd; } -FresnelData mx_init_fresnel_schlick(vec3 F0, vec3 F82, vec3 F90, float exponent, float tf_thickness, float tf_ior) +FresnelData mx_init_fresnel_schlick(vec3 F0, vec3 F82, vec3 F90, float exponent, float thinfilm_thickness, float thinfilm_ior) { FresnelData fd; fd.model = FRESNEL_MODEL_SCHLICK; - fd.airy = tf_thickness > 0.0; + fd.airy = thinfilm_thickness > 0.0; fd.ior = vec3(0.0); fd.extinction = vec3(0.0); fd.F0 = F0; fd.F82 = F82; fd.F90 = F90; fd.exponent = exponent; - fd.tf_thickness = tf_thickness; - fd.tf_ior = tf_ior; + fd.thinfilm_thickness = thinfilm_thickness; + fd.thinfilm_ior = thinfilm_ior; fd.refraction = false; return fd; } diff --git a/libraries/pbrlib/genwgsl/lib/mx_environment_fis.wgsl b/libraries/pbrlib/genwgsl/lib/mx_environment_fis.wgsl index e2a78be4ae..7e062e2bec 100644 --- a/libraries/pbrlib/genwgsl/lib/mx_environment_fis.wgsl +++ b/libraries/pbrlib/genwgsl/lib/mx_environment_fis.wgsl @@ -1,65 +1,65 @@ #include "mx_microfacet_specular.wgsl" +// This shader cannot be pre-transpiled without generator specific binding information. -// Filtered Importance Sampling environment lighting. -// http://cgg.mff.cuni.cz/~jaroslav/papers/2008-egsr-fis/2008-egsr-fis-final-embedded.pdf -// -// Iterates envRadianceSamples per pixel with GGX VNDF importance sampling. -// -// Requires (provided by the generator's shader template): -// envRadiance : texture_2d — lat-long radiance environment map -// envRadianceSampler : sampler -// envIrradiance : texture_2d — lat-long irradiance environment map -// envIrradianceSampler : sampler -// envRadianceMips : i32 — number of mip levels in envRadiance -// envRadianceSamples : i32 — number of importance samples -// envMatrix : mat4x4f — environment rotation matrix -// envLightIntensity : f32 — environment light intensity multiplier -// -// Dependencies from mx_microfacet_specular.wgsl: -// mx_average_alpha, mx_ggx_smith_G1, mx_ggx_smith_G2, mx_ggx_NDF, -// mx_ggx_importance_sample_VNDF, mx_compute_fresnel, -// mx_refraction_solid_sphere, mx_latlong_map_lookup, mx_latlong_compute_lod -// Dependencies from mx_microfacet.wgsl: -// mx_spherical_fibonacci - -fn mx_environment_radiance(N: vec3f, V_in: vec3f, X_in: vec3f, alpha: vec2f, distribution: i32, fd: FresnelData) -> vec3f { +fn mx_environment_radiance(N: vec3f, V_in: vec3f, X_in: vec3f, alpha: vec2f, distribution: i32, fd: FresnelData) -> vec3f +{ + // Generate tangent frame. let X = normalize(X_in - dot(X_in, N) * N); let Y = cross(N, X); let tangentToWorld = mat3x3f(X, Y, N); + // Transform the view vector to tangent space. let V = vec3f(dot(V_in, X), dot(V_in, Y), dot(V_in, N)); + // Compute derived properties. let NdotV = clamp(V.z, M_FLOAT_EPS, 1.0); let avgAlpha = mx_average_alpha(alpha); let G1V = mx_ggx_smith_G1(NdotV, avgAlpha); + // Integrate outgoing radiance using filtered importance sampling. + // http://cgg.mff.cuni.cz/~jaroslav/papers/2008-egsr-fis/2008-egsr-fis-final-embedded.pdf var radiance = vec3f(0.0); let envRadianceSamples: i32 = $envRadianceSamples; - for (var i: i32 = 0; i < envRadianceSamples; i++) { + for (var i: i32 = 0; i < envRadianceSamples; i++) + { let Xi = mx_spherical_fibonacci(i, envRadianceSamples); + // Compute the half vector and incoming light direction. let H = mx_ggx_importance_sample_VNDF(Xi, V, alpha); // For surface transmission, follow the refracted ray through a solid sphere; otherwise reflect. let L = select(-reflect(V, H), mx_refraction_solid_sphere(-V, H, fd.ior.x), fd.refraction); - + // Compute dot products for this sample. let NdotL = clamp(L.z, M_FLOAT_EPS, 1.0); let VdotH = clamp(dot(V, H), M_FLOAT_EPS, 1.0); + // Sample the environment light from the given direction. let Lw = tangentToWorld * L; let pdf = mx_ggx_NDF(H, alpha) * G1V / (4.0 * NdotV); let lod = mx_latlong_compute_lod(Lw, pdf, f32($envRadianceMips - 1), envRadianceSamples); let sampleColor = mx_latlong_map_lookup(Lw, $envMatrix, lod, $envRadiance, $envRadianceSampler); + // Compute the Fresnel term. let F = mx_compute_fresnel(VdotH, fd); + + // Compute the geometric term. let G = mx_ggx_smith_G2(NdotL, NdotV, avgAlpha); + // The combined FG term simplifies to inverted Fresnel for refraction. let FG = select(F * G, vec3f(1.0) - F, fd.refraction); + // Add the radiance contribution of this sample. + // From https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf + // incidentLight = sampleColor * NdotL + // microfacetSpecular = D * F * G / (4 * NdotL * NdotV) + // pdf = D * G1V / (4 * NdotV); + // radiance = incidentLight * microfacetSpecular / pdf radiance += sampleColor * FG; } + // Apply the global component of the geometric term and normalize. radiance /= G1V * f32(envRadianceSamples); + // Return the final radiance. if ($envRadianceSamples == 0) { return vec3f(0.0); } diff --git a/libraries/pbrlib/genwgsl/lib/mx_environment_prefilter.wgsl b/libraries/pbrlib/genwgsl/lib/mx_environment_prefilter.wgsl new file mode 100644 index 0000000000..e87f9e1885 --- /dev/null +++ b/libraries/pbrlib/genwgsl/lib/mx_environment_prefilter.wgsl @@ -0,0 +1,30 @@ +#include "mx_microfacet_specular.wgsl" +// This shader cannot be pre-transpiled without generator specific binding information. + +// Return the mip level associated with the given alpha in a prefiltered environment. +fn mx_latlong_alpha_to_lod(alpha: f32) -> f32 +{ + let lodBias = select(0.5 * alpha + 0.375, sqrt(alpha), alpha < 0.25); + return lodBias * f32($envRadianceMips - 1); +} + +fn mx_environment_radiance(N_in: vec3f, V: vec3f, X: vec3f, alpha: vec2f, distribution: i32, fd: FresnelData) -> vec3f +{ + let N = mx_forward_facing_normal(N_in, V); + let L = select(-reflect(V, N), mx_refraction_solid_sphere(-V, N, fd.ior.x), fd.refraction); + + let NdotV = clamp(dot(N, V), M_FLOAT_EPS, 1.0); + + let avgAlpha = mx_average_alpha(alpha); + var FG = mx_ggx_dir_albedo_fresnel(NdotV, avgAlpha, fd); + FG = select(FG, vec3f(1.0) - FG, fd.refraction); + + let Li = mx_latlong_map_lookup(L, $envMatrix, mx_latlong_alpha_to_lod(avgAlpha), $envRadiance, $envRadianceSampler); + return Li * FG * $envLightIntensity; +} + +fn mx_environment_irradiance(N: vec3f) -> vec3f +{ + let Li = mx_latlong_map_lookup(N, $envMatrix, 0.0, $envIrradiance, $envIrradianceSampler); + return Li * $envLightIntensity; +} diff --git a/libraries/pbrlib/genwgsl/lib/mx_generate_albedo_table.wgsl b/libraries/pbrlib/genwgsl/lib/mx_generate_albedo_table.wgsl index e908f395f6..b661b52e32 100644 --- a/libraries/pbrlib/genwgsl/lib/mx_generate_albedo_table.wgsl +++ b/libraries/pbrlib/genwgsl/lib/mx_generate_albedo_table.wgsl @@ -1,13 +1,9 @@ #include "mx_microfacet_sheen.wgsl" #include "mx_microfacet_specular.wgsl" +// This shader cannot be pre-transpiled without generator specific binding information. -// Bake shader: generates a directional albedo lookup table. -// The generator should provide $albedoTableSize as a uniform or constant. -// -// Usage: dispatch as a fullscreen fragment shader; gl_FragCoord maps to table UV. -// Output: vec3(ggxDirAlbedo.x, ggxDirAlbedo.y, sheenDirAlbedo) - -fn mx_generate_dir_albedo_table(fragCoord: vec2f, albedoTableSize: f32) -> vec3f { +fn mx_generate_dir_albedo_table(fragCoord: vec2f, albedoTableSize: f32) -> vec3f +{ let uv = fragCoord / albedoTableSize; let ggxDirAlbedo = mx_ggx_dir_albedo(uv.x, uv.y, vec3f(1.0, 0.0, 0.0), vec3f(0.0, 1.0, 0.0)).xy; let sheenDirAlbedo = mx_imageworks_sheen_dir_albedo(uv.x, uv.y); diff --git a/libraries/pbrlib/genwgsl/lib/mx_generate_prefilter_env.wgsl b/libraries/pbrlib/genwgsl/lib/mx_generate_prefilter_env.wgsl new file mode 100644 index 0000000000..a0c826efd6 --- /dev/null +++ b/libraries/pbrlib/genwgsl/lib/mx_generate_prefilter_env.wgsl @@ -0,0 +1,67 @@ +#include "mx_microfacet_specular.wgsl" +// This shader cannot be pre-transpiled without generator specific binding information. + +// Return the alpha associated with the given mip level in a prefiltered environment. +fn mx_latlong_lod_to_alpha(lod: f32) -> f32 +{ + let lodBias = lod / f32($envRadianceMips - 1); + return select(2.0 * (lodBias - 0.375), mx_square_f32(lodBias), lodBias < 0.5); +} + +// The inverse of mx_latlong_projection. +fn mx_latlong_map_projection_inverse(uv: vec2f) -> vec3f +{ + let latitude = (uv.y - 0.5) * M_PI; + let longitude = (uv.x - 0.5) * M_PI * 2.0; + + let x = -cos(latitude) * sin(longitude); + let y = -sin(latitude); + let z = cos(latitude) * cos(longitude); + + return vec3f(x, y, z); +} + +// `fragCoord` is the pixel center (gl_FragCoord.xy equivalent); +// `envRadianceSize` is the radiancemap's base-mip size in texels (textureDimensions of the radiance map). +fn mx_generate_prefilter_env(fragCoord: vec2f, envRadianceSize: vec2f) -> vec3f +{ + // The tangent view vector is aligned with the normal. + let V = vec3f(0.0, 0.0, 1.0); + let NdotV = 1.0; + + // Compute derived properties. + let uv = fragCoord * pow(2.0, f32($envPrefilterMip)) / envRadianceSize; + let worldN = mx_latlong_map_projection_inverse(uv); + let tangentToWorld = mx_orthonormal_basis(worldN); + let alpha = mx_latlong_lod_to_alpha(f32($envPrefilterMip)); + let G1V = mx_ggx_smith_G1(NdotV, alpha); + + // Integrate the LD term for the given environment and alpha. + var radiance = vec3f(0.0); + var weight = 0.0; + let envRadianceSamples: i32 = 1024; + for (var i: i32 = 0; i < envRadianceSamples; i++) + { + let Xi = mx_spherical_fibonacci(i, envRadianceSamples); + + // Compute the half vector and incoming light direction. + let H = mx_ggx_importance_sample_VNDF(Xi, V, vec2f(alpha)); + let L = -V + 2.0 * H.z * H; + + // Compute the geometric term for this sample. + let NdotL = clamp(L.z, M_FLOAT_EPS, 1.0); + let G = mx_ggx_smith_G2(NdotL, NdotV, alpha); + + // Sample the environment light from the given direction. + let Lw = tangentToWorld * L; + let pdf = mx_ggx_VNDF_reflection_PDF(H, vec2f(alpha), G1V, NdotV); + let lod = mx_latlong_compute_lod(Lw, pdf, f32($envRadianceMips - 1), envRadianceSamples); + let sampleColor = mx_latlong_map_lookup(Lw, $envMatrix, lod, $envRadiance, $envRadianceSampler); + + // Add the radiance contribution of this sample. + radiance += G * sampleColor; + weight += G; + } + + return radiance / weight; +} diff --git a/libraries/pbrlib/genwgsl/lib/mx_shadow.wgsl b/libraries/pbrlib/genwgsl/lib/mx_shadow.wgsl index ff560baa8e..9cb645f72a 100644 --- a/libraries/pbrlib/genwgsl/lib/mx_shadow.wgsl +++ b/libraries/pbrlib/genwgsl/lib/mx_shadow.wgsl @@ -1,7 +1,10 @@ +// This shader cannot be pre-transpiled since it needs glsl intrinsics . + // Variance shadow mapping utilities. // https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-8-summed-area-variance-shadow-maps -fn mx_variance_shadow_occlusion(moments: vec2f, fragmentDepth: f32) -> f32 { +fn mx_variance_shadow_occlusion(moments: vec2f, fragmentDepth: f32) -> f32 +{ let MIN_VARIANCE = 0.00001; // One-tailed inequality valid if fragmentDepth > moments.x. diff --git a/libraries/pbrlib/genwgsl/lib/mx_shadow_platform.wgsl b/libraries/pbrlib/genwgsl/lib/mx_shadow_platform.wgsl index 089338b693..718c98168c 100644 --- a/libraries/pbrlib/genwgsl/lib/mx_shadow_platform.wgsl +++ b/libraries/pbrlib/genwgsl/lib/mx_shadow_platform.wgsl @@ -1,9 +1,7 @@ -// Platform-specific shadow map lookup. -// Requires (provided by the renderer's shader template): -// shadowMap : texture_2d — variance shadow map -// shadowMapSampler : sampler — sampler for shadowMap +// This shader cannot be pre-transpiled without generator specific binding information. -fn mx_shadow_occlusion(shadow_matrix: mat4x4f, world_position: vec3f) -> f32 { +fn mx_shadow_occlusion(shadow_matrix: mat4x4f, world_position: vec3f) -> f32 +{ let shadowCoord4 = (shadow_matrix * vec4f(world_position, 1.0)); let shadowCoord = shadowCoord4.xyz / shadowCoord4.w * 0.5 + 0.5; let shadowMoments = textureSample(shadowMap, shadowMapSampler, shadowCoord.xy).xy; From 774d01a6c49a63f1247d7a0e70813c3229e14211 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Mon, 20 Jul 2026 17:21:58 -0700 Subject: [PATCH 13/19] Rename glsl_to_wgsl --- source/MaterialXGenWgsl/CMakeLists.txt | 4 +- source/MaterialXGenWgsl/README.md | 4 +- source/MaterialXGenWgsl/tools/README.md | 265 ++++++++++++------ .../tools/{glsl_to_wgsl.py => mxgenwgsl.py} | 0 4 files changed, 179 insertions(+), 94 deletions(-) rename source/MaterialXGenWgsl/tools/{glsl_to_wgsl.py => mxgenwgsl.py} (100%) diff --git a/source/MaterialXGenWgsl/CMakeLists.txt b/source/MaterialXGenWgsl/CMakeLists.txt index 07ab716bca..b1d4361039 100644 --- a/source/MaterialXGenWgsl/CMakeLists.txt +++ b/source/MaterialXGenWgsl/CMakeLists.txt @@ -13,7 +13,7 @@ mx_add_library(MaterialXGenWgsl MATERIALX_GENWGSL_EXPORTS) # Optional build-time regeneration of the genwgsl shader-node library from genglsl. This runs the -# offline transpiler (tools/glsl_to_wgsl.py) over libraries/ into the build tree purely for +# offline transpiler (tools/mxgenwgsl.py) over libraries/ into the build tree purely for # validation: transpile or naga errors -- and any previously-generable node that regresses -- make # the build fail. The generated genwgsl .wgsl node files are NOT committed; they are produced from # genglsl (the single source of truth) by CI, which runs the transpiler with `--out libraries` to @@ -83,7 +83,7 @@ if(MATERIALX_GENERATE_WGSL_LIBRARY) add_custom_target(MaterialXGenWgslLibrary ALL COMMAND ${CMAKE_COMMAND} -E env "NAGA=${MATERIALX_NAGA_EXECUTABLE}" - "${_wgsl_python}" "${CMAKE_CURRENT_SOURCE_DIR}/tools/glsl_to_wgsl.py" + "${_wgsl_python}" "${CMAKE_CURRENT_SOURCE_DIR}/tools/mxgenwgsl.py" --libraries "${PROJECT_SOURCE_DIR}/libraries" --out "${CMAKE_BINARY_DIR}/genwgsl_generated" WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" diff --git a/source/MaterialXGenWgsl/README.md b/source/MaterialXGenWgsl/README.md index 7140cfc5c8..a92814a91e 100644 --- a/source/MaterialXGenWgsl/README.md +++ b/source/MaterialXGenWgsl/README.md @@ -22,7 +22,7 @@ its node library is a *hybrid*, and that is the main thing to understand before before node transpilation). All 22 `genglsl/lib` files transpile via naga (including prefilter environment helpers). They are also **not committed**. * **`mx_chiang_hair_bsdf` is hand-written** and intentionally *not* generated (naga limitation on - hair scattering helpers). It is listed in `EXPECTED_FALLBACK` in `tools/glsl_to_wgsl.py`. + hair scattering helpers). It is listed in `EXPECTED_FALLBACK` in `tools/mxgenwgsl.py`. * **Texture / image and light nodes are hand-written** too — the transpiler skips them (naga's GLSL front-end has no sampler support, and light shaders use the dynamically generated `LightData`). @@ -37,7 +37,7 @@ CLI and running the transpiler) on the jobs that build or ship WGSL — the web sdist/wheels, and the tagged-release archives. Locally, populate the library in place with: ``` -python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries +python source/MaterialXGenWgsl/tools/mxgenwgsl.py --libraries libraries --out libraries ``` then configure with `-DMATERIALX_BUILD_GEN_WGSL=ON`. The transpiler exits non-zero on any lib or diff --git a/source/MaterialXGenWgsl/tools/README.md b/source/MaterialXGenWgsl/tools/README.md index 9af59a31af..9699dc7e79 100644 --- a/source/MaterialXGenWgsl/tools/README.md +++ b/source/MaterialXGenWgsl/tools/README.md @@ -1,130 +1,215 @@ # genwgsl library transpiler -`glsl_to_wgsl.py` transpiles the MaterialX **genglsl shader-node** fragments -(`libraries/{stdlib,pbrlib,lights}/genglsl/mx_*.glsl`) and **genglsl/lib/** helpers -(`libraries/{stdlib,pbrlib}/genglsl/lib/*.glsl`) into their **genwgsl** WGSL equivalents. -It exists to keep the WGSL node library in sync with the GLSL originals: the genwgsl nodes were -originally hand-ported and repeatedly drifted as genglsl improved, so most of them are now -generated, and re-running the tool (then diffing) surfaces drift. +`mxgenwgsl.py` generates the MaterialX **genwgsl** WGSL node library from the existing +**genglsl** sources, so GLSL stays the single source of truth and the WGSL library never has to be +hand-ported. It transpiles: + +* **shader-node fragments** — `libraries/{stdlib,pbrlib,lights}/genglsl/mx_*.glsl` +* **`genglsl/lib/` helpers** — `libraries/{stdlib,pbrlib}/genglsl/lib/*.glsl` + +into their `genwgsl` equivalents. The generated `.wgsl` files are **derived artifacts** (not +committed): CI regenerates them before building/shipping WGSL, and you regenerate them locally while +working on the WGSL target. Re-running is idempotent — the output is deterministic (no timestamps; +overload stubs indexed by declaration order), so regenerating after a genglsl change just brings +genwgsl back in sync. + +## Requirements + +* **Python 3** (standard library only for the core transpile). +* **[`naga`](https://github.com/gfx-rs/wgpu/tree/trunk/naga) CLI** (`naga-cli`, v29+). Install with + `cargo install naga-cli`. Resolved from `--naga `, then `$NAGA`, then `PATH`. +* **Optional: [`tree-sitter-language-pack`](https://pypi.org/project/tree-sitter-language-pack/)**, + for the readability cleanup pass (see [WGSL cleanup](#wgsl-cleanup)). It ships a precompiled WGSL + grammar (and pulls the `tree-sitter` runtime), so no Node or `tree-sitter-cli` is needed: + `pip install -r source/MaterialXGenWgsl/tools/requirements-transpile.txt`. If it is missing the + transpiler still runs and emits (more verbose) naga output. + +## Usage ``` -python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries -python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --only mx_noise3d_float mx_sheen_bsdf +# Regenerate the whole library in place (what CI runs, and the usual local command): +python source/MaterialXGenWgsl/tools/mxgenwgsl.py --libraries libraries --out libraries + +# Emit to a staging dir instead of overwriting the runtime library: +python source/MaterialXGenWgsl/tools/mxgenwgsl.py --libraries libraries --out build/genwgsl_generated + +# Iterate on specific files (node or lib stems): +python source/MaterialXGenWgsl/tools/mxgenwgsl.py --libraries libraries --only mx_noise3d_float mx_sheen_bsdf ``` -Requires [`naga`](https://github.com/gfx-rs/wgpu/tree/trunk/naga) (`naga-cli`, v29+) on `PATH` or at -`%NAGA%`. The generated node and `lib/` files are **not committed** — they are derived artifacts -produced from genglsl (the single source of truth). Passing `--out libraries` writes each generated -file straight into `libraries//genwgsl/`, populating the runtime library in place alongside the -hand-written texture/light/`EXPECTED_FALLBACK` nodes. CI runs exactly this to build/ship WGSL; run it locally -to work on the WGSL target. Use `--out ` to emit to a staging dir instead. +`--out libraries` writes each generated file straight into `libraries//genwgsl/`, populating +the runtime library in place alongside the hand-written texture/light/`EXPECTED_FALLBACK` nodes. +Set `MTLX_DEBUG=1` to dump the failing GLSL fragment for any node that naga rejects (see +[Troubleshooting](#troubleshooting)). + +### Exit codes + +`0` on success. A non-zero exit means an **unexpected** failure — a regression such as a genglsl +change that broke a previously-generable node, a new naga error, or a scan-driven validation +failure. Nodes listed in `EXPECTED_FALLBACK` failing to transpile is normal and does **not** fail +the run. ## Scope -* **In:** standalone node `.glsl` files referenced by `file=` impls in stdlib/pbrlib/lights, and - `genglsl/lib/*.glsl` helper files referenced by those nodes. -* **Out:** texture/image and light nodes (naga's GLSL frontend has no sampler / dynamic-LightData - support — auto-skipped), and `mx_chiang_hair_bsdf` (naga limitation — `EXPECTED_FALLBACK`). +* **In:** standalone node `.glsl` files referenced by `file=` implementations in + stdlib/pbrlib/lights, and the `genglsl/lib/*.glsl` helpers they use. +* **Out (auto-skipped):** texture/image and light nodes — naga's GLSL frontend has no sampler / + dynamic-`LightData` support. Also `mx_chiang_hair_bsdf` (a naga limitation), tracked in + `EXPECTED_FALLBACK`. -The result is a *reduced* library: generated nodes and `lib/` helpers for everything that resolves -cleanly against genglsl, hand-written nodes for the rest. +The result is a *reduced* library by design: generated nodes and `lib/` helpers for everything that +resolves cleanly against genglsl, and hand-written `.wgsl` for the rest. ## How it works -**Mental model (read this first).** naga does the actual GLSL→WGSL translation — the `naga` -subprocess call is a single line. Everything else in the script is two adapters around it: +**Mental model.** naga does the actual GLSL→WGSL translation — the `naga` subprocess call is a +single line. Everything else in the script is two adapters around it: * a **pre-processor** that manufactures a complete, compilable shader naga will accept from an incomplete node fragment, and * a **post-processor** that reconciles naga's (correct but verbose) output with the conventions of - the *hand-written* genwgsl library. - -The tool does **not** reimplement transpilation, and it is a *partial* generator by design (see -Scope): most nodes generate, a known few stay hand-written. The regex/bracket-matching is deliberate -string surgery, not a real parser — naga's own run is the correctness gate, and output is -deterministic (no timestamps; overload stubs indexed by declaration order) so re-running is -byte-identical and `git diff` = drift. + the hand-written genwgsl library. -**What a reviewer should focus on:** the two hand-maintained tables — `CALL_MAP` (GLSL overload → -genwgsl name) and `EXPECTED_FALLBACK` (nodes intentionally not generated). They encode overload -renaming and known naga limitations; everything else is mechanical. +The tool does **not** reimplement transpilation. The regex/bracket-matching in the script is +deliberate string surgery, not a real parser — naga's own run is the correctness gate. **Execution order in `main()`:** -1. `transpile_libs()` — topological sort of `genglsl/lib/*.glsl` (22 files), per-function naga - transpile (or sibling-body path for heavy intra-file deps), `LIB_PREAMBLE` - (`DIRECTIONAL_ALBEDO_METHOD 0`, etc.), inverted `CALL_MAP` for overload output names, - `LIB_FIELD_RENAMES` (`tf_*` → `thinfilm_*`), texture stubs (`$envPrefilterMip`, etc.), and a - special handler for `mx_closure_type` (`LIB_STRUCT_PREAMBLE` aggregates closure structs). -2. `build_wgsl_lib_symbols()` — reads generated `lib/*.wgsl` from `--out` for arity validation. -3. `transpile_nodes()` — existing per-node loop. - -The pipeline, per node function: +0. **Preflight validation** — `validateOverloadCoverage` (every scanned overload resolves via + `mangle()`), `validateTokenCoverage` (every `$`-token in transpiled lib sources is declared in + `HwConstants.cpp` or has a naga stub in `NAGA_LIB_STUBS`), and, after the lib pass, + `validateLibNames` (every `mangle()` name exists in the real genwgsl lib). Any failure is a hard + error, so a gap between genglsl, the naming tables, and the genwgsl lib is caught loudly instead + of silently mis-emitting a name. +1. `transpileLibs()` — topological sort of `genglsl/lib/*.glsl`, per-function naga transpile (or the + sibling-body path for heavy intra-file call graphs), `LIB_PREAMBLE` + (`DIRECTIONAL_ALBEDO_METHOD 0`, etc.), `mangle()` for overload output names, texture stubs (via + `NAGA_LIB_STUBS`), and a special handler for `mx_closure_type` (`wgslClosurePreamble()` + aggregates closure structs from GLSL + `GlslSyntax.cpp`). +2. `buildWgslLibSymbols()` — reads the generated `lib/*.wgsl` from `--out` for arity validation. +3. The per-node loop (`transpile()`) — runs after the lib pass so nodes can `#include` the freshly + generated `genwgsl/lib/*.wgsl`. + +**Per-node pipeline:** 1. **Pre-process** — a node fragment has no `main`, `#include`s helpers, carries MaterialX `$`-tokens, and uses generator-emitted closure structs, so none of it is valid standalone GLSL. Build a naga-parseable translation unit: shared `#define`/`const`/`struct` context + function *prototypes* (so helper calls resolve) + closure structs (`BSDF`/`surfaceshader`, from - `CLOSURE_PREAMBLE`) + `$`-token→placeholder swaps + the one function body + a dummy `main`. The - parsing (`parse_functions`, `build_context`) exists only to synthesize that context. + `glslClosurePreamble()`, parsed from `GlslSyntax.cpp`) + `$`-token→placeholder swaps + the one + function body + a dummy `main`. `parseFunctions` and `buildContext` exist only to synthesize that + context. 2. **Transpile** — run `naga --input-kind glsl --shader-stage frag` (which also validates). -3. **Post-process** — clean up naga's SSA-style output into a readable fragment (collapse param-copy - shadows, inline single-use temps, `vec3`→`vec3f`, drop float-literal `f` suffixes, ...). - -WGSL has no function overloading. Two post-process passes bridge this: - -1. **`remap_calls` / `CALL_MAP`** — overloaded GLSL helpers get a distinct type-suffixed name in the - genwgsl lib (`mx_square_f32`, `mx_perlin_noise_float_3d`, `mx_matrix_mul_mat4_vec4`, ...). - `CALL_MAP` maps `(glsl_helper, parameter_types) → genwgsl name`. For lib transpilation the map - is inverted when emitting function definitions. naga numbers overload stubs `_N` by - **prototype-declaration order** (kept and indexed deterministically even when unused), and - prototypes are emitted sorted, so each call is rewritten to the right lib function. - A `None` entry marks an intentionally-unsupported overload. - -2. **`check_lib_arity`** — parses the generated `lib/*.wgsl` signatures and fails any node whose - helper-call arity disagrees, so it falls back to hand-written instead of producing a shader that +3. **Post-process** — `mxwgslcleanup.py` collapses naga's param-copy shadows and restores GLSL + parameter names; then `mxgenwgsl.py` normalizes types, overload names, and float-literal + suffixes, and re-attaches comments. + +### WGSL cleanup + +`mxwgslcleanup.py` parses naga's WGSL output with a tree-sitter WGSL grammar and applies +conservative readability edits: collapse param-copy shadows, promote `var`→`let`, flatten else-if +chains, unwrap redundant compound blocks, etc. The grammar comes from the pip package +`tree-sitter-language-pack` (precompiled — no Node or `tree-sitter-cli`). If cleanup fails or the +package is unavailable, the transpiler keeps the verbose naga output and prints a warning — it never +blocks generation. + +### Overload handling + +WGSL has no function overloading, so two post-process passes bridge it: + +1. **`remapCalls` / `mangle()`** — overloaded GLSL helpers get a distinct type-suffixed name in the + genwgsl lib (`mx_square_f32`, `mx_perlin_noise_float_3d`, `mx_matrix_mul_mat4_vec4`, …). + `mangle(name, types, overloaded)` derives that name from a per-family `SUFFIX_SCHEME` rule + (default: suffix by the first parameter type), with an `EXCEPTIONS` table for irregular/semantic + names (the `mx_fresnel_schlick_*` / `mx_ggx_dir_albedo_*` forms, primary unsuffixed overloads). + The same function names lib definitions and rewrites node calls. naga numbers overload stubs `_N` + by prototype-declaration order, and prototypes are emitted sorted, so each call is rewritten to + the right lib function. +2. **`checkLibArity`** — parses the generated `lib/*.wgsl` signatures and rejects any node whose + helper-call arity disagrees, so it falls back to hand-written instead of emitting a shader that fails to link. -A node that hits either case is reported as `FAIL ... (kept hand-written)` and left out of the -generated set — that is expected, not an error in the tool. The set of such nodes is listed in -`EXPECTED_FALLBACK`; the tool's exit code is non-zero only when a node *outside* that set fails -(a regression — e.g. a genglsl change that breaks a previously-generable node, or a new naga -error). If a node in `EXPECTED_FALLBACK` starts transpiling cleanly, the tool prints a `WARN` so it -can be removed from the set and its generated version committed. - -## Build-time validation (CMake) +### naga identifier reconciliation (`denagaName`) + +naga's `Namer` (`proc/namer.rs`) rewrites identifiers deterministically: the first use of a +sanitized base gets a trailing `_` when it ends in a digit or is a keyword/builtin, and later +collisions get `_`. Because the transpiler re-emits some symbols itself (file-scope consts, the +`F0`/`F82`/`F90` closure-preamble struct fields, prototype names), `denagaName()` maps naga's +`NAME_` / `NAME_` references back to the clean declaration name. + +### Closure preambles and tokens (derived, not duplicated) + +Closure struct layouts are emitted from the same sources the C++ generators use: +`ClosureData`/`FresnelData` from `genglsl/lib/*.glsl`, `BSDF`/`VDF`/shader aliases from +`MaterialXGenGlsl/GlslSyntax.cpp` (`wgslClosurePreamble()` / `glslClosurePreamble()`). `FresnelData` +field names match GLSL (`thinfilm_thickness`, `thinfilm_ior` in `mx_microfacet_specular.glsl`). +MaterialX `$`-token *names* are validated against `HwConstants.cpp`; only the naga-parseable stub +*values* used during lib transpile live in `NAGA_LIB_STUBS` (plus `$closureDataConstructor`, read +from `HwConstants.cpp` at runtime). + +### Comment preservation + +naga discards comments, so the tool re-attaches them from genglsl: + +* *Doc / leading comments* (reliable) — the contiguous comment block directly above each function + (`parseFunctions` captures it) and a file-level top-of-file block are re-emitted verbatim (GLSL + and WGSL comment syntax are identical). +* *Inline body comments* (best-effort) — whole-line `//` comments inside a body are anchored to a + stable token of the following statement and re-inserted above the matching WGSL line + (`injectInlineComments`). Because naga reorders/inlines bodies this is approximate: unmatched + comments are dropped deterministically and insertion is always whole-line, so output is never + corrupted and re-runs stay byte-identical. Struct/const comments are still stripped. + +## Extending & maintaining + +Most of the tool is mechanical — the overload *keys* are discovered by scanning genglsl +(`buildContext`), so only the naming rules and known naga limitations are hand-maintained. When +genglsl changes, the pieces you may need to touch are: + +* **Overload naming** — add a family rule to `SUFFIX_SCHEME`, or an individual `(name, types)` entry + to `EXCEPTIONS`, when a new overloaded helper appears or an existing one is renamed. A `None` value + in `EXCEPTIONS` marks an overload the genwgsl lib deliberately doesn't provide (its callers stay + hand-written). `validateOverloadCoverage` / `validateLibNames` will fail loudly if a rule is + missing or produces a name absent from the lib. +* **Expected fallbacks** — `EXPECTED_FALLBACK` lists nodes intentionally left hand-written (naga + limitations). If a node there starts transpiling cleanly, the tool prints a `WARN`; remove it from + the set. If a *new* node cannot be generated, add it here (with a note on why) so the run stays + green. +* **`$`-tokens** — a new `$`-token used in a transpiled lib source needs a naga-parseable stub in + `NAGA_LIB_STUBS`; its name must also be declared in `HwConstants.cpp`. `validateTokenCoverage` + enforces both. +* **Hand-written libs** — `HANDWRITTEN_LIB_DIRS` (whole dirs kept by project choice) and + `LIB_KEEP_HANDWRITTEN` (individual sampler-bound helpers naga can't express) are skipped by the + lib pass; their nodes are still generated. + +## Troubleshooting + +* `FAIL ::: ` — naga rejected the synthesized fragment. Set `MTLX_DEBUG=1` and + re-run; the offending GLSL fragment is written next to the output as `_debug_*.frag` for inspection. +* `FAIL ... calls adapted/unmapped helper(s) ... (kept hand-written)` — the node calls a helper with + a signature the genwgsl lib doesn't provide (an `EXCEPTIONS` `None`, or an arity mismatch caught by + `checkLibArity`). Expected for nodes that must stay hand-written; add them to `EXPECTED_FALLBACK` + if permanent. +* `WARN mxwgslcleanup: ...` — the tree-sitter cleanup pass failed for one function; the verbose naga + output is kept. Non-fatal. +* `WARN : now transpiles cleanly` — a node in `EXPECTED_FALLBACK` no longer needs to be there. + +## Build integration (CMake / CI) Configure with `-DMATERIALX_GENERATE_WGSL_LIBRARY=ON` (requires `MATERIALX_BUILD_GEN_WGSL`, a Python interpreter, and the `naga` CLI) to add a `MaterialXGenWgslLibrary` target that runs this tool over `libraries/` on every build, emitting to `${CMAKE_BINARY_DIR}/genwgsl_generated`. Transpile/naga -errors and regressions then surface as **build errors** (the committed `.wgsl` files stay the source -of truth — the build-tree output is for validation only). The option defaults `OFF` so ordinary +errors and regressions then surface as **build errors**. The option defaults `OFF`, so ordinary builds need neither Python nor naga. CMake finds naga from, in order: `-DMATERIALX_NAGA_EXECUTABLE=/path/to/naga`, the `NAGA` environment variable, `PATH`, and the standard cargo bin directories (`$CARGO_HOME/bin`, `~/.cargo/bin`, `%USERPROFILE%\.cargo\bin`) — so an existing `cargo install naga-cli` is picked up even when -`~/.cargo/bin` is not on `PATH`. - -If naga is still not found, CMake locates **cargo** the same way EMSDK is located -(`-DMATERIALX_CARGO_PATH=`, then `$CARGO_HOME`, then the standard `~/.cargo` / -`%USERPROFILE%\.cargo` locations, then `PATH`) and builds naga into the build tree -(`/naga`) via `cargo install naga-cli`. This install runs at **build** time (not configure, -so configure stays fast) and is cached — it recompiles only if the binary is missing. cargo must -already be installed; MaterialX does not bootstrap the Rust toolchain. If cargo cannot be found, -generation is skipped with a warning. - -## Local workflow - -Since the generated files are not committed, there is no baseline to diff against — you simply -regenerate the library in place whenever genglsl changes: - -1. Regenerate in place: `python glsl_to_wgsl.py --libraries libraries --out libraries`. This overwrites - generated node and `lib/` files under `libraries/{stdlib,pbrlib}/genwgsl/` and leaves the - hand-written texture/light/`EXPECTED_FALLBACK` nodes untouched. A non-zero exit means an - *unexpected* failure (a regression) — the known `EXPECTED_FALLBACK` nodes failing is normal. -2. Configure with `-DMATERIALX_BUILD_GEN_WGSL=ON`, rebuild + restage test data, run the `[genwgsl]` - tests, and `naga`-validate the emitted `Default.{vertex,pixel}.wgsl`. +`~/.cargo/bin` is not on `PATH`. If naga is still not found, CMake locates **cargo** the same way +EMSDK is located and builds naga into the build tree (`/naga`) via `cargo install naga-cli` at +build time (cached; recompiled only if the binary is missing). cargo must already be installed; +MaterialX does not bootstrap the Rust toolchain. If cargo cannot be found, generation is skipped with +a warning. -CI performs step 1 on every job that builds or ships WGSL (the web viewer, sdist/wheels, and the +CI runs the transpiler on every job that builds or ships WGSL (the web viewer, sdist/wheels, and the tagged-release archives), so a change that breaks the WGSL target fails CI. diff --git a/source/MaterialXGenWgsl/tools/glsl_to_wgsl.py b/source/MaterialXGenWgsl/tools/mxgenwgsl.py similarity index 100% rename from source/MaterialXGenWgsl/tools/glsl_to_wgsl.py rename to source/MaterialXGenWgsl/tools/mxgenwgsl.py From 0cc09dfebb75768c73d0e15d19eaf47a01d8a6d2 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Mon, 20 Jul 2026 17:23:38 -0700 Subject: [PATCH 14/19] Add WGSL transpile tooling and cleanup test suite --- source/MaterialXGenWgsl/tools/.gitignore | 1 + source/MaterialXGenWgsl/tools/mxgenwgsl.py | 1917 ++++++++++------- .../MaterialXGenWgsl/tools/mxwgslcleanup.py | 769 +++++++ .../tools/requirements-transpile.txt | 4 + .../tools/test_mxwgslcleanup.py | 226 ++ 5 files changed, 2127 insertions(+), 790 deletions(-) create mode 100644 source/MaterialXGenWgsl/tools/.gitignore create mode 100644 source/MaterialXGenWgsl/tools/mxwgslcleanup.py create mode 100644 source/MaterialXGenWgsl/tools/requirements-transpile.txt create mode 100644 source/MaterialXGenWgsl/tools/test_mxwgslcleanup.py diff --git a/source/MaterialXGenWgsl/tools/.gitignore b/source/MaterialXGenWgsl/tools/.gitignore new file mode 100644 index 0000000000..c18dd8d83c --- /dev/null +++ b/source/MaterialXGenWgsl/tools/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/source/MaterialXGenWgsl/tools/mxgenwgsl.py b/source/MaterialXGenWgsl/tools/mxgenwgsl.py index a68da74f25..e608a68842 100644 --- a/source/MaterialXGenWgsl/tools/mxgenwgsl.py +++ b/source/MaterialXGenWgsl/tools/mxgenwgsl.py @@ -1,9 +1,9 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python # # Copyright Contributors to the MaterialX Project # SPDX-License-Identifier: Apache-2.0 # -""" +''' Transpile MaterialX genglsl node fragments and genglsl/lib helpers to genwgsl (WGSL). genglsl sources are not complete shaders: they use `#include`, `$`-tokens, and define functions @@ -19,20 +19,20 @@ - Skipped: image/hextiled texture nodes and light shaders (naga sampler / LightData limits). - Expected fallback: mx_chiang_hair_bsdf (naga limitation on hair scattering helpers). -Lib transpilation (`transpile_libs`, runs before nodes): +Lib transpilation (`transpileLibs`, runs before nodes): - Each genglsl/lib/*.glsl becomes genwgsl/lib/*.wgsl in topological #include order (22 files). - Default: per-function transpile with full bodies of #included lib deps inlined, plus - LIB_PREAMBLE (generator-option pins), texture stubs, and CALL_MAP overload renaming. + LIB_PREAMBLE (generator-option pins), texture stubs, and mangle() overload renaming. - Sibling-body path for mx_microfacet_specular, mx_microfacet_sheen, mx_flake, mx_noise (heavy intra-file call graphs: transitive in-file callees + overload-aware topo sort). - Special case: mx_closure_type (static struct preamble + transpiled makeClosureData). - Lib transpile failures are hard errors (no hand-port fallback). Usage: - python glsl_to_wgsl.py --libraries libraries --out libraries - python glsl_to_wgsl.py --libraries libraries --out build/genwgsl_generated - python glsl_to_wgsl.py --libraries libraries --only mx_conductor_bsdf mx_math -""" + python mxgenwgsl.py --libraries libraries --out libraries + python mxgenwgsl.py --libraries libraries --out build/genwgsl_generated + python mxgenwgsl.py --libraries libraries --only mx_conductor_bsdf mx_math +''' import argparse import os @@ -43,18 +43,21 @@ import tempfile from pathlib import Path -# The naga CLI (GLSL->WGSL). Resolved in main() as: --naga arg, then $NAGA, then a `naga` on PATH. +from mxwgslcleanup import cleanupFunction, fnBase + +# The naga CLI (GLSL->WGSL). Resolved in main() via resolveNaga(): --naga arg, then $NAGA, then a +# `naga` on PATH; main() overwrites this global before any transpilation runs. # Install with `cargo install naga-cli` (see https://github.com/gfx-rs/wgpu/tree/trunk/naga). -NAGA = os.environ.get("NAGA") or "naga" +NAGA = "naga" -def resolve_naga(explicit=None): - """Return a runnable naga command: explicit --naga, else $NAGA, else `naga` from PATH.""" +def resolveNaga(explicit=None): + '''Return a runnable naga command: explicit --naga, else $NAGA, else `naga` from PATH.''' return explicit or os.environ.get("NAGA") or shutil.which("naga") or "naga" -def naga_version(naga): - """Return naga's version string if it runs, else None (used to fail early with guidance).""" +def nagaVersion(naga): + '''Return naga's version string if it runs, else None (used to fail early with guidance).''' try: r = subprocess.run([naga, "--version"], capture_output=True, text=True) return r.stdout.strip() if r.returncode == 0 else None @@ -62,11 +65,11 @@ def naga_version(naga): return None -def discover_libs(libroot): - """Library folders to process: every immediate subdirectory of --libraries that has a `genglsl` +def discoverLibs(libroot): + '''Library folders to process: every immediate subdirectory of --libraries that has a `genglsl` node directory. Today that is stdlib/pbrlib/lights (bxdf is nodegraph-based, nprlib/targets have no genglsl node fragments), but auto-discovering means new or downstream libraries are picked up - without editing this tool. Sorted for stable output ordering.""" + without editing this tool. Sorted for stable output ordering.''' libroot = Path(libroot) if not libroot.is_dir(): return [] @@ -108,22 +111,93 @@ def discover_libs(libroot): (re.compile(r"\bmat([234])x\1"), r"mat\1x\1f"), ] -# WGSL has no function overloading, so the hand-written genwgsl lib gives each GLSL overload a -# distinct, type-suffixed name. This table maps a GLSL helper call -- keyed by (name, parameter -# types) exactly as build_context extracts them -- to the genwgsl function it resolves to. -# naga disambiguates overloaded calls with a deterministic `_N` suffix in prototype-declaration -# order (verified: stubs are kept and indexed by declaration order even when unused), so the -# remap pass below can rewrite each call to its genwgsl name. -# * value None -> intentionally unsupported (the genwgsl lib adapted this signature, e.g. the -# FresnelData-taking BSDF helpers); a node calling it stays hand-written. -# * missing key -> an unmapped overload; treated like None (node stays hand-written) and logged. -CALL_MAP = { - ("mx_square", ("float",)): "mx_square_f32", - ("mx_square", ("vec2",)): "mx_square_vec2", - ("mx_square", ("vec3",)): "mx_square_vec3", +# ----------------------------------------------------------------------------- +# overload name mangling +# ----------------------------------------------------------------------------- +# +# WGSL has no function overloading, so each GLSL overload needs a distinct genwgsl name. The *keys* +# (name + parameter types) are discovered by scanning genglsl (buildContext's `overloaded` set), +# so they are never hand-listed. Only the *naming* is specified here, and only for the families +# whose hand-written stdlib names don't follow the default first-parameter-type rule: +# +# * SUFFIX_SCHEME - families with a regular, rule-based suffix (keyed by base name). +# * EXCEPTIONS - individual (name, types) whose genwgsl name is semantic/irregular, or a +# primary overload that stays unsuffixed. A None value means "intentionally +# unsupported" (a node calling it stays hand-written). +# +# mangle() is the single source of truth for both emitting lib definitions (wgslFnName) and +# rewriting node calls (remapCalls). The generated names are validated against the real genwgsl +# lib after generation (buildWgslLibSymbols / checkLibArity), so a wrong rule fails loudly. + +def _suffixToken(t): + '''GLSL type -> the token used in a genwgsl overload suffix (float -> f32, else the GLSL name).''' + return "f32" if t == "float" else t + + +def _firstTypeSuffix(types): + '''`_`, e.g. mx_square(vec3) -> _vec3, mx_bilerp(float,...) -> _f32.''' + return "_" + _suffixToken(types[0]) + +def _allTypesSuffix(types): + '''`__...` for every param, e.g. mx_matrix_mul(vec2,mat2) -> _vec2_mat2.''' + return "".join("_" + _suffixToken(t) for t in types) + + +def _vecDimSuffix(types): + '''`_2d`/`_3d` from the first vector param's dimension (mx_perlin_noise_float(vec3) -> _3d).''' + for t in types: + m = re.match(r"u?vec([234])$", t) + if m: + return "_" + m.group(1) + "d" + raise ValueError(f"vec_dim scheme: no vector parameter in {types}") + + +def _gradientDimSuffix(types): + '''`_2d`/`_3d` from coordinate count (hash + N coords): 3 params -> _2d, 4 params -> _3d.''' + return "_" + str(len(types) - 1) + "d" + + +def _cellposDimSuffix(types): + '''`_2d`/`_3d` from cell-position arg count: 5 params -> _2d, 7 params -> _3d.''' + return "_" + str((len(types) - 1) // 2) + "d" + + +def _argcountISuffix(types): + '''`_i` from parameter count, e.g. mx_hash_int(int,int) -> _i2.''' + return "_i" + str(len(types)) + + +# Base name -> suffix scheme. Overloaded families not listed use _firstTypeSuffix. Every entry +# is a GLSL-overloaded helper; the scheme reproduces the hand-written stdlib naming exactly. +SUFFIX_SCHEME = { + "mx_square": _firstTypeSuffix, + "mx_cell_noise_float": _firstTypeSuffix, + "mx_cell_noise_vec3": _firstTypeSuffix, + "mx_bilerp": _firstTypeSuffix, + "mx_trilerp": _firstTypeSuffix, + "mx_gradient_scale2d": _firstTypeSuffix, + "mx_gradient_scale3d": _firstTypeSuffix, + "mx_f0_to_ior": _firstTypeSuffix, # vec3 form; the float form is a primary EXCEPTION below + "mx_matrix_mul": _allTypesSuffix, + "mx_perlin_noise_float": _vecDimSuffix, + "mx_perlin_noise_vec3": _vecDimSuffix, + "mx_worley_noise_float": _vecDimSuffix, + "mx_worley_noise_vec2": _vecDimSuffix, + "mx_worley_noise_vec3": _vecDimSuffix, + "mx_worley_distance": _vecDimSuffix, + "mx_gradient_float": _gradientDimSuffix, + "mx_gradient_vec3": _gradientDimSuffix, + "mx_worley_cell_position": _cellposDimSuffix, + "mx_hash_int": _argcountISuffix, + "mx_hash_vec3": _argcountISuffix, +} + +# Individual overloads whose genwgsl name is semantic/irregular or a primary unsuffixed form. A +# None value marks a signature the genwgsl lib deliberately doesn't provide (node stays +# hand-written). Keyed exactly as buildContext extracts (name, GLSL param types). +EXCEPTIONS = { ("mx_f0_to_ior", ("float",)): "mx_f0_to_ior", - ("mx_f0_to_ior", ("vec3",)): "mx_f0_to_ior_vec3", ("mx_fresnel_schlick", ("float", "float")): "mx_fresnel_schlick_f32", ("mx_fresnel_schlick", ("float", "vec3")): "mx_fresnel_schlick_vec3", @@ -132,78 +206,44 @@ def discover_libs(libroot): ("mx_fresnel_schlick", ("float", "float", "float", "float")): "mx_fresnel_schlick_f32_exp", ("mx_fresnel_schlick", ("float", "vec3", "vec3", "float")): "mx_fresnel_schlick_vec3_exp", - ("mx_perlin_noise_float", ("vec2",)): "mx_perlin_noise_float_2d", - ("mx_perlin_noise_float", ("vec3",)): "mx_perlin_noise_float_3d", - ("mx_perlin_noise_vec3", ("vec2",)): "mx_perlin_noise_vec3_2d", - ("mx_perlin_noise_vec3", ("vec3",)): "mx_perlin_noise_vec3_3d", - - ("mx_cell_noise_float", ("float",)): "mx_cell_noise_float_f32", - ("mx_cell_noise_float", ("vec2",)): "mx_cell_noise_float_vec2", - ("mx_cell_noise_float", ("vec3",)): "mx_cell_noise_float_vec3", - ("mx_cell_noise_float", ("vec4",)): "mx_cell_noise_float_vec4", - - ("mx_worley_noise_float", ("vec2", "float", "int", "int")): "mx_worley_noise_float_2d", - ("mx_worley_noise_float", ("vec3", "float", "int", "int")): "mx_worley_noise_float_3d", - ("mx_worley_noise_vec2", ("vec2", "float", "int", "int")): "mx_worley_noise_vec2_2d", - ("mx_worley_noise_vec2", ("vec3", "float", "int", "int")): "mx_worley_noise_vec2_3d", - ("mx_worley_noise_vec3", ("vec2", "float", "int", "int")): "mx_worley_noise_vec3_2d", - ("mx_worley_noise_vec3", ("vec3", "float", "int", "int")): "mx_worley_noise_vec3_3d", - - # Remaining mx_noise.glsl overloaded helpers -> type-suffixed genwgsl names (WGSL has no - # overloading). Names mirror the hand-written genwgsl lib so cross-file callers still resolve. - ("mx_bilerp", ("float", "float", "float", "float", "float", "float")): "mx_bilerp_f32", - ("mx_bilerp", ("vec3", "vec3", "vec3", "vec3", "float", "float")): "mx_bilerp_vec3", - ("mx_trilerp", ("float",) * 11): "mx_trilerp_f32", - ("mx_trilerp", ("vec3",) * 8 + ("float", "float", "float")): "mx_trilerp_vec3", - - ("mx_gradient_float", ("uint", "float", "float")): "mx_gradient_float_2d", - ("mx_gradient_float", ("uint", "float", "float", "float")): "mx_gradient_float_3d", - ("mx_gradient_vec3", ("uvec3", "float", "float")): "mx_gradient_vec3_2d", - ("mx_gradient_vec3", ("uvec3", "float", "float", "float")): "mx_gradient_vec3_3d", - ("mx_gradient_scale2d", ("float",)): "mx_gradient_scale2d_f32", - ("mx_gradient_scale2d", ("vec3",)): "mx_gradient_scale2d_vec3", - ("mx_gradient_scale3d", ("float",)): "mx_gradient_scale3d_f32", - ("mx_gradient_scale3d", ("vec3",)): "mx_gradient_scale3d_vec3", - - ("mx_hash_int", ("int",)): "mx_hash_int_i1", - ("mx_hash_int", ("int", "int")): "mx_hash_int_i2", - ("mx_hash_int", ("int", "int", "int")): "mx_hash_int_i3", - ("mx_hash_int", ("int", "int", "int", "int")): "mx_hash_int_i4", - ("mx_hash_int", ("int", "int", "int", "int", "int")): "mx_hash_int_i5", - ("mx_hash_vec3", ("int", "int")): "mx_hash_vec3_i2", - ("mx_hash_vec3", ("int", "int", "int")): "mx_hash_vec3_i3", - - ("mx_cell_noise_vec3", ("float",)): "mx_cell_noise_vec3_f32", - ("mx_cell_noise_vec3", ("vec2",)): "mx_cell_noise_vec3_vec2", - ("mx_cell_noise_vec3", ("vec3",)): "mx_cell_noise_vec3_vec3", - ("mx_cell_noise_vec3", ("vec4",)): "mx_cell_noise_vec3_vec4", - - ("mx_worley_cell_position", ("int", "int", "int", "int", "float")): "mx_worley_cell_position_2d", - ("mx_worley_cell_position", ("int", "int", "int", "int", "int", "int", "float")): "mx_worley_cell_position_3d", - ("mx_worley_distance", ("vec2", "int", "int", "int", "int", "float", "int")): "mx_worley_distance_2d", - ("mx_worley_distance", ("vec3", "int", "int", "int", "int", "int", "int", "float", "int")): "mx_worley_distance_3d", - - ("mx_matrix_mul", ("vec2", "mat2")): "mx_matrix_mul_vec2_mat2", - ("mx_matrix_mul", ("vec3", "mat3")): "mx_matrix_mul_vec3_mat3", - ("mx_matrix_mul", ("vec4", "mat4")): "mx_matrix_mul_vec4_mat4", - ("mx_matrix_mul", ("mat2", "vec2")): "mx_matrix_mul_mat2_vec2", - ("mx_matrix_mul", ("mat3", "vec3")): "mx_matrix_mul_mat3_vec3", - ("mx_matrix_mul", ("mat4", "vec4")): "mx_matrix_mul_mat4_vec4", - ("mx_matrix_mul", ("mat2", "mat2")): "mx_matrix_mul_mat2_mat2", - ("mx_matrix_mul", ("mat3", "mat3")): "mx_matrix_mul_mat3_mat3", - ("mx_matrix_mul", ("mat4", "mat4")): "mx_matrix_mul_mat4_mat4", - - # vec3/vec3-F90 directional albedo maps cleanly; float overload delegates to vec3. ("mx_ggx_dir_albedo", ("float", "float", "vec3", "vec3")): "mx_ggx_dir_albedo", ("mx_ggx_dir_albedo", ("float", "float", "float", "float")): "mx_ggx_dir_albedo_scalar", ("mx_ggx_dir_albedo", ("float", "float", "FresnelData")): "mx_ggx_dir_albedo_fresnel", - ("mx_ggx_energy_compensation", ("float", "float", "FresnelData")): "mx_ggx_energy_compensation", } -# ---------------------------------------------------------------------------- lib transpilation tables +# Bases mangle() can resolve (drives call remapping). Every such base is GLSL-overloaded. +SUPPORTED_BASES = set(SUFFIX_SCHEME) | {b for (b, _t) in EXCEPTIONS} + + +def _isMapped(base, types): + '''True if (base, types) has an explicit or scheme-derived genwgsl name.''' + return (base, types) in EXCEPTIONS or base in SUFFIX_SCHEME + + +def mangle(name, types, overloaded): + '''Return the genwgsl name for a GLSL overload, or None if intentionally unsupported. + + Resolution order: an explicit EXCEPTIONS entry, then the family's SUFFIX_SCHEME rule, else the + name is returned unchanged (non-overloaded / not a renamed helper). An overloaded base with no + scheme and no exception for this signature is unsupported (None), so the caller stays + hand-written -- matching the previous CALL_MAP 'missing key' behavior.''' + key = (name, types) + if key in EXCEPTIONS: + return EXCEPTIONS[key] + scheme = SUFFIX_SCHEME.get(name) + if scheme is not None: + return name + scheme(types) + if name in overloaded: + return None + return name + + +# ----------------------------------------------------------------------------- +# lib transpilation tables +# ----------------------------------------------------------------------------- # Lib files where per-function transpile without in-file siblings fails because helpers call -# each other in the same file. These use transpile_lib_file_siblings (transitive in-file +# each other in the same file. These use transpileLibFileSiblings (transitive in-file # callee bodies + overload-aware topo sort) instead of one-function-at-a-time. LIB_USE_SIBLING_BODIES = { "mx_microfacet_specular", "mx_microfacet_sheen", "mx_flake", "mx_noise", @@ -225,89 +265,53 @@ def discover_libs(libroot): float mtlx_tex_size_x() { return 256.0; } """ -# MaterialX $-tokens are not valid GLSL identifiers. Lib files use them for sampler uniforms, -# environment tables, and the closure constructor macro — expand to literals/stubs before naga. -LIB_TOKEN_FIXUPS = [ - (re.compile(r"\$texSamplerSignature\b"), "sampler2D mtlx_tex_sampler"), - (re.compile(r"\$texSamplerSampler2D\b"), "mtlx_tex_sampler"), - (re.compile(r"\$albedoTable\b"), "mtlx_albedo_table"), - (re.compile(r"\$albedoTableSize\b"), "vec2(256.0)"), - (re.compile(r"\$envRadianceSamples\b"), "16"), - (re.compile(r"\$envRadianceMips\b"), "8"), - (re.compile(r"\$envMatrix\b"), "mat4(1.0)"), - (re.compile(r"\$envRadiance\b"), "mtlx_env_radiance_stub"), - (re.compile(r"\$envIrradiance\b"), "mtlx_env_irradiance_stub"), - (re.compile(r"\$envLightIntensity\b"), "vec3(1.0)"), - (re.compile(r"\$envPrefilterMip\b"), "0.0"), - (re.compile(r"\$envRadianceSampler2D\b"), "mtlx_tex_sampler"), - (re.compile(r"\$closureDataConstructor\b"), - "ClosureData(closureType, L, V, N, P, occlusion)"), - (re.compile(r"\$refractionTwoSided\b"), "false"), -] +# naga's GLSL frontend requires a staged shader with an entry point; every wrapped fragment ends +# with this do-nothing fragment `main`. naga keeps the non-entry function bodies we actually want. +FRAG_MAIN_EPILOGUE = "\nlayout(location=0) out vec4 mtlx_o;\nvoid main() { mtlx_o = vec4(0.0); }\n" -# GLSL FresnelData uses tf_* field names; genwgsl lib uses thinfilm_* (hand-port convention). -LIB_FIELD_RENAMES = [ - (re.compile(r"\btf_thickness\b"), "thinfilm_thickness"), - (re.compile(r"\btf_ior\b"), "thinfilm_ior"), - # naga's WGSL backend appends `_` to any identifier ending in a digit (F0 -> F0_). The genwgsl - # FresnelData (LIB_STRUCT_PREAMBLE) keeps the digit-suffixed field names, so strip naga's - # trailing underscore back off these field accesses to match the struct definition. - (re.compile(r"\bF0_\b"), "F0"), - (re.compile(r"\bF82_\b"), "F82"), - (re.compile(r"\bF90_\b"), "F90"), -] - -# mx_closure_type.glsl only defines ClosureData + makeClosureData; the genwgsl file must also -# expose BSDF/VDF/FresnelData/EDF/material that nodes include. Prepended verbatim (not from naga). -# NOTE: surfaceshader is intentionally NOT here -- WgslShaderGenerator::emitTypeDefinitions emits it -# (and displacement/lightshader) *before* including this file, so the `material = surfaceshader` -# alias below resolves. Re-declaring it here would redeclare the struct in the assembled shader. -LIB_STRUCT_PREAMBLE = """ -struct ClosureData { - closureType: i32, - L: vec3f, - V: vec3f, - N: vec3f, - P: vec3f, - occlusion: f32, +# MaterialX $-tokens are not valid GLSL identifiers. Lib transpile expands a subset to +# naga-parseable GLSL stubs (NAGA_LIB_STUBS) before calling naga. Token *names* are validated +# against HwConstants.cpp (loadHwTokenNames); stub *values* are transpiler policy only. +# +# Replaces the former monolithic LIB_TOKEN_FIXUPS table: names come from the C++ generator, +# values stay here because naga needs parseable placeholders (sampler stubs, literal defaults). +NAGA_LIB_STUBS = { + "$texSamplerSignature": "sampler2D mtlx_tex_sampler", + "$texSamplerSampler2D": "mtlx_tex_sampler", + "$albedoTable": "mtlx_albedo_table", + "$albedoTableSize": "vec2(256.0)", + "$envRadianceSamples": "16", + "$envRadianceMips": "8", + "$envMatrix": "mat4(1.0)", + "$envRadiance": "mtlx_env_radiance_stub", + "$envIrradiance": "mtlx_env_irradiance_stub", + "$envLightIntensity": "vec3(1.0)", + "$envPrefilterMip": "0.0", + "$envRadianceSampler2D": "mtlx_tex_sampler", + "$refractionTwoSided": "false", } -struct BSDF { - response: vec3f, - throughput: vec3f, -} +# Struct types emitted centrally into mx_closure_type.wgsl (and skipped when transpiling other +# lib files). surfaceshader is emitted by WgslShaderGenerator::emitTypeDefinitions, not here. +# Layouts are derived from genglsl + GlslSyntax.cpp (wgslClosurePreamble), not hand-listed. +LIB_PREAMBLE_STRUCTS = {"surfaceshader", "ClosureData", "BSDF", "VDF", "FresnelData"} -struct VDF { - response: vec3f, - throughput: vec3f, -} +# Aggregate/shader struct names parsed from MaterialXGenGlsl/GlslSyntax.cpp definition strings. +# Used by glslClosurePreamble (node context) and wgslClosurePreamble (BSDF/VDF only). +_GLSL_SYNTAX_STRUCT_NAMES = ( + "BSDF", "VDF", "surfaceshader", "volumeshader", "displacementshader", "lightshader", +) -alias EDF = vec3f; -alias material = surfaceshader; - -struct FresnelData { - model: i32, - airy: bool, - ior: vec3f, - extinction: vec3f, - F0: vec3f, - F82: vec3f, - F90: vec3f, - exponent: f32, - thinfilm_thickness: f32, - thinfilm_ior: f32, - refraction: bool, -} -""" +# Set by main() before transpilation; used for preamble/token stub resolution. +_ACTIVE_LIBROOT = None -# Struct types emitted centrally by LIB_STRUCT_PREAMBLE (into mx_closure_type.wgsl). Other lib -# files that also declare them in GLSL (e.g. FresnelData in mx_microfacet_specular.glsl) must NOT -# re-emit a definition, or the assembled shader has duplicate/conflicting structs -- and the -# preamble's hand-port field names (thinfilm_* vs. the GLSL tf_*) would diverge from the bodies. -LIB_PREAMBLE_STRUCTS = {"surfaceshader", "ClosureData", "BSDF", "VDF", "FresnelData"} +# Cached preambles keyed by resolved libroot path (replaces hand-written CLOSURE_PREAMBLE / +# LIB_STRUCT_PREAMBLE tables). +_WGSL_CLOSURE_PREAMBLE = {} +_GLSL_CLOSURE_PREAMBLE = {} GLSL_TO_WGSL_TYPE = { - # Used by transpile_glsl_structs/consts when emitting WGSL struct/const preamble for lib files. + # Used by transpileGlslStructs/consts when emitting WGSL struct/const preamble for lib files. "float": "f32", "int": "i32", "bool": "bool", "vec2": "vec2f", "vec3": "vec3f", "vec4": "vec4f", "mat2": "mat2x2f", "mat3": "mat3x3f", "mat4": "mat4x4f", @@ -315,14 +319,16 @@ def discover_libs(libroot): } -# ---------------------------------------------------------------------------- parsing +# ----------------------------------------------------------------------------- +# parsing +# ----------------------------------------------------------------------------- # These two scanners do depth-aware bracket matching so we can carve functions and argument lists # out of source text without a full parser. They are the foundation the rest of the parsing relies # on -- naga gives us no AST, so everything here is string surgery over balanced brackets. -def _match_brace(text, i): - """Given index of an opening '{', return index just past the matching '}'.""" +def _matchBrace(text, i): + '''Given index of an opening '{', return index just past the matching '}'.''' depth = 0 while i < len(text): if text[i] == "{": @@ -335,11 +341,11 @@ def _match_brace(text, i): return len(text) -def _match_paren(text, i): - """Given index of an opening '(', return the index OF the matching ')' (-1 if unbalanced). +def _matchParen(text, i): + '''Given index of an opening '(', return the index OF the matching ')' (-1 if unbalanced). - Note the asymmetry with _match_brace: this returns the closing paren's own index (callers slice - text[open+1:close] to get the contents), whereas _match_brace returns one past the '}'.""" + Note the asymmetry with _matchBrace: this returns the closing paren's own index (callers slice + text[open+1:close] to get the contents), whereas _matchBrace returns one past the '}'.''' depth = 0 while i < len(text): if text[i] == "(": @@ -355,38 +361,168 @@ def _match_paren(text, i): GLSL_KEYWORDS = {"if", "else", "for", "while", "switch", "do", "return", "case", "default"} -def parse_functions(text): - """Return [{ret, name, params, full}] for every top-level function *definition*. +def _leadingComment(text, start): + '''Return the comment block (//... or /*...*/) directly above the definition at `start`. + + Walks whole lines upward from the definition's line, collecting comment lines; stops at a blank + line, a non-comment line, or the end of a previous definition. GLSL and WGSL comment syntax are + identical, so the captured text is re-emitted verbatim (naga discards comments, so we preserve + doc comments by re-attaching them here). Returns the block with a trailing newline, or '' when + there is no contiguous comment directly above.''' + lineStart = text.rfind("\n", 0, start) + 1 + lines = text[:lineStart].split("\n") + if lines and lines[-1] == "": + lines.pop() # drop the empty element following the final newline + collected = [] + inBlock = False # walking upward inside a /* ... */ whose opener is on an earlier line + for ln in reversed(lines): + s = ln.strip() + if inBlock: + collected.append(ln) + if s.startswith("/*"): + inBlock = False + continue + if s == "": + break + if s.startswith("//"): + collected.append(ln) + elif s.endswith("*/"): + collected.append(ln) + if not s.startswith("/*"): + inBlock = True + else: + break + if inBlock or not collected: + return "" # unbalanced block, or nothing found -- emit nothing rather than a partial + collected.reverse() + return "\n".join(collected) + "\n" + + +def fileLeadComment(text): + '''Return the file-level comment block at the very top of `text`, or ''. + + Only treated as a file-level comment when a blank line separates it from the code below; + otherwise it is the leading doc comment of the first function and is captured as that + function's 'lead' instead (avoids emitting it twice). Returned with a trailing blank line.''' + m = re.match(r"((?:[ \t]*(?://[^\n]*|/\*.*?\*/)[ \t]*\n)+)", text, re.S) + if not m: + return "" + if text[m.end():].startswith("\n"): + return m.group(1).rstrip("\n") + "\n\n" + return "" + + +def _stableAnchor(stmt): + '''Pick a token from a GLSL statement likely to survive naga's restructuring, or None. + + Preference order (most to least stable): an assignment's LHS name, a distinctive mx_* call, a + returned identifier. Pure literals are intentionally excluded -- they recur too often to place + a comment reliably. Used only to re-anchor best-effort inline comments; a miss just drops the + comment, never corrupts output.''' + m = re.match(r"(?:[A-Za-z_]\w*\s+)?([A-Za-z_]\w*)\s*(?:\[[^\]]*\])?\s*=(?!=)", stmt) + if m and m.group(1) not in GLSL_KEYWORDS: + return m.group(1) + m = re.search(r"\b(mx_\w+)\s*\(", stmt) + if m: + return m.group(1) + m = re.match(r"return\s+([A-Za-z_]\w*)", stmt) + if m and m.group(1) not in GLSL_KEYWORDS: + return m.group(1) + return None + + +def extractInlineComments(glslFull): + '''Return [(anchor, [comment_line, ...])] for whole-line // comments inside a GLSL body. + + Each contiguous run of `// ...` lines is anchored to a stable token of the next statement, so + it can be re-placed in naga's output. Best-effort: runs whose following statement has no stable + anchor are dropped. Only whole-line comments are considered (trailing comments are ignored).''' + groups = [] + pending = [] + started = False + for ln in glslFull.split("\n"): + s = ln.strip() + if not started: + if "{" in ln: + started = True + continue + if s.startswith("//"): + pending.append(s) + elif s == "" or s.startswith("/*") or s.endswith("*/"): + continue # blank / block comment -- keep pending attached to the next statement + elif pending: + anchor = _stableAnchor(s) + if anchor: + groups.append((anchor, list(pending))) + pending = [] + return groups + + +def injectInlineComments(glslFull, wgslText): + '''Re-attach GLSL inline comments to the transpiled WGSL body (best-effort, never corrupting). + + naga discards comments and restructures bodies, so each comment run is placed on its own line(s) + above the first WGSL line (at or after the previous placement) containing its anchor token. + Anchors are matched monotonically so ordering is preserved and deterministic; an unmatched + anchor's comments are dropped. Only whole-line insertion -- never mid-expression.''' + groups = extractInlineComments(glslFull) + if not groups: + return wgslText + lines = wgslText.split("\n") + searchFrom = 1 # skip the `fn ...(` signature line + inserts = [] + for anchor, comments in groups: + pat = re.compile(r"\b" + re.escape(anchor) + r"\b") + found = next((i for i in range(searchFrom, len(lines)) if pat.search(lines[i])), -1) + if found < 0: + continue # deterministic drop + inserts.append((found, comments)) + searchFrom = found + 1 + for idx, comments in reversed(inserts): + indent = lines[idx][:len(lines[idx]) - len(lines[idx].lstrip())] + for c in reversed(comments): + lines.insert(idx, indent + c) + if os.environ.get("MTLX_DEBUG"): + dropped = len(groups) - len(inserts) + if dropped: + print(f" inline comments: placed {len(inserts)}, dropped {dropped}") + return "\n".join(lines) + + +def parseFunctions(text): + '''Return [{ret, name, params, full, lead}] for every top-level function *definition*. Heuristic, not a real parser: we match the ` (` shape, then confirm it is a definition (not a call or a prototype) by checking that a `{` follows the closing paren. The keyword filter rejects control-flow that looks like ` (` (e.g. `else if (`), and the `{`-lookahead rejects prototypes ending in `;`. `full` is the entire definition text (signature - through matching `}`) so callers can re-emit or wrap it.""" + through matching `}`) so callers can re-emit or wrap it; `lead` is the doc comment directly above + the definition (re-attached after transpilation since naga discards comments).''' fns = [] for m in re.finditer(r"\b([A-Za-z_]\w*)\s+([A-Za-z_]\w*)\s*\(", text): if m.group(1) in GLSL_KEYWORDS or m.group(2) in GLSL_KEYWORDS: continue # control-flow construct (e.g. `else if (...) {`), not a definition - open_p = text.index("(", m.start(2)) - close_p = _match_paren(text, open_p) - if close_p < 0: + openP = text.index("(", m.start(2)) + closeP = _matchParen(text, openP) + if closeP < 0: continue # Skip whitespace after `)`; a definition has `{` here, a call/prototype has `;` or `,`. - j = close_p + 1 + j = closeP + 1 while j < len(text) and text[j].isspace(): j += 1 if j >= len(text) or text[j] != "{": continue # a call/prototype, not a definition - end = _match_brace(text, j) + end = _matchBrace(text, j) fns.append({"ret": m.group(1), "name": m.group(2), - "params": text[open_p + 1:close_p].strip(), "full": text[m.start(1):end]}) + "params": text[openP + 1:closeP].strip(), "full": text[m.start(1):end], + "lead": _leadingComment(text, m.start(1))}) return fns -def fn_param_types(params_str): - """Extract bare GLSL parameter types from a parameter list string.""" +def fnParamTypes(paramsStr): + '''Extract bare GLSL parameter types from a parameter list string.''' types = [] - for p in params_str.split(","): + for p in paramsStr.split(","): p = re.sub(r"^\s*(const|in|out|inout)\s+", "", p.strip()) if p: types.append(re.sub(r"\s+", " ", @@ -394,12 +530,33 @@ def fn_param_types(params_str): return tuple(types) -def expand_lib_tokens(text): - """Replace MaterialX $-tokens and texture/sampler calls with naga-parseable GLSL. +def fnParamNames(paramsStr): + '''Extract GLSL parameter identifiers from a parameter list string.''' + names = [] + for p in paramsStr.split(","): + p = re.sub(r"^\s*(const|in|out|inout)\s+", "", p.strip()) + if not p or "sampler2D" in p: + continue + tokens = p.split() + if tokens: + names.append(tokens[-1].split("[")[0]) + return names + + +def fnSigKey(fn): + '''(name, param-types) identity for a parsed function; distinguishes overloads sharing a name.''' + return (fn["name"], fnParamTypes(fn["params"])) + - Node transpile uses per-function MTLXTOK_* sentinels; lib transpile uses fixed substitutions - from LIB_TOKEN_FIXUPS plus regex rewrites below so included lib bodies compile as a unit.""" - for rx, repl in LIB_TOKEN_FIXUPS: +def expandLibTokens(text, libroot=None): + '''Replace MaterialX $-tokens and texture/sampler calls with naga-parseable GLSL. + + Node transpile uses per-function MTLXTOK_* sentinels; lib transpile uses libTokenFixups() + (NAGA_LIB_STUBS + $closureDataConstructor from HwConstants.cpp) plus the regex rewrites below + that stub sampler types and texture ops naga's GLSL frontend cannot parse.''' + if libroot is None: + libroot = _activeLibroot() + for rx, repl in libTokenFixups(libroot): text = rx.sub(repl, text) # Replace sampler types and texture ops with stubs naga accepts. text = re.sub(r"\bsampler2D\s+mtlx_\w+\b", "int mtlx_sampler_stub", text) @@ -428,74 +585,192 @@ def expand_lib_tokens(text): return text -def wgsl_fn_name(glsl_name, types): - """Return the genwgsl function name for a GLSL overload. +def wgslFnName(glslName, types, overloaded): + '''Return the genwgsl function name for a GLSL overload. - CALL_MAP is keyed by (name, param_types). For lib *output* we use this to emit type-suffixed - definitions (e.g. mx_square_f32); for node *calls* remap_calls() applies the same map.""" - target = CALL_MAP.get((glsl_name, types)) - if target: - return target - return glsl_name + Delegates to mangle(): scheme/exception families are renamed (e.g. mx_square -> mx_square_f32), + everything else is unchanged. Used for lib *definitions*; remapCalls() applies the same map to + node *calls*. A None result (intentionally unsupported) falls back to the plain name here, since + a definition always needs a name; unsupported *calls* are handled in remapCalls.''' + return mangle(glslName, types, overloaded) or glslName -def apply_field_renames(text): - """Apply LIB_FIELD_RENAMES to struct bodies and field accesses in transpiled WGSL.""" - for rx, repl in LIB_FIELD_RENAMES: - text = rx.sub(repl, text) - return text +def denagaName(text, knownNames, collisions=True): + '''Reverse naga's identifier renaming for symbols the transpiler re-emits with clean names. + + naga's `Namer::call` (proc/namer.rs) rewrites an identifier deterministically: the first use of + a sanitized base gets a trailing `_` iff it ends in a digit or is a keyword/builtin, and any + later collision with an already-used name gets `_`. Our declarations (consts, struct fields, + prototypes) keep the clean GLSL spelling, so references naga emitted as `NAME_` or `NAME_` + must be mapped back to `NAME`. + - collisions=True handles both `NAME_` (digit/keyword rule) and `NAME_` (collision rule); + use for consts/fields naga may see more than once. + - collisions=False handles only the single trailing `_`; use for names where a `_` form is a + *distinct* symbol that must be preserved (e.g. overloaded function stubs mx_foo_1, remapped + elsewhere). -def resolve_const_refs(text, const_names): - """Rewrite naga-suffixed references to module-scope consts back to their declared name. - - naga's WGSL backend suffixes an identifier reference with `_` when the name ends in a digit - (guarding its own `name_N` SSA scheme) and with `_` when the name collides with another - declaration in naga's input. Both bite our file-scope consts: build_context puts every lib - const in naga's parse context, and the sibling-body path *also* emits the file's preamble into - the wrapped fragment, so naga sees `FRESNEL_MODEL_SCHLICK` twice and renames body references to - `FRESNEL_MODEL_SCHLICK_1`; a digit-ending const like FUJII_CONSTANT_1 becomes FUJII_CONSTANT_1_. - The declarations we emit keep the clean GLSL name, so map `NAME_` / `NAME_` back to `NAME`. - Longest name first so a shorter const can't capture a longer one's suffix; a suffixed token that - is itself a declared const (e.g. FUJII_CONSTANT_1) is left untouched. Keyed only on known const - names, so naga's numbered *locals* (e.g. F0_1 = F0) are not affected.""" - names = set(n for n in const_names if n) + Longest name first so a shorter symbol can't capture a longer one's suffix; a suffixed token + that is itself a known name (e.g. FUJII_CONSTANT_1) is left untouched. Only listed names are + keyed, so naga's numbered *locals* are unaffected.''' + names = set(n for n in knownNames if n) + suffix = r"_\d*" if collisions else r"_" for nm in sorted(names, key=len, reverse=True): - text = re.sub(r"\b" + re.escape(nm) + r"_\d*\b", + text = re.sub(r"\b" + re.escape(nm) + suffix + r"\b", lambda m: m.group(0) if m.group(0) in names else nm, text) return text -def const_names_from_wgsl(consts): - """Names of `const NAME: T = ...` declarations (WGSL form, from transpile_glsl_consts).""" +def applyFieldRenames(text, libroot=None): + '''Reverse naga's ends-in-digit `_` on struct fields declared in the closure preamble. + + FresnelData fields F0/F82/F90 end in a digit; naga suffixes references (F0_) but our preamble + re-emits the clean names. Digit-field names are discovered from wgslClosurePreamble() rather + than a hand-maintained list. The former tf_* -> thinfilm_* rename was removed: GLSL FresnelData + now uses thinfilm_* field names directly (mx_microfacet_specular.glsl).''' + if libroot is None: + libroot = _activeLibroot() + return denagaName(text, preambleDigitFields(libroot), collisions=False) + + +def resolveConstRefs(text, constNames): + '''Rewrite naga-suffixed references to module-scope consts back to their declared name. + + buildContext puts every lib const in naga's parse context, and the sibling-body path *also* + emits the file's preamble into the wrapped fragment, so naga sees `FRESNEL_MODEL_SCHLICK` twice + and renames body references to `FRESNEL_MODEL_SCHLICK_1`; a digit-ending const like + FUJII_CONSTANT_1 becomes FUJII_CONSTANT_1_. Delegates to denagaName() (both the digit/keyword + and collision rules apply to consts).''' + return denagaName(text, constNames, collisions=True) + + +def constNamesFromWgsl(consts): + '''Names of `const NAME: T = ...` declarations (WGSL form, from transpileGlslConsts).''' return [m.group(1) for c in consts for m in [re.match(r"const (\w+):", c)] if m] -def glsl_const_names(glsl_text): - """Names of file-scope `const TYPE NAME = ...` declarations in GLSL context text (e.g. base).""" - return re.findall(r"\bconst\s+[\w<>]+\s+([A-Za-z_]\w*)\s*=", glsl_text) +def glslConstNames(glslText): + '''Names of file-scope `const TYPE NAME = ...` declarations in GLSL context text (e.g. base).''' + return re.findall(r"\bconst\s+[\w<>]+\s+([A-Za-z_]\w*)\s*=", glslText) -def glsl_type_to_wgsl(typename): +def glslTypeToWgsl(typename): return GLSL_TO_WGSL_TYPE.get(typename, typename) -def transpile_glsl_structs(text): - """Convert top-level GLSL struct definitions to WGSL (emitted above lib fn bodies). +# ----------------------------------------------------------------------------- +# derived preambles & token registry +# ----------------------------------------------------------------------------- +# +# Closure struct layouts and MaterialX $-token handling were previously duplicated as hand-written +# tables (CLOSURE_PREAMBLE, LIB_STRUCT_PREAMBLE, LIB_TOKEN_FIXUPS, LIB_FIELD_RENAMES). These helpers +# derive the same output from the canonical C++/GLSL sources so genglsl and the WGSL generator stay +# the single source of truth. + + +def setActiveLibroot(libroot): + '''Record the libraries root for preamble/token resolution (called from main()). + + libroot is typically `libraries/`; repo-relative paths (HwConstants.cpp, GlslSyntax.cpp, + genglsl/lib/*.glsl) are resolved via _repoRoot().''' + global _ACTIVE_LIBROOT + _ACTIVE_LIBROOT = Path(libroot).resolve() + + +def _activeLibroot(): + '''Return the libroot set by setActiveLibroot(); required before any preamble/token helper.''' + if _ACTIVE_LIBROOT is None: + raise RuntimeError("setActiveLibroot() must be called before transpilation") + return _ACTIVE_LIBROOT + + +def _repoRoot(libroot): + '''Parent of libroot (the MaterialX repo root when libroot is `libraries/`).''' + return Path(libroot).resolve().parent + + +def _readSourceText(path): + '''Read a repo source file as UTF-8 text.''' + return path.read_text(encoding="utf-8") + + +def loadHwTokenNames(libroot): + '''Return every MaterialX $-token name declared in HwConstants.cpp (T_* constants). + + Used by validateTokenCoverage to ensure lib GLSL only references tokens the C++ generator + knows about. Does not include stub values -- those are transpiler policy (NAGA_LIB_STUBS).''' + path = _repoRoot(libroot) / "source" / "MaterialXGenHw" / "HwConstants.cpp" + if not path.is_file(): + return set() + text = _readSourceText(path) + return set(re.findall(r'const string T_\w+\s*=\s*"(\$[^"]+)"', text)) + + +def nagaLibStubs(libroot): + '''Merge NAGA_LIB_STUBS with HwConstants values needed for naga-parseable lib transpile. + + Currently adds $closureDataConstructor (the makeClosureData function name string) so lib + sources using that token expand to a real GLSL identifier before naga runs.''' + stubs = dict(NAGA_LIB_STUBS) + hw = _repoRoot(libroot) / "source" / "MaterialXGenHw" / "HwConstants.cpp" + if hw.is_file(): + text = _readSourceText(hw) + m = re.search(r'const string CLOSURE_DATA_CONSTRUCTOR\s*=\s*"([^"]+)"', text) + if m: + stubs["$closureDataConstructor"] = m.group(1) + return stubs + + +def libTokenFixups(libroot): + '''(compiled_regex, replacement) pairs for expandLibTokens, longest token first. + + Longest-first ordering prevents a short $-token prefix from partially matching a longer one + (e.g. $texSampler vs $texSamplerSignature).''' + stubs = nagaLibStubs(libroot) + return [(re.compile(re.escape(tok)), repl) + for tok, repl in sorted(stubs.items(), key=lambda x: len(x[0]), reverse=True)] + + +def loadGlslSyntaxSnippets(repoRoot): + '''Extract struct and #define snippets from MaterialXGenGlsl/GlslSyntax.cpp. + + Parses the AggregateTypeSyntax definition strings (BSDF, VDF, surfaceshader, etc.) and the + EDF/material aliases exactly as the GLSL generator emits them. Node transpile uses the full set; + wgslClosurePreamble uses BSDF/VDF only (ClosureData/FresnelData come from genglsl/lib).''' + path = repoRoot / "source" / "MaterialXGenGlsl" / "GlslSyntax.cpp" + text = _readSourceText(path) + snippets = {} + for name in _GLSL_SYNTAX_STRUCT_NAMES: + m = re.search(rf'"struct {name} \{{[^"]+\}};"', text) + if m: + snippets[name] = m.group(0).strip('"') + for key, pattern in (("EDF", r'"#define EDF vec3"'), + ("material", r'"#define material surfaceshader"')): + m = re.search(pattern, text) + if m: + snippets[key] = m.group(0).strip('"') + return snippets + - Skips types provided centrally by LIB_STRUCT_PREAMBLE (see LIB_PREAMBLE_STRUCTS). Strips GLSL - comments before splitting fields -- otherwise a comment word is read as a `type name` pair (e.g. - `// Fresnel model` -> `Fresnel: //,`) and the real following field is dropped. Rewrites GLSL - array fields to WGSL (`vec2 coords[3]` -> `coords: array`).""" +def _emitWgslStructsFromGlsl(text, only=None, skip=None): + '''Convert selected GLSL struct definitions in `text` to WGSL struct blocks. + + Shared by wgslClosurePreamble and transpileGlslStructs. Strips GLSL comments, maps scalar/ + vector types via GLSL_TO_WGSL_TYPE, and rewrites array fields (`name[N]` -> `array`). + When `only` is set, emit just those struct names (used to pull ClosureData/FresnelData from lib + files without re-emitting every struct in the source). When `skip` is set, omit those names + (used to drop closure structs provided centrally by wgslClosurePreamble).''' out = [] for m in re.finditer(r"\bstruct\s+(\w+)\s*\{([^}]*)\}\s*;?", text): name = m.group(1) - if name in LIB_PREAMBLE_STRUCTS: + if only is not None and name not in only: + continue + if skip is not None and name in skip: continue - body_src = re.sub(r"/\*.*?\*/", "", m.group(2), flags=re.S) # block comments - body_src = re.sub(r"//[^\n]*", "", body_src) # line comments + bodySrc = re.sub(r"/\*.*?\*/", "", m.group(2), flags=re.S) + bodySrc = re.sub(r"//[^\n]*", "", bodySrc) fields = [] - for part in body_src.split(";"): + for part in bodySrc.split(";"): part = part.strip() if not part: continue @@ -503,8 +778,8 @@ def transpile_glsl_structs(text): if len(tokens) < 2: continue ftype, fname = tokens[0], tokens[1] - wtype = glsl_type_to_wgsl(ftype) - arr = re.match(r"(\w+)\[(\d+)\]$", fname) # GLSL array field `name[N]` + wtype = glslTypeToWgsl(ftype) + arr = re.match(r"(\w+)\[(\d+)\]$", fname) if arr: fname, wtype = arr.group(1), f"array<{wtype}, {arr.group(2)}>" fields.append(f" {fname}: {wtype},") @@ -514,39 +789,129 @@ def transpile_glsl_structs(text): return out -def transpile_glsl_consts(text): - """Convert file-scope GLSL `const T name = ...` to WGSL `const name: T = ...` syntax. +def _buildWgslClosurePreamble(libroot): + '''Build the WGSL struct preamble prepended to mx_closure_type.wgsl. + + Replaces the former LIB_STRUCT_PREAMBLE hand-written block. Sources: + - ClosureData from libraries/pbrlib/genglsl/lib/mx_closure_type.glsl + - BSDF, VDF from GlslSyntax.cpp (same layout as GLSL generator) + - EDF, material aliases (vec3f / surfaceshader) + - FresnelData from libraries/pbrlib/genglsl/lib/mx_microfacet_specular.glsl + surfaceshader is omitted here (WgslShaderGenerator::emitTypeDefinitions owns it).''' + libroot = Path(libroot).resolve() + repo = _repoRoot(libroot) + parts = [] + closureSrc = _readSourceText(libroot / "pbrlib" / "genglsl" / "lib" / "mx_closure_type.glsl") + specSrc = _readSourceText(libroot / "pbrlib" / "genglsl" / "lib" / "mx_microfacet_specular.glsl") + parts.extend(_emitWgslStructsFromGlsl(closureSrc, only={"ClosureData"})) + snippets = loadGlslSyntaxSnippets(repo) + for name in ("BSDF", "VDF"): + parts.extend(_emitWgslStructsFromGlsl(snippets[name])) + parts.append("alias EDF = vec3f;") + parts.append("alias material = surfaceshader;") + parts.extend(_emitWgslStructsFromGlsl(specSrc, only={"FresnelData"})) + return "\n\n".join(parts) + "\n" + + +def wgslClosurePreamble(libroot): + '''Cached WGSL closure preamble for mx_closure_type.wgsl (see _buildWgslClosurePreamble).''' + key = str(Path(libroot).resolve()) + if key not in _WGSL_CLOSURE_PREAMBLE: + _WGSL_CLOSURE_PREAMBLE[key] = _buildWgslClosurePreamble(libroot) + return _WGSL_CLOSURE_PREAMBLE[key] + + +def _buildGlslClosurePreamble(libroot): + '''Build the GLSL type preamble injected into node transpile context (buildContext). + + Replaces the former CLOSURE_PREAMBLE constant. Emits every aggregate/shader struct and the + EDF/material #defines from GlslSyntax.cpp so node fragments can reference BSDF, surfaceshader, + etc. without #include-ing the full lib tree.''' + repo = _repoRoot(libroot) + snippets = loadGlslSyntaxSnippets(repo) + lines = [snippets[n] for n in _GLSL_SYNTAX_STRUCT_NAMES] + lines.append(snippets["EDF"]) + lines.append(snippets["material"]) + return "\n".join(lines) + "\n" + + +def glslClosurePreamble(libroot): + '''Cached GLSL closure preamble for node transpile (see _buildGlslClosurePreamble).''' + key = str(Path(libroot).resolve()) + if key not in _GLSL_CLOSURE_PREAMBLE: + _GLSL_CLOSURE_PREAMBLE[key] = _buildGlslClosurePreamble(libroot) + return _GLSL_CLOSURE_PREAMBLE[key] + + +def preambleDigitFields(libroot): + '''Field names ending in a digit in the WGSL closure preamble (for denagaName). + + Scans wgslClosurePreamble() output so F0/F82/F90 (and any future digit-ending fields) are + reconciled automatically when the derived preamble changes.''' + return sorted(set(re.findall(r"^\s*(\w*\d)\s*:", wgslClosurePreamble(libroot), re.M))) + + +def transpileGlslStructs(text): + '''Convert top-level GLSL struct definitions to WGSL (emitted above lib fn bodies). + + Skips types provided centrally by wgslClosurePreamble (see LIB_PREAMBLE_STRUCTS). Delegates + to _emitWgslStructsFromGlsl, which strips GLSL comments before splitting fields (otherwise a + comment word is read as a `type name` pair) and rewrites array fields to WGSL.''' + return _emitWgslStructsFromGlsl(text, skip=LIB_PREAMBLE_STRUCTS) + + +def transpileGlslConsts(text): + '''Convert file-scope GLSL `const T name = ...` to WGSL `const name: T = ...` syntax. Only *top-level* consts are emitted. Function-local consts (e.g. the `const int SAMPLE_COUNT` inside several directional-albedo helpers) are handled by naga inside the transpiled bodies; hoisting them here would produce duplicate module-scope consts -- and since the same name recurs across microfacet libs, redeclaration errors once several are included together. Blank - out function bodies first so their indented consts aren't matched by the file-scope regex.""" + out function bodies first so their indented consts aren't matched by the file-scope regex.''' scope = text - for fn in parse_functions(text): + for fn in parseFunctions(text): scope = scope.replace(fn["full"], "") out = [] for m in re.finditer(r"^\s*const\s+(\w+)\s+(\w+)\s*=\s*([^;]+);", scope, re.M): - out.append(f"const {m.group(2)}: {glsl_type_to_wgsl(m.group(1))} = {m.group(3)};") + out.append(f"const {m.group(2)}: {glslTypeToWgsl(m.group(1))} = {m.group(3)};") return out -def lib_includes(text): - """Return #include paths from a lib file (lib/... only).""" +def libIncludes(text): + '''Return #include paths from a lib file (lib/... only).''' return [inc for inc in re.findall(r'#include\s+"([^"]+)"', text) if inc.startswith("lib/")] -def topo_sort_lib_files(lib_files): - """Topological sort of lib/*.glsl by `#include \"lib/...\"` dependencies. +def stripIncludes(text): + '''Remove whole `#include "..."` lines (naga sees inlined bodies, not include directives).''' + return re.sub(r'#include\s+"[^"]+"\s*\n', "", text) + + +def generatedBanner(srcRel): + '''The two-line "generated, do not edit" header prepended to every emitted .wgsl file.''' + return (f"// Generated from {srcRel} by source/MaterialXGenWgsl/tools/mxgenwgsl.py.\n" + f"// Do not edit -- re-run the transpiler to regenerate " + f"(see source/MaterialXGenWgsl/README.md).\n\n") + + +def writeGenerated(outPath, content, label): + '''Create parent dirs, write a generated .wgsl file, and log its OK line.''' + outPath.parent.mkdir(parents=True, exist_ok=True) + outPath.write_text(content, encoding="utf-8") + print(f" OK {label} -> {outPath}") + + +def topoSortLibFiles(libFiles): + '''Topological sort of lib/*.glsl by `#include \"lib/...\"` dependencies. Ensures mx_microfacet.glsl is processed before mx_microfacet_specular.glsl, etc., so - generated genwgsl/lib/*.wgsl includes resolve when nodes are transpiled later.""" - stems = {f.stem: f for f in lib_files} + generated genwgsl/lib/*.wgsl includes resolve when nodes are transpiled later.''' + stems = {f.stem: f for f in libFiles} deps = {} - for f in lib_files: + for f in libFiles: txt = f.read_text(encoding="utf-8") - deps[f.stem] = {Path(inc).stem for inc in lib_includes(txt) if Path(inc).stem in stems} + deps[f.stem] = {Path(inc).stem for inc in libIncludes(txt) if Path(inc).stem in stems} order, seen = [], set() def visit(stem): @@ -562,43 +927,45 @@ def visit(stem): return order -def postprocess_wgsl_fn(text, fn_name, protos, lib_symbols=None, remap=True): - """Shared post-processing for one transpiled WGSL function (lib or node). +def postprocessWgslFn(text, fnName, protos, overloaded, libSymbols=None, remap=True, + glslParamNames=None): + '''Shared post-processing for one transpiled WGSL function (lib or node). - Runs cleanup, type normalization, CALL_MAP remapping, and optional lib arity checks. - Returns (text, []) on success or (None, unsupported_calls) when remap/arity fails.""" - text = cleanup_function(text) + Runs cleanup, type normalization, overload remapping, and optional lib arity checks. + Returns (text, []) on success or (None, unsupported_calls) when remap/arity fails.''' + text = cleanupFunction(text, glslParamNames) for rx, repl in TYPE_FIXUPS: text = rx.sub(repl, text) text = re.sub(r"\b(\d+\.\d+(?:[eE][-+]?\d+)?)f\b", r"\1", text) text = re.sub(r"(?').""" - sig = re.match(r"fn\s+\w+\s*\(([^)]*)\)", fn_text, re.S) +def _wgslSigTypes(fnText): + '''Parameter types from a (raw naga) WGSL fn signature, e.g. ('f32', 'vec3').''' + sig = re.match(r"fn\s+\w+\s*\(([^)]*)\)", fnText, re.S) if not sig: return () out = [] @@ -628,52 +995,42 @@ def _wgsl_sig_types(fn_text): return tuple(out) -def extract_transpiled_fn(wgsl, glsl_name, wgsl_name, glsl_types=None): - """Pull one non-stub function from naga output and rename to the target genwgsl name. +def extractTranspiledFn(wgsl, glslName, wgslName, glslTypes=None): + '''Pull one non-stub function from naga output and rename to the target genwgsl name. Per-function lib/node transpile feeds naga a module with one real body; this extracts that - body and renames it (e.g. mx_square -> mx_square_f32) via wgsl_fn_name. When several same-named + body and renames it (e.g. mx_square -> mx_square_f32) via wgslFnName. When several same-named overloads appear in one naga module (sibling path, e.g. mx_ggx_dir_albedo's vec3, scalar, and - FresnelData forms), `glsl_types` disambiguates by matching the parameter signature; with a single + FresnelData forms), `glslTypes` disambiguates by matching the parameter signature; with a single candidate the base-name match is used directly (signature match is skipped, since naga rewrites - `out` params to pointers and would otherwise not compare equal).""" - cands = [(name, t) for name, t in top_level_fns(wgsl) - if fn_base(name) == glsl_name and "{\n}" not in t and t.count("\n") > 1] + `out` params to pointers and would otherwise not compare equal).''' + cands = [(name, t) for name, t in topLevelFns(wgsl) + if fnBase(name) == glslName and "{\n}" not in t and t.count("\n") > 1] if not cands: return None chosen = cands[0] - if len(cands) > 1 and glsl_types is not None: - want = tuple(GLSL_TO_NAGA_TYPE.get(t, t) for t in glsl_types) - chosen = next(((n, t) for n, t in cands if _wgsl_sig_types(t) == want), cands[0]) + if len(cands) > 1 and glslTypes is not None: + want = tuple(GLSL_TO_NAGA_TYPE.get(t, t) for t in glslTypes) + chosen = next(((n, t) for n, t in cands if _wgslSigTypes(t) == want), cands[0]) name, t = chosen - return re.sub(r"(\bfn\s+)" + re.escape(name) + r"\b", r"\1" + wgsl_name, t) - - -# Closure/shader types the generator emits (GlslShaderGenerator::emitTypeDefinitions), not the -# lib files -- supply them so node BSDF/EDF signatures parse. -CLOSURE_PREAMBLE = """ -struct BSDF { vec3 response; vec3 throughput; }; -struct VDF { vec3 response; vec3 throughput; }; -#define EDF vec3 -struct surfaceshader { vec3 color; vec3 transparency; }; -struct volumeshader { vec3 color; vec3 transparency; }; -struct displacementshader { vec3 offset; float scale; }; -struct lightshader { vec3 intensity; vec3 direction; }; -#define material surfaceshader -""" + return re.sub(r"(\bfn\s+)" + re.escape(name) + r"\b", r"\1" + wgslName, t) + +# Closure/shader types for node transpile are built from GlslSyntax.cpp (glslClosurePreamble). +# WGSL lib types for mx_closure_type.wgsl use wgslClosurePreamble (genglsl/lib + GlslSyntax.cpp). -def naga_proto_params(params_str): - """Normalize GLSL prototype params for naga: strip the semantically-neutral `const`/`in` + +def nagaProtoParams(paramsStr): + '''Normalize GLSL prototype params for naga: strip the semantically-neutral `const`/`in` qualifiers, KEEP `out`/`inout`, and drop sampler params naga's prototype parser rejects. Keeping `out`/`inout` is essential: naga renders those params as `ptr`, and when it transpiles a *caller* it must see the callee's prototype as by-pointer so it passes the pointer (e.g. `mx_normalmap_vector2(..., result)`) rather than dereferencing it (`(*result)`), which would mismatch the transpiled definition's pointer parameter. naga accepts `out`/`inout` in a - forward declaration.""" + forward declaration.''' parts = [] - for p in params_str.split(","): + for p in paramsStr.split(","): p = p.strip() p = re.sub(r"^const\s+", "", p) # const: no call-site effect p = re.sub(r"^in\s+", "", p) # `in` is the default (by value); `\s+` guards `int`/`inout` @@ -683,18 +1040,19 @@ def naga_proto_params(params_str): return ", ".join(parts) -def build_context(libroot, libs, nodes=True): - """Build the naga parse context. Returns (base, protos, overloaded): - base - CLOSURE_PREAMBLE + dedup'd #defines/consts/struct-defs from every genglsl lib. +def buildContext(libroot, libs, nodes=True): + '''Build the naga parse context. Returns (base, protos, overloaded): + base - glslClosurePreamble + dedup'd #defines/consts/struct-defs from every genglsl lib. protos - {(name, types): "prototype;"} for every function defined in any genglsl lib OR node file, so helpers AND cross-node calls (e.g. mx_burn_color3 -> mx_burn_float) and generator-supplied helpers (mx_environment_radiance) all resolve. overloaded - the set of names appearing with more than one signature (informational; the - actual overload remapping is driven by CALL_MAP in remap_calls). + actual overload remapping is driven by mangle() in remapCalls). Per node we emit `base` + every prototype except the one being defined. Pass nodes=False for lib transpilation: skip mx_*.glsl node files (their `inout` protos break - naga) and rely on inlined lib bodies + LIB_PREAMBLE instead of CLOSURE_PREAMBLE duplication.""" + naga) and omit glslClosurePreamble (lib bodies are inlined whole; closure types come from + wgslClosurePreamble in mx_closure_type.wgsl only).''' defines, consts, structs, protos = {}, {}, {}, {} # Scan lib/ helpers and the node files themselves (the latter for cross-node prototypes). sources = [] @@ -719,82 +1077,34 @@ def build_context(libroot, libs, nodes=True): consts.setdefault(m.group(1), m.group(0).strip()) for m in re.finditer(r"\bstruct\s+(\w+)\s*\{[^}]*\}\s*;?", txt): structs.setdefault(m.group(1), m.group(0)) - for fn in parse_functions(txt): - types = fn_param_types(fn["params"]) + for fn in parseFunctions(txt): + types = fnParamTypes(fn["params"]) protos.setdefault((fn["name"], types), - f"{fn['ret']} {fn['name']}({naga_proto_params(fn['params'])});") - base_items = list(defines.values()) + list(consts.values()) + list(structs.values()) - base = CLOSURE_PREAMBLE + "\n".join(s for s in base_items if "$" not in s) + f"{fn['ret']} {fn['name']}({nagaProtoParams(fn['params'])});") + baseItems = list(defines.values()) + list(consts.values()) + list(structs.values()) + # Node context needs closure/shader types from GlslSyntax.cpp; lib-only pass skips them. + preamble = glslClosurePreamble(libroot) if nodes else "" + base = preamble + "\n".join(s for s in baseItems if "$" not in s) protos = {k: v for k, v in protos.items() if "$" not in v} # Names with more than one signature are GLSL-overloaded. WGSL has no overloading and the # hand-written genwgsl lib renames/consolidates these (e.g. mx_square_f32, a single # mx_ggx_dir_albedo), so the util cannot guarantee a transpiled call resolves against the lib. - proto_names = [nm for (nm, _t) in protos] - overloaded = {nm for nm in proto_names if proto_names.count(nm) > 1} + protoNames = [nm for (nm, _t) in protos] + overloaded = {nm for nm in protoNames if protoNames.count(nm) > 1} return base, protos, overloaded -# ---------------------------------------------------------------------------- cleanup - -# naga emits SSA-style output: function parameters are immutable in its IR, so it copies each into a -# mutable local shadow, and it spills every subexpression into a numbered `let _eN`. Faithful but -# unreadable. The two passes below undo that churn with conservative, single-assignment-safe rewrites -# (anything ambiguous is left as-is -- correct but verbose), so the committed WGSL reads like the -# hand-written files. - -def collapse_param_copies(body, params): - """Fold naga's read-only param shadows: `var p_1: T; p_1 = p;` -> use `p` directly. - - Only safe when the shadow is assigned exactly once (the `p_1 = p;` init). If naga reassigns the - shadow later, the single-assignment test fails and we leave it alone.""" - for p in params: - for m in re.finditer(r"\bvar\s+(" + re.escape(p) + r"_\d+)\s*:\s*[\w<>]+\s*;", body): - shadow = m.group(1) - if len(re.findall(r"\b" + re.escape(shadow) + r"\s*=", body)) == 1 and \ - re.search(r"\b" + re.escape(shadow) + r"\s*=\s*" + re.escape(p) + r"\s*;", body): - body = re.sub(r"[ \t]*\bvar\s+" + re.escape(shadow) + r"\s*:\s*[\w<>]+\s*;\n?", "", body) - body = re.sub(r"[ \t]*\b" + re.escape(shadow) + r"\s*=\s*" + re.escape(p) + r"\s*;\n?", "", body) - body = re.sub(r"\b" + re.escape(shadow) + r"\b", p, body) - return body - - -def inline_single_use_temps(body): - """Inline naga's single-use `let _eN = expr;` temporaries. - - Iterates to a fixpoint: each pass inlines one temp used exactly once (the `- 1` discounts the - definition itself from the match count), then restarts because inlining can make another temp - single-use. The expr is wrapped in parens when it contains an operator, so precedence is - preserved at the splice site.""" - changed = True - while changed: - changed = False - for m in re.finditer(r"[ \t]*let\s+(_e\d+)\s*=\s*(.+?);\n", body): - name, expr = m.group(1), m.group(2) - if len(re.findall(r"\b" + re.escape(name) + r"\b", body)) - 1 == 1: - repl = "(" + expr + ")" if re.search(r"[-+*/ ]", expr.strip()) else expr - body = body[:m.start()] + body[m.end():] - body = re.sub(r"\b" + re.escape(name) + r"\b", lambda _m: repl, body, count=1) - changed = True - break # body changed; restart the scan from the top - return body - - -def cleanup_function(fn_text): - """Run both readability passes over one WGSL function, in dependency order (param shadows first, - then temps, since collapsing a shadow can leave a temp single-use).""" - sig = re.match(r"fn\s+\w+\s*\(([^)]*)\)", fn_text, re.S) - params = re.findall(r"(\w+)\s*:", sig.group(1)) if sig else [] - return inline_single_use_temps(collapse_param_copies(fn_text, params)) - - -def fn_base(name): - """naga may append `_`/`_N` to disambiguate; strip it to recover the source name.""" - return re.sub(r"_\d*$", "", name) - - -def _count_items(s, brackets="()[]<>"): - """Count top-level comma-separated items in `s` (0 if blank), ignoring commas nested inside - the given bracket pairs.""" +# ----------------------------------------------------------------------------- +# cleanup (see mxwgslcleanup.py) +# ----------------------------------------------------------------------------- +# +# Param-copy shadow collapse lives in mxwgslcleanup (tree-sitter). inlineSingleUseTemps runs there +# after collapse; nothing else to define in this module. + + +def _countItems(s, brackets="()[]<>"): + '''Count top-level comma-separated items in `s` (0 if blank), ignoring commas nested inside + the given bracket pairs.''' s = s.strip() if not s: return 0 @@ -812,11 +1122,11 @@ def _count_items(s, brackets="()[]<>"): return n -def build_wgsl_lib_symbols(libroot, libs, outroot=None): - """Parse genwgsl `lib/` files into {function name: parameter count}. +def buildWgslLibSymbols(libroot, libs, outroot=None): + '''Parse genwgsl `lib/` files into {function name: parameter count}. Reads from `outroot` when set (post-generation), else `libroot` (legacy hand-written tree). - """ + ''' symbols = {} root = outroot if outroot is not None else libroot for lib in libs: @@ -827,156 +1137,222 @@ def build_wgsl_lib_symbols(libroot, libs, outroot=None): txt = f.read_text(encoding="utf-8") for m in re.finditer(r"\bfn\s+([A-Za-z_]\w*)\s*\(", txt): op = txt.index("(", m.start()) - cp = _match_paren(txt, op) + cp = _matchParen(txt, op) if cp >= 0: - symbols[m.group(1)] = _count_items(txt[op + 1:cp]) + symbols[m.group(1)] = _countItems(txt[op + 1:cp]) return symbols -def check_lib_arity(text, lib_symbols): - """Return a list of calls whose arg count disagrees with the genwgsl lib's parameter count.""" +def checkLibArity(text, libSymbols): + '''Return a list of calls whose arg count disagrees with the genwgsl lib's parameter count.''' bad = [] for m in re.finditer(r"\b(mx_\w+)\s*\(", text): name = m.group(1) - if name not in lib_symbols: + if name not in libSymbols: continue # cross-node helper, builtin, or the node's own fn -- not a lib symbol op = text.index("(", m.start()) - cp = _match_paren(text, op) + cp = _matchParen(text, op) if cp < 0: continue - nargs = _count_items(text[op + 1:cp], "()[]") # call args: no generics, keep '<'/'>' literal - if nargs != lib_symbols[name]: - bad.append(f"{name} (call has {nargs}, lib expects {lib_symbols[name]})") + nargs = _countItems(text[op + 1:cp], "()[]") # call args: no generics, keep '<'/'>' literal + if nargs != libSymbols[name]: + bad.append(f"{name} (call has {nargs}, lib expects {libSymbols[name]})") return bad -def remap_calls(text, node_fn_name, protos): - """Rewrite overloaded/diverged helper calls to their genwgsl names via CALL_MAP. +# ----------------------------------------------------------------------------- +# scan-driven validation +# ----------------------------------------------------------------------------- +# +# These run off the same scan buildContext performs, so gaps between genglsl and the mangling +# tables (or the genwgsl lib) surface loudly instead of silently mis-emitting a name. + +def validateOverloadCoverage(protos, overloaded): + '''Return sorted overloaded (name, types) that mangle() cannot resolve. + + Every GLSL-overloaded helper must map to a genwgsl name (scheme or exception); an unresolved + overload would leak naga's `_N`-suffixed name into the output. A genuinely unsupported overload + should be an explicit `None` in EXCEPTIONS (which this treats as covered).''' + missing = [] + for key in sorted(protos): + name, types = key + if name in overloaded and key not in EXCEPTIONS and mangle(name, types, overloaded) is None: + missing.append(key) + return missing + + +def validateTokenCoverage(libroot, libs): + '''Return $-tokens in transpiled genglsl/lib sources missing from HwConstants or NAGA_LIB_STUBS. + + Two-tier check (replaces a single hand-maintained LIB_TOKEN_FIXUPS list): + 1. Token must be declared in HwConstants.cpp (loadHwTokenNames) -- same names the C++ + generator substitutes at shader link time. + 2. If the lib file is transpiled (not LIB_KEEP_HANDWRITTEN), token must also have a naga + stub (nagaLibStubs) so expandLibTokens can produce parseable GLSL before naga runs. + Hand-written lib dirs and LIB_KEEP_HANDWRITTEN files are skipped (they are not fed to naga).''' + known = loadHwTokenNames(libroot) + stubs = set(nagaLibStubs(libroot)) + unhandled = {} + for lib in libs: + if lib in HANDWRITTEN_LIB_DIRS: + continue + gllib = libroot / lib / "genglsl" / "lib" + if not gllib.is_dir(): + continue + for f in sorted(gllib.glob("*.glsl")): + if f.stem in LIB_KEEP_HANDWRITTEN: + continue + txt = f.read_text(encoding="utf-8") + for tok in re.findall(r"\$[A-Za-z_]\w*", txt): + if tok not in known: + unhandled.setdefault(tok, f"{f.name} (unknown to HwConstants)") + elif tok not in stubs: + # Declared in C++ but no naga-parseable stub for lib transpile. + unhandled.setdefault(tok, f"{f.name} (no NAGA stub)") + return unhandled + + +def validateLibNames(protos, libSymbols): + '''Return mangle() targets that are absent from the genwgsl lib symbol table. + + libSymbols should union the generated output and the hand-written stdlib names, so a scheme + that produces a name the real lib does not define (e.g. after a hand-written rename) fails + loudly. Overloads mangle() marks unsupported (None) are skipped -- their nodes stay hand-written.''' + missing = [] + for (name, types) in sorted(protos): + if name not in SUPPORTED_BASES: + continue + target = mangle(name, types, {name}) # name treated as overloaded for resolution + if target and target not in libSymbols: + missing.append(f"{name}{types} -> {target}") + return missing + + +def remapCalls(text, nodeFnName, protos, overloaded): + '''Rewrite overloaded/diverged helper calls to their genwgsl names via mangle(). naga numbers overload stubs (`mx_foo`, `mx_foo_1`, ...) by prototype-declaration order, and - proto_block is emitted sorted by (name, types), so the i-th call name corresponds to the i-th + protoBlock is emitted sorted by (name, types), so the i-th call name corresponds to the i-th entry of the base's sorted type list. Returns (text, unsupported) where unsupported lists any - call whose CALL_MAP entry is None/missing -- the caller fails the node so it stays hand-written. - """ - by_base = {} + call mangle() resolves to None -- the caller fails the node so it stays hand-written. + ''' + byBase = {} for (nm, types) in protos: - by_base.setdefault(nm, []).append(types) - for nm in by_base: - by_base[nm] = sorted(by_base[nm]) + byBase.setdefault(nm, []).append(types) + for nm in byBase: + byBase[nm] = sorted(byBase[nm]) unsupported = [] - for base in sorted({b for (b, _t) in CALL_MAP}, key=len, reverse=True): - if base == node_fn_name or base not in by_base: + for base in sorted(SUPPORTED_BASES, key=len, reverse=True): + if base == nodeFnName or base not in byBase: continue - for i, types in enumerate(by_base[base]): + for i, types in enumerate(byBase[base]): callname = base if i == 0 else f"{base}_{i}" if not re.search(r"\b" + re.escape(callname) + r"\s*\(", text): continue - target = CALL_MAP.get((base, types)) - if not target: # None (adapted) or missing (unmapped overload) -> keep hand-written + target = mangle(base, types, overloaded) + if not target: # None (adapted) or unmapped overload -> keep hand-written unsupported.append(callname + "(" + ", ".join(types) + ")") continue text = re.sub(r"\b" + re.escape(callname) + r"\s*\(", target + "(", text) return text, unsupported -# naga's canonical type spelling -> GLSL type used to key CALL_MAP (inverse of GLSL_TO_NAGA_TYPE). +# naga's canonical type spelling -> GLSL type used to key the mangle tables (inverse of +# GLSL_TO_NAGA_TYPE). NAGA_TO_GLSL_TYPE = {v: k for k, v in GLSL_TO_NAGA_TYPE.items()} -def remap_calls_by_naga_sig(text, wgsl_module, self_base): - """Remap overloaded helper calls using the signatures naga actually assigned in wgsl_module. +def remapCallsByNagaSig(text, wgslModule, selfBase, overloaded): + '''Remap overloaded helper calls using the signatures naga actually assigned in wgslModule. The sibling-body path defines several overloads of a name in one module, and naga numbers the stubs (`mx_foo`, `mx_foo_1`, ...) in an order that depends on its internal processing -- not the - sorted-proto order remap_calls assumes. Rather than guess the index, read each naga function's + sorted-proto order remapCalls assumes. Rather than guess the index, read each naga function's real parameter signature straight from the module, recover its GLSL types, and resolve - (base, types) through CALL_MAP. Longest names first so `mx_foo_1` isn't clobbered by `mx_foo`. - `self_base` is the target's own name (skip, so a recursive call isn't misrouted). Returns - (text, unsupported) where unsupported lists calls whose CALL_MAP entry is None/missing.""" - call_bases = {b for (b, _t) in CALL_MAP} + (base, types) through mangle(). Longest names first so `mx_foo_1` isn't clobbered by `mx_foo`. + `selfBase` is the target's own name (skip, so a recursive call isn't misrouted). Returns + (text, unsupported) where unsupported lists calls mangle() resolves to None.''' sigs = {} - for name, t in top_level_fns(wgsl_module): - wt = _wgsl_sig_types(t) + for name, t in topLevelFns(wgslModule): + wt = _wgslSigTypes(t) sigs[name] = tuple(NAGA_TO_GLSL_TYPE.get(x, x) for x in wt) unsupported = [] for name in sorted(sigs, key=len, reverse=True): - base = fn_base(name) - if base == self_base or base not in call_bases: + base = fnBase(name) + if base == selfBase or base not in SUPPORTED_BASES: continue if not re.search(r"\b" + re.escape(name) + r"\s*\(", text): continue - key = (base, sigs[name]) - if key not in CALL_MAP: - continue # this exact overload isn't a CALL_MAP entry (e.g. a same-base local helper) - target = CALL_MAP[key] - if not target: # explicit None -> adapted signature, keep hand-written + if not _isMapped(base, sigs[name]): + continue # this exact overload isn't mapped (e.g. a same-base local helper) + target = mangle(base, sigs[name], overloaded) + if not target: # None -> adapted signature, keep hand-written unsupported.append(name + "(" + ", ".join(sigs[name]) + ")") continue text = re.sub(r"\b" + re.escape(name) + r"\s*\(", target + "(", text) return text, unsupported -def top_level_fns(wgsl): - """Yield (name, text) for each top-level `fn` in a WGSL module.""" +def topLevelFns(wgsl): + '''Yield (name, text) for each top-level `fn` in a WGSL module.''' for m in re.finditer(r"\bfn\s+([A-Za-z_]\w*)\s*\(", wgsl): brace = wgsl.index("{", m.start()) - yield m.group(1), wgsl[m.start():_match_brace(wgsl, brace)] + yield m.group(1), wgsl[m.start():_matchBrace(wgsl, brace)] -# ---------------------------------------------------------------------------- lib driver +# ----------------------------------------------------------------------------- +# lib driver +# ----------------------------------------------------------------------------- # -# transpile_libs() walks genglsl/lib/*.glsl in include order and writes genwgsl/lib/*.wgsl. +# transpileLibs() walks genglsl/lib/*.glsl in include order and writes genwgsl/lib/*.wgsl. # Strategy mirrors nodes (wrap -> naga -> cleanup) but emits full helper libraries: # - default: one naga invocation per function, with #included deps inlined as bodies # - LIB_USE_SIBLING_BODIES: transitive in-file callee bodies (overload-aware topo sort) # - mx_closure_type: static struct preamble + transpiled makeClosureData only -def lib_needs_samplers(src): - """True if this lib references samplers or $-token texture uniforms (needs TEXTURE_STUB_PREAMBLE).""" +def libNeedsSamplers(src): + '''True if this lib references samplers or $-token texture uniforms (needs TEXTURE_STUB_PREAMBLE).''' return bool(re.search(r"\btexture\w*\s*\(|\$texSampler|\$albedoTable|\$envRadiance|\$envIrradiance", src)) -def _extract_mx_calls(fn_body, known_names): - """Return mx_* callees invoked in fn_body (body only, not the signature).""" - if "{" not in fn_body: +def _extractMxCalls(fnBody, knownNames): + '''Return mx_* callees invoked in fnBody (body only, not the signature).''' + if "{" not in fnBody: return set() - body = fn_body[fn_body.index("{") + 1:fn_body.rfind("}")] - return {m.group(1) for m in re.finditer(r"\b(mx_\w+)\s*\(", body) if m.group(1) in known_names} + body = fnBody[fnBody.index("{") + 1:fnBody.rfind("}")] + return {m.group(1) for m in re.finditer(r"\b(mx_\w+)\s*\(", body) if m.group(1) in knownNames} -def sort_functions_topo(fns): - """Reorder function definitions so callees precede callers (naga requirement). +def sortFunctionsTopo(fns): + '''Reorder function definitions so callees precede callers (naga requirement). Overload-aware: each (name, param-types) pair is a distinct node so naga receives every - callee body, not just the first overload sharing a name.""" + callee body, not just the first overload sharing a name.''' if len(fns) <= 1: return fns - def fn_key(fn): - return (fn["name"], fn_param_types(fn["params"])) - - by_name = {} - key_to_fn = {} + byName = {} + keyToFn = {} for fn in fns: - k = fn_key(fn) - by_name.setdefault(fn["name"], []).append(fn) - key_to_fn[k] = fn + k = fnSigKey(fn) + byName.setdefault(fn["name"], []).append(fn) + keyToFn[k] = fn - local_names = set(by_name.keys()) + localNames = set(byName.keys()) deps = {} for fn in fns: - k = fn_key(fn) - dep_keys = [] - for callee in _extract_mx_calls(fn["full"], local_names): - for callee_fn in by_name.get(callee, []): - ck = fn_key(callee_fn) - if ck in key_to_fn: - dep_keys.append(ck) - deps[k] = dep_keys + k = fnSigKey(fn) + depKeys = [] + for callee in _extractMxCalls(fn["full"], localNames): + for calleeFn in byName.get(callee, []): + ck = fnSigKey(calleeFn) + if ck in keyToFn: + depKeys.append(ck) + deps[k] = depKeys - order_keys, temp, perm = [], set(), set() + orderKeys, temp, perm = [], set(), set() def visit(k): if k in perm: @@ -988,85 +1364,49 @@ def visit(k): visit(dk) temp.remove(k) perm.add(k) - order_keys.append(k) + orderKeys.append(k) for fn in fns: - visit(fn_key(fn)) + visit(fnSigKey(fn)) - return [key_to_fn[k] for k in order_keys] + return [keyToFn[k] for k in orderKeys] -def rebuild_fn_body(fns): - """Concatenate function definitions in topo order for a single naga translation unit.""" +def rebuildFnBody(fns): + '''Concatenate function definitions in topo order for a single naga translation unit.''' return "\n\n".join(fn["full"] for fn in fns) -def expand_lib_includes(text, gllib_dir, seen=None): - """Inline `#include \"lib/...\"` bodies so naga sees full dependency definitions. +def expandLibIncludes(text, gllibDir, seen=None): + '''Inline `#include \"lib/...\"` bodies so naga sees full dependency definitions. Recursive with `seen` to break include cycles. Only `lib/` includes are expanded (not - cross-library paths). Used for per-function dep context and whole-file transpile.""" + cross-library paths). Used for per-function dep context and whole-file transpile.''' seen = seen or set() def replacer(match): - inc_path = match.group(1) - if not inc_path.startswith("lib/"): + incPath = match.group(1) + if not incPath.startswith("lib/"): return match.group(0) - stem = Path(inc_path).stem + stem = Path(incPath).stem if stem in seen: return "" seen.add(stem) - inc_file = gllib_dir / (stem + ".glsl") - if not inc_file.is_file(): + incFile = gllibDir / (stem + ".glsl") + if not incFile.is_file(): return "" - inc_text = expand_lib_tokens(inc_file.read_text(encoding="utf-8")) - inc_text = expand_lib_includes(inc_text, gllib_dir, seen) - inc_text = re.sub(r'#include\s+"[^"]+"\s*\n', "", inc_text) - return inc_text + "\n" + incText = expandLibTokens(incFile.read_text(encoding="utf-8")) + incText = expandLibIncludes(incText, gllibDir, seen) + incText = stripIncludes(incText) + return incText + "\n" return re.sub(r'#include\s+"([^"]+)"\s*\n', replacer, text) -def _strip_wgsl_stubs(wgsl): - """Remove empty stub functions naga emits for unused GLSL prototypes.""" - out = [] - for name, text in top_level_fns(wgsl): - if "{\n}" in text and text.count("\n") <= 2: - continue - out.append(text) - return "\n\n".join(out) - - -def _match_glsl_to_wgsl_fns(glsl_fns, wgsl): - """Pair GLSL function definitions with naga output by name and overload order. - - Whole-file transpile emits every function in one module; this maps each GLSL definition to - its naga counterpart (mx_foo, mx_foo_1, ...) and renames to the genwgsl overload name.""" - wgsl_by_base = {} - for name, text in top_level_fns(wgsl): - if "{\n}" in text and text.count("\n") <= 2: - continue - wgsl_by_base.setdefault(fn_base(name), []).append((name, text)) - - outputs = [] - seen = {} - for fn in glsl_fns: - i = seen.get(fn["name"], 0) - seen[fn["name"]] = i + 1 - wgsl_list = wgsl_by_base.get(fn["name"], []) - if i >= len(wgsl_list): - return None, fn["name"] - naga_name, text = wgsl_list[i] - out_name = wgsl_fn_name(fn["name"], fn_param_types(fn["params"])) - text = re.sub(r"(\bfn\s+)" + re.escape(naga_name) + r"\b", r"\1" + out_name, text) - outputs.append((fn["name"], text)) - return outputs, None - - -def _mx_calls_in_text(text): - """Return mx_* names invoked in text (function bodies and preamble, not signatures).""" +def _mxCallsInText(text): + '''Return mx_* names invoked in text (function bodies and preamble, not signatures).''' calls = set() - fns = parse_functions(text) + fns = parseFunctions(text) if fns: preamble = text[:text.index(fns[0]["full"])] calls |= {m.group(1) for m in re.finditer(r"\b(mx_\w+)\s*\(", preamble)} @@ -1078,56 +1418,53 @@ def _mx_calls_in_text(text): return calls -def proto_block_for_body(body_src, protos, defined_sigs): - """Emit only prototypes for mx_* helpers referenced in body_src and not already defined.""" - called = _mx_calls_in_text(body_src) +def protoBlockForBody(bodySrc, protos, definedSigs): + '''Emit only prototypes for mx_* helpers referenced in bodySrc and not already defined.''' + called = _mxCallsInText(bodySrc) lines = [] for (nm, t), p in sorted(protos.items()): - if (nm, t) in defined_sigs: + if (nm, t) in definedSigs: continue if nm in called: lines.append(p) return "\n".join(lines) -def sibling_callee_closure(target_fn, local_fns): - """Local functions transitively called by target_fn (all overloads of each called name).""" - by_name = {} - for fn in local_fns: - by_name.setdefault(fn["name"], []).append(fn) - local_names = set(by_name.keys()) - - def fn_key(fn): - return (fn["name"], fn_param_types(fn["params"])) +def siblingCalleeClosure(targetFn, localFns): + '''Local functions transitively called by targetFn (all overloads of each called name).''' + byName = {} + for fn in localFns: + byName.setdefault(fn["name"], []).append(fn) + localNames = set(byName.keys()) needed = set() - stack = [target_fn] + stack = [targetFn] while stack: fn = stack.pop() - key = fn_key(fn) + key = fnSigKey(fn) if key in needed: continue needed.add(key) - for callee in _extract_mx_calls(fn["full"], local_names): - for callee_fn in by_name.get(callee, []): - if fn_key(callee_fn) not in needed: - stack.append(callee_fn) + for callee in _extractMxCalls(fn["full"], localNames): + for calleeFn in byName.get(callee, []): + if fnSigKey(calleeFn) not in needed: + stack.append(calleeFn) - closure = [fn for fn in local_fns if fn_key(fn) in needed] - return sort_functions_topo(closure) + closure = [fn for fn in localFns if fnSigKey(fn) in needed] + return sortFunctionsTopo(closure) -def file_preamble_before_fns(src): - """Struct/const text before the first function definition in a lib fragment.""" - stripped = re.sub(r'#include\s+"[^"]+"\s*\n', "", src) - fns = parse_functions(stripped) +def filePreambleBeforeFns(src): + '''Struct/const text before the first function definition in a lib fragment.''' + stripped = stripIncludes(src) + fns = parseFunctions(stripped) if not fns: return "" return stripped[:stripped.index(fns[0]["full"])] -def apply_lib_wgsl_patches(stem, text): - """Stem-specific fixes for transpiled lib output (generator/runtime conventions).""" +def applyLibWgslPatches(stem, text): + '''Stem-specific fixes for transpiled lib output (generator/runtime conventions).''' if stem == "mx_microfacet_specular": # Generator emits split env texture + sampler; match hand-port latlong signature. text = re.sub( @@ -1144,230 +1481,227 @@ def apply_lib_wgsl_patches(stem, text): return text -def transpile_lib_file_siblings(glsl_path, out_path, base, protos, lib_name, src, raw, header, - sampler_preamble, gllib_dir): - """Per-function transpile with transitive in-file callee bodies (naga ordering fix).""" - stem = glsl_path.stem - stripped = re.sub(r'#include\s+"[^"]+"\s*\n', "", src) - local_fns = parse_functions(stripped) - if not local_fns: +def transpileLibFileSiblings(glslPath, outPath, base, protos, overloaded, libName, src, raw, + header, samplerPreamble, gllibDir): + '''Per-function transpile with transitive in-file callee bodies (naga ordering fix).''' + stem = glslPath.stem + stripped = stripIncludes(src) + localFns = parseFunctions(stripped) + if not localFns: return False - file_preamble = file_preamble_before_fns(src) - inc_lines = "\n".join( + filePreamble = filePreambleBeforeFns(src) + incLines = "\n".join( f'#include "{i}"\n' for i in re.findall(r'#include\s+"([^"]+)"', raw) if i.startswith("lib/")) - dep_src = expand_lib_includes(inc_lines, gllib_dir) if inc_lines else "" - dep_src = re.sub(r'#include\s+"[^"]+"\s*\n', "", dep_src) - for f in local_fns: - dep_src = dep_src.replace(f["full"], "") + depSrc = expandLibIncludes(incLines, gllibDir) if incLines else "" + depSrc = stripIncludes(depSrc) + for f in localFns: + depSrc = depSrc.replace(f["full"], "") - dep_fns = parse_functions(dep_src) if dep_src else [] - dep_preamble = "" - if dep_fns: - dep_preamble = dep_src[:dep_src.index(dep_fns[0]["full"])] - dep_names = {fn["name"] for fn in dep_fns} - - def fn_key(fn): - return (fn["name"], fn_param_types(fn["params"])) + depFns = parseFunctions(depSrc) if depSrc else [] + depPreamble = "" + if depFns: + depPreamble = depSrc[:depSrc.index(depFns[0]["full"])] + depNames = {fn["name"] for fn in depFns} outputs = [] - for target_fn in local_fns: - types = fn_param_types(target_fn["params"]) - out_name = wgsl_fn_name(target_fn["name"], types) - siblings = sibling_callee_closure(target_fn, local_fns) + for targetFn in localFns: + types = fnParamTypes(targetFn["params"]) + outName = wgslFnName(targetFn["name"], types, overloaded) + siblings = siblingCalleeClosure(targetFn, localFns) # Pull in dep-file helpers transitively called from the sibling closure. - needed_dep = [] - needed_dep_keys = set() + neededDep = [] + neededDepKeys = set() stack = list(siblings) while stack: fn = stack.pop() - for callee in _extract_mx_calls(fn["full"], dep_names): - for dep_fn in dep_fns: - if dep_fn["name"] == callee: - k = fn_key(dep_fn) - if k not in needed_dep_keys: - needed_dep_keys.add(k) - needed_dep.append(dep_fn) - stack.append(dep_fn) - - all_body_fns = sort_functions_topo(needed_dep + siblings) - callees = [fn for fn in all_body_fns if fn_key(fn) != fn_key(target_fn)] - callees = sort_functions_topo(callees) - ordered = callees + [target_fn] - sibling_body = rebuild_fn_body(ordered) - sibling_sigs = {fn_key(fn) for fn in ordered} - defined_sigs = sibling_sigs - combined = file_preamble + dep_preamble + sibling_body - proto_block = proto_block_for_body(combined, protos, defined_sigs) - glsl = ("#version 450\n" + LIB_PREAMBLE + sampler_preamble + base + "\n" + - file_preamble + dep_preamble + "\n" + proto_block + "\n" + sibling_body + - "\nlayout(location=0) out vec4 mtlx_o;\nvoid main() { mtlx_o = vec4(0.0); }\n") - wgsl, err = naga_transpile_glsl( - glsl, out_path.parent / f"_debug_lib_{stem}_{target_fn['name']}.frag" + for callee in _extractMxCalls(fn["full"], depNames): + for depFn in depFns: + if depFn["name"] == callee: + k = fnSigKey(depFn) + if k not in neededDepKeys: + neededDepKeys.add(k) + neededDep.append(depFn) + stack.append(depFn) + + allBodyFns = sortFunctionsTopo(neededDep + siblings) + callees = [fn for fn in allBodyFns if fnSigKey(fn) != fnSigKey(targetFn)] + callees = sortFunctionsTopo(callees) + ordered = callees + [targetFn] + siblingBody = rebuildFnBody(ordered) + siblingSigs = {fnSigKey(fn) for fn in ordered} + definedSigs = siblingSigs + combined = filePreamble + depPreamble + siblingBody + protoBlock = protoBlockForBody(combined, protos, definedSigs) + glsl = ("#version 450\n" + LIB_PREAMBLE + samplerPreamble + base + "\n" + + filePreamble + depPreamble + "\n" + protoBlock + "\n" + siblingBody + + FRAG_MAIN_EPILOGUE) + wgsl, err = nagaTranspileGlsl( + glsl, outPath.parent / f"_debug_lib_{stem}_{targetFn['name']}.frag" if os.environ.get("MTLX_DEBUG") else None) if wgsl is None: - print(f" FAIL lib/{glsl_path.name}::{target_fn['name']}: {err}") + print(f" FAIL lib/{glslPath.name}::{targetFn['name']}: {err}") return False - text = extract_transpiled_fn(wgsl, target_fn["name"], out_name, glsl_types=types) + text = extractTranspiledFn(wgsl, targetFn["name"], outName, glslTypes=types) if text is None: - print(f" FAIL lib/{glsl_path.name}::{target_fn['name']}: not found in naga output") + print(f" FAIL lib/{glslPath.name}::{targetFn['name']}: not found in naga output") return False # Overloaded calls: resolve by naga's actual per-module signatures (its stub numbering here - # is topological, not the sorted order remap_calls assumes), then run the shared cleanup. - text, unsupported = remap_calls_by_naga_sig(text, wgsl, target_fn["name"]) + # is topological, not the sorted order remapCalls assumes), then run the shared cleanup. + text, unsupported = remapCallsByNagaSig(text, wgsl, targetFn["name"], overloaded) if unsupported: - print(f" FAIL lib/{glsl_path.name}::{target_fn['name']}: unmapped call(s) {unsupported}") + print(f" FAIL lib/{glslPath.name}::{targetFn['name']}: unmapped call(s) {unsupported}") return False - text, _ = postprocess_wgsl_fn(text, target_fn["name"], protos, remap=False) - outputs.append(text) + text, _ = postprocessWgslFn(text, targetFn["name"], protos, overloaded, remap=False, + glslParamNames=fnParamNames(targetFn["params"])) + outputs.append(targetFn.get("lead", "") + injectInlineComments(targetFn["full"], text)) - structs = transpile_glsl_structs(stripped) - consts = transpile_glsl_consts(stripped) + structs = transpileGlslStructs(stripped) + consts = transpileGlslConsts(stripped) preamble = ("\n\n".join(structs + consts) + "\n\n") if structs or consts else "" - src_rel = f"libraries/{lib_name}/genglsl/lib/{glsl_path.name}" - banner = (f"// Generated from {src_rel} by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py.\n" - f"// Do not edit -- re-run the transpiler to regenerate " - f"(see source/MaterialXGenWgsl/README.md).\n\n") - all_consts = glsl_const_names(base) + const_names_from_wgsl(consts) - body = resolve_const_refs("\n\n".join(outputs), all_consts) - body = apply_lib_wgsl_patches(stem, body) - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(banner + header + preamble + body + "\n", encoding="utf-8") - print(f" OK lib/{glsl_path.name} -> {out_path}") + srcRel = f"libraries/{libName}/genglsl/lib/{glslPath.name}" + banner = generatedBanner(srcRel) + allConsts = glslConstNames(base) + constNamesFromWgsl(consts) + body = resolveConstRefs("\n\n".join(outputs), allConsts) + body = applyLibWgslPatches(stem, body) + fileLead = fileLeadComment(raw) + writeGenerated(outPath, banner + fileLead + header + preamble + body + "\n", + f"lib/{glslPath.name}") return True -def transpile_lib_file(glsl_path, out_path, base, protos, lib_name): - """Transpile one genglsl/lib/*.glsl file to genwgsl/lib/*.wgsl. +def transpileLibFile(glslPath, outPath, base, protos, overloaded, libName, libroot): + '''Transpile one genglsl/lib/*.glsl file to genwgsl/lib/*.wgsl. - Dispatches to _transpile_closure_type_lib, transpile_lib_file_siblings, or per-function - transpile (default). Output: generated banner + #include lines + structs/consts + fn bodies.""" - raw = glsl_path.read_text(encoding="utf-8") - src = expand_lib_tokens(raw) - stem = glsl_path.stem - sampler_preamble = TEXTURE_STUB_PREAMBLE if lib_needs_samplers(raw) else "" + Dispatches to _transpileClosureTypeLib, transpileLibFileSiblings, or per-function + transpile (default). Output: generated banner + #include lines + structs/consts + fn bodies.''' + raw = glslPath.read_text(encoding="utf-8") + src = expandLibTokens(raw) + stem = glslPath.stem + samplerPreamble = TEXTURE_STUB_PREAMBLE if libNeedsSamplers(raw) else "" if stem == "mx_closure_type": - return _transpile_closure_type_lib(glsl_path, out_path, base, protos, lib_name) + return _transpileClosureTypeLib(glslPath, outPath, base, protos, overloaded, libName, + libroot) includes = [f'#include "{inc.replace(".glsl", ".wgsl")}"' for inc in re.findall(r'#include\s+"([^"]+)"', raw)] header = ("\n".join(includes) + "\n\n") if includes else "" - gllib_dir = glsl_path.parent + gllibDir = glslPath.parent if stem in LIB_USE_SIBLING_BODIES: - return transpile_lib_file_siblings(glsl_path, out_path, base, protos, lib_name, src, raw, - header, sampler_preamble, gllib_dir) + return transpileLibFileSiblings(glslPath, outPath, base, protos, overloaded, libName, + src, raw, header, samplerPreamble, gllibDir) - local_fns = parse_functions(re.sub(r'#include\s+"[^"]+"\s*\n', "", src)) - if not local_fns: - print(f" SKIP lib/{glsl_path.name}: no functions found") + stripped = stripIncludes(src) + localFns = parseFunctions(stripped) + if not localFns: + print(f" SKIP lib/{glslPath.name}: no functions found") return False - local_sigs = {(fn["name"], fn_param_types(fn["params"])) for fn in local_fns} - local_names = {fn["name"] for fn in local_fns} - inc_lines = "\n".join( + incLines = "\n".join( f'#include "{i}"\n' for i in re.findall(r'#include\s+"([^"]+)"', raw) if i.startswith("lib/")) - dep_src = expand_lib_includes(inc_lines, gllib_dir) if inc_lines else "" - dep_src = re.sub(r'#include\s+"[^"]+"\s*\n', "", dep_src) - # Remove local function bodies from dep_src — only the target fn body is transpiled per pass. - for f in local_fns: - dep_src = dep_src.replace(f["full"], "") + depSrc = expandLibIncludes(incLines, gllibDir) if incLines else "" + depSrc = stripIncludes(depSrc) + # Remove local function bodies from depSrc — only the target fn body is transpiled per pass. + for f in localFns: + depSrc = depSrc.replace(f["full"], "") outputs = [] - for fn in local_fns: - types = fn_param_types(fn["params"]) - out_name = wgsl_fn_name(fn["name"], types) - proto_block = "\n".join( + for fn in localFns: + types = fnParamTypes(fn["params"]) + outName = wgslFnName(fn["name"], types, overloaded) + protoBlock = "\n".join( p for (nm, t), p in sorted(protos.items()) if not (nm == fn["name"] and t == types)) - glsl = ("#version 450\n" + LIB_PREAMBLE + sampler_preamble + base + "\n" + - dep_src + "\n" + proto_block + "\n" + fn["full"] + - "\nlayout(location=0) out vec4 mtlx_o;\nvoid main() { mtlx_o = vec4(0.0); }\n") - wgsl, err = naga_transpile_glsl( - glsl, out_path.parent / f"_debug_lib_{stem}_{fn['name']}.frag" + glsl = ("#version 450\n" + LIB_PREAMBLE + samplerPreamble + base + "\n" + + depSrc + "\n" + protoBlock + "\n" + fn["full"] + + FRAG_MAIN_EPILOGUE) + wgsl, err = nagaTranspileGlsl( + glsl, outPath.parent / f"_debug_lib_{stem}_{fn['name']}.frag" if os.environ.get("MTLX_DEBUG") else None) if wgsl is None: - print(f" FAIL lib/{glsl_path.name}::{fn['name']}: {err}") + print(f" FAIL lib/{glslPath.name}::{fn['name']}: {err}") return False - text = extract_transpiled_fn(wgsl, fn["name"], out_name) + text = extractTranspiledFn(wgsl, fn["name"], outName) if text is None: - print(f" FAIL lib/{glsl_path.name}::{fn['name']}: not found in naga output") + print(f" FAIL lib/{glslPath.name}::{fn['name']}: not found in naga output") return False - text, unsupported = postprocess_wgsl_fn(text, fn["name"], protos, remap=True) + text, unsupported = postprocessWgslFn(text, fn["name"], protos, overloaded, remap=True, + glslParamNames=fnParamNames(fn["params"])) if unsupported: - print(f" FAIL lib/{glsl_path.name}::{fn['name']}: unmapped call(s) {unsupported}") + print(f" FAIL lib/{glslPath.name}::{fn['name']}: unmapped call(s) {unsupported}") return False - outputs.append(text) - - structs = transpile_glsl_structs(re.sub(r'#include\s+"[^"]+"\s*\n', "", src)) - consts = transpile_glsl_consts(re.sub(r'#include\s+"[^"]+"\s*\n', "", src)) - preamble_parts = structs + consts - preamble = ("\n\n".join(preamble_parts) + "\n\n") if preamble_parts else "" - - src_rel = f"libraries/{lib_name}/genglsl/lib/{glsl_path.name}" - banner = (f"// Generated from {src_rel} by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py.\n" - f"// Do not edit -- re-run the transpiler to regenerate " - f"(see source/MaterialXGenWgsl/README.md).\n\n") - all_consts = glsl_const_names(base) + const_names_from_wgsl(consts) - body = resolve_const_refs("\n\n".join(outputs), all_consts) - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(banner + header + preamble + body + "\n", encoding="utf-8") - print(f" OK lib/{glsl_path.name} -> {out_path}") + outputs.append(fn.get("lead", "") + injectInlineComments(fn["full"], text)) + + structs = transpileGlslStructs(stripped) + consts = transpileGlslConsts(stripped) + preambleParts = structs + consts + preamble = ("\n\n".join(preambleParts) + "\n\n") if preambleParts else "" + + srcRel = f"libraries/{libName}/genglsl/lib/{glslPath.name}" + banner = generatedBanner(srcRel) + allConsts = glslConstNames(base) + constNamesFromWgsl(consts) + body = resolveConstRefs("\n\n".join(outputs), allConsts) + fileLead = fileLeadComment(raw) + writeGenerated(outPath, banner + fileLead + header + preamble + body + "\n", + f"lib/{glslPath.name}") return True -def _transpile_closure_type_lib(glsl_path, out_path, base, protos, lib_name): - """Special handler for mx_closure_type: prepend shared structs, transpile makeClosureData. +def _transpileClosureTypeLib(glslPath, outPath, base, protos, overloaded, libName, libroot): + '''Special handler for mx_closure_type: prepend shared structs, transpile makeClosureData. GLSL only defines ClosureData + constructor macro; genwgsl nodes expect BSDF/VDF/FresnelData - types from this file. Struct block is static (LIB_STRUCT_PREAMBLE); functions come from naga.""" - src = expand_lib_tokens(glsl_path.read_text(encoding="utf-8")) + types from this file. The struct block is wgslClosurePreamble(libroot) -- derived from + mx_closure_type.glsl, mx_microfacet_specular.glsl, and GlslSyntax.cpp -- not a hand-written + preamble. Function bodies still come from naga.''' + raw = glslPath.read_text(encoding="utf-8") + src = expandLibTokens(raw) outputs = [] - for fn in parse_functions(src): - types = fn_param_types(fn["params"]) - out_name = wgsl_fn_name(fn["name"], types) - proto_block = "\n".join( + for fn in parseFunctions(src): + types = fnParamTypes(fn["params"]) + outName = wgslFnName(fn["name"], types, overloaded) + protoBlock = "\n".join( p for (nm, t), p in sorted(protos.items()) if not (nm == fn["name"] and t == types)) - glsl = ("#version 450\n" + LIB_PREAMBLE + base + "\n" + proto_block + - "\n" + fn["full"] + - "\nlayout(location=0) out vec4 mtlx_o;\nvoid main() { mtlx_o = vec4(0.0); }\n") - wgsl, err = naga_transpile_glsl(glsl) + glsl = ("#version 450\n" + LIB_PREAMBLE + base + "\n" + protoBlock + + "\n" + fn["full"] + FRAG_MAIN_EPILOGUE) + wgsl, err = nagaTranspileGlsl(glsl) if wgsl is None: - print(f" FAIL lib/{glsl_path.name}::{fn['name']}: {err}") + print(f" FAIL lib/{glslPath.name}::{fn['name']}: {err}") return False - text = extract_transpiled_fn(wgsl, fn["name"], out_name) + text = extractTranspiledFn(wgsl, fn["name"], outName) if text is None: - print(f" FAIL lib/{glsl_path.name}::{fn['name']}: not found in naga output") + print(f" FAIL lib/{glslPath.name}::{fn['name']}: not found in naga output") return False - text, unsupported = postprocess_wgsl_fn(text, fn["name"], protos, remap=False) + text, unsupported = postprocessWgslFn(text, fn["name"], protos, overloaded, remap=False, + glslParamNames=fnParamNames(fn["params"])) if unsupported: - print(f" FAIL lib/{glsl_path.name}::{fn['name']}: {unsupported}") + print(f" FAIL lib/{glslPath.name}::{fn['name']}: {unsupported}") return False - outputs.append(apply_field_renames(text)) - - src_rel = f"libraries/{lib_name}/genglsl/lib/{glsl_path.name}" - banner = (f"// Generated from {src_rel} by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py.\n" - f"// Do not edit -- re-run the transpiler to regenerate " - f"(see source/MaterialXGenWgsl/README.md).\n\n") - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(banner + LIB_STRUCT_PREAMBLE.strip() + "\n\n" + "\n\n".join(outputs) + "\n", - encoding="utf-8") - print(f" OK lib/{glsl_path.name} -> {out_path}") + outputs.append(fn.get("lead", "") + injectInlineComments(fn["full"], applyFieldRenames(text))) + + srcRel = f"libraries/{libName}/genglsl/lib/{glslPath.name}" + banner = generatedBanner(srcRel) + fileLead = fileLeadComment(raw) + writeGenerated(outPath, + banner + fileLead + wgslClosurePreamble(libroot).strip() + "\n\n" + + "\n\n".join(outputs) + "\n", + f"lib/{glslPath.name}") return True -def transpile_libs(libroot, outroot, libs, base, protos, only=None): - """Transpile all genglsl/lib/*.glsl helpers to genwgsl/lib/*.wgsl. +def transpileLibs(libroot, outroot, libs, base, protos, only=None): + '''Transpile all genglsl/lib/*.glsl helpers to genwgsl/lib/*.wgsl. Runs before node transpilation in main(). Returns a list of stems that failed. - Uses build_context(nodes=False) for lib-only prototypes.""" + Uses buildContext(nodes=False) for lib-only prototypes.''' ok = failed = 0 unexpected = [] # Lib-only context: no node protos (inout) and no duplicate closure preamble. - lib_base, lib_protos, _ = build_context(libroot, libs, nodes=False) + libBase, libProtos, libOverloaded = buildContext(libroot, libs, nodes=False) print("\nTranspiling genglsl/lib/ helpers...") for lib in libs: if lib in HANDWRITTEN_LIB_DIRS: @@ -1376,15 +1710,15 @@ def transpile_libs(libroot, outroot, libs, base, protos, only=None): gllib = libroot / lib / "genglsl" / "lib" if not gllib.is_dir(): continue - lib_files = topo_sort_lib_files(sorted(gllib.glob("*.glsl"))) - for glsl in lib_files: + libFiles = topoSortLibFiles(sorted(gllib.glob("*.glsl"))) + for glsl in libFiles: if glsl.stem in LIB_KEEP_HANDWRITTEN: print(f" KEEP lib/{glsl.name} (hand-written; naga cannot express its sampler bindings)") continue if only and glsl.stem not in only: continue out = outroot / lib / "genwgsl" / "lib" / (glsl.stem + ".wgsl") - if transpile_lib_file(glsl, out, lib_base, lib_protos, lib): + if transpileLibFile(glsl, out, libBase, libProtos, libOverloaded, lib, libroot): ok += 1 else: failed += 1 @@ -1393,34 +1727,34 @@ def transpile_libs(libroot, outroot, libs, base, protos, only=None): return unexpected -# ---------------------------------------------------------------------------- node driver +# ----------------------------------------------------------------------------- +# node driver +# ----------------------------------------------------------------------------- -def transpile(node_path, out_path, base, protos, lib_symbols): - node_src = node_path.read_text(encoding="utf-8") - node_fns = parse_functions(node_src) - if not node_fns: - print(f" SKIP {node_path.name}: no functions found") +def transpile(nodePath, outPath, base, protos, overloaded, libSymbols): + nodeSrc = nodePath.read_text(encoding="utf-8") + nodeFns = parseFunctions(nodeSrc) + if not nodeFns: + print(f" SKIP {nodePath.name}: no functions found") return False - all_names = {nm for (nm, _t) in protos} - outputs = [] - for fn in node_fns: + for fn in nodeFns: # Supply every known prototype except the function being defined here (siblings and # cross-node helpers are all in `protos`, keyed by name+types). # Emit sorted by (name, types) so each overloaded base's prototypes are declared in the - # same order remap_calls indexes them (naga numbers overload stubs by declaration order). - proto_block = "\n".join(p for (nm, _t), p in sorted(protos.items()) if nm != fn["name"]) + # same order remapCalls indexes them (naga numbers overload stubs by declaration order). + protoBlock = "\n".join(p for (nm, _t), p in sorted(protos.items()) if nm != fn["name"]) body = fn["full"] # MaterialX `$`-tokens (e.g. $blur, $albedoTable) aren't valid GLSL identifiers, so naga # can't parse them. Swap each `$tok` for a legal placeholder `MTLXTOK_tok` before transpiling - # and restore it afterwards; token_map records the reverse mapping for this function. - token_map = {} + # and restore it afterwards; tokenMap records the reverse mapping for this function. + tokenMap = {} def sentinel(m): s = "MTLXTOK_" + m.group(1) - token_map[s] = m.group(0) + tokenMap[s] = m.group(0) return s body = re.sub(r"\$([A-Za-z_]\w*)", sentinel, body) @@ -1428,84 +1762,56 @@ def sentinel(m): # #defines, prototypes) + this one real function body + a do-nothing entry point. naga's GLSL # frontend requires a staged shader with a main(); it keeps non-entry functions in the output, # which is exactly the one body we want back. - glsl = ("#version 450\n" + base + "\n" + proto_block + "\n" + body + - "\nlayout(location=0) out vec4 mtlx_o;\nvoid main() { mtlx_o = vec4(0.0); }\n") - - with tempfile.TemporaryDirectory() as td: - frag = Path(td) / "in.frag" - wtmp = Path(td) / "out.wgsl" - frag.write_text(glsl, encoding="utf-8") - r = subprocess.run([NAGA, "--input-kind", "glsl", "--shader-stage", "frag", - str(frag), str(wtmp)], capture_output=True, text=True) - if r.returncode != 0: - err = next((l for l in r.stderr.splitlines() if "error" in l.lower()), "naga error") - print(f" FAIL {node_path.name}::{fn['name']}: {err.strip()}") - if os.environ.get("MTLX_DEBUG"): - dbg = out_path.parent / f"_debug_{fn['name']}.frag" - dbg.parent.mkdir(parents=True, exist_ok=True) - dbg.write_text(glsl, encoding="utf-8") - print(f" wrote {dbg}") - return False - wgsl = wtmp.read_text(encoding="utf-8") - - # Pull our one real function back out of naga's module. The context prototypes become empty - # stub functions in the output, so match by source name (fn_base strips any naga `_N` suffix) - # AND require a non-trivial body: `"{\n}"` excludes the empty stubs and the `> 1` line count - # guards against a one-liner stub slipping through. Then rename the suffixed fn back to clean. - text = None - for name, t in top_level_fns(wgsl): - if fn_base(name) == fn["name"] and "{\n}" not in t and t.count("\n") > 1: - text = re.sub(r"(\bfn\s+)" + re.escape(name) + r"\b", r"\1" + fn["name"], t) - break - if text is None: - print(f" FAIL {node_path.name}::{fn['name']}: not found in naga output") + glsl = "#version 450\n" + base + "\n" + protoBlock + "\n" + body + FRAG_MAIN_EPILOGUE + + wgsl, err = nagaTranspileGlsl( + glsl, outPath.parent / f"_debug_{fn['name']}.frag" if os.environ.get("MTLX_DEBUG") else None) + if wgsl is None: + print(f" FAIL {nodePath.name}::{fn['name']}: {err}") return False - text = cleanup_function(text) - for rx, repl in TYPE_FIXUPS: - text = rx.sub(repl, text) - # Float literal suffixes: drop `f` from decimals first (so the int rule can't bite a - # decimal's fractional digits, e.g. 0.00000001f -> 0.00000001), then pad bare ints (2f -> 2.0). - text = re.sub(r"\b(\d+\.\d+(?:[eE][-+]?\d+)?)f\b", r"\1", text) - text = re.sub(r"(?.wgsl extension swap, so the generated # fragment pulls in the same lib helpers (now their WGSL versions) the GLSL source did. A node # file may define several functions (e.g. a vector2 + float variant); join them in source order. includes = [f'#include "{inc.replace(".glsl", ".wgsl")}"' - for inc in re.findall(r'#include\s+"([^"]+)"', node_src)] + for inc in re.findall(r'#include\s+"([^"]+)"', nodeSrc)] header = ("\n".join(includes) + "\n\n") if includes else "" # Banner marking the file as generated, so it is visually distinct from the hand-written # genwgsl nodes it sits next to. Names the genglsl source (lib-relative, so the line is stable # regardless of where --libraries points) and the tool. No timestamp: re-running must produce # byte-identical output so `git diff` cleanly surfaces drift. - src_rel = f"libraries/{node_path.parent.parent.name}/genglsl/{node_path.name}" - banner = (f"// Generated from {src_rel} by source/MaterialXGenWgsl/tools/glsl_to_wgsl.py.\n" - f"// Do not edit -- re-run the transpiler to regenerate " - f"(see source/MaterialXGenWgsl/README.md).\n\n") - - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(banner + header + "\n\n".join(outputs) + "\n", encoding="utf-8") - print(f" OK {node_path.name} -> {out_path}") + srcRel = f"libraries/{nodePath.parent.parent.name}/genglsl/{nodePath.name}" + banner = generatedBanner(srcRel) + + fileLead = fileLeadComment(nodeSrc) + writeGenerated(outPath, banner + fileLead + header + "\n\n".join(outputs) + "\n", + nodePath.name) return True @@ -1519,8 +1825,8 @@ def main(): # Resolve and preflight naga so a missing tool fails once, with guidance, instead of per node. global NAGA - NAGA = resolve_naga(args.naga) - version = naga_version(NAGA) + NAGA = resolveNaga(args.naga) + version = nagaVersion(NAGA) if not version: print(f"ERROR: naga CLI not found or not runnable (tried '{NAGA}').\n" " Install it with `cargo install naga-cli`, or pass --naga / set the\n" @@ -1530,46 +1836,77 @@ def main(): print(f"Using naga: {NAGA} ({version})") libroot, outroot = Path(args.libraries), Path(args.out) - libs = discover_libs(libroot) - base, protos, _overloaded = build_context(libroot, libs) - - # Lib helpers first: nodes #include genwgsl/lib/*.wgsl and check_lib_arity reads fresh output. - lib_unexpected = transpile_libs(libroot, outroot, libs, base, protos, only=args.only) - lib_symbols = build_wgsl_lib_symbols(libroot, libs, outroot=outroot) + # Preamble/token helpers resolve repo paths relative to libroot; must run before transpilation. + setActiveLibroot(libroot) + libs = discoverLibs(libroot) + base, protos, overloaded = buildContext(libroot, libs) + + # Scan-driven preflight: unresolved overloads or unhandled $-tokens are hard errors (they would + # mis-emit a name or fail naga later, with a more obscure message). + validationFailed = False + uncovered = validateOverloadCoverage(protos, overloaded) + if uncovered: + validationFailed = True + print("ERROR: overloaded helpers with no mangle() mapping (add a SUFFIX_SCHEME/EXCEPTIONS " + "entry): " + ", ".join(f"{n}{t}" for n, t in uncovered)) + unhandledTokens = validateTokenCoverage(libroot, libs) + if unhandledTokens: + validationFailed = True + print("ERROR: $-tokens in transpiled lib sources not covered by HwConstants/NAGA_LIB_STUBS: " + + ", ".join(f"{tok} ({src})" for tok, src in sorted(unhandledTokens.items()))) + + # Lib helpers first: nodes #include genwgsl/lib/*.wgsl and checkLibArity reads fresh output. + libUnexpected = transpileLibs(libroot, outroot, libs, base, protos, only=args.only) + libSymbols = buildWgslLibSymbols(libroot, libs, outroot=outroot) + + # Validate mangle() names against the actual genwgsl lib (hand-written stdlib from libroot, + # generated helpers from outroot). Skipped for a partial --only run, which won't regenerate + # every referenced helper. + if not args.only: + valSymbols = dict(buildWgslLibSymbols(libroot, libs)) + valSymbols.update(libSymbols) + missingNames = validateLibNames(protos, valSymbols) + if missingNames: + validationFailed = True + print("ERROR: mangle() produced names absent from the genwgsl lib: " + + ", ".join(missingNames)) ok = skip = 0 - node_expected, node_unexpected = [], [] + nodeExpected, nodeUnexpected = [], [] print("\nTranspiling genglsl node fragments...") for lib in libs: gldir = libroot / lib / "genglsl" if not gldir.is_dir(): continue for glsl in sorted(gldir.glob("mx_*.glsl")): - base_name = glsl.stem - if args.only and base_name not in args.only: + baseName = glsl.stem + if args.only and baseName not in args.only: continue - if any(p.search(base_name) for p in SKIP_PATTERNS): + if any(p.search(baseName) for p in SKIP_PATTERNS): print(f" SKIP {glsl.name}: texture/sampler node (unsupported by naga)") skip += 1 continue - out = outroot / lib / "genwgsl" / (base_name + ".wgsl") - if transpile(glsl, out, base, protos, lib_symbols): + out = outroot / lib / "genwgsl" / (baseName + ".wgsl") + if transpile(glsl, out, base, protos, overloaded, libSymbols): ok += 1 - if base_name in EXPECTED_FALLBACK: + if baseName in EXPECTED_FALLBACK: # A node we expected to stay hand-written now transpiles cleanly -- the lib # divergence it depended on was probably resolved. Not fatal, but worth flagging. print(f" WARN {glsl.name}: now transpiles cleanly; remove it from " f"EXPECTED_FALLBACK and commit the generated version.") - elif base_name in EXPECTED_FALLBACK: - node_expected.append(base_name) + elif baseName in EXPECTED_FALLBACK: + nodeExpected.append(baseName) else: - node_unexpected.append(base_name) + nodeUnexpected.append(baseName) print(f"\nDone: {ok} nodes transpiled, {skip} skipped, " - f"{len(node_expected)} expected fallbacks, {len(node_unexpected)} unexpected failures.") - all_unexpected = lib_unexpected + node_unexpected - if all_unexpected: - print("ERROR: files that should transpile failed: " + ", ".join(sorted(set(all_unexpected)))) + f"{len(nodeExpected)} expected fallbacks, {len(nodeUnexpected)} unexpected failures.") + allUnexpected = libUnexpected + nodeUnexpected + if allUnexpected: + print("ERROR: files that should transpile failed: " + ", ".join(sorted(set(allUnexpected)))) + return 1 + if validationFailed: + print("ERROR: scan-driven validation failed (see messages above).") return 1 return 0 diff --git a/source/MaterialXGenWgsl/tools/mxwgslcleanup.py b/source/MaterialXGenWgsl/tools/mxwgslcleanup.py new file mode 100644 index 0000000000..970666e9c7 --- /dev/null +++ b/source/MaterialXGenWgsl/tools/mxwgslcleanup.py @@ -0,0 +1,769 @@ +'''Post-process naga-emitted WGSL function bodies for readability. + +Uses tree-sitter (the `tree-sitter-language-pack` WGSL grammar) for statement/identifier +analysis, then applies conservative source edits: collapse param-copy shadows, restore GLSL +parameter names, promote readonly/mutable locals, unwrap naga's redundant compound blocks, +flatten else-if chains, and simplify ptr/assignment parens. Parsing only — emission is +byte-range surgery on naga output. +''' + +from __future__ import annotations + +import re +import sys + +_PARSER = None + + +def fnBase(name): + '''Strip naga's trailing `_` / `_N` disambiguation suffix.''' + return re.sub(r"_\d*$", "", name) + + +def _parser(): + global _PARSER + if _PARSER is None: + from tree_sitter_language_pack import get_parser + _PARSER = get_parser("wgsl") + return _PARSER + + +def _nodeText(node, src): + return src[node.start_byte:node.end_byte].decode("utf-8") + + +def _firstIdent(node, src): + if node.type == "identifier": + return _nodeText(node, src) + for child in node.children: + found = _firstIdent(child, src) + if found: + return found + return None + + +def _functionDecl(root): + if root.type == "function_declaration": + return root + for child in root.children: + if child.type == "function_declaration": + return child + return None + + +def _compoundBody(fnDecl): + for child in fnDecl.children: + if child.type == "compound_statement": + return child + return None + + +def _paramNames(fnDecl, src): + names = [] + for child in fnDecl.children: + if child.type != "parameter_list": + continue + for param in child.children: + if param.type == "parameter": + ident = _firstIdent(param, src) + if ident: + names.append(ident) + return names + + +def _stmtList(compound): + '''Direct statement children of a compound_statement (the language-pack grammar + has no `statement` wrapper; real statement nodes sit directly under the block).''' + return [c for c in compound.children if c.is_named and c.type != "comment"] + + +def _statements(body): + yield from _stmtList(body) + + +def _varDeclName(stmt, src): + '''Declared name for a `var`/`let` variable_statement, else None.''' + if stmt.type != "variable_statement": + return None + return _firstIdent(stmt, src) + + +def _copyAssignment(stmt, src): + '''If stmt is `shadow = param` with simple idents, return (shadow, param).''' + if stmt.type != "assignment_statement": + return None + kids = list(stmt.children) + if len(kids) != 3: + return None + lhsNode, opNode, rhsNode = kids + if lhsNode.type != "lhs_expression" or opNode.type != "=": + return None + if rhsNode.type != "identifier": + return None + lhs = _firstIdent(lhsNode, src) + if not lhs or _nodeText(lhsNode, src).strip() != lhs: + return None # member/index write (`x.f = ...`), not a whole-variable copy + rhs = _nodeText(rhsNode, src) + if lhs and rhs: + return lhs, rhs + return None + + +def _lineRange(stmt, src): + '''Full source line(s) for a statement, including indent and trailing newline. + + The language-pack grammar leaves the trailing `;` as a sibling token outside the + statement node, so extend to the end of the physical line (next newline).''' + lineStart = src.rfind(b"\n", 0, stmt.start_byte) + 1 + nl = src.find(b"\n", stmt.end_byte) + lineEnd = len(src) if nl == -1 else nl + 1 + return lineStart, lineEnd + + +def _stmtSpan(stmt, src): + '''Node byte span extended past an immediately following `;` sibling token.''' + end = stmt.end_byte + i = end + while i < len(src) and src[i:i + 1] in (b" ", b"\t"): + i += 1 + if i < len(src) and src[i:i + 1] == b";": + end = i + 1 + return stmt.start_byte, end + + +def _assignmentTargets(body, src): + '''Map assigned identifier -> list of assignment_statement nodes.''' + targets = {} + stack = [body] + while stack: + node = stack.pop() + if node.type == "assignment_statement": + lhs = None + for part in node.children: + if part.type == "lhs_expression": + lhs = _firstIdent(part, src) + break + if lhs: + targets.setdefault(lhs, []).append(node) + stack.extend(node.children) + return targets + + +def _isWholeVarAssign(assignNode, src, name): + '''True if assignNode rebinds the whole variable `name` (`name = ...`), not a member/index + write (`name.field = ...`, `name[i] = ...`). Promotion may only fold whole-variable assigns.''' + for child in assignNode.children: + if child.type == "lhs_expression": + return _nodeText(child, src).strip() == name + return False + + +def _identUsesOutside(body, src, name, skipRanges): + '''Count ident uses not wholly inside any (start, end) skip range.''' + count = 0 + stack = [body] + while stack: + node = stack.pop() + if node.type == "identifier" and _nodeText(node, src) == name: + pos = node.start_byte + if not any(s <= pos < e for s, e in skipRanges): + count += 1 + stack.extend(node.children) + return count + + +def _applyDeletes(src, deleteRanges): + '''Delete byte ranges from UTF-8 source (end-exclusive).''' + data = bytearray(src.encode("utf-8")) + for start, end in sorted(deleteRanges, key=lambda r: r[0], reverse=True): + del data[start:end] + return data.decode("utf-8") + + +def _applyRenames(text, renames): + '''Word-boundary identifier renames, longest key first.''' + for old, new in sorted(renames.items(), key=lambda kv: len(kv[0]), reverse=True): + if old != new: + text = re.sub(r"\b" + re.escape(old) + r"\b", new, text) + return text + + +def _fixMutateShadowReads(text, shadow, param): + '''After removing a param copy-in, pre-init reads and the first assign RHS use the param.''' + assignPat = re.compile( + r"^[ \t]*" + re.escape(shadow) + r"\s*=\s*(.+?);\s*$", + re.MULTILINE) + m = assignPat.search(text) + if not m: + return text + + before = text[:m.start()] + assignLine = m.group(0) + expr = m.group(1) + after = text[m.end():] + + varDecl = re.compile(r"^[ \t]*var\s+" + re.escape(shadow) + r"\b") + fixedBefore = [] + for line in before.splitlines(keepends=True): + if varDecl.match(line): + fixedBefore.append(line) + else: + fixedBefore.append( + re.sub(r"\b" + re.escape(shadow) + r"\b", param, line)) + before = "".join(fixedBefore) + + exprFixed = re.sub(r"\b" + re.escape(shadow) + r"\b", param, expr) + stripped = exprFixed.strip() + if re.fullmatch(r"-\s*\(\s*" + re.escape(param) + r"\s*\)", stripped): + exprOut = f"(-{param})" + else: + exprOut = exprFixed + + indent = re.match(r"^([ \t]*)", assignLine).group(1) + newline = "\n" if assignLine.endswith("\n") else "" + return before + f"{indent}{shadow} = {exprOut};{newline}" + after + + +def _collapseParamShadows(fnText, glslParamNames=None): + src = fnText.encode("utf-8") + tree = _parser().parse(src) + root = tree.root_node + # The language-pack grammar cannot parse bare multi-arg function-call statements + # (`foo(a, b, c);`) and marks the tree as having errors, but those errors are local + # to the call statements; the declarations/copy-ins we act on parse correctly. So do + # not bail on has_error -- operate on the well-formed statements best-effort. + fnDecl = _functionDecl(root) + if fnDecl is None: + return fnText + + body = _compoundBody(fnDecl) + if body is None: + return fnText + + wgslParams = _paramNames(fnDecl, src) + assignTargets = _assignmentTargets(body, src) + stmts = list(_statements(body)) + topLevel = {(s.start_byte, s.end_byte) for s in stmts} + + varDeclStmts = {} + copyStmts = {} + shadowToParam = {} + for stmt in stmts: + shadow = _varDeclName(stmt, src) + if shadow: + varDeclStmts[shadow] = stmt + pair = _copyAssignment(stmt, src) + if pair: + lhs, rhs = pair + if rhs in wgslParams and fnBase(lhs) == fnBase(rhs): + shadowToParam[lhs] = rhs + copyStmts[lhs] = stmt + + # Pair naga param names with GLSL source names (same position). + paramRename = {} + if glslParamNames and len(glslParamNames) == len(wgslParams): + for wgslName, glslName in zip(wgslParams, glslParamNames): + if wgslName != glslName: + paramRename[wgslName] = glslName + + deleteRanges = [] + identRenames = {} + mutateFold = [] # (shadow, param) pairs needing self-negate fold after copy removal + + for shadow, param in shadowToParam.items(): + varStmt = varDeclStmts.get(shadow) + copyStmt = copyStmts.get(shadow) + if varStmt is None or copyStmt is None: + continue + + assigns = assignTargets.get(shadow, []) + skip = [_stmtSpan(varStmt, src), _stmtSpan(copyStmt, src)] + usesOutside = _identUsesOutside(body, src, shadow, skip) + + if len(assigns) == 1 and usesOutside == 0: + deleteRanges.extend(skip) + continue + + if len(assigns) == 1: + deleteRanges.extend(skip) + identRenames[shadow] = param + continue + + # The shadow is reassigned (beyond the copy-in). Dropping the `shadow = param` copy and + # folding is only safe when every reassignment is at the SAME (top-level) scope as the copy: + # otherwise a sibling branch or loop iteration would read the shadow uninitialized (e.g. a + # tangent reassigned only in the reflection branch but read in the environment branch). If + # any reassignment is nested, keep naga's copy-in intact. + keepCopy = False + for a in assigns: + if a.start_byte >= copyStmt.start_byte and a.end_byte <= copyStmt.end_byte: + continue # this is the copy-in itself + # A member/index write (`shadow.field = …`, `shadow[i] = …`) mutates the copy in + # place and has no whole-variable reassignment for `_fixMutateShadowReads` to fold, + # so the `shadow = param` copy-in must stay to seed the other fields — otherwise the + # shadow is read uninitialized (e.g. `fd.refraction = true` on a FresnelData copy). + if not _isWholeVarAssign(a, src, shadow): + keepCopy = True + break + st = _stmtContaining(body, a) + if st is None or (st.start_byte, st.end_byte) not in topLevel: + keepCopy = True + break + if keepCopy: + continue + + deleteRanges.append(_stmtSpan(copyStmt, src)) + mutateFold.append((shadow, param)) + + for wgslName, glslName in paramRename.items(): + identRenames[wgslName] = glslName + for shadow, target in list(identRenames.items()): + if target == wgslName: + identRenames[shadow] = glslName + + text = _applyDeletes(fnText, deleteRanges) + text = _applyRenames(text, identRenames) + for shadow, param in mutateFold: + text = _fixMutateShadowReads( + text, shadow, identRenames.get(param, param)) + while True: + cleaned = re.sub(r"\n[ \t]*\n[ \t]*\n", "\n\n", text) + if cleaned == text: + break + text = cleaned + text = re.sub(r"(\{\n)\s*\n+", r"\1", text) + return text + + +def _assignmentRhsText(assignNode, src): + '''RHS expression text of `lhs = ` (the rhs is the node's last child; + it is not wrapped in an `expression` node in the language-pack grammar).''' + kids = list(assignNode.children) + if len(kids) >= 3 and kids[0].type == "lhs_expression": + return _nodeText(kids[-1], src) + return None + + +def _stmtContaining(body, innerNode): + '''Smallest statement node (a direct child of some compound_statement) that + contains innerNode. With no `statement` wrapper, a statement is any named node + whose parent is a compound_statement.''' + node = innerNode + while node is not None and node != body: + parent = node.parent + if parent is not None and parent.type == "compound_statement": + return node + node = parent + return None + + +def _lineIndent(stmt, src): + lineStart = src.rfind(b"\n", 0, stmt.start_byte) + 1 + prefix = src[lineStart:stmt.start_byte] + m = re.match(rb"[ \t]*", prefix) + return prefix[: m.end()].decode("utf-8") if m else "" + + +def _pickLocalName(name, wgslParams, taken): + '''Map naga locals to readable names without shadowing params.''' + base = fnBase(name) + if any(fnBase(p) == base for p in wgslParams): + candidate = f"{base}_f" + else: + candidate = base + if candidate in taken: + return name + return candidate + + +def _applyEdits(text, deleteRanges, replacements): + '''Apply byte-range deletes and (start, end, newText) replacements.''' + edits = [(start, end, "") for start, end in deleteRanges] + edits.extend(replacements) + edits.sort(key=lambda item: item[0], reverse=True) + data = bytearray(text.encode("utf-8")) + for start, end, new in edits: + replacement = new.encode("utf-8") if isinstance(new, str) else new + data[start:end] = replacement + return data.decode("utf-8") + + +def _braceTokens(compound): + openBrace = closeBrace = None + for child in compound.children: + if child.type == "{": + openBrace = child + elif child.type == "}": + closeBrace = child + return openBrace, closeBrace + + +def _compoundInnerWrapper(outer): + '''If outer's sole statement is a nested compound, return that inner compound.''' + stmts = _stmtList(outer) + if len(stmts) != 1: + return None + return stmts[0] if stmts[0].type == "compound_statement" else None + + +def _cleanupMutableLocals(fnText, glslParamNames=None): + """Promote hoisted `var` + first assign to `var clean = …` when later reassigned.""" + src = fnText.encode("utf-8") + tree = _parser().parse(src) + root = tree.root_node + fnDecl = _functionDecl(root) + if fnDecl is None: + return fnText + body = _compoundBody(fnDecl) + if body is None: + return fnText + + wgslParams = _paramNames(fnDecl, src) + reserved = set(wgslParams) + if glslParamNames: + reserved.update(glslParamNames) + + varDecls = {} + for stmt in _statements(body): + name = _varDeclName(stmt, src) + # Only naga's UNINITIALIZED hoist (`var name: T;`) is a promotion candidate. An initialized + # declaration (`var i = 0i;`, `var I = vec3(0.0);`) is a real accumulator/counter whose + # value must survive; merging a later (often self-referencing) assignment into it is wrong. + if name and b"=" not in src[stmt.start_byte:stmt.end_byte]: + varDecls[name] = stmt + + assignTargets = _assignmentTargets(body, src) + # Byte ranges of the function body's direct (top-level) statements. naga hoists a `var name: T;` + # to this scope when the value is assigned in multiple branches; the promotion below may only + # move that declaration to the first assignment if that assignment is ALSO at this scope. + topLevel = {(s.start_byte, s.end_byte) for s in _statements(body)} + deleteRanges = [] + replacements = [] + renames = {} + taken = set(reserved) + + for name in sorted(varDecls): + allAssigns = assignTargets.get(name, []) + wholeAssigns = [a for a in allAssigns if _isWholeVarAssign(a, src, name)] + # Fold only when EVERY assignment rebinds the whole variable (and there are >=2 of them). + # If any is a field/index write (`fd.model = ...`, `x[i] = ...`) the value is built in place, + # so keep naga's hoisted `var` declaration and leave the writes untouched. + if len(wholeAssigns) != len(allAssigns) or len(wholeAssigns) < 2: + continue + firstAssign = min(wholeAssigns, key=lambda node: node.start_byte) + firstStmt = _stmtContaining(body, firstAssign) + varStmt = varDecls[name] + if firstStmt is None: + continue + if (firstStmt.start_byte, firstStmt.end_byte) not in topLevel: + continue # first assignment is inside a nested block (e.g. an if branch); relocating + # the `var` decl there would narrow its scope and break references in sibling + # branches or after the block. Keep naga's hoisted declaration as-is. + rhs = _assignmentRhsText(firstAssign, src) + if not rhs: + continue + + clean = _pickLocalName(name, wgslParams, taken) + taken.add(clean) + deleteRanges.append(_lineRange(varStmt, src)) + indent = _lineIndent(firstStmt, src) + replacements.append(( + *_lineRange(firstStmt, src), + f"{indent}var {clean} = {rhs};\n")) + if name != clean: + renames[name] = clean + + if not deleteRanges and not replacements: + return fnText + + text = _applyEdits(fnText, deleteRanges, replacements) + text = _applyRenames(text, renames) + return text + + +def _dedentBlock(text, spaces=4): + '''Remove one indent level from non-empty lines.''' + prefix = " " * spaces + lines = [] + for line in text.splitlines(keepends=True): + if line.strip() and line.startswith(prefix): + lines.append(line[spaces:]) + else: + lines.append(line) + return "".join(lines) + + +def _unwrapRedundantCompounds(fnText): + '''Remove naga's `if (c) { { stmts } }` wrapper compounds.''' + while True: + src = fnText.encode("utf-8") + tree = _parser().parse(src) + fnDecl = _functionDecl(tree.root_node) + if fnDecl is None: + break + body = _compoundBody(fnDecl) + if body is None: + break + + best = None + stack = [body] + while stack: + node = stack.pop() + if node.type == "compound_statement": + inner = _compoundInnerWrapper(node) + if inner is not None: + wrapperStmt = inner + size = wrapperStmt.end_byte - wrapperStmt.start_byte + if best is None or size > best[0]: + best = (size, wrapperStmt, inner) + stack.extend(node.children) + + if best is None: + break + + _, wrapperStmt, inner = best + innerOpen, innerClose = _braceTokens(inner) + if not innerOpen or not innerClose: + break + + raw = src[innerOpen.end_byte:innerClose.start_byte].decode("utf-8") + body = _dedentBlock(raw.strip("\n")).rstrip() + content = (body + "\n") if body else "" + start, end = _lineRange(wrapperStmt, src) + fnText = _applyEdits(fnText, [], [(start, end, content)]) + return fnText + + +def _simplifyPtrParens(text): + '''`((*bsdf)).field` -> `(*bsdf).field`.''' + return re.sub(r"\(\(\*([^)]+)\)\)", r"(*\1)", text) + + +def _balancedParens(expr): + depth = 0 + for ch in expr: + if ch == "(": + depth += 1 + elif ch == ")": + depth -= 1 + if depth < 0: + return False + return depth == 0 + + +def _unwrapAssignmentParens(text): + '''Drop a single outer paren pair around `let`/`var` rhs when balanced.''' + def repl(match): + indent, kind, name, expr = (match.group(1), match.group(2), + match.group(3), match.group(4)) + if _balancedParens(expr): + return f"{indent}{kind} {name} = {expr};" + return match.group(0) + + changed = True + while changed: + changed = False + new = re.sub( + r"^([ \t]*)\b(let|var)\s+(\w+)\s*=\s*\((.+)\);\s*$", + repl, + text, + flags=re.MULTILINE) + if new != text: + text = new + changed = True + return text + + +def _elseIfWrapper(elseClause): + '''If else { if (...) { ... } } return (elseClause, innerIf, expr). + + `expr` is the if-condition parenthesized_expression (parens included).''' + compound = None + for child in elseClause.children: + if child.type == "compound_statement": + compound = child + if compound is None: + return None + stmts = _stmtList(compound) + if len(stmts) != 1 or stmts[0].type != "if_statement": + return None + innerIf = stmts[0] + expr = next((c for c in innerIf.children + if c.type == "parenthesized_expression"), None) + if expr is None: + return None + return elseClause, innerIf, expr + + +def _flattenElseIfChains(fnText): + '''`} else { if (c) {` -> `} else if (c) {`.''' + while True: + src = fnText.encode("utf-8") + tree = _parser().parse(src) + fnDecl = _functionDecl(tree.root_node) + if fnDecl is None: + break + body = _compoundBody(fnDecl) + if body is None: + break + + candidate = None + stack = [body] + while stack: + node = stack.pop() + if node.type == "else_statement": + wrapped = _elseIfWrapper(node) + if wrapped is not None: + candidate = wrapped + break + stack.extend(node.children) + + if candidate is None: + break + + elseClause, innerIf, expr = candidate + # The `else` keyword is a sibling token BEFORE else_statement in this grammar (not part + # of the else node), so the rewrite must start at the keyword to avoid a duplicate `else`. + elseKw = elseClause.prev_sibling + cutStart = (elseKw.start_byte if elseKw is not None and elseKw.type == "else" + else elseClause.start_byte) + lineStart = src.rfind(b"\n", 0, elseClause.start_byte) + 1 + prefix = src[lineStart:elseClause.start_byte].decode("utf-8") + indent = re.match(r"^[ \t]*", prefix).group(0) + innerPart = src[expr.start_byte:innerIf.end_byte].decode("utf-8") + newElse = indent + "else if " + _dedentBlock(innerPart, 4) + fnText = (fnText[:cutStart] + newElse + + fnText[elseClause.end_byte:]) + return fnText + + +def _cleanupReadonlyLocals(fnText, glslParamNames=None): + '''Promote single-assignment `var` locals to `let` with GLSL-style names.''' + src = fnText.encode("utf-8") + tree = _parser().parse(src) + root = tree.root_node + # See _collapseParamShadows: errors from bare call statements are local; do not bail. + fnDecl = _functionDecl(root) + if fnDecl is None: + return fnText + body = _compoundBody(fnDecl) + if body is None: + return fnText + + wgslParams = _paramNames(fnDecl, src) + reserved = set(wgslParams) + if glslParamNames: + reserved.update(glslParamNames) + + varDecls = {} + for stmt in _statements(body): + name = _varDeclName(stmt, src) + # Only naga's UNINITIALIZED hoist (`var name: T;`) is a promotion candidate; an initialized + # declaration (`var i = 0i;`) is a real accumulator/counter and must be left intact. + if name and b"=" not in src[stmt.start_byte:stmt.end_byte]: + varDecls[name] = stmt + + assignTargets = _assignmentTargets(body, src) + # Byte ranges of the function body's direct (top-level) statements. The `var` declaration is + # always at this scope; converting it to a `let` at the assignment site is only valid when the + # assignment is ALSO at this scope (see the guard below). + topLevel = {(s.start_byte, s.end_byte) for s in _statements(body)} + deleteRanges = [] + replacements = [] + renames = {} + taken = set(reserved) + + for name in sorted(varDecls): + allAssigns = assignTargets.get(name, []) + # Promote to `let` only when the variable is assigned exactly once, as a whole (never a + # field/index write like `fd.model = ...`, which requires it to remain a mutable `var`). + if len(allAssigns) != 1 or not _isWholeVarAssign(allAssigns[0], src, name): + continue + assignNode = allAssigns[0] + assignStmt = _stmtContaining(body, assignNode) + varStmt = varDecls[name] + if assignStmt is None: + continue + rhs = _assignmentRhsText(assignNode, src) + if not rhs: + continue + skip = [(varStmt.start_byte, varStmt.end_byte), + (assignNode.start_byte, assignNode.end_byte)] + uses = _identUsesOutside(body, src, name, skip) + if uses == 0: + deleteRanges.append(_lineRange(varStmt, src)) + deleteRanges.append(_lineRange(assignStmt, src)) + continue + + if (assignStmt.start_byte, assignStmt.end_byte) not in topLevel: + continue # the single assignment is in a nested block (e.g. a loop `continuing` block or + # an if branch); relocating the declaration there as a `let` would move it out + # of the scope where the variable is read. Keep naga's hoisted declaration. + + clean = _pickLocalName(name, wgslParams, taken) + taken.add(clean) + deleteRanges.append(_lineRange(varStmt, src)) + indent = _lineIndent(assignStmt, src) + replacements.append(( + *_lineRange(assignStmt, src), + f"{indent}let {clean} = {rhs};\n")) + if name != clean: + renames[name] = clean + + if not deleteRanges and not replacements: + return fnText + + text = _applyEdits(fnText, deleteRanges, replacements) + text = _applyRenames(text, renames) + while True: + cleaned = re.sub(r"\n[ \t]*\n[ \t]*\n", "\n\n", text) + if cleaned == text: + break + text = cleaned + return text + + +def inlineSingleUseTemps(body): + '''Inline naga's single-use `let _eN = expr;` temporaries.''' + changed = True + while changed: + changed = False + for m in re.finditer(r"[ \t]*let\s+(_e\d+)\s*=\s*(.+?);\n", body): + name, expr = m.group(1), m.group(2) + if len(re.findall(r"\b" + re.escape(name) + r"\b", body)) - 1 == 1: + repl = "(" + expr + ")" if re.search(r"[-+*/ ]", expr.strip()) else expr + body = body[:m.start()] + body[m.end():] + body = re.sub(r"\b" + re.escape(name) + r"\b", lambda _m: repl, body, count=1) + changed = True + break + return body + + +def cleanupFunction(fnText, glslParamNames=None): + '''Run readability cleanup on one transpiled WGSL function.''' + try: + # Inline naga temps first so param-mutate patterns like `N_25 = -(_e39)` become + # `N_25 = -(N_25)` before tree-sitter param-shadow analysis runs. + text = inlineSingleUseTemps(fnText) + text = _collapseParamShadows(text, glslParamNames) + while True: + newText = _cleanupReadonlyLocals(text, glslParamNames) + if newText == text: + break + text = newText + text = _cleanupMutableLocals(text, glslParamNames) + text = _unwrapRedundantCompounds(text) + text = _flattenElseIfChains(text) + text = _simplifyPtrParens(text) + text = _unwrapAssignmentParens(text) + text = re.sub(r"\}\s+else\b", "} else", text) + text = re.sub(r"\n[ \t]+\n", "\n", text) + while True: + cleaned = re.sub(r"\n[ \t]*\n[ \t]*\n", "\n\n", text) + if cleaned == text: + break + text = cleaned + except Exception as exc: + print(f" WARN mxwgslcleanup: {exc}; keeping verbose naga output", file=sys.stderr) + text = fnText + return text diff --git a/source/MaterialXGenWgsl/tools/requirements-transpile.txt b/source/MaterialXGenWgsl/tools/requirements-transpile.txt new file mode 100644 index 0000000000..a59777d0dc --- /dev/null +++ b/source/MaterialXGenWgsl/tools/requirements-transpile.txt @@ -0,0 +1,4 @@ +# Deps for WGSL post-process cleanup (mxwgslcleanup.py). +# tree-sitter-language-pack ships a precompiled WGSL grammar (and pulls the tree-sitter +# runtime), so no Node/tree-sitter-cli or vendored grammar is needed. +tree-sitter-language-pack>=1.13,<2 diff --git a/source/MaterialXGenWgsl/tools/test_mxwgslcleanup.py b/source/MaterialXGenWgsl/tools/test_mxwgslcleanup.py new file mode 100644 index 0000000000..2c2034f994 --- /dev/null +++ b/source/MaterialXGenWgsl/tools/test_mxwgslcleanup.py @@ -0,0 +1,226 @@ +"""Tests for mxwgslcleanup.py. + +These guard against the class of bug that is otherwise painful to find: a readability +pass that emits VALID WGSL but silently changes behaviour (e.g. reading an uninitialized +variable). Run with `pytest`, or standalone: `python test_mxwgslcleanup.py`. + +Only requires `tree-sitter-language-pack` (the transpile requirement) — no naga needed, +since the raw naga output for the tricky functions is embedded as fixtures below. +""" +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import mxwgslcleanup as m + + +# --- differential semantic invariant ---------------------------------------- + +def uninitialized_whole_reads(fn_text): + """Count locals that are read as a WHOLE value yet never assigned as a whole. + + A variable declared `var x: T;` (no initializer) that is passed to a call, returned, + or used on an assignment RHS, but only ever written via member/index stores + (`x.f = ...`) — or not written at all — reads its zero default. `collapse` removing a + `x = param` copy-in is exactly what turns a safe shadow into one of these. + + This is a per-function count. It is deliberately compared RAW-vs-CLEANED (not asserted + to be zero) so that legitimate struct builders (`var fd: FresnelData; fd.a=..; return fd;`) + — which have the same shape before and after cleanup — never trip it; only a NEW + occurrence introduced by cleanup is a regression. + """ + src = fn_text.encode("utf-8") + tree = m._parser().parse(src) + fn = m._functionDecl(tree.root_node) + if fn is None: + return 0 + body = m._compoundBody(fn) + if body is None: + return 0 + + uninit = {} + for stmt in m._statements(body): + name = m._varDeclName(stmt, src) + if name and b"=" not in src[stmt.start_byte:stmt.end_byte]: + uninit[name] = stmt + + assigns = m._assignmentTargets(body, src) + count = 0 + for name, decl in uninit.items(): + name_assigns = assigns.get(name, []) + has_whole = any(m._isWholeVarAssign(a, src, name) for a in name_assigns) + # Skip the decl and every assignment's lhs target; whatever `name` occurrences + # remain are genuine reads of the whole value. + skip = [(decl.start_byte, decl.end_byte)] + for a in name_assigns: + for child in a.children: + if child.type == "lhs_expression": + skip.append((child.start_byte, child.end_byte)) + reads = m._identUsesOutside(body, src, name, skip) + if reads > 0 and not has_whole: + count += 1 + return count + + +# --- fixtures: raw naga output (input to cleanup) ---------------------------- + +# The regression that caused this test to exist: `fd_8` is a copy of the `fd` param that is +# mutated by a FIELD write (`fd_8.refraction = true`) then read as a whole struct. collapse +# must keep the `fd_8 = fd` copy-in; deleting it left fd_8 zero-initialized -> broken glass. +FD_REFRACT_SHADOW = ( + "fn mx_surface_transmission(N_13: vec3f, V_7: vec3f, X_1: vec3f, alpha_15: vec2f, " + "distribution_1: i32, fd_7: FresnelData, tint: vec3f) -> vec3f {\n" + " var N_14: vec3f;\n" + " var V_8: vec3f;\n" + " var X_2: vec3f;\n" + " var alpha_16: vec2f;\n" + " var distribution_2: i32;\n" + " var fd_8: FresnelData;\n" + " var tint_1: vec3f;\n" + "\n" + " N_14 = N_13;\n" + " V_8 = V_7;\n" + " X_2 = X_1;\n" + " alpha_16 = alpha_15;\n" + " distribution_2 = distribution_1;\n" + " fd_8 = fd_7;\n" + " tint_1 = tint;\n" + " fd_8.refraction = true;\n" + " if false {\n" + " {\n" + " let _e33 = tint_1;\n" + " let _e34 = mx_square_vec3(_e33);\n" + " tint_1 = _e34;\n" + " }\n" + " }\n" + " let _e35 = N_14;\n" + " let _e36 = V_8;\n" + " let _e37 = X_2;\n" + " let _e38 = alpha_16;\n" + " let _e39 = distribution_2;\n" + " let _e40 = fd_8;\n" + " let _e41 = mx_environment_radiance(_e35, _e36, _e37, _e38, _e39, _e40);\n" + " let _e42 = tint_1;\n" + " return (_e41 * _e42);\n" + "}\n" +) +FD_REFRACT_PARAMS = ["N", "V", "X", "alpha", "distribution", "fd", "tint"] + +# A plain read-only param shadow: should collapse away entirely and restore the GLSL name. +READONLY_SHADOW = ( + "fn f(a_1: f32) -> f32 {\n" + " var a_2: f32;\n" + " a_2 = a_1;\n" + " let _e5 = a_2;\n" + " return (_e5 * 2.0);\n" + "}\n" +) +READONLY_PARAMS = ["a"] + +# A legitimate struct builder (NOT a param shadow): zero-init then fill field-by-field. +# cleanup must leave it working; the invariant must NOT flag it (same shape raw and cleaned). +STRUCT_BUILDER = ( + "fn mk() -> FresnelData {\n" + " var fd: FresnelData;\n" + " fd.ior = 1.5;\n" + " fd.model = 0i;\n" + " return fd;\n" + "}\n" +) + +# A param shadow reassigned in a NESTED scope only: copy-in must be kept (else read +# uninitialized in a sibling branch). Mirrors the tangent-X dielectric case. +NESTED_REASSIGN_SHADOW = ( + "fn g(X_1: vec3f, flag: bool) -> vec3f {\n" + " var X_2: vec3f;\n" + " X_2 = X_1;\n" + " if flag {\n" + " X_2 = (X_2 * 2.0);\n" + " }\n" + " let _e9 = X_2;\n" + " return _e9;\n" + "}\n" +) +NESTED_PARAMS = ["X", "flag"] + +FIXTURES = [ + ("fd_refract_field_shadow", FD_REFRACT_SHADOW, FD_REFRACT_PARAMS), + ("readonly_shadow", READONLY_SHADOW, READONLY_PARAMS), + ("struct_builder", STRUCT_BUILDER, None), + ("nested_reassign_shadow", NESTED_REASSIGN_SHADOW, NESTED_PARAMS), +] + + +# --- tests ------------------------------------------------------------------- + +def test_cleanup_introduces_no_uninitialized_reads(): + """Core guard: cleanup must never turn a safely-initialized variable into one that + is read while uninitialized. Would have caught the fd_8 glass-transmission bug.""" + for name, raw, params in FIXTURES: + cleaned = m.cleanupFunction(raw, params) + before = uninitialized_whole_reads(raw) + after = uninitialized_whole_reads(cleaned) + assert after <= before, ( + f"[{name}] cleanup introduced {after - before} uninitialized whole-value " + f"read(s) (raw={before}, cleaned={after}).\n--- cleaned ---\n{cleaned}") + + +def test_field_mutated_shadow_keeps_copy_in(): + """Regression: the FresnelData copy-in must survive so the struct is fully seeded + before `fd.refraction = true` and the whole-struct read.""" + cleaned = m.cleanupFunction(FD_REFRACT_SHADOW, FD_REFRACT_PARAMS) + assert uninitialized_whole_reads(cleaned) == 0, cleaned + # The struct is read whole; whatever name it keeps, it must be whole-assigned first. + src = cleaned.encode("utf-8") + tree = m._parser().parse(src) + body = m._compoundBody(m._functionDecl(tree.root_node)) + assigns = m._assignmentTargets(body, src) + # find the FresnelData local decl name + fd_name = None + for stmt in m._statements(body): + n = m._varDeclName(stmt, src) + if n and b"FresnelData" in src[stmt.start_byte:stmt.end_byte]: + fd_name = n + assert fd_name is not None, cleaned + assert any(m._isWholeVarAssign(a, src, fd_name) for a in assigns.get(fd_name, [])), ( + f"FresnelData local '{fd_name}' has no whole-variable initializer:\n{cleaned}") + + +def test_readonly_shadow_collapses_and_renames(): + cleaned = m.cleanupFunction(READONLY_SHADOW, READONLY_PARAMS) + assert "a_2" not in cleaned, cleaned # shadow removed + assert "a_1" not in cleaned, cleaned # naga name restored to GLSL 'a' + assert "(a * 2.0)" in cleaned, cleaned + + +def test_struct_builder_unchanged_shape(): + """A field-filled local struct is a legitimate zero-init pattern; it should still + read whole (count 1) in both raw and cleaned — proving no false positive.""" + assert uninitialized_whole_reads(STRUCT_BUILDER) == 1 + cleaned = m.cleanupFunction(STRUCT_BUILDER, None) + assert uninitialized_whole_reads(cleaned) == 1, cleaned + + +def test_nested_reassign_keeps_copy_in(): + cleaned = m.cleanupFunction(NESTED_REASSIGN_SHADOW, NESTED_PARAMS) + assert uninitialized_whole_reads(cleaned) == 0, cleaned + + +def test_cleanup_is_idempotent(): + for _name, raw, params in FIXTURES: + once = m.cleanupFunction(raw, params) + twice = m.cleanupFunction(once, params) + assert once == twice, f"not idempotent for {_name}:\n{once!r}\n!=\n{twice!r}" + + +if __name__ == "__main__": + failed = 0 + for fn in sorted(k for k in dict(globals()) if k.startswith("test_")): + try: + globals()[fn]() + print(f"PASS {fn}") + except AssertionError as exc: + failed += 1 + print(f"FAIL {fn}: {exc}") + print(f"\n{'OK' if not failed else str(failed) + ' FAILED'}") + sys.exit(1 if failed else 0) From 0eaf114d8754b889e7452f085a7a188b05f5dd09 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Mon, 20 Jul 2026 17:24:07 -0700 Subject: [PATCH 15/19] Update naga and documentation --- .github/workflows/main.yml | 18 +++++++--- .github/workflows/release.yml | 6 ++-- .../DeveloperGuide/WGSLShaderGeneration.md | 35 ++++++++++--------- .../DeveloperGuide/WGSLTranspilerFixes.md | 32 +++++++++-------- 4 files changed, 54 insertions(+), 37 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 179d119ef6..f6313a3b9a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,7 +14,7 @@ permissions: env: CMAKE_BUILD_PARALLEL_LEVEL: 4 CTEST_PARALLEL_LEVEL: 4 - NAGA_VERSION: "29.0.0" + NAGA_VERSION: "30.0.0" jobs: @@ -246,7 +246,13 @@ jobs: - name: Generate WGSL library if: matrix.python != 'None' - run: python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries + run: | + python -m pip install -r source/MaterialXGenWgsl/tools/requirements-transpile.txt + python source/MaterialXGenWgsl/tools/mxgenwgsl.py --libraries libraries --out libraries + + - name: Test WGSL cleanup + if: matrix.python != 'None' + run: python source/MaterialXGenWgsl/tools/test_mxwgslcleanup.py - name: CMake Generate shell: bash @@ -477,7 +483,9 @@ jobs: python-version: 3.13 - name: Generate WGSL library - run: python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries + run: | + python -m pip install -r source/MaterialXGenWgsl/tools/requirements-transpile.txt + python source/MaterialXGenWgsl/tools/mxgenwgsl.py --libraries libraries --out libraries - name: JavaScript CMake Generate run: cmake -S . -B javascript/build -DMATERIALX_BUILD_JS=ON -DMATERIALX_BUILD_GEN_WGSL=ON -DMATERIALX_EMSDK_PATH=${{ env.EMSDK }} @@ -535,7 +543,9 @@ jobs: run: cargo install naga-cli --locked --version ${{ env.NAGA_VERSION }} - name: Generate WGSL library - run: python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries + run: | + python -m pip install -r source/MaterialXGenWgsl/tools/requirements-transpile.txt + python source/MaterialXGenWgsl/tools/mxgenwgsl.py --libraries libraries --out libraries - name: Build SDist id: generate diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e968aaf364..350f526195 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest env: RELEASE_TAG: ${{ github.ref_name }} - NAGA_VERSION: "29.0.0" + NAGA_VERSION: "30.0.0" permissions: contents: write id-token: write @@ -38,7 +38,9 @@ jobs: run: cargo install naga-cli --locked --version ${NAGA_VERSION} - name: Generate WGSL library - run: python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries + run: | + python -m pip install -r source/MaterialXGenWgsl/tools/requirements-transpile.txt + python source/MaterialXGenWgsl/tools/mxgenwgsl.py --libraries libraries --out libraries - name: Generate Archives run: | diff --git a/documents/DeveloperGuide/WGSLShaderGeneration.md b/documents/DeveloperGuide/WGSLShaderGeneration.md index 015eb27d63..2e61361d7d 100644 --- a/documents/DeveloperGuide/WGSLShaderGeneration.md +++ b/documents/DeveloperGuide/WGSLShaderGeneration.md @@ -10,8 +10,8 @@ The `genwgsl` uses a **hybrid node library**: | Library content | Source | Committed to git? | | --- | --- | --- | -| Most node `.wgsl` files | Transpiled from `genglsl` by `glsl_to_wgsl.py` | No — derived artifact | -| Core `lib/` math and closure helpers | Transpiled from `genglsl/lib/` by `glsl_to_wgsl.py` | No — derived artifact | +| Most node `.wgsl` files | Transpiled from `genglsl` by `mxgenwgsl.py` | No — derived artifact | +| Core `lib/` math and closure helpers | Transpiled from `genglsl/lib/` by `mxgenwgsl.py` | No — derived artifact | | Texture, image, and light nodes | Hand-maintained | Yes | | `mx_chiang_hair_bsdf` (`EXPECTED_FALLBACK`) | Hand-maintained | Yes | @@ -43,33 +43,33 @@ The library lives under `libraries/{stdlib,pbrlib,lights}/genwgsl/`, with the ta ## Tooling -### `glsl_to_wgsl.py` +### `mxgenwgsl.py` -The transpiler at `source/MaterialXGenWgsl/tools/glsl_to_wgsl.py` converts `genglsl` node fragments into `genwgsl` equivalents using [naga](https://github.com/gfx-rs/wgpu/tree/trunk/naga). +The transpiler at `source/MaterialXGenWgsl/tools/mxgenwgsl.py` converts `genglsl` node fragments into `genwgsl` equivalents using [naga](https://github.com/gfx-rs/wgpu/tree/trunk/naga). **NOTE:*** It is **not** a general-purpose GLSL-to-WGSL converter. It is scoped to MaterialX shader-node fragments. **Regenerate the full library in place:** ```sh -python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries +python source/MaterialXGenWgsl/tools/mxgenwgsl.py --libraries libraries --out libraries ``` **Regenerate specific nodes only:** ```sh -python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries --only mx_noise3d_float mx_sheen_bsdf +python source/MaterialXGenWgsl/tools/mxgenwgsl.py --libraries libraries --out libraries --only mx_noise3d_float mx_sheen_bsdf ``` A non-zero exit code means an *unexpected* node failed (a regression). Known fallback nodes listed in `EXPECTED_FALLBACK` are tolerated. ### What the transpiler does -`glsl_to_wgsl.py` is **not** a general-purpose GLSL-to-WGSL converter. It transpiles MaterialX **shader-node fragments** — one function per file, referenced by `file=` implementations in `stdlib`, `pbrlib`, and `lights`. naga performs the actual translation; the script wraps it with a pre-processor and post-processor so incomplete node fragments become valid input and the output matches genwgsl library conventions. +`mxgenwgsl.py` is **not** a general-purpose GLSL-to-WGSL converter. It transpiles MaterialX **shader-node fragments** — one function per file, referenced by `file=` implementations in `stdlib`, `pbrlib`, and `lights`. naga performs the actual translation; the script wraps it with a pre-processor and post-processor so incomplete node fragments become valid input and the output matches genwgsl library conventions. **Pipeline (per node function):** -1. **Lib helpers** — transpile `genglsl/lib/*.glsl` first (topological include order, `LIB_PREAMBLE`, overload renaming via inverted `CALL_MAP`) +1. **Lib helpers** — transpile `genglsl/lib/*.glsl` first (topological include order, `LIB_PREAMBLE`, overload renaming via `mangle()`) 2. **Pre-process** — wrap the node fragment in a complete GLSL shader naga can parse 3. **Transpile** — `naga --input-kind glsl --shader-stage frag` 4. **Post-process** — clean up naga output and remap helper calls to genwgsl names @@ -102,7 +102,8 @@ naga's output is correct but verbose (SSA-style parameter shadows, `vec3` s - Collapses single-use temporaries and parameter-copy shadows - Normalizes types (`vec3` → `vec3f`, `2f` → `2.0`) -- Remaps overloaded GLSL helper calls via `CALL_MAP` to type-suffixed genwgsl names +- Remaps overloaded GLSL helper calls via `mangle()` to type-suffixed genwgsl names +- Re-attaches GLSL comments naga discarded (doc/leading blocks verbatim; inline body comments best-effort) WGSL has no function overloading, so the genwgsl `lib/` gives each GLSL overload a distinct name. For the noise example above, `mx_perlin_noise_float(position)` with a `vec3` argument is rewritten to `mx_perlin_noise_float_3d(position)`. @@ -112,7 +113,7 @@ Similarly, GLSL `mx_square` overloads map to `mx_square_f32`, `mx_square_vec2`, | Category | Example | Notes | | --- | --- | --- | -| Standalone math / utility nodes | `mx_noise3d_float`, `mx_mix_surfaceshader` | `#include` lib helpers; calls remapped via `CALL_MAP` | +| Standalone math / utility nodes | `mx_noise3d_float`, `mx_mix_surfaceshader` | `#include` lib helpers; calls remapped via `mangle()` | | PBR nodes with standard lib signatures | Most BSDF combiners, EDF nodes | Generated when all helper calls resolve | | Closure / `inout` parameters | `inout BSDF bsdf` | Closure preamble supplies `BSDF` struct; `inout` becomes `ptr` | | Cross-node helper calls | One node calling another node's function | Prototypes collected from all genglsl files | @@ -137,7 +138,7 @@ Generated files carry a `// Generated from … do not edit` banner. | Texture / image nodes | `mx_image_color3` | naga's GLSL frontend has no sampler support — auto-skipped by filename pattern (`image`, `hextiled`) | | Light shaders | `mx_point_light` | Use dynamically generated `LightData` — auto-skipped (`_light$` pattern) | | Chiang hair BSDF | `mx_chiang_hair_bsdf` | naga limitation on hair scattering helpers — listed in `EXPECTED_FALLBACK`, kept hand-written | -| Unmapped overloads | Any call not in `CALL_MAP` | Node stays hand-written; logged as unsupported | +| Unmapped overloads | An overload `mangle()` resolves to `None` (e.g. an adapted-signature BSDF helper) | Node stays hand-written; logged as unsupported | **Specular environment IBL:** `WgslShaderGenerator` supports FIS, prefilter, and none methods (`mx_environment_fis.wgsl`, `mx_environment_prefilter.wgsl`, `mx_environment_none.wgsl`). The @@ -165,7 +166,7 @@ For full transpiler internals see [`source/MaterialXGenWgsl/tools/README.md`](.. ### CI -GitHub Actions runs `glsl_to_wgsl.py` on Python-enabled build jobs, the JavaScript job, the Python sdist job, and release archives. A GLSL change that breaks WGSL generation will fail CI even without a local naga install. +GitHub Actions runs `mxgenwgsl.py` on Python-enabled build jobs, the JavaScript job, the Python sdist job, and release archives. A GLSL change that breaks WGSL generation will fail CI even without a local naga install. ## Local Developer Workflows @@ -173,7 +174,7 @@ GitHub Actions runs `glsl_to_wgsl.py` on Python-enabled build jobs, the JavaScri 1. Regenerate the WGSL library: ```sh - python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries + python source/MaterialXGenWgsl/tools/mxgenwgsl.py --libraries libraries --out libraries ``` 2. Configure with `-DMATERIALX_BUILD_GEN_WGSL=ON` and rebuild. 3. Run the `[genwgsl]` unit tests: @@ -186,7 +187,7 @@ GitHub Actions runs `glsl_to_wgsl.py` on Python-enabled build jobs, the JavaScri This is the fastest path for validating WGSL output without Emscripten: ```sh -python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries +python source/MaterialXGenWgsl/tools/mxgenwgsl.py --libraries libraries --out libraries cmake -S . -B build -DMATERIALX_BUILD_GEN_WGSL=ON cmake --build build --config Release @@ -200,7 +201,7 @@ The `[genwgsl]` tests in `source/MaterialXTest/MaterialXGenWgsl/GenWgsl.cpp` cov For end-to-end testing in the browser (Three.js WebGPU renderer, TSL bridge), build with both JavaScript and WGSL enabled: ```sh -python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries +python source/MaterialXGenWgsl/tools/mxgenwgsl.py --libraries libraries --out libraries cmake -S . -B javascript/build \ -DMATERIALX_BUILD_JS=ON \ @@ -245,12 +246,12 @@ This adds the `MaterialXGenWgslLibrary` target, which surfaces transpile and nag ## Release Artifacts -Generated WGSL node and `lib/` files are not committed to git, but they are included in release archives. The release workflow runs `glsl_to_wgsl.py` before packaging, so `libraries/*/genwgsl/**/*.wgsl` files ship in the archive. +Generated WGSL node and `lib/` files are not committed to git, but they are included in release archives. The release workflow runs `mxgenwgsl.py` before packaging, so `libraries/*/genwgsl/**/*.wgsl` files ship in the archive. ## Related Documentation - [WGSL Transpiler Fixes](WGSLTranspilerFixes.md) — generation policy and the naga-reconciliation fixes for full-library generation - [Shader Generation](ShaderGeneration.md) — general shader generation framework - [`source/MaterialXGenWgsl/README.md`](../../source/MaterialXGenWgsl/README.md) — back-end layout and design -- [`source/MaterialXGenWgsl/tools/README.md`](../../source/MaterialXGenWgsl/tools/README.md) — transpiler internals, `CALL_MAP`, and `EXPECTED_FALLBACK` +- [`source/MaterialXGenWgsl/tools/README.md`](../../source/MaterialXGenWgsl/tools/README.md) — transpiler internals, `mangle()` overload naming, and `EXPECTED_FALLBACK` - [`javascript/README.md`](../../javascript/README.md) — JavaScript bindings and viewer setup diff --git a/documents/DeveloperGuide/WGSLTranspilerFixes.md b/documents/DeveloperGuide/WGSLTranspilerFixes.md index eb44a44964..6e3f27179b 100644 --- a/documents/DeveloperGuide/WGSLTranspilerFixes.md +++ b/documents/DeveloperGuide/WGSLTranspilerFixes.md @@ -1,6 +1,6 @@ # WGSL Transpiler: Full-Library Generation Fixes -This note summarizes the changes that make `glsl_to_wgsl.py` generate the **entire** `genwgsl` node +This note summarizes the changes that make `mxgenwgsl.py` generate the **entire** `genwgsl` node and lib library from `genglsl` (previously many files were hand-written). It covers (1) the generation policy — what is generated vs. kept hand-written — and (2) the transpiler correctness fixes needed to reconcile naga's WGSL output with the genwgsl library conventions. @@ -36,36 +36,40 @@ cleanly and are generated; only `mx_chiang_hair_bsdf` remains an `EXPECTED_FALLB naga produces valid but SSA-style WGSL and deterministically rewrites identifiers. The following post-processing was added/fixed so the generated output matches the genwgsl library and the `WgslShaderGenerator` runtime template. All changes are in -`source/MaterialXGenWgsl/tools/glsl_to_wgsl.py`. +`source/MaterialXGenWgsl/tools/mxgenwgsl.py`. | Area | Symptom before fix | Fix | | --- | --- | --- | -| **Struct parsing** (`transpile_glsl_structs`) | GLSL comments inside a struct (e.g. `// Fresnel model`) were parsed as fields (`Fresnel: //,`) and array fields (`vec2 c[3]`) became `c[3]: vec2f` | Strip line/block comments; rewrite array fields to `array`; skip types provided centrally by `LIB_STRUCT_PREAMBLE` (`LIB_PREAMBLE_STRUCTS`) so `FresnelData`/`ClosureData` aren't redeclared | -| **`surfaceshader`** | Redeclared — emitted both by `WgslShaderGenerator::emitTypeDefinitions` and by the lib preamble | Removed `surfaceshader` from `LIB_STRUCT_PREAMBLE` (the generator owns it; the `material = surfaceshader` alias still resolves) | +| **Struct parsing** (`transpile_glsl_structs`) | GLSL comments inside a struct (e.g. `// Fresnel model`) were parsed as fields (`Fresnel: //,`) and array fields (`vec2 c[3]`) became `c[3]: vec2f` | Strip line/block comments; rewrite array fields to `array`; skip types provided centrally by `wgsl_closure_preamble()` (`LIB_PREAMBLE_STRUCTS`) so `FresnelData`/`ClosureData` aren't redeclared | +| **`surfaceshader`** | Redeclared — emitted both by `WgslShaderGenerator::emitTypeDefinitions` and by the lib preamble | `surfaceshader` is parsed from `GlslSyntax.cpp` for node context only; the WGSL lib preamble omits it (the generator owns it; the `material = surfaceshader` alias still resolves) | | **File-scope consts** (`transpile_glsl_consts`) | Function-local `const int SAMPLE_COUNT` was hoisted to module scope → duplicate / redeclared consts across microfacet libs | Blank out function bodies before matching, so only true file-scope consts are emitted | -| **Const references** (`resolve_const_refs`) | naga suffixes references to a digit-ending const (`FUJII_CONSTANT_1_`) or a name it sees twice in its input (`FRESNEL_MODEL_SCHLICK_1`), leaving them unresolved | Rewrite `NAME_` / `NAME_` references back to the declared const name (keyed only on known const names, so numbered locals are untouched) | -| **`FresnelData` fields** (`LIB_FIELD_RENAMES`) | naga renders digit-ending fields `F0`/`F82`/`F90` as `F0_`/`F82_`/`F90_`, mismatching the struct | Strip the trailing `_` from those field accesses | -| **`mx_noise` overloads** (`CALL_MAP`) | WGSL has no overloading, so `mx_bilerp`, `mx_hash_int`, `mx_cell_noise_vec3`, etc. collided | Added type-suffixed `CALL_MAP` entries (e.g. `mx_hash_int_i2`, `mx_cell_noise_vec3_vec2`) matching the hand-written naming | +| **naga identifier reconciliation** (`denaga_name`) | naga suffixes references to a digit-ending const (`FUJII_CONSTANT_1_`), a name it sees twice (`FRESNEL_MODEL_SCHLICK_1`), the digit-ending fields `F0`/`F82`/`F90` (`F0_`), and prototype names — leaving them mismatched | One Namer-grounded helper (`proc/namer.rs`) maps `NAME_` / `NAME_` references back to the clean declaration name; used by `resolve_const_refs`, `apply_field_renames` (digit fields from the derived preamble), and the prototype-underscore strip. Keyed only on known names, so numbered locals are untouched | +| **`FresnelData` fields** | GLSL used `tf_*` while the hand-ported WGSL lib used `thinfilm_*`, requiring a transpiler rename | Renamed `tf_*` → `thinfilm_*` in `mx_microfacet_specular.glsl` so GLSL and emitted WGSL share one field layout; removed `LIB_FIELD_RENAMES` | +| **Overloaded helpers** (`mangle`) | WGSL has no overloading, so `mx_bilerp`, `mx_hash_int`, `mx_cell_noise_vec3`, `mx_fresnel_schlick`, etc. collided | `mangle(name, types, overloaded)` derives a type-suffixed name from a per-family `SUFFIX_SCHEME` (default: first parameter type) plus a small `EXCEPTIONS` table for irregular/semantic names; the overload *keys* come from scanning genglsl, not a hand-listed table | | **Overload resolution** (`extract_transpiled_fn`, `remap_calls_by_naga_sig`) | For overloaded siblings (`mx_ggx_dir_albedo` vec3/scalar/FresnelData) the wrong body was extracted and calls resolved to the wrong overload | Extract by parameter **signature** when a name is ambiguous; remap calls using the **actual per-module signatures** naga assigned, instead of assuming a sorted index order | | **`out`/`inout` params** (`naga_proto_params`) | Stripping `out`/`inout` from prototypes made callers pass a dereferenced value (`mx_normalmap_vector2(..., (*result))`) instead of the pointer | Keep `out`/`inout` in prototypes (only strip the neutral `const`/`in`) so naga passes `ptr` | +| **Comment preservation** (`parse_functions`, `inject_inline_comments`) | naga discards all GLSL comments, so doc comments and references were lost | Re-attach from genglsl: function `lead` (doc) blocks and file-level top comments verbatim; inline `//` body comments best-effort via stable-token anchors (whole-line insertion, unmatched dropped, so output is never corrupted). Struct/const comments stay stripped | +| **Scan-driven validation** (`validate_overload_coverage` / `_token_coverage` / `_lib_names`) | A new genglsl overload or `$`-token could silently mis-emit a name or fail naga with an obscure error | Preflight hard-errors when an overload has no `mangle()` mapping, a `$`-token in a transpiled lib source is undeclared in `HwConstants.cpp` and unstubbed in `NAGA_LIB_STUBS`, or a `mangle()` name is absent from the real genwgsl lib | +| **Derived closure preambles** (`wgsl_closure_preamble` / `glsl_closure_preamble`) | `CLOSURE_PREAMBLE`, `LIB_STRUCT_PREAMBLE`, and `LIB_TOKEN_FIXUPS` duplicated C++/GLSL sources | Emit closure structs from `genglsl/lib` + `GlslSyntax.cpp`; validate token names from `HwConstants.cpp`; keep only naga-specific stub values in `NAGA_LIB_STUBS` | ### The recurring theme -naga is deterministic but rewrites identifiers to keep its output valid: it appends `_` to any name -ending in a digit, `_N` to colliding names, and renders `out`/`inout` params as `ptr`. -Wherever the transpiler re-emits a symbol itself (a hand-written struct/const, or a prototype), the -generated references must be reconciled back to that symbol. The fixes above are all instances of -that reconciliation. +naga is deterministic but rewrites identifiers to keep its output valid: its `Namer` +(`proc/namer.rs`) appends `_` to any name ending in a digit or a keyword/builtin, `_N` to colliding +names, and renders `out`/`inout` params as `ptr`. Wherever the transpiler re-emits a +symbol itself (a hand-written struct/const, or a prototype), the generated references must be +reconciled back to that symbol — now via the single Namer-grounded `denaga_name()` helper. The fixes +above are all instances of that reconciliation. ## Build / repo workflow -Generated `.wgsl` files carry a `// Generated from … glsl_to_wgsl.py` banner and are **not +Generated `.wgsl` files carry a `// Generated from … mxgenwgsl.py` banner and are **not committed** — they are regenerated from `genglsl` (the single source of truth) in CI and locally. Because `MATERIALX_BUILD_GEN_WGSL` now defaults **OFF**, a clean local build of the web viewer is: ```sh # 1. regenerate the WGSL library (genglsl -> genwgsl) -python source/MaterialXGenWgsl/tools/glsl_to_wgsl.py --libraries libraries --out libraries +python source/MaterialXGenWgsl/tools/mxgenwgsl.py --libraries libraries --out libraries # 2. configure with the WGSL backend enabled cmake -S . -B javascript/build \ From 63759341ca327a7ef87c4c4f838dc38ff7278474 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Mon, 20 Jul 2026 22:18:23 -0700 Subject: [PATCH 16/19] Add list of glsl transpile skip list --- .gitignore | 6 +- source/MaterialXGenWgsl/tools/README.md | 38 ++++-- source/MaterialXGenWgsl/tools/mxgenwgsl.py | 127 +++++++++++++----- .../MaterialXGenWgsl/tools/skip_transpile.txt | 43 ++++++ 4 files changed, 165 insertions(+), 49 deletions(-) create mode 100644 source/MaterialXGenWgsl/tools/skip_transpile.txt diff --git a/.gitignore b/.gitignore index 5722236269..7444050b1c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,4 @@ build dist .DS_Store -CMakeUserPresets.json - -/libraries/stdlib/genwgsl/*.wgsl -/libraries/pbrlib/genwgsl/*.wgsl -/libraries/pbrlib/genwgsl/lib/*.wgsl \ No newline at end of file +CMakeUserPresets.json \ No newline at end of file diff --git a/source/MaterialXGenWgsl/tools/README.md b/source/MaterialXGenWgsl/tools/README.md index 9699dc7e79..98cd042ec9 100644 --- a/source/MaterialXGenWgsl/tools/README.md +++ b/source/MaterialXGenWgsl/tools/README.md @@ -7,11 +7,13 @@ hand-ported. It transpiles: * **shader-node fragments** — `libraries/{stdlib,pbrlib,lights}/genglsl/mx_*.glsl` * **`genglsl/lib/` helpers** — `libraries/{stdlib,pbrlib}/genglsl/lib/*.glsl` -into their `genwgsl` equivalents. The generated `.wgsl` files are **derived artifacts** (not -committed): CI regenerates them before building/shipping WGSL, and you regenerate them locally while -working on the WGSL target. Re-running is idempotent — the output is deterministic (no timestamps; -overload stubs indexed by declaration order), so regenerating after a genglsl change just brings -genwgsl back in sync. +into their `genwgsl` equivalents. Only **hand-written** `.wgsl` are committed — the files listed in +[`skip_transpile.txt`](skip_transpile.txt) (the stdlib `lib/` dir, the sampler-bound pbrlib `lib/` +helpers, and the texture/light node shaders naga can't express). Every other `genwgsl/*.wgsl` is a +**derived artifact**: not committed, regenerated by CI before building/shipping WGSL and by you +locally while working on the WGSL target, and removed from an in-place tree with [`--clean`](#usage). +Re-running is idempotent — the output is deterministic (no timestamps; overload stubs indexed by +declaration order), so regenerating after a genglsl change just brings genwgsl back in sync. ## Requirements @@ -35,6 +37,10 @@ python source/MaterialXGenWgsl/tools/mxgenwgsl.py --libraries libraries --out bu # Iterate on specific files (node or lib stems): python source/MaterialXGenWgsl/tools/mxgenwgsl.py --libraries libraries --only mx_noise3d_float mx_sheen_bsdf + +# Delete generated .wgsl from an in-place tree, keeping only the hand-written skip_transpile.txt set +# (needs no naga): +python source/MaterialXGenWgsl/tools/mxgenwgsl.py --libraries libraries --clean ``` `--out libraries` writes each generated file straight into `libraries//genwgsl/`, populating @@ -53,9 +59,17 @@ the run. * **In:** standalone node `.glsl` files referenced by `file=` implementations in stdlib/pbrlib/lights, and the `genglsl/lib/*.glsl` helpers they use. -* **Out (auto-skipped):** texture/image and light nodes — naga's GLSL frontend has no sampler / - dynamic-`LightData` support. Also `mx_chiang_hair_bsdf` (a naga limitation), tracked in - `EXPECTED_FALLBACK`. +* **Out (auto-skipped):** + * *Texture/image nodes* — the genglsl uses the generator tokens `$texSamplerSignature` / + `$texSamplerSampler2D`, substituted by the GLSL backend as one **combined** `sampler2D`. WGSL + needs **separate** `texture_2d` + `sampler` bindings and `textureSample*` calls, and naga's + GLSL frontend won't split the combined sampler — so any signature it emits is wrong. Skipped by + `SKIP_PATTERNS` (`image`, `hextiled`). + * *Light shaders* — take the dynamically-generated `LightData` struct the fragment context can't + supply (`SKIP_PATTERNS` `_light$`). + * *`mx_chiang_hair_bsdf`* (in `EXPECTED_FALLBACK`) — array-valued `out` params + (`mx_hair_alpha_angles`/`mx_hair_attenuation`), `isinf()` in `mx_hair_azimuthal_scattering`, and + an `mx_environment_radiance` (sampler-bound) call in its indirect branch. The result is a *reduced* library by design: generated nodes and `lib/` helpers for everything that resolves cleanly against genglsl, and hand-written `.wgsl` for the rest. @@ -178,9 +192,11 @@ genglsl changes, the pieces you may need to touch are: * **`$`-tokens** — a new `$`-token used in a transpiled lib source needs a naga-parseable stub in `NAGA_LIB_STUBS`; its name must also be declared in `HwConstants.cpp`. `validateTokenCoverage` enforces both. -* **Hand-written libs** — `HANDWRITTEN_LIB_DIRS` (whole dirs kept by project choice) and - `LIB_KEEP_HANDWRITTEN` (individual sampler-bound helpers naga can't express) are skipped by the - lib pass; their nodes are still generated. +* **Hand-written `.wgsl`** — [`skip_transpile.txt`](skip_transpile.txt) lists every hand-maintained + genwgsl file (the stdlib `lib/` dir, the sampler-bound pbrlib `lib/` helpers naga can't express, + and the texture/light node shaders). The lib pass leaves any `lib/*.wgsl` listed there untouched, + and `--clean` keeps exactly this set while deleting everything else. When you add a new hand-written + file, add its repo-relative path here so it survives regeneration and `--clean`. ## Troubleshooting diff --git a/source/MaterialXGenWgsl/tools/mxgenwgsl.py b/source/MaterialXGenWgsl/tools/mxgenwgsl.py index e608a68842..dfce1d08e8 100644 --- a/source/MaterialXGenWgsl/tools/mxgenwgsl.py +++ b/source/MaterialXGenWgsl/tools/mxgenwgsl.py @@ -10,14 +10,17 @@ with no entry point. This tool wraps each target in a naga-parseable GLSL fragment shader, runs `naga` (GLSL -> WGSL), then post-processes naga's SSA-style output into readable genwgsl fragments. -Generated node and lib `.wgsl` files are derived artifacts (not committed; CI regenerates them -before build). Re-running keeps genwgsl in sync with genglsl. +Only hand-written `.wgsl` (the files listed in skip_transpile.txt) are committed; every other genwgsl +`.wgsl` is a derived artifact — not committed, regenerated by CI before build and removed from an +in-place tree with `--clean`. Re-running keeps genwgsl in sync with genglsl. Node transpilation (per mx_*.glsl function): - ONE function body per naga call; lib context is *prototypes + #defines + structs*, not full lib bodies — keeps parameter names clean and avoids `$`-tokens inside lib implementations. - - Skipped: image/hextiled texture nodes and light shaders (naga sampler / LightData limits). - - Expected fallback: mx_chiang_hair_bsdf (naga limitation on hair scattering helpers). + - Skipped: image/hextiled texture nodes (combined GLSL sampler2D has no split WGSL + texture_2d+sampler equivalent) and light shaders (need the dynamic LightData struct). + - Expected fallback: mx_chiang_hair_bsdf (array `out` params, isinf, and a sampler-bound + indirect term — see EXPECTED_FALLBACK). Lib transpilation (`transpileLibs`, runs before nodes): - Each genglsl/lib/*.glsl becomes genwgsl/lib/*.wgsl in topological #include order (22 files). @@ -32,6 +35,7 @@ python mxgenwgsl.py --libraries libraries --out libraries python mxgenwgsl.py --libraries libraries --out build/genwgsl_generated python mxgenwgsl.py --libraries libraries --only mx_conductor_bsdf mx_math + python mxgenwgsl.py --libraries libraries --clean # delete generated .wgsl, keep skip_transpile.txt ''' import argparse @@ -76,33 +80,69 @@ def discoverLibs(libroot): return sorted(p.name for p in libroot.iterdir() if (p / "genglsl").is_dir()) +# Node shaders that can't be transpiled and stay hand-written, matched by file-stem regex. Two +# root causes: +# * Texture nodes -- the genglsl uses the generator tokens $texSamplerSignature/$texSamplerSampler2D +# (see mx_image_*.glsl, mx_hextiledimage.glsl), which the GLSL backend substitutes as ONE combined +# `sampler2D`. WGSL instead needs SEPARATE `texture_2d` + `sampler` params and textureSample* +# calls (cf. the hand-written mx_hextiledimage.wgsl). naga's GLSL frontend models the combined +# sampler and won't split it, so any signature it emits is wrong -- skip rather than mis-emit. +# * Light shaders -- take the dynamically-generated `LightData` struct the fragment context can't +# supply, so they can't be assembled into a standalone naga-parseable shader. SKIP_PATTERNS = [re.compile(p) for p in ( - r"image", # texture nodes: naga's GLSL frontend has no sampler support - r"hextiled", # texture nodes + r"image", # texture nodes: combined sampler2D has no split texture_2d+sampler equivalent + r"hextiled", # texture nodes (same combined-sampler limitation) r"_light$", # light shaders take the dynamically-generated LightData struct )] -# Nodes that are intentionally NOT generated and remain hand-written, by file stem. These hit a -# naga limitation (chiang hair). They are EXPECTED to fail transpile, so they don't constitute a -# build error -- only an *unexpected* failure (a previously-generable node that broke, e.g. after a -# genglsl change) sets a non-zero exit. Keep this in sync with the committed hand-written .wgsl -# files: if one starts transpiling cleanly, the tool warns so it can be removed here. +# Nodes that are intentionally NOT generated and remain hand-written, by file stem. They are +# EXPECTED to fail transpile, so they don't constitute a build error -- only an *unexpected* failure +# (a previously-generable node that broke, e.g. after a genglsl change) sets a non-zero exit. Keep +# this in sync with the committed hand-written .wgsl files: if one starts transpiling cleanly, the +# tool warns so it can be removed here. +# +# mx_chiang_hair_bsdf hits three distinct blockers in its genglsl source: +# 1. array-valued `out` params -- mx_hair_alpha_angles(out vec2 angles[4]) and +# mx_hair_attenuation(out vec3 Ap[4]); naga's GLSL frontend can't lower `out` arrays to a WGSL +# ptr> parameter; +# 2. isinf() in mx_hair_azimuthal_scattering -- no WGSL builtin, and naga doesn't synthesize one; +# 3. the indirect branch calls mx_environment_radiance(), a sampler-bound helper kept hand-written, +# so the post-process arity/unsupported check would reject the node even if 1-2 were solved. EXPECTED_FALLBACK = { "mx_chiang_hair_bsdf", } -# genwgsl/lib helpers kept hand-written rather than transpiled, for two reasons: -# * whole library dirs the project maintains by hand (HANDWRITTEN_LIB_DIRS) -- their lib .wgsl are -# left untouched; nodes in those dirs are still generated; -# * individual helpers whose bindings naga's GLSL frontend cannot express: environment -# radiance/prefilter and shadow-map lookups sample generator-substituted texture+sampler -# `$`-tokens, which naga rejects (same sampler limitation that skips image/light nodes). The -# transpiler stubs the texture ops to parse, but the stub calls would leak into the output. -HANDWRITTEN_LIB_DIRS = {"stdlib"} -LIB_KEEP_HANDWRITTEN = { - "mx_environment_fis", "mx_environment_none", "mx_environment_prefilter", - "mx_generate_albedo_table", "mx_generate_prefilter_env", "mx_shadow", "mx_shadow_platform", -} +# Hand-written genwgsl .wgsl files that must never be transpiled, listed in skip_transpile.txt +# (next to this script). Covers both whole hand-maintained lib dirs and individual sampler-bound +# helpers naga's GLSL frontend cannot express (environment radiance/prefilter, shadow-map lookups), +# plus the SKIP_PATTERNS node shaders so `--clean` retains them. Entries are repo-relative paths; +# keyed here as libroot-relative POSIX (e.g. stdlib/genwgsl/lib/mx_math.wgsl). +DO_NOT_TRANSPILE_FILE = Path(__file__).with_name("skip_transpile.txt") +_doNotTranspile = None + + +def loadDoNotTranspile(): + '''Set of hand-written genwgsl .wgsl files (libroot-relative POSIX paths) that must never be + transpiled or removed by --clean. Parsed once from skip_transpile.txt; a leading + `libraries/` component is stripped so the keys are anchored at --libraries.''' + global _doNotTranspile + if _doNotTranspile is None: + keep = set() + for raw in DO_NOT_TRANSPILE_FILE.read_text(encoding="utf-8").splitlines(): + line = raw.split("#", 1)[0].strip().replace("\\", "/").lstrip("/") + if not line: + continue + if line.startswith("libraries/"): + line = line[len("libraries/"):] + keep.add(line) + _doNotTranspile = keep + return _doNotTranspile + + +def isHandWrittenLib(lib, stem): + '''True if genwgsl/lib/.wgsl for this library is hand-written (in skip_transpile.txt) + and must be left untouched rather than transpiled from its genglsl source.''' + return f"{lib}/genwgsl/lib/{stem}.wgsl" in loadDoNotTranspile() TYPE_FIXUPS = [ (re.compile(r"\bvec([234])"), r"vec\1f"), @@ -1187,20 +1227,18 @@ def validateTokenCoverage(libroot, libs): Two-tier check (replaces a single hand-maintained LIB_TOKEN_FIXUPS list): 1. Token must be declared in HwConstants.cpp (loadHwTokenNames) -- same names the C++ generator substitutes at shader link time. - 2. If the lib file is transpiled (not LIB_KEEP_HANDWRITTEN), token must also have a naga + 2. If the lib file is transpiled (not hand-written), token must also have a naga stub (nagaLibStubs) so expandLibTokens can produce parseable GLSL before naga runs. - Hand-written lib dirs and LIB_KEEP_HANDWRITTEN files are skipped (they are not fed to naga).''' + Hand-written lib files (skip_transpile.txt) are skipped (they are not fed to naga).''' known = loadHwTokenNames(libroot) stubs = set(nagaLibStubs(libroot)) unhandled = {} for lib in libs: - if lib in HANDWRITTEN_LIB_DIRS: - continue gllib = libroot / lib / "genglsl" / "lib" if not gllib.is_dir(): continue for f in sorted(gllib.glob("*.glsl")): - if f.stem in LIB_KEEP_HANDWRITTEN: + if isHandWrittenLib(lib, f.stem): continue txt = f.read_text(encoding="utf-8") for tok in re.findall(r"\$[A-Za-z_]\w*", txt): @@ -1704,16 +1742,13 @@ def transpileLibs(libroot, outroot, libs, base, protos, only=None): libBase, libProtos, libOverloaded = buildContext(libroot, libs, nodes=False) print("\nTranspiling genglsl/lib/ helpers...") for lib in libs: - if lib in HANDWRITTEN_LIB_DIRS: - print(f" KEEP {lib}/genwgsl/lib (hand-written by project choice; not transpiled)") - continue gllib = libroot / lib / "genglsl" / "lib" if not gllib.is_dir(): continue libFiles = topoSortLibFiles(sorted(gllib.glob("*.glsl"))) for glsl in libFiles: - if glsl.stem in LIB_KEEP_HANDWRITTEN: - print(f" KEEP lib/{glsl.name} (hand-written; naga cannot express its sampler bindings)") + if isHandWrittenLib(lib, glsl.stem): + print(f" KEEP lib/{glsl.name} (hand-written; in skip_transpile.txt)") continue if only and glsl.stem not in only: continue @@ -1815,14 +1850,40 @@ def sentinel(m): return True +def cleanGenerated(libroot): + '''Delete every generated genwgsl *.wgsl under --libraries, keeping only the hand-written files + listed in skip_transpile.txt. Restores a hand-written-only library after an in-place + (`--out libraries`) generation run. Needs no naga.''' + libroot = Path(libroot) + keep = loadDoNotTranspile() + removed = kept = 0 + print(f"Cleaning generated genwgsl .wgsl under {libroot} ...") + for wgsl in sorted(libroot.glob("*/genwgsl/**/*.wgsl")): + if wgsl.relative_to(libroot).as_posix() in keep: + kept += 1 + continue + print(f" RM {wgsl.relative_to(libroot).as_posix()}") + wgsl.unlink() + removed += 1 + print(f"Clean done: {removed} generated file(s) removed, {kept} hand-written kept.") + return removed + + def main(): ap = argparse.ArgumentParser(description="Transpile genglsl node fragments to genwgsl.") ap.add_argument("--libraries", default="libraries") ap.add_argument("--out", default="build/genwgsl_generated") ap.add_argument("--only", nargs="*") + ap.add_argument("--clean", action="store_true", + help="Delete generated genwgsl .wgsl under --libraries (keeping the hand-written " + "files in skip_transpile.txt), then exit. Does not run naga.") ap.add_argument("--naga", help="Path to the naga CLI (overrides the NAGA env var / PATH lookup).") args = ap.parse_args() + if args.clean: + cleanGenerated(args.libraries) + return 0 + # Resolve and preflight naga so a missing tool fails once, with guidance, instead of per node. global NAGA NAGA = resolveNaga(args.naga) diff --git a/source/MaterialXGenWgsl/tools/skip_transpile.txt b/source/MaterialXGenWgsl/tools/skip_transpile.txt new file mode 100644 index 0000000000..3e1eb57e9b --- /dev/null +++ b/source/MaterialXGenWgsl/tools/skip_transpile.txt @@ -0,0 +1,43 @@ +# Hand-written genwgsl .wgsl files that must NOT be transpiled from genglsl. +# +# This is the authoritative list of hand-maintained WGSL, used by mxgenwgsl.py for two things: +# * the lib transpile pass skips any genwgsl/lib/*.wgsl listed here (naga can't express them); +# * `mxgenwgsl.py --clean` deletes every generated genwgsl *.wgsl EXCEPT the files listed here. +# +# Paths are relative to the repository root (a leading `libraries/` is optional). Blank lines and +# `#` comments are ignored. Node shaders skipped by SKIP_PATTERNS (image/hextiled/light) are also +# listed here so --clean retains them. + +# --- lights: light shaders take the dynamically-generated LightData struct --- +libraries/lights/genwgsl/mx_directional_light.wgsl +libraries/lights/genwgsl/mx_point_light.wgsl +libraries/lights/genwgsl/mx_spot_light.wgsl + +# --- stdlib nodes: texture nodes (naga's GLSL frontend has no sampler support) --- +libraries/stdlib/genwgsl/mx_hextiledimage.wgsl +libraries/stdlib/genwgsl/mx_hextilednormalmap.wgsl +libraries/stdlib/genwgsl/mx_image_color3.wgsl +libraries/stdlib/genwgsl/mx_image_color4.wgsl +libraries/stdlib/genwgsl/mx_image_float.wgsl +libraries/stdlib/genwgsl/mx_image_vector2.wgsl +libraries/stdlib/genwgsl/mx_image_vector3.wgsl +libraries/stdlib/genwgsl/mx_image_vector4.wgsl + +# --- stdlib lib helpers: whole dir maintained by hand (was HANDWRITTEN_LIB_DIRS = {"stdlib"}) --- +libraries/stdlib/genwgsl/lib/mx_flake.wgsl +libraries/stdlib/genwgsl/lib/mx_geometry.wgsl +libraries/stdlib/genwgsl/lib/mx_hextile.wgsl +libraries/stdlib/genwgsl/lib/mx_hsv.wgsl +libraries/stdlib/genwgsl/lib/mx_math.wgsl +libraries/stdlib/genwgsl/lib/mx_noise.wgsl +libraries/stdlib/genwgsl/lib/mx_transform_uv.wgsl +libraries/stdlib/genwgsl/lib/mx_transform_uv_vflip.wgsl + +# --- pbrlib lib helpers: sampler-bound helpers naga cannot express (was LIB_KEEP_HANDWRITTEN) --- +libraries/pbrlib/genwgsl/lib/mx_environment_fis.wgsl +libraries/pbrlib/genwgsl/lib/mx_environment_none.wgsl +libraries/pbrlib/genwgsl/lib/mx_environment_prefilter.wgsl +libraries/pbrlib/genwgsl/lib/mx_generate_albedo_table.wgsl +libraries/pbrlib/genwgsl/lib/mx_generate_prefilter_env.wgsl +libraries/pbrlib/genwgsl/lib/mx_shadow.wgsl +libraries/pbrlib/genwgsl/lib/mx_shadow_platform.wgsl From 4474257c536d8fde217d11620a5e6fb6ca1a52e6 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Tue, 21 Jul 2026 12:58:22 -0700 Subject: [PATCH 17/19] Introduce a math helper mx_isinf --- .../pbrlib/genglsl/mx_chiang_hair_bsdf.glsl | 2 +- libraries/stdlib/genglsl/lib/mx_math.glsl | 2 + libraries/stdlib/genwgsl/lib/mx_math.wgsl | 4 ++ source/MaterialXGenWgsl/tools/README.md | 8 ++-- source/MaterialXGenWgsl/tools/mxgenwgsl.py | 44 ++++++++++--------- 5 files changed, 35 insertions(+), 25 deletions(-) diff --git a/libraries/pbrlib/genglsl/mx_chiang_hair_bsdf.glsl b/libraries/pbrlib/genglsl/mx_chiang_hair_bsdf.glsl index 31199d82ed..a0412d462c 100644 --- a/libraries/pbrlib/genglsl/mx_chiang_hair_bsdf.glsl +++ b/libraries/pbrlib/genglsl/mx_chiang_hair_bsdf.glsl @@ -145,7 +145,7 @@ float mx_hair_azimuthal_scattering( // Np return float(0.5 / M_PI); float dphi = phi - mx_hair_phi(p, gammaO, gammaT); - if (isinf(dphi)) + if (mx_isinf(dphi)) return float(0.5 / M_PI); while (dphi > M_PI) dphi -= (2.0 * M_PI); diff --git a/libraries/stdlib/genglsl/lib/mx_math.glsl b/libraries/stdlib/genglsl/lib/mx_math.glsl index 623fbcb39e..4537568aed 100644 --- a/libraries/stdlib/genglsl/lib/mx_math.glsl +++ b/libraries/stdlib/genglsl/lib/mx_math.glsl @@ -13,6 +13,8 @@ #define mx_radians radians #define mx_float_bits_to_int floatBitsToInt +bool mx_isinf(float v) { return isinf(v); } + vec2 mx_matrix_mul(vec2 v, mat2 m) { return v * m; } vec3 mx_matrix_mul(vec3 v, mat3 m) { return v * m; } vec4 mx_matrix_mul(vec4 v, mat4 m) { return v * m; } diff --git a/libraries/stdlib/genwgsl/lib/mx_math.wgsl b/libraries/stdlib/genwgsl/lib/mx_math.wgsl index 689081c8ed..db0c561bd4 100644 --- a/libraries/stdlib/genwgsl/lib/mx_math.wgsl +++ b/libraries/stdlib/genwgsl/lib/mx_math.wgsl @@ -31,6 +31,10 @@ fn mx_square_vec3(x: vec3f) -> vec3f { return (x * x); } +fn mx_isinf(v: f32) -> bool { + return abs(v) > 3.40282347e+38; +} + // Modulo with GLSL mod() semantics: x - y * floor(x / y) // WGSL '%' operator is remainder (fmod), not modulo, so we need explicit functions. diff --git a/source/MaterialXGenWgsl/tools/README.md b/source/MaterialXGenWgsl/tools/README.md index 98cd042ec9..cd208eab7b 100644 --- a/source/MaterialXGenWgsl/tools/README.md +++ b/source/MaterialXGenWgsl/tools/README.md @@ -67,9 +67,11 @@ the run. `SKIP_PATTERNS` (`image`, `hextiled`). * *Light shaders* — take the dynamically-generated `LightData` struct the fragment context can't supply (`SKIP_PATTERNS` `_light$`). - * *`mx_chiang_hair_bsdf`* (in `EXPECTED_FALLBACK`) — array-valued `out` params - (`mx_hair_alpha_angles`/`mx_hair_attenuation`), `isinf()` in `mx_hair_azimuthal_scattering`, and - an `mx_environment_radiance` (sampler-bound) call in its indirect branch. + +There are currently no `EXPECTED_FALLBACK` nodes: everything else transpiles. (`mx_chiang_hair_bsdf` +was a fallback until its `isinf()` call was replaced by `mx_isinf` — naga rejects GLSL `isinf` with +`Unsupported relational function: IsInf`; the hand-written `mx_math.wgsl` implements `mx_isinf` as a +finite-magnitude check.) The result is a *reduced* library by design: generated nodes and `lib/` helpers for everything that resolves cleanly against genglsl, and hand-written `.wgsl` for the rest. diff --git a/source/MaterialXGenWgsl/tools/mxgenwgsl.py b/source/MaterialXGenWgsl/tools/mxgenwgsl.py index dfce1d08e8..72c50daf4d 100644 --- a/source/MaterialXGenWgsl/tools/mxgenwgsl.py +++ b/source/MaterialXGenWgsl/tools/mxgenwgsl.py @@ -19,8 +19,8 @@ lib bodies — keeps parameter names clean and avoids `$`-tokens inside lib implementations. - Skipped: image/hextiled texture nodes (combined GLSL sampler2D has no split WGSL texture_2d+sampler equivalent) and light shaders (need the dynamic LightData struct). - - Expected fallback: mx_chiang_hair_bsdf (array `out` params, isinf, and a sampler-bound - indirect term — see EXPECTED_FALLBACK). + - Expected fallbacks: none currently. EXPECTED_FALLBACK stays as the regression guard for future + naga limitations (mx_chiang_hair_bsdf left it once isinf was replaced by mx_isinf). Lib transpilation (`transpileLibs`, runs before nodes): - Each genglsl/lib/*.glsl becomes genwgsl/lib/*.wgsl in topological #include order (22 files). @@ -95,22 +95,13 @@ def discoverLibs(libroot): r"_light$", # light shaders take the dynamically-generated LightData struct )] -# Nodes that are intentionally NOT generated and remain hand-written, by file stem. They are -# EXPECTED to fail transpile, so they don't constitute a build error -- only an *unexpected* failure -# (a previously-generable node that broke, e.g. after a genglsl change) sets a non-zero exit. Keep -# this in sync with the committed hand-written .wgsl files: if one starts transpiling cleanly, the -# tool warns so it can be removed here. -# -# mx_chiang_hair_bsdf hits three distinct blockers in its genglsl source: -# 1. array-valued `out` params -- mx_hair_alpha_angles(out vec2 angles[4]) and -# mx_hair_attenuation(out vec3 Ap[4]); naga's GLSL frontend can't lower `out` arrays to a WGSL -# ptr> parameter; -# 2. isinf() in mx_hair_azimuthal_scattering -- no WGSL builtin, and naga doesn't synthesize one; -# 3. the indirect branch calls mx_environment_radiance(), a sampler-bound helper kept hand-written, -# so the post-process arity/unsupported check would reject the node even if 1-2 were solved. -EXPECTED_FALLBACK = { - "mx_chiang_hair_bsdf", -} +# Nodes expected to fail transpile and stay hand-written, by file stem. A failure listed here is not +# a build error; only an *unexpected* failure (a previously-generable node that broke, e.g. after a +# genglsl change) sets a non-zero exit. If a node here starts transpiling cleanly, the tool warns so +# it can be removed. Currently empty -- every node either transpiles or is skipped outright by +# SKIP_PATTERNS. (mx_chiang_hair_bsdf used to live here: naga rejected its isinf() call until it was +# replaced with mx_isinf -- see libraries/stdlib/genwgsl/lib/mx_math.wgsl.) +EXPECTED_FALLBACK = set() # Hand-written genwgsl .wgsl files that must never be transpiled, listed in skip_transpile.txt # (next to this script). Covers both whole hand-maintained lib dirs and individual sampler-bound @@ -546,10 +537,21 @@ def parseFunctions(text): closeP = _matchParen(text, openP) if closeP < 0: continue - # Skip whitespace after `)`; a definition has `{` here, a call/prototype has `;` or `,`. + # Skip whitespace and comments after `)`; a definition has `{` here, a call/prototype has + # `;` or `,`. Comments (e.g. a trailing `// Ap` after the parameter list) must be stepped + # over too, or the definition is misread as a prototype and dropped. j = closeP + 1 - while j < len(text) and text[j].isspace(): - j += 1 + while j < len(text): + if text[j].isspace(): + j += 1 + elif text.startswith("//", j): + nl = text.find("\n", j) + j = len(text) if nl < 0 else nl + 1 + elif text.startswith("/*", j): + end = text.find("*/", j) + j = len(text) if end < 0 else end + 2 + else: + break if j >= len(text) or text[j] != "{": continue # a call/prototype, not a definition end = _matchBrace(text, j) From c1063746adbf2b7798f3e1761c7a0cf8482d3c77 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Tue, 21 Jul 2026 20:50:23 -0700 Subject: [PATCH 18/19] Fix: mx_isinf --- libraries/stdlib/genwgsl/lib/mx_math.wgsl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/stdlib/genwgsl/lib/mx_math.wgsl b/libraries/stdlib/genwgsl/lib/mx_math.wgsl index db0c561bd4..87af1a1961 100644 --- a/libraries/stdlib/genwgsl/lib/mx_math.wgsl +++ b/libraries/stdlib/genwgsl/lib/mx_math.wgsl @@ -32,7 +32,11 @@ fn mx_square_vec3(x: vec3f) -> vec3f { } fn mx_isinf(v: f32) -> bool { - return abs(v) > 3.40282347e+38; + // WGSL has no isInf. +/-inf is the only bit pattern with all exponent bits set and a zero + // mantissa; masking the sign bit matches both infinities, while NaN (nonzero mantissa) does + // not -- matching GLSL isinf(). A magnitude compare is avoided because the only correct + // threshold is exactly FLT_MAX, and that literal overflows f32 const-eval on some drivers. + return (bitcast(v) & 0x7fffffffu) == 0x7f800000u; } // Modulo with GLSL mod() semantics: x - y * floor(x / y) From c9693a0472d0c99630ffd9585e70532380367404 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Thu, 23 Jul 2026 15:53:32 -0700 Subject: [PATCH 19/19] [fixup!] Make shader cleanup optional mxwgslcleanup uses tree-sitter WGSL grammar parser and only available on python 3.10+ --- .github/workflows/main.yml | 6 +++++- source/MaterialXGenWgsl/tools/README.md | 12 +++++++----- source/MaterialXGenWgsl/tools/mxgenwgsl.py | 9 ++++++++- .../MaterialXGenWgsl/tools/mxwgslcleanup.py | 19 +++++++++++++++++++ .../tools/requirements-transpile.txt | 4 ++++ .../tools/test_mxwgslcleanup.py | 11 +++++++++++ 6 files changed, 54 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f6313a3b9a..414fbcc412 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -247,7 +247,11 @@ jobs: - name: Generate WGSL library if: matrix.python != 'None' run: | - python -m pip install -r source/MaterialXGenWgsl/tools/requirements-transpile.txt + # Cleanup deps (tree-sitter) are optional and have no wheel on some interpreters (e.g. 3.9); + # install best-effort so their absence doesn't fail the build. Generation degrades to + # verbose-but-valid WGSL, and mxgenwgsl.py stays fatal so real failures still surface. + python -m pip install -r source/MaterialXGenWgsl/tools/requirements-transpile.txt \ + || echo "WGSL cleanup deps unavailable; generating verbose WGSL" python source/MaterialXGenWgsl/tools/mxgenwgsl.py --libraries libraries --out libraries - name: Test WGSL cleanup diff --git a/source/MaterialXGenWgsl/tools/README.md b/source/MaterialXGenWgsl/tools/README.md index cd208eab7b..cac3c3b538 100644 --- a/source/MaterialXGenWgsl/tools/README.md +++ b/source/MaterialXGenWgsl/tools/README.md @@ -23,8 +23,9 @@ declaration order), so regenerating after a genglsl change just brings genwgsl b * **Optional: [`tree-sitter-language-pack`](https://pypi.org/project/tree-sitter-language-pack/)**, for the readability cleanup pass (see [WGSL cleanup](#wgsl-cleanup)). It ships a precompiled WGSL grammar (and pulls the `tree-sitter` runtime), so no Node or `tree-sitter-cli` is needed: - `pip install -r source/MaterialXGenWgsl/tools/requirements-transpile.txt`. If it is missing the - transpiler still runs and emits (more verbose) naga output. + `pip install -r source/MaterialXGenWgsl/tools/requirements-transpile.txt`. If it is missing (e.g. + a Python version with no wheel, such as 3.9), the transpiler prints a single `INFO` line and still + runs, emitting (more verbose) naga output; the cleanup test self-skips. CI installs it best-effort. ## Usage @@ -125,9 +126,10 @@ deliberate string surgery, not a real parser — naga's own run is the correctne `mxwgslcleanup.py` parses naga's WGSL output with a tree-sitter WGSL grammar and applies conservative readability edits: collapse param-copy shadows, promote `var`→`let`, flatten else-if chains, unwrap redundant compound blocks, etc. The grammar comes from the pip package -`tree-sitter-language-pack` (precompiled — no Node or `tree-sitter-cli`). If cleanup fails or the -package is unavailable, the transpiler keeps the verbose naga output and prints a warning — it never -blocks generation. +`tree-sitter-language-pack` (precompiled — no Node or `tree-sitter-cli`). It never blocks generation: +if the package is unavailable, the transpiler prints one `INFO` line and keeps the verbose naga +output for every function; if cleanup raises on an individual function while the grammar *is* +present, that function keeps its verbose output with a per-function `WARN`. ### Overload handling diff --git a/source/MaterialXGenWgsl/tools/mxgenwgsl.py b/source/MaterialXGenWgsl/tools/mxgenwgsl.py index 72c50daf4d..5895b55e4c 100644 --- a/source/MaterialXGenWgsl/tools/mxgenwgsl.py +++ b/source/MaterialXGenWgsl/tools/mxgenwgsl.py @@ -47,7 +47,7 @@ import tempfile from pathlib import Path -from mxwgslcleanup import cleanupFunction, fnBase +from mxwgslcleanup import cleanupAvailable, cleanupFunction, fnBase # The naga CLI (GLSL->WGSL). Resolved in main() via resolveNaga(): --naga arg, then $NAGA, then a # `naga` on PATH; main() overwrites this global before any transpilation runs. @@ -1898,6 +1898,13 @@ def main(): return 2 print(f"Using naga: {NAGA} ({version})") + # tree-sitter (readability cleanup) is optional; announce once if absent so the verbose output + # isn't mistaken for a bug. Generation itself does not need it. + if not cleanupAvailable(): + print("INFO: tree-sitter-language-pack not installed; skipping WGSL readability cleanup " + "(output is valid WGSL but verbose). Install: pip install -r " + "source/MaterialXGenWgsl/tools/requirements-transpile.txt") + libroot, outroot = Path(args.libraries), Path(args.out) # Preamble/token helpers resolve repo paths relative to libroot; must run before transpilation. setActiveLibroot(libroot) diff --git a/source/MaterialXGenWgsl/tools/mxwgslcleanup.py b/source/MaterialXGenWgsl/tools/mxwgslcleanup.py index 970666e9c7..9bb2ad1cb0 100644 --- a/source/MaterialXGenWgsl/tools/mxwgslcleanup.py +++ b/source/MaterialXGenWgsl/tools/mxwgslcleanup.py @@ -28,6 +28,23 @@ def _parser(): return _PARSER +_CLEANUP_AVAILABLE = None + + +def cleanupAvailable(): + '''Whether the tree-sitter WGSL grammar can be loaded (probed once, cached). When False the + readability cleanup is skipped and callers keep the raw -- valid but verbose -- naga output. + Lets generation run on interpreters with no tree-sitter-language-pack wheel (e.g. Python 3.9).''' + global _CLEANUP_AVAILABLE + if _CLEANUP_AVAILABLE is None: + try: + _parser() + _CLEANUP_AVAILABLE = True + except Exception: + _CLEANUP_AVAILABLE = False + return _CLEANUP_AVAILABLE + + def _nodeText(node, src): return src[node.start_byte:node.end_byte].decode("utf-8") @@ -741,6 +758,8 @@ def inlineSingleUseTemps(body): def cleanupFunction(fnText, glslParamNames=None): '''Run readability cleanup on one transpiled WGSL function.''' + if not cleanupAvailable(): + return fnText # no tree-sitter grammar: keep raw naga output (announced once by the caller) try: # Inline naga temps first so param-mutate patterns like `N_25 = -(_e39)` become # `N_25 = -(N_25)` before tree-sitter param-shadow analysis runs. diff --git a/source/MaterialXGenWgsl/tools/requirements-transpile.txt b/source/MaterialXGenWgsl/tools/requirements-transpile.txt index a59777d0dc..1fd72ab8b1 100644 --- a/source/MaterialXGenWgsl/tools/requirements-transpile.txt +++ b/source/MaterialXGenWgsl/tools/requirements-transpile.txt @@ -1,4 +1,8 @@ # Deps for WGSL post-process cleanup (mxwgslcleanup.py). # tree-sitter-language-pack ships a precompiled WGSL grammar (and pulls the tree-sitter # runtime), so no Node/tree-sitter-cli or vendored grammar is needed. +# +# OPTIONAL: only the readability cleanup pass needs this. Generation runs without it (emitting +# verbose but valid WGSL), so CI installs it best-effort -- interpreters with no wheel (e.g. some +# early/pre-release Python versions) still generate the library. tree-sitter-language-pack>=1.13,<2 diff --git a/source/MaterialXGenWgsl/tools/test_mxwgslcleanup.py b/source/MaterialXGenWgsl/tools/test_mxwgslcleanup.py index 2c2034f994..eccb5995e8 100644 --- a/source/MaterialXGenWgsl/tools/test_mxwgslcleanup.py +++ b/source/MaterialXGenWgsl/tools/test_mxwgslcleanup.py @@ -13,6 +13,14 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import mxwgslcleanup as m +# These tests need the tree-sitter WGSL grammar. On interpreters with no tree-sitter-language-pack +# wheel (e.g. Python 3.9) skip rather than fail. Only skip at collection when actually running under +# pytest (`pytest` already imported) -- a plain `python test_mxwgslcleanup.py` run must not call +# pytest.skip() outside a session; that path is handled by the __main__ guard below. +if not m.cleanupAvailable() and "pytest" in sys.modules: + import pytest + pytest.skip("tree-sitter-language-pack unavailable", allow_module_level=True) + # --- differential semantic invariant ---------------------------------------- @@ -214,6 +222,9 @@ def test_cleanup_is_idempotent(): if __name__ == "__main__": + if not m.cleanupAvailable(): + print("SKIP test_mxwgslcleanup: tree-sitter-language-pack unavailable") + sys.exit(0) failed = 0 for fn in sorted(k for k in dict(globals()) if k.startswith("test_")): try: