From 9baaf05c52fd3104665c058c8af1d08e0511e17c Mon Sep 17 00:00:00 2001 From: Louis Deconinck Date: Thu, 10 Sep 2026 19:37:14 +0200 Subject: [PATCH 1/4] Fix OpenPBR thin-walled subsurface color scaling Signed-off-by: Louis Deconinck Assisted-by: Codex --- libraries/bxdf/open_pbr_surface.mtlx | 4 ++-- .../MaterialXGenShader/GenShader.cpp | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/libraries/bxdf/open_pbr_surface.mtlx b/libraries/bxdf/open_pbr_surface.mtlx index f1a757470b..5097feafb9 100644 --- a/libraries/bxdf/open_pbr_surface.mtlx +++ b/libraries/bxdf/open_pbr_surface.mtlx @@ -147,7 +147,7 @@ - + @@ -163,7 +163,7 @@ - + diff --git a/source/MaterialXTest/MaterialXGenShader/GenShader.cpp b/source/MaterialXTest/MaterialXGenShader/GenShader.cpp index f95a7300b7..c54664e605 100644 --- a/source/MaterialXTest/MaterialXGenShader/GenShader.cpp +++ b/source/MaterialXTest/MaterialXGenShader/GenShader.cpp @@ -139,6 +139,28 @@ TEST_CASE("GenShader: Duplicate Output Color Transforms", "[genshader]") } #endif +TEST_CASE("GenShader: OpenPBR Thin-Walled Subsurface", "[genshader]") +{ + mx::FileSearchPath searchPath = mx::getDefaultDataSearchPath(); + mx::DocumentPtr libraries = mx::createDocument(); + mx::loadLibraries({ "libraries" }, searchPath, libraries); + + mx::NodeDefPtr nodeDef = libraries->getNodeDef("ND_open_pbr_surface_surfaceshader"); + REQUIRE(nodeDef); + mx::NodeGraphPtr graph = nodeDef->getImplementation()->asA(); + REQUIRE(graph); + + for (const std::string& name : { "subsurface_thin_walled_brdf_factor", "subsurface_thin_walled_btdf_factor" }) + { + mx::NodePtr factor = graph->getNode(name); + REQUIRE(factor); + mx::InputPtr color = factor->getInput("in1"); + REQUIRE(color); + REQUIRE(color->getInterfaceName().empty()); + REQUIRE(color->getValueString() == "1.0, 1.0, 1.0"); + } +} + TEST_CASE("GenShader: TypeDesc Check", "[genshader]") { mx::TypeSystemPtr ts = mx::TypeSystem::create(); From 47b8e60ceb5f131676d125529239179add099c66 Mon Sep 17 00:00:00 2001 From: Louis Deconinck Date: Thu, 10 Sep 2026 19:50:46 +0200 Subject: [PATCH 2/4] test: Address GCC warning in OpenPBR regression test Signed-off-by: Louis Deconinck Assisted-by: Codex --- source/MaterialXTest/MaterialXGenShader/GenShader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/MaterialXTest/MaterialXGenShader/GenShader.cpp b/source/MaterialXTest/MaterialXGenShader/GenShader.cpp index c54664e605..cc786811fd 100644 --- a/source/MaterialXTest/MaterialXGenShader/GenShader.cpp +++ b/source/MaterialXTest/MaterialXGenShader/GenShader.cpp @@ -150,7 +150,7 @@ TEST_CASE("GenShader: OpenPBR Thin-Walled Subsurface", "[genshader]") mx::NodeGraphPtr graph = nodeDef->getImplementation()->asA(); REQUIRE(graph); - for (const std::string& name : { "subsurface_thin_walled_brdf_factor", "subsurface_thin_walled_btdf_factor" }) + for (const char* name : { "subsurface_thin_walled_brdf_factor", "subsurface_thin_walled_btdf_factor" }) { mx::NodePtr factor = graph->getNode(name); REQUIRE(factor); From 59ca0806663a91c06b36e5d229ae621645f28387 Mon Sep 17 00:00:00 2001 From: Louis Deconinck Date: Fri, 18 Sep 2026 08:38:46 +0200 Subject: [PATCH 3/4] Express thin-walled subsurface as a single mix of the two lobes The 0.5*(1-g) and 0.5*(1+g) weights sum to one, so the OpenPBR equations are exactly a mix of the transmission and reflection lobes. This keeps every weight within [0, 1] across the full anisotropy range, avoids the clamp in the hardware multiply implementations, applies subsurface_color once through the lobe color inputs, and reduces this section of the graph from ten nodes to six. Also updates the functional notation in the PBR spec and replaces the literal-value unit test with a render test material exercising thin-walled subsurface across all shading languages. --- documents/Specification/MaterialX.PBRSpec.md | 13 +++---- libraries/bxdf/open_pbr_surface.mtlx | 36 ++++++------------- .../open_pbr_thin_walled_subsurface.mtlx | 12 +++++++ .../MaterialXGenShader/GenShader.cpp | 22 ------------ 4 files changed, 29 insertions(+), 54 deletions(-) create mode 100644 resources/Materials/TestSuite/pbrlib/surfaceshader/open_pbr_thin_walled_subsurface.mtlx diff --git a/documents/Specification/MaterialX.PBRSpec.md b/documents/Specification/MaterialX.PBRSpec.md index a186cae990..ca5857308e 100644 --- a/documents/Specification/MaterialX.PBRSpec.md +++ b/documents/Specification/MaterialX.PBRSpec.md @@ -2005,15 +2005,16 @@ surfaceshader open_pbr_surface( // Coat facing reflectance (F0), reused by the coat-darkening and emission terms below. float coat_F0 = ior_to_f0(coat_ior); - // Thin-walled subsurface: a translucent reflection/transmission pair scaled by the subsurface + // Thin-walled subsurface: a translucent reflection/transmission pair tinted by the subsurface // color, with 'subsurface_scatter_anisotropy' shifting weight from reflection toward - // transmission, blended in equal measure. + // transmission. Since the two lobe weights 0.5 * (1 - g) and 0.5 * (1 + g) sum to one, the + // OpenPBR equations are exactly a mix, which also keeps every weight within [0, 1]. color3 ss_color = max(subsurface_color, 0.0); BSDF ss_reflection = oren_nayar_diffuse_bsdf(color = ss_color, roughness = base_diffuse_roughness, - normal = geometry_normal) * (subsurface_color * (1.0 - subsurface_scatter_anisotropy)); - BSDF ss_transmission = translucent_bsdf(color = ss_color, normal = geometry_normal) - * (subsurface_color * (1.0 + subsurface_scatter_anisotropy)); - BSDF subsurface_thin_walled = mix(ss_reflection, ss_transmission, 0.5); + normal = geometry_normal); + BSDF ss_transmission = translucent_bsdf(color = ss_color, normal = geometry_normal); + BSDF subsurface_thin_walled = mix(ss_transmission, ss_reflection, + 0.5 * (1.0 + subsurface_scatter_anisotropy)); // Closed (non-thin-walled) subsurface: volumetric scattering over the scaled radius. BSDF subsurface_volume = subsurface_bsdf(color = ss_color, diff --git a/libraries/bxdf/open_pbr_surface.mtlx b/libraries/bxdf/open_pbr_surface.mtlx index 5097feafb9..72828f25a3 100644 --- a/libraries/bxdf/open_pbr_surface.mtlx +++ b/libraries/bxdf/open_pbr_surface.mtlx @@ -142,38 +142,22 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - + + + diff --git a/resources/Materials/TestSuite/pbrlib/surfaceshader/open_pbr_thin_walled_subsurface.mtlx b/resources/Materials/TestSuite/pbrlib/surfaceshader/open_pbr_thin_walled_subsurface.mtlx new file mode 100644 index 0000000000..759f205a5d --- /dev/null +++ b/resources/Materials/TestSuite/pbrlib/surfaceshader/open_pbr_thin_walled_subsurface.mtlx @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/source/MaterialXTest/MaterialXGenShader/GenShader.cpp b/source/MaterialXTest/MaterialXGenShader/GenShader.cpp index cc786811fd..f95a7300b7 100644 --- a/source/MaterialXTest/MaterialXGenShader/GenShader.cpp +++ b/source/MaterialXTest/MaterialXGenShader/GenShader.cpp @@ -139,28 +139,6 @@ TEST_CASE("GenShader: Duplicate Output Color Transforms", "[genshader]") } #endif -TEST_CASE("GenShader: OpenPBR Thin-Walled Subsurface", "[genshader]") -{ - mx::FileSearchPath searchPath = mx::getDefaultDataSearchPath(); - mx::DocumentPtr libraries = mx::createDocument(); - mx::loadLibraries({ "libraries" }, searchPath, libraries); - - mx::NodeDefPtr nodeDef = libraries->getNodeDef("ND_open_pbr_surface_surfaceshader"); - REQUIRE(nodeDef); - mx::NodeGraphPtr graph = nodeDef->getImplementation()->asA(); - REQUIRE(graph); - - for (const char* name : { "subsurface_thin_walled_brdf_factor", "subsurface_thin_walled_btdf_factor" }) - { - mx::NodePtr factor = graph->getNode(name); - REQUIRE(factor); - mx::InputPtr color = factor->getInput("in1"); - REQUIRE(color); - REQUIRE(color->getInterfaceName().empty()); - REQUIRE(color->getValueString() == "1.0, 1.0, 1.0"); - } -} - TEST_CASE("GenShader: TypeDesc Check", "[genshader]") { mx::TypeSystemPtr ts = mx::TypeSystem::create(); From c5aac9ba22d92bc9080fa032b8bb02d6406002fa Mon Sep 17 00:00:00 2001 From: Jonathan Stone Date: Fri, 18 Sep 2026 14:55:55 -0700 Subject: [PATCH 4/4] Minor adjustment of subsurface note in functional definition This changelist makes a minor adjustment to the subsurface note in the functional definition of OpenPBR, describing the lobes and their albedo directly, rather than restating the derivation of the mix. --- documents/Specification/MaterialX.PBRSpec.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/documents/Specification/MaterialX.PBRSpec.md b/documents/Specification/MaterialX.PBRSpec.md index 6982bbc9a7..f9ad782dc1 100644 --- a/documents/Specification/MaterialX.PBRSpec.md +++ b/documents/Specification/MaterialX.PBRSpec.md @@ -2005,10 +2005,9 @@ surfaceshader open_pbr_surface( // Coat facing reflectance (F0), reused by the coat-darkening and emission terms below. float coat_F0 = ior_to_f0(coat_ior); - // Thin-walled subsurface: a translucent reflection/transmission pair tinted by the subsurface - // color, with 'subsurface_scatter_anisotropy' shifting weight from reflection toward - // transmission. Since the two lobe weights 0.5 * (1 - g) and 0.5 * (1 + g) sum to one, the - // OpenPBR equations are exactly a mix, which also keeps every weight within [0, 1]. + // Thin-walled subsurface: diffuse reflection and transmission lobes that share the subsurface + // color as their albedo, with 'subsurface_scatter_anisotropy' shifting weight from reflection + // toward transmission. The two weights sum to one, so the total albedo is the subsurface color. color3 ss_color = max(subsurface_color, 0.0); BSDF ss_reflection = oren_nayar_diffuse_bsdf(color = ss_color, roughness = base_diffuse_roughness, normal = geometry_normal);