From 5a722b1ef039bdabbfa5a4c0aefc5187dba2f408 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Wed, 12 Aug 2026 12:27:44 +0100 Subject: [PATCH 01/11] CatalogueTest : Fix wait for Catalogue save This fixes errors like the following : ``` Error: FAIL: testDisplayDriverAndPromotion (GafferSceneTest.CatalogueTest.CatalogueTest.testDisplayDriverAndPromotion) ---------------------------------------------------------------------- Traceback (most recent call last): File "/__w/gaffer/gaffer/build/python/GafferSceneTest/CatalogueTest.py", line 387, in testDisplayDriverAndPromotion self.sendImage( r["out"], s["b"]["c"] ) File "/__w/gaffer/gaffer/build/python/GafferSceneTest/CatalogueTest.py", line 61, in sendImage result = GafferSceneTest.DisplayTest.Driver.sendImage( image, GafferScene.Catalogue.displayDriverServer().portNumber(), extraParameters, close = close ) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/__w/gaffer/gaffer/build/python/GafferSceneTest/DisplayTest.py", line 150, in sendImage driver.close() File "/__w/gaffer/gaffer/build/python/GafferSceneTest/DisplayTest.py", line 115, in close with GafferTest.ParallelAlgoTest.UIThreadCallHandler() as h : File "/__w/gaffer/gaffer/build/python/GafferTest/ParallelAlgoTest.py", line 70, in __exit__ raise AssertionError( "UIThread call queue not empty" ) AssertionError: UIThread call queue not empty ``` We have only seen these on CI, but they can be reproduced artificially by inserting `time.sleep()` before L119 in DisplayTest. The problem is that with the right thread timings, the Catalogue can save the image to disk and request an extra UI thread call before `close()` calls `assertDone()`. The `sleep()` just makes the bad timing inevitable, but it could occur naturally when the CI machine is under unusual load. The solution is to move the `close()` call under the same UIThreadCallHandler as the one used to check for saving, and only call `assertDone()` once at the end. The weaving of `DisplayTest.sendImage()` and `CatalogueTest.sendImage()` with all their various permutations is getting a bit much. It does seem tempting to attempt an approach based on `assertEventually()` instead. --- python/GafferSceneTest/CatalogueTest.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/python/GafferSceneTest/CatalogueTest.py b/python/GafferSceneTest/CatalogueTest.py index df112b3b7d..963280ddd7 100644 --- a/python/GafferSceneTest/CatalogueTest.py +++ b/python/GafferSceneTest/CatalogueTest.py @@ -57,13 +57,24 @@ class CatalogueTest( GafferImageTest.ImageTestCase ) : @staticmethod def sendImage( image, catalogue, extraParameters = {}, waitForSave = True, close = True ) : - with GafferTest.ParallelAlgoTest.UIThreadCallHandler() as h : - result = GafferSceneTest.DisplayTest.Driver.sendImage( image, GafferScene.Catalogue.displayDriverServer().portNumber(), extraParameters, close = close ) - if catalogue["directory"].getValue() and waitForSave : - # When the image has been received, the Catalogue will - # save it to disk on a background thread, and we need - # to wait for that to complete. + result = GafferSceneTest.DisplayTest.Driver.sendImage( image, GafferScene.Catalogue.displayDriverServer().portNumber(), extraParameters, close = False ) + + if close : + + # We always do the closing ourselves, because we need to manage + # the UI thread call triggered by the Catalogue saving the + # image. + with GafferTest.ParallelAlgoTest.UIThreadCallHandler() as h : + + result.close( withCallHandler = False ) h.assertCalled() + + if catalogue["directory"].getValue() and waitForSave : + # When the image has been received, the Catalogue will + # save it to disk on a background thread, and we need + # to wait for that to complete. + h.assertCalled() + h.assertDone() return result From 5c9bb025f05f9373e25ce44403785d1839a9bc7d Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Mon, 18 May 2026 17:07:25 -0400 Subject: [PATCH 02/11] USDMeshLight : Add node to make light from mesh --- Changes.md | 1 + include/GafferUSD/TypeIds.h | 1 + include/GafferUSD/USDMeshLight.h | 61 +++++++++++++++++ python/GafferSceneUI/MeshLightUI.py | 1 + python/GafferUSDTest/USDMeshLightTest.py | 87 ++++++++++++++++++++++++ python/GafferUSDTest/USDShaderTest.py | 11 +++ python/GafferUSDTest/__init__.py | 1 + python/GafferUSDUI/USDMeshLightUI.py | 71 +++++++++++++++++++ python/GafferUSDUI/USDShaderUI.py | 7 +- python/GafferUSDUI/__init__.py | 1 + src/GafferUSD/USDMeshLight.cpp | 55 +++++++++++++++ src/GafferUSD/USDShader.cpp | 19 ++++-- src/GafferUSDModule/GafferUSDModule.cpp | 2 + startup/GafferScene/usdLights.py | 4 +- startup/gui/menus.py | 1 + 15 files changed, 316 insertions(+), 7 deletions(-) create mode 100644 include/GafferUSD/USDMeshLight.h create mode 100644 python/GafferUSDTest/USDMeshLightTest.py create mode 100644 python/GafferUSDUI/USDMeshLightUI.py create mode 100644 src/GafferUSD/USDMeshLight.cpp diff --git a/Changes.md b/Changes.md index c80a765b77..fedee8a328 100644 --- a/Changes.md +++ b/Changes.md @@ -9,6 +9,7 @@ Features - Prototypes and points that are editable downstream. - Export to USD. - Faster rendering. +- USDMeshLight : Added node to add necessary attributes to geometry to convert to a USDMeshLight. Fixes ----- diff --git a/include/GafferUSD/TypeIds.h b/include/GafferUSD/TypeIds.h index 995524d4be..c5f7e74305 100644 --- a/include/GafferUSD/TypeIds.h +++ b/include/GafferUSD/TypeIds.h @@ -46,6 +46,7 @@ enum TypeId USDAttributesTypeId = 119101, USDShaderTypeId = 119102, USDLightTypeId = 119103, + USDMeshLightTypeId = 119104, LastTypeId = 119199 diff --git a/include/GafferUSD/USDMeshLight.h b/include/GafferUSD/USDMeshLight.h new file mode 100644 index 0000000000..3899d86fe7 --- /dev/null +++ b/include/GafferUSD/USDMeshLight.h @@ -0,0 +1,61 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2026, Cinesite VFX Ltd. 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 John Haddon nor the names of +// any other contributors to this software 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. +// +////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "GafferUSD/Export.h" +#include "GafferUSD/TypeIds.h" + +#include "GafferScene/MeshLight.h" + +namespace GafferUSD +{ + +class GAFFERUSD_API USDMeshLight : public GafferScene::MeshLight +{ + + public : + + explicit USDMeshLight( const std::string &name=defaultName() ); + ~USDMeshLight() override; + + GAFFER_NODE_DECLARE_TYPE( GafferUSD::USDMeshLight, USDMeshLightTypeId, GafferScene::MeshLight ); + +}; + +IE_CORE_DECLAREPTR( USDMeshLight ) + +} // namespace GafferUSD diff --git a/python/GafferSceneUI/MeshLightUI.py b/python/GafferSceneUI/MeshLightUI.py index 4ce839428e..4d554693e2 100644 --- a/python/GafferSceneUI/MeshLightUI.py +++ b/python/GafferSceneUI/MeshLightUI.py @@ -79,6 +79,7 @@ def __shaderMetadata( plug, name ) : "presetNames" : functools.partial( __shaderMetadata, name = "presetNames" ), "presetValues" : functools.partial( __shaderMetadata, name = "presetValues" ), "layout:section" : functools.partial( __shaderMetadata, name = "layout:section" ), + "layout:index" : functools.partial( __shaderMetadata, name = "layout:index" ), }, diff --git a/python/GafferUSDTest/USDMeshLightTest.py b/python/GafferUSDTest/USDMeshLightTest.py new file mode 100644 index 0000000000..390b122b54 --- /dev/null +++ b/python/GafferUSDTest/USDMeshLightTest.py @@ -0,0 +1,87 @@ +########################################################################## +# +# Copyright (c) 2026, Cinesite VFX Ltd. 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 John Haddon nor the names of +# any other contributors to this software 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. +# +########################################################################## + +import unittest + +import IECore + +import Gaffer +import GafferScene +import GafferSceneTest +import GafferUSD + +class USDMeshLightTest( GafferSceneTest.SceneTestCase ) : + + def testParameters( self ) : + + light = GafferUSD.USDMeshLight() + + # Should have all the parameters of a MeshLight shader. + + shader = GafferUSD.USDShader() + shader.loadShader( "MeshLight" ) + self.assertEqual( light["parameters"].keys(), shader["parameters"].keys() ) + + # Parameters should drive a light shader in the scene. + + sphere = GafferScene.Sphere() + sphereFilter = GafferScene.PathFilter() + sphereFilter["paths"].setValue( IECore.StringVectorData( [ "/sphere" ] ) ) + light["in"].setInput( sphere["out"] ) + light["filter"].setInput( sphereFilter["out"] ) + + light["parameters"]["exposure"].setValue( 10 ) + self.assertIn( "light", light["out"].attributes( "/sphere" ) ) + self.assertEqual( light["out"].attributes( "/sphere" )["light"].outputShader().parameters["exposure"], IECore.FloatData( 10 ) ) + + def testSerialisation( self ) : + + script = Gaffer.ScriptNode() + script["light"] = GafferUSD.USDMeshLight() + script["light"]["parameters"]["intensity"].setValue( 10 ) + + serialisation = script.serialise() + + script2 = Gaffer.ScriptNode() + script2.execute( serialisation ) + self.assertEqual( script2["light"]["parameters"]["intensity"].getValue(), 10 ) + + # One for the node. None for plugs, since they are not dynamic. + self.assertEqual( serialisation.count( "addChild" ), 1 ) + + +if __name__ == "__main__" : + unittest.main() diff --git a/python/GafferUSDTest/USDShaderTest.py b/python/GafferUSDTest/USDShaderTest.py index 50a200e5c6..cd7bd6e7c3 100644 --- a/python/GafferUSDTest/USDShaderTest.py +++ b/python/GafferUSDTest/USDShaderTest.py @@ -265,3 +265,14 @@ def testUsdPreviewSurfaceAssignment( self ) : shaderAssignment["shader"].setInput( shader["out"]["displacement"] ) self.assertEqual( shaderAssignment["out"].attributes( "/sphere" ).keys(), [ "displacement" ] ) self.assertIsInstance( shaderAssignment["out"].attributes( "/sphere" )["displacement"], IECoreScene.ShaderNetwork ) + + def testMeshLight( self ) : + + shader = GafferUSD.USDShader() + shader.loadShader( "MeshLight" ) + + self.assertEqual( shader["name"].getValue(), "MeshLight" ) + self.assertEqual( shader["type"].getValue(), "light" ) + + self.assertTrue( "exposure" in shader["parameters"] ) + self.assertEqual( shader["out"].typeId(), Gaffer.Plug.staticTypeId() ) diff --git a/python/GafferUSDTest/__init__.py b/python/GafferUSDTest/__init__.py index 8da28b2286..faabf7b36b 100644 --- a/python/GafferUSDTest/__init__.py +++ b/python/GafferUSDTest/__init__.py @@ -39,5 +39,6 @@ from .USDLayerWriterTest import USDLayerWriterTest from .USDShaderTest import USDShaderTest from .USDLightTest import USDLightTest +from .USDMeshLightTest import USDMeshLightTest from ._PointInstancerAdaptorTest import _PointInstancerAdaptorTest from .PromotePointInstancesTest import PromotePointInstancesTest diff --git a/python/GafferUSDUI/USDMeshLightUI.py b/python/GafferUSDUI/USDMeshLightUI.py new file mode 100644 index 0000000000..b5bb669c0f --- /dev/null +++ b/python/GafferUSDUI/USDMeshLightUI.py @@ -0,0 +1,71 @@ +########################################################################## +# +# Copyright (c) 2026, Cinesite VFX Ltd. 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 John Haddon nor the names of +# any other contributors to this software 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. +# +########################################################################## + +import Gaffer +import GafferUSD + +Gaffer.Metadata.registerNode( + + GafferUSD.USDMeshLight, + + "description", + """ + Turns mesh primitives into USD mesh lights by assigning a MeshLight + shader and adding the meshes to the default lights set. + """, + + plugs = { + + "parameters" : { + + "layout:section:Basic:collapsed" : False, + + }, + + "parameters.*" : { + + # USD light parameters don't accept connections. `MeshLightUI` forwards + # metadata requests to the internal shader, which means `USDShaderUI` + # is supplying metadata for `USDMeshLight`. The USD schemas used there + # don't supply connectability metadata, so we force nodules to be removed + # here. ( For `USDLight`, this is handled in `LightUI` ). + "nodule:type" : "", + + }, + + } + +) diff --git a/python/GafferUSDUI/USDShaderUI.py b/python/GafferUSDUI/USDShaderUI.py index 70dcb553d1..1f4af29b13 100644 --- a/python/GafferUSDUI/USDShaderUI.py +++ b/python/GafferUSDUI/USDShaderUI.py @@ -66,7 +66,11 @@ def __primProperty( plug ) : elif plugName.startswith( "shadow:" ) : primDefinition = Usd.SchemaRegistry().FindAppliedAPIPrimDefinition( "ShadowAPI" ) else : - primDefinition = Usd.SchemaRegistry().FindConcretePrimDefinition( __shaderName( plug ) ) + shaderName = __shaderName( plug ) + if shaderName == "MeshLight" : + primDefinition = Usd.SchemaRegistry().FindAppliedAPIPrimDefinition( "MeshLightAPI" ) + else : + primDefinition = Usd.SchemaRegistry().FindConcretePrimDefinition( shaderName ) if primDefinition : return primDefinition.GetPropertyDefinition( "inputs:" + plug.getName() ) @@ -320,6 +324,7 @@ def __orderDict( names ) : "RectLight" : __orderDict( __lightPropertyOrder + [ "width", "height", "texture:file" ] + __apiPropertyOrder ), "SphereLight" : __orderDict( __lightPropertyOrder + [ "radius" ] + __apiPropertyOrder ), "CylinderLight" : __orderDict( __lightPropertyOrder + [ "length", "radius" ] + __apiPropertyOrder ), + "MeshLight" : __orderDict( __lightPropertyOrder + __apiPropertyOrder ), } diff --git a/python/GafferUSDUI/__init__.py b/python/GafferUSDUI/__init__.py index 363c4f92cf..de46299790 100644 --- a/python/GafferUSDUI/__init__.py +++ b/python/GafferUSDUI/__init__.py @@ -38,6 +38,7 @@ from . import USDLayerWriterUI from . import USDShaderUI from . import USDLightUI +from . import USDMeshLightUI from . import _PointInstancerAdaptorUI from . import PromotePointInstancesUI diff --git a/src/GafferUSD/USDMeshLight.cpp b/src/GafferUSD/USDMeshLight.cpp new file mode 100644 index 0000000000..f5a806a59d --- /dev/null +++ b/src/GafferUSD/USDMeshLight.cpp @@ -0,0 +1,55 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2026, Cinesite VFX Ltd. 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 John Haddon nor the names of +// any other contributors to this software 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. +// +////////////////////////////////////////////////////////////////////////// + +#include "GafferUSD/USDMeshLight.h" +#include "GafferUSD/USDShader.h" + +using namespace GafferUSD; + +GAFFER_NODE_DEFINE_TYPE( USDMeshLight ); + +USDMeshLight::USDMeshLight( const std::string &name ) + : GafferScene::MeshLight( + name, + [] { USDShaderPtr shader = new USDShader; shader->loadShader( "MeshLight" ); return shader; }() + ) +{ + +} + +USDMeshLight::~USDMeshLight() +{ +} diff --git a/src/GafferUSD/USDShader.cpp b/src/GafferUSD/USDShader.cpp index a762f03e17..df60bafa63 100644 --- a/src/GafferUSD/USDShader.cpp +++ b/src/GafferUSD/USDShader.cpp @@ -57,6 +57,7 @@ #include "pxr/usd/usd/schemaRegistry.h" #include "pxr/usd/usdLux/boundableLightBase.h" #include "pxr/usd/usdLux/nonboundableLightBase.h" +#include "pxr/usd/usdLux/meshLightAPI.h" #include "boost/algorithm/string/predicate.hpp" @@ -351,12 +352,17 @@ void USDShader::loadShader( const std::string &shaderName, bool keepExistingValu // for renderer-specific light extensions. std::string shaderType = "surface"; - const TfToken shaderNameToken( shaderName ); + const TfToken apiNameToken( shaderName != "MeshLight" ? shaderName : "MeshLightAPI" ); UsdSchemaRegistry &schemaRegistry = UsdSchemaRegistry::GetInstance(); std::vector primDefinitions; std::vector autoAppliedPropertyNames; - if( auto primDefinition = schemaRegistry.FindConcretePrimDefinition( shaderNameToken ) ) + + auto primDefinition = apiNameToken == TfToken( "MeshLightAPI" ) ? + schemaRegistry.FindAppliedAPIPrimDefinition( TfToken( "MeshLightAPI" ) ) : + schemaRegistry.FindConcretePrimDefinition( apiNameToken ) + ; + if( primDefinition ) { primDefinitions.push_back( primDefinition ); // The main prim definition contains properties from auto-applied API schemas, but doesn't @@ -364,7 +370,7 @@ void USDShader::loadShader( const std::string &shaderName, bool keepExistingValu // represent them using OptionalValuePlugs. for( const auto &[apiSchema, autoAppliedTo] : schemaRegistry.GetAutoApplyAPISchemas() ) { - if( std::find( autoAppliedTo.begin(), autoAppliedTo.end(), shaderNameToken ) != autoAppliedTo.end() ) + if( std::find( autoAppliedTo.begin(), autoAppliedTo.end(), apiNameToken ) != autoAppliedTo.end() ) { auto apiDefinition = schemaRegistry.FindAppliedAPIPrimDefinition( apiSchema ); autoAppliedPropertyNames.insert( @@ -374,13 +380,18 @@ void USDShader::loadShader( const std::string &shaderName, bool keepExistingValu } } - const TfType schemaType = schemaRegistry.GetTypeFromName( shaderNameToken ); + const TfType schemaType = schemaRegistry.GetTypeFromName( apiNameToken ); if( schemaType.IsA() || schemaType.IsA() ) { shaderType = "light"; primDefinitions.push_back( schemaRegistry.FindAppliedAPIPrimDefinition( TfToken( "ShadowAPI" ) ) ); primDefinitions.push_back( schemaRegistry.FindAppliedAPIPrimDefinition( TfToken( "ShapingAPI" ) ) ); } + else if( schemaType.IsA() ) + { + shaderType = "light"; + primDefinitions.push_back( schemaRegistry.FindAppliedAPIPrimDefinition( TfToken( "ShadowAPI" ) ) ); + } } SdrShaderNodeConstPtr shader = nullptr; diff --git a/src/GafferUSDModule/GafferUSDModule.cpp b/src/GafferUSDModule/GafferUSDModule.cpp index 9bd5c20a28..2f7f584151 100644 --- a/src/GafferUSDModule/GafferUSDModule.cpp +++ b/src/GafferUSDModule/GafferUSDModule.cpp @@ -39,6 +39,7 @@ #include "GafferUSD/USDAttributes.h" #include "GafferUSD/USDLayerWriter.h" #include "GafferUSD/USDLight.h" +#include "GafferUSD/USDMeshLight.h" #include "GafferUSD/USDShader.h" #include "GafferDispatchBindings/TaskNodeBinding.h" @@ -53,5 +54,6 @@ BOOST_PYTHON_MODULE( _GafferUSD ) GafferBindings::DependencyNodeClass(); GafferDispatchBindings::TaskNodeClass(); GafferBindings::DependencyNodeClass(); + GafferBindings::DependencyNodeClass(); } diff --git a/startup/GafferScene/usdLights.py b/startup/GafferScene/usdLights.py index 773fdc3fff..207576f426 100644 --- a/startup/GafferScene/usdLights.py +++ b/startup/GafferScene/usdLights.py @@ -84,10 +84,10 @@ def __defaultValue( target ) : Gaffer.Metadata.registerValue( "light:DiskLight", "type", "disk" ) Gaffer.Metadata.registerValue( "light:CylinderLight", "type", "cylinder" ) Gaffer.Metadata.registerValue( "light:DistantLight", "type", "distant" ) -Gaffer.Metadata.registerValue( "light:GeometryLight", "type", "mesh" ) +Gaffer.Metadata.registerValue( "light:MeshLight", "type", "mesh" ) Gaffer.Metadata.registerValue( "light:DomeLight", "type", "environment" ) -for light in [ "RectLight", "SphereLight", "DiskLight", "CylinderLight", "DistantLight", "GeometryLight", "DomeLight" ] : +for light in [ "RectLight", "SphereLight", "DiskLight", "CylinderLight", "DistantLight", "MeshLight", "DomeLight" ] : metadataTarget = "light:{}".format( light ) Gaffer.Metadata.registerValue( metadataTarget, "colorParameter", "color" ) diff --git a/startup/gui/menus.py b/startup/gui/menus.py index 073a3c062a..fdc118234d 100644 --- a/startup/gui/menus.py +++ b/startup/gui/menus.py @@ -555,6 +555,7 @@ def __usdLightCreator( lightType ) : "DistantLight", "DiskLight", "RectLight", "SphereLight", "CylinderLight", "DomeLight", "SpotLight" ] : nodeMenu.append( "/USD/Light/{}".format( IECore.CamelCase.toSpaced( lightType ) ), functools.partial( __usdLightCreator, lightType ), searchText = lightType ) + nodeMenu.append( "/USD/Light/Mesh Light", GafferUSD.USDMeshLight, searchText = "MeshLight" ) nodeMenu.append( "/USD/Attributes", GafferUSD.USDAttributes, searchText = "USDAttributes" ) nodeMenu.append( "/USD/Layer Writer", GafferUSD.USDLayerWriter, searchText = "USDLayerWriter" ) From 5cf68779d5b234d1aa8d6a88b36dbb6b7c6445ea Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Wed, 20 May 2026 17:06:36 -0400 Subject: [PATCH 03/11] USDMeshLight : Add renderer icons to parameters --- python/GafferSceneUI/MeshLightUI.py | 2 ++ python/GafferUSDUI/USDLightUI.py | 48 +++++++++++++--------------- python/GafferUSDUI/USDMeshLightUI.py | 7 ++++ 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/python/GafferSceneUI/MeshLightUI.py b/python/GafferSceneUI/MeshLightUI.py index 4d554693e2..c7660b1963 100644 --- a/python/GafferSceneUI/MeshLightUI.py +++ b/python/GafferSceneUI/MeshLightUI.py @@ -80,6 +80,8 @@ def __shaderMetadata( plug, name ) : "presetValues" : functools.partial( __shaderMetadata, name = "presetValues" ), "layout:section" : functools.partial( __shaderMetadata, name = "layout:section" ), "layout:index" : functools.partial( __shaderMetadata, name = "layout:index" ), + "labelPlugValueWidget:icon" : functools.partial( __shaderMetadata, name = "labelPlugValueWidget:icon" ), + "labelPlugValueWidget:iconToolTip" : functools.partial( __shaderMetadata, name = "labelPlugValueWidget:iconToolTip" ), }, diff --git a/python/GafferUSDUI/USDLightUI.py b/python/GafferUSDUI/USDLightUI.py index b0467a7108..08d215d480 100644 --- a/python/GafferUSDUI/USDLightUI.py +++ b/python/GafferUSDUI/USDLightUI.py @@ -75,38 +75,34 @@ } ) -# \todo Remove this method when merging to `1.7`. Instead, register -# dynamic metadata methods to `light:*:*` and determine the renderer -# in that method. This will be consistent with `ShaderUI` and `LightUI` -# registrations of the form `{shaderType}:{shaderName}:{parameterName}`. -def __registerIcons() : +def __renderer( key ) : - visited = set() for rendererTarget in Gaffer.Metadata.targetsWithMetadata( "renderer:*", "optionPrefix" ) : renderer = rendererTarget[9:] # Trim off "renderer:" # \todo Once we standardize on `arnold:` prefix instead of `ai:`, we can remove this special case. prefix = "arnold:" if renderer == "Arnold" else Gaffer.Metadata.value( rendererTarget, "optionPrefix" ) - if prefix in visited : - # A prefix can be registered for multiple renderers. We register them in - # order of importance, so skip all but the first. - continue - - Gaffer.Metadata.registerValue( - GafferUSD.USDLight.staticTypeId(), - f"parameters.{prefix}*", - "labelPlugValueWidget:icon", - "renderer" + renderer + "OnIcon.png" - ) - Gaffer.Metadata.registerValue( - GafferUSD.USDLight.staticTypeId(), - f"parameters.{prefix}*", - "labelPlugValueWidget:iconToolTip", - f"Parameter is specific to {renderer}." - ) - visited.add( prefix ) - -__registerIcons() + if key.split( ":" )[2] == prefix.rstrip( ":" ) : + return renderer + + return None + +def __labelPlugValueWidgetIcon( key ) : + + if ( renderer := __renderer( key ) ) is not None : + return "renderer" + renderer + "OnIcon.png" + + return None + +def __labelPlugValueWidgetToolTip( key ) : + + if ( renderer := __renderer( key ) ) is not None : + return f"Parameter is specific to {renderer}." + + return None + +Gaffer.Metadata.registerValue( "light:*:*", "labelPlugValueWidget:icon", __labelPlugValueWidgetIcon ) +Gaffer.Metadata.registerValue( "light:*:*", "labelPlugValueWidget:iconToolTip", __labelPlugValueWidgetToolTip ) class _RendererFilter( GafferUI.Widget ) : diff --git a/python/GafferUSDUI/USDMeshLightUI.py b/python/GafferUSDUI/USDMeshLightUI.py index b5bb669c0f..a76a532a3e 100644 --- a/python/GafferUSDUI/USDMeshLightUI.py +++ b/python/GafferUSDUI/USDMeshLightUI.py @@ -53,6 +53,13 @@ "layout:section:Basic:collapsed" : False, + "layout:customWidget:rendererFilter:widgetType" : "GafferUSDUI.USDLightUI._RendererFilter", + "layout:customWidget:rendererFilter:index" : 0, + + "layout:customWidget:standardFilter:widgetType" : "GafferUI.PlugLayout.StandardFilterWidget", + "layout:customWidget:standardFilter:index" : 1, + "layout:customWidget:standardFilter:accessory" : True, + }, "parameters.*" : { From ad92415cfc4b04a5b621158e9faa7544ede7aba2 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Thu, 21 May 2026 15:21:40 -0400 Subject: [PATCH 04/11] StandardLightVisualiser : Textured mesh lights --- Changes.md | 5 ++ .../Private/LightVisualiserAlgo.h | 2 + src/GafferSceneUI/LightVisualiserAlgo.cpp | 50 +++++++++++-------- src/GafferSceneUI/StandardLightVisualiser.cpp | 16 +++++- 4 files changed, 51 insertions(+), 22 deletions(-) diff --git a/Changes.md b/Changes.md index fedee8a328..b7c924c498 100644 --- a/Changes.md +++ b/Changes.md @@ -11,6 +11,11 @@ Features - Faster rendering. - USDMeshLight : Added node to add necessary attributes to geometry to convert to a USDMeshLight. +Improvements +------------ + +- MeshLight : Added viewport visualisation of textures. + Fixes ----- diff --git a/include/GafferSceneUI/Private/LightVisualiserAlgo.h b/include/GafferSceneUI/Private/LightVisualiserAlgo.h index f6e45a6067..8aece29a6f 100644 --- a/include/GafferSceneUI/Private/LightVisualiserAlgo.h +++ b/include/GafferSceneUI/Private/LightVisualiserAlgo.h @@ -146,4 +146,6 @@ GAFFERSCENEUI_API void addWireframeCurveState( IECoreGL::Group *group, const flo /// made into a function that can be called by implementation shaders. GAFFERSCENEUI_API void addConstantShader( IECoreGL::Group *group, const Imath::Color3f &tint, int aimType = -1 ); +GAFFERSCENEUI_API void addTexturedConstantShader( IECoreGL::State *state, IECore::ConstDataPtr textureData, const Imath::Color3f &tint, const float saturation, const Imath::Color3f &gamma, int maxTextureResolution ); + } // namespace GafferSceneUI::Private::LightVisualiserAlgo diff --git a/src/GafferSceneUI/LightVisualiserAlgo.cpp b/src/GafferSceneUI/LightVisualiserAlgo.cpp index f2c8f63135..9948fe3405 100644 --- a/src/GafferSceneUI/LightVisualiserAlgo.cpp +++ b/src/GafferSceneUI/LightVisualiserAlgo.cpp @@ -312,24 +312,7 @@ void addTexturedConstantShader( const float saturation, const Color3f &gamma, int maxTextureResolution ) { - CompoundObjectPtr shaderParameters = new CompoundObject; - - shaderParameters->members()["texture"] = const_cast( textureData.get() ); - shaderParameters->members()["texture:maxResolution"] = new IntData( maxTextureResolution ); - shaderParameters->members()["tint"] = new Color3fData( tint ); - shaderParameters->members()["saturation"] = new FloatData( saturation ); - shaderParameters->members()["gamma"] = new Color3fData( gamma ); - - group->getState()->add( - new IECoreGL::ShaderStateComponent( - IECoreGL::ShaderLoader::defaultShaderLoader(), - IECoreGL::TextureLoader::defaultTextureLoader(), - "", - "", - texturedConstantFragSource(), - shaderParameters - ) - ); + GafferSceneUI::Private::LightVisualiserAlgo::addTexturedConstantShader( group->getState(), textureData, tint, saturation, gamma, maxTextureResolution ); } // Customized IECoreGL primitive supporting `uvOrientation` @@ -546,7 +529,7 @@ IECoreGL::ConstRenderablePtr roundedQuadSurface( IECoreGL::GroupPtr group = new IECoreGL::Group(); if( textureData ) { - addTexturedConstantShader( group.get(), textureData, tint, saturation, gamma, maxTextureResolution ); + ::addTexturedConstantShader( group.get(), textureData, tint, saturation, gamma, maxTextureResolution ); } else { @@ -824,7 +807,7 @@ IECoreGL::ConstRenderablePtr environmentSphereSurface( if( textureData ) { - addTexturedConstantShader( sphereGroup.get(), textureData, tint, saturation, gamma, maxTextureResolution ); + ::addTexturedConstantShader( sphereGroup.get(), textureData, tint, saturation, gamma, maxTextureResolution ); } else { @@ -871,7 +854,7 @@ IECoreGL::ConstRenderablePtr diskSurface( IECoreGL::GroupPtr group = new IECoreGL::Group(); if( textureData ) { - addTexturedConstantShader( group.get(), textureData, tint, saturation, gamma, maxTextureResolution ); + ::addTexturedConstantShader( group.get(), textureData, tint, saturation, gamma, maxTextureResolution ); } else { @@ -1070,4 +1053,29 @@ void addConstantShader( IECoreGL::Group *group, const Color3f &tint, int aimType ); } +void addTexturedConstantShader( + IECoreGL::State *state, ConstDataPtr textureData, const Color3f &tint, + const float saturation, const Color3f &gamma, int maxTextureResolution +) +{ + CompoundObjectPtr shaderParameters = new CompoundObject; + + shaderParameters->members()["texture"] = const_cast( textureData.get() ); + shaderParameters->members()["texture:maxResolution"] = new IntData( maxTextureResolution ); + shaderParameters->members()["tint"] = new Color3fData( tint ); + shaderParameters->members()["saturation"] = new FloatData( saturation ); + shaderParameters->members()["gamma"] = new Color3fData( gamma ); + + state->add( + new IECoreGL::ShaderStateComponent( + IECoreGL::ShaderLoader::defaultShaderLoader(), + IECoreGL::TextureLoader::defaultTextureLoader(), + "", + "", + texturedConstantFragSource(), + shaderParameters + ) + ); +} + } // namespace GafferSceneUI::Private::LightVisualiserAlgo diff --git a/src/GafferSceneUI/StandardLightVisualiser.cpp b/src/GafferSceneUI/StandardLightVisualiser.cpp index cc0505a769..53ccdeea11 100644 --- a/src/GafferSceneUI/StandardLightVisualiser.cpp +++ b/src/GafferSceneUI/StandardLightVisualiser.cpp @@ -322,10 +322,24 @@ Visualisations StandardLightVisualiser::visualise( const IECore::InternedString // There isn't any meaningful place to draw anything for the mesh // light, so instead we make the mesh outline visible and light coloured. IECoreGL::StatePtr meshState = new IECoreGL::State( false ); - meshState->add( new IECoreGL::Primitive::DrawSolid( false ) ); meshState->add( new IECoreGL::Primitive::DrawOutline( true ) ); meshState->add( new IECoreGL::Primitive::OutlineWidth( 2.0f ) ); meshState->add( new IECoreGL::OutlineColorStateComponent( lightWireframeColor4( muted ) ) ); + + if( drawShaded ) + { + ConstDataPtr textureData = drawTextured ? surfaceTexture( attributeName, shaderNetwork, attributes, maxTextureResolution ) : nullptr; + if( textureData ) + { + addTexturedConstantShader( meshState.get(), textureData, tint, /* saturation = */ 1.f, /* gamma = */ Color3f( 1.f ), maxTextureResolution ); + } + meshState->add( new IECoreGL::Primitive::DrawSolid( textureData != nullptr ) ); + } + else + { + meshState->add( new IECoreGL::Primitive::DrawSolid( false ) ); + } + state = meshState; } else if( type == "photometric" ) From 7f4b99aabb3b754f7da7785e1fe58298af3e4af4 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Thu, 11 Jun 2026 14:56:23 -0400 Subject: [PATCH 05/11] USDShaderUI : Restore tooltips for USD lights --- python/GafferUSDUI/USDShaderUI.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/python/GafferUSDUI/USDShaderUI.py b/python/GafferUSDUI/USDShaderUI.py index 1f4af29b13..41cc423eaf 100644 --- a/python/GafferUSDUI/USDShaderUI.py +++ b/python/GafferUSDUI/USDShaderUI.py @@ -126,12 +126,7 @@ def __description( plug ) : property = __primProperty( plug ) if property : - description = property.GetMetadata( "documentation" ) - if description is not None : - # Spare UsdLux from embarrassment until it defines what - # various parameters are actually intended to do. - description = description.replace( "TODO: clarify semantics", "" ) - return description + return property.GetDocumentation() ## \todo Get USD to actually provide help metadata. It's defined in a `doc` # attribute in `shaderDefs.usda`, but not actually converted to Sdr by From 8391ca01f70f3b417980f427b5e8804544d7df82 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Tue, 23 Jun 2026 10:33:43 -0400 Subject: [PATCH 06/11] Arnold : Add support for USD mesh lights --- Changes.md | 9 +- include/IECoreArnold/ShaderNetworkAlgo.h | 3 + .../IECoreArnoldTest/ShaderNetworkAlgoTest.py | 373 ++++++++++++++++++ src/IECoreArnold/Renderer.cpp | 3 + src/IECoreArnold/ShaderNetworkAlgo.cpp | 205 ++++++++++ src/IECoreArnoldModule/IECoreArnoldModule.cpp | 10 + startup/gui/usd.py | 3 +- usdSchemas/GafferArnold.usda | 2 +- 8 files changed, 605 insertions(+), 3 deletions(-) diff --git a/Changes.md b/Changes.md index b7c924c498..41b90a70f7 100644 --- a/Changes.md +++ b/Changes.md @@ -9,7 +9,9 @@ Features - Prototypes and points that are editable downstream. - Export to USD. - Faster rendering. -- USDMeshLight : Added node to add necessary attributes to geometry to convert to a USDMeshLight. +- USDMeshLight : + - Added node to add necessary attributes to geometry to convert to a USDMeshLight. + - Added Arnold rendering. Improvements ------------ @@ -298,6 +300,11 @@ Features - CopyFiles : Added node for copying files. - RenameFiles : Added node for renaming files. +Improvements +------------ + +- MeshLight : Added viewport visualisation of textures. + Fixes ----- diff --git a/include/IECoreArnold/ShaderNetworkAlgo.h b/include/IECoreArnold/ShaderNetworkAlgo.h index 9b16575636..e521d9ec23 100644 --- a/include/IECoreArnold/ShaderNetworkAlgo.h +++ b/include/IECoreArnold/ShaderNetworkAlgo.h @@ -113,6 +113,9 @@ IECOREARNOLD_API void hashSubstitutions( const IECoreScene::ShaderNetwork *shade /// \deprecated Use `IECoreScene::ShaderNetworkAlgo::applyRenderAdaptors()` instead. IECOREARNOLD_API void applySubstitutions( IECoreScene::ShaderNetwork *shaderNetwork, IECore::InternedString attributeName, const IECore::CompoundObject *attributes ); +/// Returns a modified set of attributes conforming to the USDMeshLight specification. +IECOREARNOLD_API IECore::ConstCompoundObjectPtr convertUSDMeshLightAttributes( const IECore::CompoundObject *attributes ); + } // namespace ShaderNetworkAlgo } // namespace IECoreArnold diff --git a/python/IECoreArnoldTest/ShaderNetworkAlgoTest.py b/python/IECoreArnoldTest/ShaderNetworkAlgoTest.py index 2278a76ff5..268dbe71e3 100644 --- a/python/IECoreArnoldTest/ShaderNetworkAlgoTest.py +++ b/python/IECoreArnoldTest/ShaderNetworkAlgoTest.py @@ -1395,3 +1395,376 @@ def testMissingShaderWithBlindData( self ) : nodes = IECoreArnold.ShaderNetworkAlgo.convert( network, universe, "test" ) self.assertEqual( len( nodes ), 0 ) + + def testUSDMeshLight( self ) : + + # No surface network + + attributes = IECore.CompoundObject( + { + "light" : IECoreScene.ShaderNetwork( + shaders = { + "light" : IECoreScene.Shader( "MeshLight", "light" ) + }, + output = "light" + ) + } + ) + + with IECoreArnold.UniverseBlock( writable = True ) as universe : + + modifiedAttributes = IECoreArnold.ShaderNetworkAlgo.convertUSDMeshLightAttributes( attributes ) + lightNetwork = modifiedAttributes["light"] + + self.assertEqual( len( lightNetwork.shaders() ), 1 ) + shader = lightNetwork.getShader( "light" ) + self.assertIsNotNone( shader ) + self.assertEqual( shader.name, "mesh_light" ) + self.assertEqual( shader.type, "ai:light" ) + self.assertEqual( shader.parameters["color"].value, imath.Color3f( 1, 1, 1 ) ) + self.assertFalse( lightNetwork.input( ( "light", "color" ) ) ) + + for shaderName, emissionColorParameter in [ + ( "standard_surface", "emission_color" ), + ( "openpbr_surface", "emission_color" ), + ( "standard_hair", "emission_color" ), + ( "toon", "emission_color" ), + ( "UsdPreviewSurface", "emissiveColor" ) + ] : + + with self.subTest( shaderName = shaderName ) : + + # Surface color only, light color only (no color inputs) + + attributes = IECore.CompoundObject( + { + "light" : IECoreScene.ShaderNetwork( + shaders = { + "light" : IECoreScene.Shader( + "MeshLight", "light", + { "color" : imath.Color3f( 0.1, 0.2, 0.3 ), "intensity" : 2.0, "exposure" : 3.0 } + ) + }, + output = "light" + ), + "surface" : IECoreScene.ShaderNetwork( + shaders = { + "surface" : IECoreScene.Shader( + shaderName, "ai:surface", + { emissionColorParameter : imath.Color3f( 0.4, 0.5, 0.6 ) } + ), + }, + output = "surface" + ) + } + ) + + with IECoreArnold.UniverseBlock( writable = True ) as universe : + + originalSurfaceNetwork = attributes["surface"].copy() + modifiedAttributes = IECoreArnold.ShaderNetworkAlgo.convertUSDMeshLightAttributes( attributes ) + lightNetwork = modifiedAttributes["light"] + surfaceNetwork = modifiedAttributes["surface"] + + self.assertEqual( surfaceNetwork, originalSurfaceNetwork ) + + self.assertEqual( len( lightNetwork.shaders() ), 1 ) + light = lightNetwork.getShader( "light" ) + self.assertIsNotNone( light ) + self.assertEqual( light.name, "mesh_light" ) + self.assertEqual( light.type, "ai:light" ) + for i in range( 0, 3 ) : + self.assertAlmostEqual( light.parameters["color"].value[i], imath.Color3f( 0.1 * 0.4, 0.2 * 0.5, 0.3 * 0.6 )[i] ) + self.assertEqual( light.parameters["intensity"].value, 2.0 ) + self.assertEqual( light.parameters["exposure"].value, 3.0 ) + self.assertFalse( lightNetwork.input( ( "light", "color" ) ) ) + + # Surface with color input, light with color only + + for index, lightColor in enumerate( [ imath.Color3f( 0.0 ), imath.Color3f( 0.0, 0.5, 1.0 ), imath.Color3f( 1.0 ) ] ) : + + with self.subTest( lightColor = lightColor ) : + + attributes = IECore.CompoundObject ( + { + "light" : IECoreScene.ShaderNetwork( + shaders = { "light" : IECoreScene.Shader( "MeshLight", "light", { "color" : lightColor } ) }, + output = "light" + ), + "surface" : IECoreScene.ShaderNetwork( + shaders = { + "switch" : IECoreScene.Shader( "switch_shader", "ai:surface" ), + "surface" : IECoreScene.Shader( + shaderName, "ai:surface", + { emissionColorParameter : imath.Color3f( 0.4, 0.5, 0.6 ) } + ), + "correct" : IECoreScene.Shader( + "color_correct", "ai:surface", + { "main_gain" : 2 } + ), + "texture" : IECoreScene.Shader( + "image", "ai:surface", + { "filename" : "testFile.tx" } + ), + }, + connections = [ + ( ( "texture", "out" ), ( "correct", "input" ) ), + ( ( "correct", "out" ), ( "surface", emissionColorParameter ) ), + ( ( "surface", "out" ), ( "switch", "input0" ) ), + ], + output = "switch" + ) + } + ) + + with IECoreArnold.UniverseBlock( writable = True ) as universe : + + originalSurfaceNetwork = attributes["surface"].copy() + modifiedAttributes = IECoreArnold.ShaderNetworkAlgo.convertUSDMeshLightAttributes( attributes ) + lightNetwork = modifiedAttributes["light"] + surfaceNetwork = modifiedAttributes["surface"] + + self.assertEqual( surfaceNetwork, originalSurfaceNetwork ) + + self.assertEqual( + len( lightNetwork.shaders() ), + [ + 1, # Light color is 0, so no color input needed + 4, # Shaders from the light, the surface color inputs and a tint + 3, # Same as above but no tint needed for white light + ][index] + ) + + light = lightNetwork.getShader( "light" ) + self.assertIsNotNone( light ) + self.assertEqual( light.name, "mesh_light" ) + self.assertEqual( light.type, "ai:light" ) + for i in range( 0, 3 ) : + self.assertAlmostEqual( light.parameters["color"].value[i], ( lightColor * imath.Color3f( 0.4, 0.5, 0.6 ) )[i] ) + + if lightColor != imath.Color3f( 0 ) : + correct = lightNetwork.getShader( "correct" ) + self.assertIsNotNone( correct ) + self.assertEqual( correct.parameters["main_gain"].value, 2 ) + + texture = lightNetwork.getShader( "texture" ) + self.assertIsNotNone( texture ) + self.assertEqual( texture.parameters["filename"].value, "testFile.tx" ) + + if lightColor != imath.Color3f( 1 ) : + tint = lightNetwork.getShader( "tint" ) + self.assertIsNotNone ( tint ) + self.assertEqual( tint.parameters["input2"].value, lightColor ) + self.assertEqual( lightNetwork.input( ( "light", "color" ) ), ( "tint", "out" ) ) + self.assertEqual( lightNetwork.input( ( "tint", "input1" ) ), ( "correct", "out" ) ) + else : + self.assertEqual( lightNetwork.input( ( "light", "color" ) ), ( "correct", "out" ) ) + + self.assertEqual( lightNetwork.input( ( "correct", "input" ) ), ( "texture", "out" ) ) + + # Surface color only, light with color input + # Arnold's Hydra renderer supports inputs to USDMeshLight `color` so we do as well. + + for index, surfaceEmitColor in enumerate( [ imath.Color3f( 0.0 ), imath.Color3f( 0.0, 0.5, 1.0 ), imath.Color3f( 1.0 ) ] ) : + + with self.subTest( surfaceEmitColor = surfaceEmitColor ) : + + attributes = IECore.CompoundObject( + { + "light" : IECoreScene.ShaderNetwork( + shaders = { + "light" : IECoreScene.Shader( "MeshLight", "light", { "color" : imath.Color3f( 0.1, 0.2, 0.3 ) } ), + "correct" : IECoreScene.Shader( "color_correct", "ai:surface", { "main_gain" : 2 } ), + "texture" : IECoreScene.Shader( "image", "ai:surface", { "filename" : "testFile.tx" } ), + }, + connections = [ + ( ( "texture", "out" ), ( "correct", "input" ) ), + ( ( "correct", "out" ), ( "light", "color" ) ), + ], + output = "light" + ), + "surface" : IECoreScene.ShaderNetwork( + shaders = { + "surface" : IECoreScene.Shader( shaderName, "ai:surface", { emissionColorParameter : surfaceEmitColor } ), + }, + output = "surface" + ) + } + ) + + with IECoreArnold.UniverseBlock( writable = True ) as universe : + + originalLightNetwork = attributes["light"].copy() + originalSurfaceNetwork = attributes["surface"].copy() + modifiedAttributes = IECoreArnold.ShaderNetworkAlgo.convertUSDMeshLightAttributes( attributes ) + lightNetwork = modifiedAttributes["light"] + surfaceNetwork = modifiedAttributes["surface"] + + self.assertEqual( surfaceNetwork, originalSurfaceNetwork ) + + self.assertEqual( + len( lightNetwork.shaders() ), + [ + 1, # No surface color to transfer so no input needed + len( originalLightNetwork.shaders() ) + 1, # Original shaders plus tint + len( originalLightNetwork.shaders() ), # Full white surface glow, no tint needed + ][index] + ) + + light = lightNetwork.getShader( "light" ) + self.assertIsNotNone( light ) + self.assertEqual( light.name, "mesh_light" ) + self.assertEqual( light.type, "ai:light" ) + for i in range( 0, 3 ) : + self.assertAlmostEqual( light.parameters["color"].value[i], ( imath.Color3f( 0.1, 0.2, 0.3 ) * surfaceEmitColor )[i] ) + + if surfaceEmitColor != imath.Color3f( 0.0 ) : + correct = lightNetwork.getShader( "correct" ) + self.assertIsNotNone( correct ) + self.assertEqual( correct.parameters["main_gain"].value, 2 ) + + texture = lightNetwork.getShader( "texture" ) + self.assertIsNotNone( texture ) + self.assertEqual( texture.parameters["filename"].value, "testFile.tx" ) + + if surfaceEmitColor == imath.Color3f( 1.0 ) : + self.assertEqual( lightNetwork.input( ( "light", "color" ) ), ( "correct", "out" ) ) + else : + tint = lightNetwork.getShader( "tint" ) + self.assertIsNotNone ( tint ) + self.assertEqual( tint.parameters["input1"].value, surfaceEmitColor ) + self.assertEqual( lightNetwork.input( ( "light", "color" ) ), ( "tint", "out" ) ) + self.assertEqual( lightNetwork.input( ( "tint", "input2" ) ), ( "correct", "out" ) ) + self.assertEqual( lightNetwork.input( ( "correct", "input" ) ), ( "texture", "out" ) ) + + # Light and surface with color inputs + + attributes = IECore.CompoundObject( + { + "light" : IECoreScene.ShaderNetwork( + shaders = { + "light" : IECoreScene.Shader( "MeshLight", "light", { "color" : imath.Color3f( 0.1, 0.2, 0.3 ) } ), + "lightTexture" : IECoreScene.Shader( "image", "ai:surface", { "filename" : "lightTestFile.tx" } ), + }, + connections = [ ( ( "lightTexture", "out" ), ( "light", "color" ) ) ], + output = "light" + ), + "surface" : IECoreScene.ShaderNetwork( + shaders = { + "surface" : IECoreScene.Shader( + shaderName, "ai:surface", + { emissionColorParameter : imath.Color3f( 0.4, 0.5, 0.6 ) } + ), + "correct" : IECoreScene.Shader( + "color_correct", "ai:surface", + { "main_gain" : 2 } + ), + "texture" : IECoreScene.Shader( + "image", "ai:surface", + { "filename" : "testFile.tx" } + ), + }, + connections = [ + ( ( "texture", "out" ), ( "correct", "input" ) ), + ( ( "correct", "out" ), ( "surface", emissionColorParameter ) ), + ], + output = "surface" + ) + } + ) + + with IECoreArnold.UniverseBlock( writable = True ) as universe : + + originalSurfaceNetwork = attributes["surface"].copy() + modifiedAttributes = IECoreArnold.ShaderNetworkAlgo.convertUSDMeshLightAttributes( attributes ) + lightNetwork = modifiedAttributes["light"] + surfaceNetwork = modifiedAttributes["surface"] + + self.assertEqual( surfaceNetwork, originalSurfaceNetwork ) + + self.assertEqual( len( lightNetwork.shaders() ), 5 ) + + light = lightNetwork.getShader( "light" ) + self.assertIsNotNone( light ) + self.assertEqual( light.name, "mesh_light" ) + self.assertEqual( light.type, "ai:light" ) + for i in range( 0, 3 ) : + self.assertAlmostEqual( light.parameters["color"].value[i], (imath.Color3f( 0.1, 0.2, 0.3 ) * imath.Color3f( 0.4, 0.5, 0.6 ) )[i] ) + + correct = lightNetwork.getShader( "correct" ) + self.assertIsNotNone( correct ) + self.assertEqual( correct.parameters["main_gain"].value, 2 ) + + texture = lightNetwork.getShader( "texture" ) + self.assertIsNotNone( texture ) + self.assertEqual( texture.parameters["filename"].value, "testFile.tx" ) + + lightTexture = lightNetwork.getShader( "lightTexture" ) + self.assertIsNotNone( lightTexture ) + self.assertEqual( lightTexture.parameters["filename"].value, "lightTestFile.tx" ) + + tint = lightNetwork.getShader( "tint" ) + self.assertIsNotNone( tint ) + + self.assertEqual( lightNetwork.input( ( "light", "color" ) ), ( "tint", "out" ) ) + self.assertEqual( lightNetwork.input( ( "tint", "input1" ) ), ( "correct", "out" ) ) + self.assertEqual( lightNetwork.input( ( "correct", "input" ) ), ( "texture", "out" ) ) + self.assertEqual( lightNetwork.input( ( "tint", "input2" ) ), ( "lightTexture", "out" ) ) + + def testUSDMeshLightAttributes( self ) : + + attributes = IECore.CompoundObject( + { + "light" : IECoreScene.ShaderNetwork( { "light" : IECoreScene.Shader( "RectLight", "light" ) }, output = ( "light", "out" ) ) + } + ) + + modifiedAttributes = IECoreArnold.ShaderNetworkAlgo.convertUSDMeshLightAttributes( attributes ) + self.assertNotIn( "ai:visibility:camera", modifiedAttributes ) + self.assertNotIn( "ai:visibility:shadow", modifiedAttributes ) + self.assertNotIn( "ai:visibility:diffuse_reflect", modifiedAttributes ) + self.assertNotIn( "ai:visibility:specular_reflect", modifiedAttributes ) + self.assertNotIn( "ai:visibility:diffuse_transmit", modifiedAttributes ) + self.assertNotIn( "ai:visibility:specular_transmit", modifiedAttributes ) + self.assertNotIn( "ai:visibility:volume", modifiedAttributes ) + self.assertNotIn( "ai:visibility:subsurface", modifiedAttributes ) + + attributes = IECore.CompoundObject( + { + "light" : IECoreScene.ShaderNetwork( { "light" : IECoreScene.Shader( "MeshLight", "light" ) }, output = ( "light", "out" ) ) + } + ) + + modifiedAttributes = IECoreArnold.ShaderNetworkAlgo.convertUSDMeshLightAttributes( attributes ) + self.assertEqual( modifiedAttributes["ai:visibility:camera"].value, True ) + self.assertEqual( modifiedAttributes["ai:visibility:shadow"].value, False ) + self.assertEqual( modifiedAttributes["ai:visibility:diffuse_reflect"].value, False ) + self.assertEqual( modifiedAttributes["ai:visibility:specular_reflect"].value, False ) + self.assertEqual( modifiedAttributes["ai:visibility:diffuse_transmit"].value, False ) + self.assertEqual( modifiedAttributes["ai:visibility:specular_transmit"].value, False ) + self.assertEqual( modifiedAttributes["ai:visibility:volume"].value, False ) + self.assertEqual( modifiedAttributes["ai:visibility:subsurface"].value, False ) + + attributes = IECore.CompoundObject( + { + "light" : IECoreScene.ShaderNetwork( { "light" : IECoreScene.Shader( "MeshLight", "light" ) }, output = ( "light", "out" ) ), + "ai:visibility:camera" : IECore.BoolData( False ), + "ai:visibility:shadow" : IECore.BoolData( True ), + "ai:visibility:diffuse_reflect" : IECore.BoolData( True ), + "ai:visibility:specular_reflect" : IECore.BoolData( True ), + "ai:visibility:diffuse_transmit" : IECore.BoolData( True ), + "ai:visibility:specular_transmit" : IECore.BoolData( True ), + "ai:visibility:volume" : IECore.BoolData( True ), + "ai:visibility:subsurface" : IECore.BoolData( True ), + } + ) + + modifiedAttributes = IECoreArnold.ShaderNetworkAlgo.convertUSDMeshLightAttributes( attributes ) + self.assertEqual( modifiedAttributes["ai:visibility:camera"].value, False ) + self.assertEqual( modifiedAttributes["ai:visibility:shadow"].value, True ) + self.assertEqual( modifiedAttributes["ai:visibility:diffuse_reflect"].value, True ) + self.assertEqual( modifiedAttributes["ai:visibility:specular_reflect"].value, True ) + self.assertEqual( modifiedAttributes["ai:visibility:diffuse_transmit"].value, True ) + self.assertEqual( modifiedAttributes["ai:visibility:specular_transmit"].value, True ) + self.assertEqual( modifiedAttributes["ai:visibility:volume"].value, True ) + self.assertEqual( modifiedAttributes["ai:visibility:subsurface"].value, True ) diff --git a/src/IECoreArnold/Renderer.cpp b/src/IECoreArnold/Renderer.cpp index 710e2c9fe2..58527a0fe9 100644 --- a/src/IECoreArnold/Renderer.cpp +++ b/src/IECoreArnold/Renderer.cpp @@ -1316,6 +1316,9 @@ class ArnoldAttributes : public IECoreScenePreview::Renderer::AttributesInterfac ArnoldAttributes( const IECore::CompoundObject *attributes, ShaderCache *shaderCache ) : m_visibility( AI_RAY_ALL ), m_sidedness( AI_RAY_ALL ), m_shadingFlags( Default ), m_stepSize( 0.0f ), m_stepScale( 1.0f ), m_volumePadding( 0.0f ), m_polyMesh( attributes ), m_displacement( attributes, shaderCache ), m_curves( attributes ), m_points( attributes ), m_volume( attributes ), m_allAttributes( attributes ) { + IECore::ConstCompoundObjectPtr modifiedAttributes = ShaderNetworkAlgo::convertUSDMeshLightAttributes( attributes ); + attributes = modifiedAttributes.get(); + updateVisibility( m_visibility, g_cameraVisibilityAttributeName, AI_RAY_CAMERA, attributes ); updateVisibility( m_visibility, g_shadowVisibilityAttributeName, AI_RAY_SHADOW, attributes ); updateVisibility( m_visibility, g_diffuseReflectVisibilityAttributeName, AI_RAY_DIFFUSE_REFLECT, attributes ); diff --git a/src/IECoreArnold/ShaderNetworkAlgo.cpp b/src/IECoreArnold/ShaderNetworkAlgo.cpp index 3788eb95c4..c618579140 100644 --- a/src/IECoreArnold/ShaderNetworkAlgo.cpp +++ b/src/IECoreArnold/ShaderNetworkAlgo.cpp @@ -397,6 +397,42 @@ ShaderNetworkPtr preprocessedNetwork( const IECoreScene::ShaderNetwork *shaderNe return result; } +template +T *reportedCast( const IECore::RunTimeTyped *v, const char *type, const IECore::InternedString &name ) +{ + T *t = IECore::runTimeCast( v ); + if( t ) + { + return t; + } + + IECore::msg( IECore::Msg::Warning, "IECoreArnold::ShaderNetworkAlgo", fmt::format( "Expected {} but got {} for {} \"{}\".", T::staticTypeName(), v->typeName(), type, name.c_str() ) ); + return nullptr; +} + +template +const T *attribute( const IECore::CompoundObject::ObjectMap &attributes, IECore::InternedString name ) +{ + auto it = attributes.find( name ); + if( it == attributes.end() ) + { + return nullptr; + } + + return reportedCast( it->second.get(), "attribute", name ); +} + +pair shaderNetworkAttribute( const vector &attributeNames, const IECore::CompoundObject::ObjectMap &attributes ) +{ + for( const auto &name : attributeNames ) + { + if( const auto *shaderNetwork = attribute( attributes, name ) ) + { + return { name, shaderNetwork }; + } + } + return { IECore::InternedString(), nullptr }; +} } // namespace @@ -718,6 +754,26 @@ const InternedString g_widthParameter( "width" ); const InternedString g_wrapSParameter( "wrapS" ); const InternedString g_wrapTParameter( "wrapT" ); +const InternedString g_cameraVisibilityAttributeName( "ai:visibility:camera" ); +const InternedString g_diffuseReflectVisibilityAttributeName( "ai:visibility:diffuse_reflect" ); +const InternedString g_diffuseTransmitVisibilityAttributeName( "ai:visibility:diffuse_transmit" ); +const InternedString g_lightAttributeName( "light" ); +const InternedString g_shadowVisibilityAttributeName( "ai:visibility:shadow" ); +const InternedString g_specularReflectVisibilityAttributeName( "ai:visibility:specular_reflect" ); +const InternedString g_specularTransmitVisibilityAttributeName( "ai:visibility:specular_transmit" ); +const InternedString g_subsurfaceVisibilityAttributeName( "ai:visibility:subsurface" ); +const InternedString g_volumeVisibilityAttributeName( "ai:visibility:volume" ); + +const InternedString g_emptyString( "" ); + +const std::vector g_surfaceShaderAttributeNames = { + "ai:surface", + "osl:surface", + /// \todo Remove support for interpreting "osl:shader" as a surface shader assignment. + "osl:shader", + "surface" +}; + const string g_arnoldNamespace( "arnold:" ); void transferUSDLightParameters( ShaderNetwork *network, InternedString shaderHandle, const Shader *usdShader, Shader *shader ) @@ -907,6 +963,41 @@ void convertUSDUVTextures( ShaderNetwork *network ) } } +std::pair surfaceGlowParameters( const IECoreScene::ShaderNetwork *shaderNetwork ) +{ + ShaderNetwork::Parameter emissionColorParameter; + ShaderNetwork::Parameter emissionColorInput; + if( !shaderNetwork ) + { + return { emissionColorParameter, emissionColorInput }; + } + + for( const auto &[handle, shader] : shaderNetwork->shaders() ) + { + if( + shader->getName() == "standard_surface" || + shader->getName() == "standard_hair" || + shader->getName() == "toon" || + shader->getName() == "openpbr_surface" + ) + { + emissionColorParameter = { handle, g_emissionColorParameter }; + break; + } + else if( shader->getName() == "UsdPreviewSurface" ) + { + emissionColorParameter = { handle, g_emissiveColorParameter }; + break; + } + } + if( emissionColorParameter ) + { + emissionColorInput = shaderNetwork->input( emissionColorParameter ); + } + + return { emissionColorParameter, emissionColorInput }; +} + } // namespace void IECoreArnold::ShaderNetworkAlgo::convertUSDShaders( ShaderNetwork *shaderNetwork ) @@ -1183,4 +1274,118 @@ void applySubstitutions( IECoreScene::ShaderNetwork *shaderNetwork, InternedStri IECoreScene::ShaderNetworkAlgo::applyRenderAdaptors( shaderNetwork, attributeName, attributes ); } +////////////////////////////////////////////////////////////////////////// +// USDMeshLight Conversion +////////////////////////////////////////////////////////////////////////// + +ConstCompoundObjectPtr convertUSDMeshLightAttributes( const CompoundObject *attributes ) +{ + const auto *lightNetwork = attribute( attributes->members(), g_lightAttributeName ); + if( !lightNetwork ) + { + return attributes; + } + + const IECoreScene::Shader *outputShader = lightNetwork->outputShader(); + if( !outputShader || outputShader->getName() != "MeshLight" ) + { + return attributes; + } + + IECore::CompoundObjectPtr result = attributes->copy(); + + IECoreScene::ShaderNetworkPtr newLightShaderNetwork = lightNetwork->copy(); + const IECoreScene::ShaderNetwork *surfaceNetwork = shaderNetworkAttribute( g_surfaceShaderAttributeNames, attributes->members() ).second; + + const auto &[emissionColorParameter, emissionColorInput] = surfaceGlowParameters( surfaceNetwork ); + + ShaderNetwork::Parameter lightOutputParameter = lightNetwork->getOutput(); + const Shader *lightOutputShader = lightNetwork->outputShader(); + + ShaderPtr newLightShader = new Shader( "mesh_light", "ai:light" ); + transferUSDLightParameters( newLightShaderNetwork.get(), lightOutputParameter.shader, lightOutputShader, newLightShader.get() ); + + // The potential light inputs are in the first row of this matrix. + // The potential surface inputs are in the first column. + // The cells are the resulting mesh light color / input. + // C = light color x surface color. If 0 or 1 in parenthesis, it means it's known to be that value. + // TINT = A multiply shader combining the surface and light colors. + // Light / Emission Tex = the texture is connected directly without tint. + // | LightColor 0 | LightColor 0-1 | LightColor 1 | LightColor Textured + // EmissionColor 0 | C(0) | C(0) | C(0) | C(0) + // EmissionColor 0-1 | C(0) | C | C | TINT + // EmissionColor 1 | C(0) | C | C(1) | Light Tex + // EmissionColor Textured | C(0) | TINT | Emission Tex | TINT + + const Color3f lightColor = parameterValue( lightOutputShader, g_colorParameter, Color3f( 1.f ) ); + const Color3f emissionColor = emissionColorParameter ? parameterValue( surfaceNetwork->getShader( emissionColorParameter.shader ), emissionColorParameter.name, Color3f( 0.f ) ) : Color3f( 0.f ); + if( emissionColorParameter ) + { + newLightShader->parameters()[g_colorParameter] = new Color3fData( emissionColor * lightColor ); + } + + InternedString tintHandle; + const ShaderNetwork::Parameter meshLightColorParameter = { lightOutputParameter.shader, g_colorParameter }; + ShaderNetwork::Parameter meshLightColorInput = newLightShaderNetwork->input( meshLightColorParameter ); + // Remove the input to the light color. We will add it back later if needed. + removeInput( newLightShaderNetwork.get(), meshLightColorParameter ); + + if( emissionColorInput && ( lightColor != Color3f( 0.f ) || meshLightColorInput ) ) + { + ShaderNetworkPtr glowNetwork = surfaceNetwork->copy(); + glowNetwork->setOutput( emissionColorInput ); + IECoreScene::ShaderNetworkAlgo::removeUnusedShaders( glowNetwork.get() ); + ShaderNetwork::Parameter newGlowColorInput = IECoreScene::ShaderNetworkAlgo::addShaders( newLightShaderNetwork.get(), glowNetwork.get(), /* connections = */ true ); + + if( lightColor != Color3f( 1.f ) || meshLightColorInput ) + { + ShaderPtr tintShader = new Shader( "multiply", "ai:surface", { { "input2", new Color3fData( lightColor ) } } ); + tintHandle = newLightShaderNetwork->addShader( InternedString( "tint" ), std::move( tintShader ) ); + + newLightShaderNetwork->addConnection( { newGlowColorInput, { tintHandle, "input1" } } ); + newLightShaderNetwork->addConnection( { { tintHandle, "out" }, meshLightColorParameter } ); + } + else + { + newLightShaderNetwork->addConnection( { newGlowColorInput, meshLightColorParameter } ); + } + } + + if( meshLightColorInput && ( emissionColor != Color3f( 0.f ) || emissionColorInput ) ) + { + if( emissionColor != Color3f( 1.f ) || emissionColorInput ) + { + if( tintHandle == g_emptyString ) + { + ShaderPtr tintShader = new Shader( "multiply", "ai:surface", { { "input1", new Color3fData( emissionColor ) } } ); + tintHandle = newLightShaderNetwork->addShader( InternedString( "tint" ), std::move( tintShader ) ); + + newLightShaderNetwork->addConnection( { { tintHandle, "out" }, meshLightColorParameter } ); + } + + newLightShaderNetwork->addConnection( { meshLightColorInput, { tintHandle, "input2" } } ); + } + else + { + newLightShaderNetwork->addConnection( { meshLightColorInput, meshLightColorParameter } ); + } + } + + replaceUSDShader( newLightShaderNetwork.get(), lightOutputParameter.shader, std::move( newLightShader ) ); + IECoreScene::ShaderNetworkAlgo::removeUnusedShaders( newLightShaderNetwork.get() ); + + result->members()[g_lightAttributeName] = std::move( newLightShaderNetwork ); + + result->members().try_emplace( g_cameraVisibilityAttributeName, new IECore::BoolData( true ) ); + result->members().try_emplace( g_shadowVisibilityAttributeName, new IECore::BoolData( false ) ); + result->members().try_emplace( g_diffuseReflectVisibilityAttributeName, new IECore::BoolData( false ) ); + result->members().try_emplace( g_specularReflectVisibilityAttributeName, new IECore::BoolData( false ) ); + result->members().try_emplace( g_diffuseTransmitVisibilityAttributeName, new IECore::BoolData( false ) ); + result->members().try_emplace( g_specularTransmitVisibilityAttributeName, new IECore::BoolData( false ) ); + result->members().try_emplace( g_volumeVisibilityAttributeName, new IECore::BoolData( false ) ); + result->members().try_emplace( g_subsurfaceVisibilityAttributeName, new IECore::BoolData( false ) ); + + return result; +} + } // namespace IECoreArnold::ShaderNetworkAlgo diff --git a/src/IECoreArnoldModule/IECoreArnoldModule.cpp b/src/IECoreArnoldModule/IECoreArnoldModule.cpp index cf1908442f..2ad4f97095 100644 --- a/src/IECoreArnoldModule/IECoreArnoldModule.cpp +++ b/src/IECoreArnoldModule/IECoreArnoldModule.cpp @@ -36,6 +36,8 @@ #include "boost/python.hpp" +#include "IECorePython/ScopedGILRelease.h" + #include "IECoreArnold/NodeAlgo.h" #include "IECoreArnold/ParameterAlgo.h" #include "IECoreArnold/ShaderNetworkAlgo.h" @@ -179,6 +181,13 @@ bool shaderNetworkAlgoUpdate( list pythonNodes, const IECoreScene::ShaderNetwork return result; } +IECore::CompoundObjectPtr convertUSDMeshLightAttributesWrapper( const IECore::CompoundObject &attributes, bool copy ) +{ + IECorePython::ScopedGILRelease r; + IECore::ConstCompoundObjectPtr result = ShaderNetworkAlgo::convertUSDMeshLightAttributes( &attributes ); + return copy ? result->copy() : boost::const_pointer_cast( result ); +} + } // namespace BOOST_PYTHON_MODULE( _IECoreArnold ) @@ -216,6 +225,7 @@ BOOST_PYTHON_MODULE( _IECoreArnold ) def( "convert", &shaderNetworkAlgoConvert ); def( "update", &shaderNetworkAlgoUpdate ); def( "convertUSDShaders", &ShaderNetworkAlgo::convertUSDShaders ); + def( "convertUSDMeshLightAttributes", &convertUSDMeshLightAttributesWrapper, ( arg_( "_copy" ) = true ) ); } } diff --git a/startup/gui/usd.py b/startup/gui/usd.py index 0ff075241c..f8ff6ade64 100644 --- a/startup/gui/usd.py +++ b/startup/gui/usd.py @@ -54,7 +54,8 @@ "transmission", "sss", "indirect", "volume", "max_bounces", "lens_radius", "aspect_ratio", "cast_volumetric_shadows", "shadow_density", "samples", "volume_samples", "resolution" ] ) : - Gaffer.Metadata.registerValue( GafferUSD.USDLight, f"parameters.arnold:{parameter}", "layout:index", 1000 + i ) + for lightType in [ GafferUSD.USDLight, GafferUSD.USDMeshLight ] : + Gaffer.Metadata.registerValue( lightType, f"parameters.arnold:{parameter}", "layout:index", 1000 + i ) Gaffer.Metadata.registerValue( GafferUSD.USDLight, "parameters", "layout:activator:coneAngleEnabled", lambda plug : plug["shaping:cone:angle"]["enabled"].getValue() ) Gaffer.Metadata.registerValue( GafferUSD.USDLight, "parameters.arnold:lens_radius", "layout:activator", "coneAngleEnabled" ) diff --git a/usdSchemas/GafferArnold.usda b/usdSchemas/GafferArnold.usda index 7f972a8d15..fa957595c1 100644 --- a/usdSchemas/GafferArnold.usda +++ b/usdSchemas/GafferArnold.usda @@ -37,7 +37,7 @@ over "GLOBAL" ( class "GafferArnoldLightAPI" ( customData = { - token[] apiSchemaAutoApplyTo = [ "CylinderLight", "DistantLight", "DiskLight", "DomeLight", "RectLight", "SphereLight" ] + token[] apiSchemaAutoApplyTo = [ "CylinderLight", "DistantLight", "DiskLight", "DomeLight", "RectLight", "SphereLight", "MeshLightAPI" ] string apiSchemaType = "singleApply" string className = "GafferArnoldLightAPI" } From 24755f9044777163d54ce094440241aca744a1e1 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Thu, 11 Jun 2026 15:13:13 -0400 Subject: [PATCH 07/11] USDMeshLightUI : Add Arnold plug descriptions These match our current descriptions for Arnold light plugs. --- python/GafferUSDUI/USDMeshLightUI.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/python/GafferUSDUI/USDMeshLightUI.py b/python/GafferUSDUI/USDMeshLightUI.py index a76a532a3e..f59a63d860 100644 --- a/python/GafferUSDUI/USDMeshLightUI.py +++ b/python/GafferUSDUI/USDMeshLightUI.py @@ -73,6 +73,12 @@ }, + "parameters.arnold:*" : { + + "description" : "Refer to Arnold's documentation for further details.", + + }, + } ) From b1fb00b6089ac458546b6afbeb02c48db9cb5076 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Tue, 23 Jun 2026 11:12:38 -0400 Subject: [PATCH 08/11] RenderMan : Add support for USD mesh lights --- Changes.md | 2 +- include/IECoreRenderMan/ShaderNetworkAlgo.h | 3 + python/IECoreRenderManTest/RendererTest.py | 82 ++++++++++ .../ShaderNetworkAlgoTest.py | 149 ++++++++++++++++++ src/IECoreRenderMan/Attributes.cpp | 49 +++++- src/IECoreRenderMan/Attributes.h | 6 + src/IECoreRenderMan/Light.cpp | 6 +- src/IECoreRenderMan/Renderer.cpp | 23 ++- src/IECoreRenderMan/ShaderNetworkAlgo.cpp | 149 ++++++++++++++++++ src/IECoreRenderMan/USDMeshLight.cpp | 82 ++++++++++ src/IECoreRenderMan/USDMeshLight.h | 76 +++++++++ .../IECoreRenderManModule.cpp | 11 ++ 12 files changed, 628 insertions(+), 10 deletions(-) create mode 100644 src/IECoreRenderMan/USDMeshLight.cpp create mode 100644 src/IECoreRenderMan/USDMeshLight.h diff --git a/Changes.md b/Changes.md index 41b90a70f7..b43fc8a4a2 100644 --- a/Changes.md +++ b/Changes.md @@ -11,7 +11,7 @@ Features - Faster rendering. - USDMeshLight : - Added node to add necessary attributes to geometry to convert to a USDMeshLight. - - Added Arnold rendering. + - Added Arnold and RenderMan rendering. Improvements ------------ diff --git a/include/IECoreRenderMan/ShaderNetworkAlgo.h b/include/IECoreRenderMan/ShaderNetworkAlgo.h index d13dc4a5ec..de1f501986 100644 --- a/include/IECoreRenderMan/ShaderNetworkAlgo.h +++ b/include/IECoreRenderMan/ShaderNetworkAlgo.h @@ -84,4 +84,7 @@ IECORERENDERMAN_API VStructAction evaluateVStructConditional( const std::string /// as `convert()` resolves vstructs internally anyway. IECORERENDERMAN_API void resolveVStructs( IECoreScene::ShaderNetwork *shaderNetwork ); +/// Returns a modified set of attributes conforming to the USDMeshLight specification. +IECORERENDERMAN_API IECore::ConstCompoundObjectPtr convertUSDMeshLightAttributes( const IECore::CompoundObject *attributes ); + } // namespace IECoreRenderMan::ShaderNetworkAlgo diff --git a/python/IECoreRenderManTest/RendererTest.py b/python/IECoreRenderManTest/RendererTest.py index 2e655a3a4b..e1b0c32517 100755 --- a/python/IECoreRenderManTest/RendererTest.py +++ b/python/IECoreRenderManTest/RendererTest.py @@ -1024,6 +1024,88 @@ def assertGreenSphere() : del sphere, light del renderer + def testUSDMeshLightAttributes( self ) : + + sidesParameter = "Ri:Sides" + + with IECoreRenderManTest.RileyCapture() as capture : + + renderer = GafferScene.Private.IECoreScenePreview.Renderer.create( + self.renderer, + GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch + ) + + renderer.light( + "sphere", + IECoreScene.MeshPrimitive.createSphere( 1 ), + renderer.attributes( IECore.CompoundObject( { + "light" : IECoreScene.ShaderNetwork( + shaders = { + "output" : IECoreScene.Shader( + "MeshLight", "light", + { "lightColor" : imath.Color3f( 0.0, 1.0, 1.0 ) } + ), + }, + output = "output", + ), + } ) ) + ) + + del renderer + + attributes = next( x for x in capture.json if x["method"] == "CreateLightInstance" )["attributes"]["params"] + self.__assertParameterEqual( attributes, "visibility:camera", [ 0 ] ) + self.__assertParameterEqual( attributes, "visibility:indirect", [ 0 ] ) + self.__assertParameterEqual( attributes, "visibility:transmission", [ 0 ] ) + self.__assertParameterEqual( attributes, sidesParameter, [ 1 ] ) + + attributes = next( x for x in capture.json if x["method"] == "CreateGeometryInstance" )["attributes"]["params"] + self.__assertNotInParameters( attributes, "visibility:camera" ) + self.__assertNotInParameters( attributes, "visibility:indirect" ) + self.__assertNotInParameters( attributes, "visibility:transmission" ) + self.__assertNotInParameters( attributes, sidesParameter ) + + with IECoreRenderManTest.RileyCapture() as capture : + + renderer = GafferScene.Private.IECoreScenePreview.Renderer.create( + self.renderer, + GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch + ) + + renderer.light( + "sphere", + IECoreScene.MeshPrimitive.createSphere( 1 ), + renderer.attributes( IECore.CompoundObject( { + "light" : IECoreScene.ShaderNetwork( + shaders = { + "output" : IECoreScene.Shader( + "MeshLight", "light", + { "lightColor" : imath.Color3f( 0.0, 1.0, 1.0 ) } + ), + }, + output = "output", + ), + "ri:visibility:camera" : IECore.BoolData( False ), + "ri:visibility:indirect" : IECore.BoolData( True ), + "ri:visibility:transmission" : IECore.BoolData( True ), + "doubleSided" : IECore.BoolData( True ), + } ) ) + ) + + del renderer + + attributes = next( x for x in capture.json if x["method"] == "CreateLightInstance" )["attributes"]["params"] + self.__assertParameterEqual( attributes, "visibility:camera", [ 0 ] ) + self.__assertParameterEqual( attributes, "visibility:indirect", [ 0 ] ) + self.__assertParameterEqual( attributes, "visibility:transmission", [ 0 ] ) + self.__assertParameterEqual( attributes, sidesParameter, [ 1 ] ) + + attributes = next( x for x in capture.json if x["method"] == "CreateGeometryInstance" )["attributes"]["params"] + self.__assertParameterEqual( attributes, "visibility:camera", [ 0 ] ) + self.__assertParameterEqual( attributes, "visibility:indirect", [ 1 ] ) + self.__assertParameterEqual( attributes, "visibility:transmission", [ 1 ] ) + self.__assertParameterEqual( attributes, sidesParameter, [ 2 ] ) + def testConnectionToMissingShader( self ) : # This test doesn't assert anything, but demonstrates that making diff --git a/python/IECoreRenderManTest/ShaderNetworkAlgoTest.py b/python/IECoreRenderManTest/ShaderNetworkAlgoTest.py index 9a7b983cd3..a6efc5e0cb 100644 --- a/python/IECoreRenderManTest/ShaderNetworkAlgoTest.py +++ b/python/IECoreRenderManTest/ShaderNetworkAlgoTest.py @@ -748,6 +748,155 @@ def testResolveVStructs( self ) : self.assertFalse( shaderNetwork.input( ( "layerMixer", "baselayer_diffuseGain" ) ) ) self.assertEqual( shaderNetwork.getShader( "layerMixer" ).parameters["baselayer_enableDiffuse"].value, False ) + def testUSDMeshLight( self ) : + + attributes = IECore.CompoundObject( + { + "light" : IECoreScene.ShaderNetwork( + shaders = { + "light" : IECoreScene.Shader( "MeshLight", "light" ) + }, + output = "light" + ) + } + ) + + modifiedAttributes = IECoreRenderMan.ShaderNetworkAlgo.convertUSDMeshLightAttributes( attributes ) + lightNetwork = modifiedAttributes["light"] + + self.assertEqual( len( lightNetwork.shaders() ), 1 ) + shader = lightNetwork.getShader( "light" ) + self.assertIsNotNone( shader ) + self.assertEqual( shader.name, "PxrMeshLight" ) + self.assertEqual( shader.type, "ri:light" ) + self.assertNotIn( "textureColor", shader.parameters ) + self.assertFalse( lightNetwork.input( ( "light", "textureColor" ) ) ) + + for shaderName, emissionColorParameter in [ + ( "PxrSurface", "glowColor" ), + ( "PxrLayerSurface", "glowColor" ), + ( "PxrMarschnerHair", "glowColor" ), + ( "LamaEmission", "emissionColor" ), + ( "PxrConstant", "emitColor" ), + ( "PxrDisney", "emitColor" ), + ( "UsdPreviewSurface", "emissiveColor" ), + ] : + with self.subTest( shaderName = shaderName ) : + + # Surface color only, light color only (no color inputs) + + for surfaceColor in [ imath.Color3f( 0.0 ), imath.Color3f( 1.0 ) ] : + with self.subTest( surfaceColor = surfaceColor ) : + attributes = IECore.CompoundObject( + { + "light" : IECoreScene.ShaderNetwork( + shaders = { + "light" : IECoreScene.Shader( + "MeshLight", "light", + { "color" : imath.Color3f( 0, 1, 0 ), "intensity" : 2.0, "exposure" : 3.0 } + ) + }, + output = "light" + ), + "surface" : IECoreScene.ShaderNetwork( + shaders = { + "surface" : IECoreScene.Shader( + shaderName, "ri:surface", + { emissionColorParameter : surfaceColor } + ), + }, + output = "surface" + ) + } + ) + + originalSurfaceNetwork = attributes["surface"].copy() + modifiedAttributes = IECoreRenderMan.ShaderNetworkAlgo.convertUSDMeshLightAttributes( attributes ) + lightNetwork = modifiedAttributes["light"] + surfaceNetwork = modifiedAttributes["surface"] + + self.assertEqual( surfaceNetwork, originalSurfaceNetwork ) + + self.assertEqual( len( lightNetwork.shaders() ), 1 ) + shader = lightNetwork.getShader( "light" ) + self.assertIsNotNone( shader ) + self.assertEqual( shader.name, "PxrMeshLight" ) + self.assertEqual( shader.type, "ri:light" ) + self.assertEqual( shader.parameters["lightColor"].value, imath.Color3f( 0, 1, 0 ) ) + self.assertEqual( shader.parameters["textureColor"].value, surfaceColor ) + self.assertEqual( shader.parameters["intensity"].value, 2.0 ) + self.assertEqual( shader.parameters["exposure"].value, 3.0 ) + self.assertFalse( lightNetwork.input( ( "light", "textureColor" ) ) ) + + # Surface with color input, light with color only + # We always connect the texture and let PxrMeshLight deal with the tint internally. + + for lightColor in [ imath.Color3f( 0.0 ), imath.Color3f( 0.0, 0.5, 1.0 ), imath.Color3f( 1.0 ) ] : + + with self.subTest( lightColor = lightColor ) : + + attributes = IECore.CompoundObject( + { + "light" : IECoreScene.ShaderNetwork( + shaders = { "light" : IECoreScene.Shader( "MeshLight", "light", { "color" : lightColor } ) }, + output = "light" + ), + "surface": IECoreScene.ShaderNetwork( + shaders = { + "surface" : IECoreScene.Shader( + shaderName, "ri:surface", + { emissionColorParameter : imath.Color3f( 0.25, 0.5, 0.75 ) } + ), + "correct" : IECoreScene.Shader( + "PxrColorCorrect", "ri:shader", + { "rgbGain" : 2 } + ), + "texture" : IECoreScene.Shader( + "PxrTexture", "ri:shader", + { "filename" : "testFile.tex" } + ), + }, + connections = [ + ( ( "texture", "resultRGB" ), ( "correct", "inputRGB" ) ), + ( ( "correct", "resultRGB" ), ( "surface", emissionColorParameter ) ), + ], + output = "surface" + ) + } + ) + + if shaderName == "LamaEmission" : + attributes["surface"].addShader( "lamaSurface", IECoreScene.Shader( "LamaSurface", "ri:surface" ) ) + attributes["surface"].setOutput( "lamaSurface" ) + attributes["surface"].addConnection( ( ( "surface", "bxdf_out"), ( "lamaSurface", "materialFront" ) ) ) + + originalSurfaceNetwork = attributes["surface"].copy() + modifiedAttributes = IECoreRenderMan.ShaderNetworkAlgo.convertUSDMeshLightAttributes( attributes ) + lightNetwork = modifiedAttributes["light"] + surfaceNetwork = modifiedAttributes["surface"] + + self.assertEqual( surfaceNetwork, originalSurfaceNetwork ) + + self.assertEqual( len( lightNetwork.shaders() ), 3 ) + + light = lightNetwork.getShader( "light" ) + self.assertIsNotNone( light ) + self.assertEqual( light.name, "PxrMeshLight" ) + self.assertEqual( light.type, "ri:light" ) + self.assertEqual( light.parameters["lightColor"].value, lightColor ) + self.assertEqual( light.parameters["textureColor"].value, imath.Color3f( 0.25, 0.5, 0.75 ) ) + self.assertEqual( lightNetwork.input( ( "light", "textureColor" ) ), ( "correct", "resultRGB" ) ) + self.assertEqual( lightNetwork.input( ( "correct", "inputRGB" ) ), ( "texture", "resultRGB" ) ) + + shader = lightNetwork.getShader( "correct" ) + self.assertIsNotNone( shader ) + self.assertEqual( shader.parameters["rgbGain"].value, 2 ) + + shader = lightNetwork.getShader( "texture" ) + self.assertIsNotNone( shader ) + self.assertEqual( shader.parameters["filename"].value, "testFile.tex" ) + + def __assertShadersEqual( self, shader1, shader2, message = None ) : self.assertEqual( shader1.name, shader2.name, message ) diff --git a/src/IECoreRenderMan/Attributes.cpp b/src/IECoreRenderMan/Attributes.cpp index 38e330fcc7..32eb328484 100644 --- a/src/IECoreRenderMan/Attributes.cpp +++ b/src/IECoreRenderMan/Attributes.cpp @@ -39,6 +39,8 @@ #include "ParamListAlgo.h" #include "Loader.h" +#include "IECoreRenderMan/ShaderNetworkAlgo.h" + #include "IECoreScene/ShaderNetwork.h" #include "IECore/SimpleTypedData.h" @@ -111,6 +113,7 @@ boost::container::flat_map g_prototypeAttributes = { const string g_renderManPrefix( "ri:" ); const IECore::InternedString g_automaticInstancingAttributeName( "gaffer:automaticInstancing" ); const InternedString g_doubleSidedAttributeName( "doubleSided" ); +const InternedString g_lightAttributeName( "light" ); const InternedString g_lightMuteAttributeName( "light:mute" ); const InternedString g_renderManLightFilterAttributeName( "ri:lightFilter" ); const RtUString g_userMaterialId( "user:__materialid" ); @@ -178,7 +181,13 @@ pair shaderNetworkAttribute( const Compou return { InternedString(), nullptr }; } -bool isMeshLight( const IECoreScene::ShaderNetwork *lightShader ) +bool isUSDMeshLight( const IECoreScene::ShaderNetwork *lightShader ) +{ + const IECoreScene::Shader *outputShader = lightShader->outputShader(); + return outputShader && outputShader->getName() == "MeshLight"; +} + +bool isPxrMeshLight( const IECoreScene::ShaderNetwork *lightShader ) { const IECoreScene::Shader *outputShader = lightShader->outputShader(); return outputShader && outputShader->getName() == "PxrMeshLight"; @@ -225,6 +234,12 @@ const std::string g_userAttributePrefix( "user:" ); Attributes::Attributes( const IECore::CompoundObject *attributes, MaterialCache *materialCache ) { + const auto *lightShader = attribute( attributes->members(), g_lightAttributeName ); + m_isUSDMeshLight = lightShader && ::isUSDMeshLight( lightShader ); + + ConstCompoundObjectPtr modifiedAttributes = ShaderNetworkAlgo::convertUSDMeshLightAttributes( attributes ); + attributes = modifiedAttributes.get(); + // Convert shaders. const auto [surfaceName, surface] = shaderNetworkAttribute( attributes->members(), g_surfaceAttributeNames ); @@ -258,13 +273,16 @@ Attributes::Attributes( const IECore::CompoundObject *attributes, MaterialCache } m_lightShader = shaderNetworkAttribute( attributes->members(), g_lightAttributeNames ).second; - if( m_lightShader && isMeshLight( m_lightShader.get() ) ) + if( m_lightShader && isPxrMeshLight( m_lightShader.get() ) ) { // Mesh lights default to having a black material so they don't appear // in indirect rays, but the user can override with a surface assignment // if they want further control. Other lights don't have materials. // We assume that a volume shader makes no sense here. - m_lightMaterial = materialCache->getMaterial( surface ? surface : g_black.get(), surface ? surfaceName : InternedString(), attributes ); + // We check for `m_isUSDMeshLight` because after the `convertUSDMeshLightAttributes()` + // call above, all mesh lights are PxrMeshLight and the only remaining evidence we have + // that a light started as a USDMeshLight is `m_isUSDMeshLight`. + m_lightMaterial = materialCache->getMaterial( ( surface && !m_isUSDMeshLight ) ? surface : g_black.get(), ( surface && !m_isUSDMeshLight ) ? surfaceName : InternedString(), attributes ); } // Set up material id for PxrCryptomatte. This can be overridden if desired @@ -338,6 +356,21 @@ Attributes::Attributes( const IECore::CompoundObject *attributes, MaterialCache } m_lightFilter = attribute( attributes->members(), g_renderManLightFilterAttributeName ); + + // Only USD mesh lights get these visibility overrides : they have a + // separate camera-visible surface `Object`, so the light emitter itself + // should be hidden from camera, indirect and transmission rays. Native + // PxrMeshLights have no such surface and must remain camera-visible by + // default (and honour any user-authored visibility). + if( m_isUSDMeshLight ) + { + m_lightInstanceAttributes = RtParamList( m_instanceAttributes ); + + m_lightInstanceAttributes->SetInteger( Loader::strings().k_visibility_camera, 0 ); + m_lightInstanceAttributes->SetInteger( Loader::strings().k_visibility_indirect, 0 ); + m_lightInstanceAttributes->SetInteger( Loader::strings().k_visibility_transmission, 0 ); + m_lightInstanceAttributes->SetInteger( Loader::strings().k_Ri_Sides, 1 ); + } } Attributes::~Attributes() @@ -364,6 +397,11 @@ const IECore::MurmurHash &Attributes::instanceAttributesHash() const return m_instanceAttributesHash; } +const RtParamList &Attributes::lightInstanceAttributes() const +{ + return m_lightInstanceAttributes ? *m_lightInstanceAttributes : m_instanceAttributes; +} + const Material *Attributes::material() const { return m_material.get(); @@ -383,3 +421,8 @@ const IECoreScene::ShaderNetwork *Attributes::lightFilter() const { return m_lightFilter.get(); } + +bool Attributes::isUSDMeshLight() const +{ + return m_isUSDMeshLight; +} diff --git a/src/IECoreRenderMan/Attributes.h b/src/IECoreRenderMan/Attributes.h index 9abde19895..d7bd61952f 100644 --- a/src/IECoreRenderMan/Attributes.h +++ b/src/IECoreRenderMan/Attributes.h @@ -64,6 +64,8 @@ class Attributes : public IECoreScenePreview::Renderer::AttributesInterface /// Attributes to be applied to GeometryInstances. const RtParamList &instanceAttributes() const; const IECore::MurmurHash &instanceAttributesHash() const; + /// Attributes to be applied to light GeometryInstances. + const RtParamList &lightInstanceAttributes() const; const Material *material() const; const Displacement *displacement() const { return m_displacement.get(); } @@ -76,17 +78,21 @@ class Attributes : public IECoreScenePreview::Renderer::AttributesInterface const IECoreScene::ShaderNetwork *lightFilter() const; + bool isUSDMeshLight() const; + private : std::optional m_prototypeHash; RtParamList m_prototypeAttributes; RtParamList m_instanceAttributes; IECore::MurmurHash m_instanceAttributesHash; + std::optional m_lightInstanceAttributes; ConstMaterialPtr m_material; ConstDisplacementPtr m_displacement; IECoreScene::ConstShaderNetworkPtr m_lightShader; ConstMaterialPtr m_lightMaterial; IECoreScene::ConstShaderNetworkPtr m_lightFilter; + bool m_isUSDMeshLight = false; }; diff --git a/src/IECoreRenderMan/Light.cpp b/src/IECoreRenderMan/Light.cpp index 133d016754..07784978e8 100755 --- a/src/IECoreRenderMan/Light.cpp +++ b/src/IECoreRenderMan/Light.cpp @@ -143,7 +143,7 @@ Light::Light( const ConstGeometryPrototypePtr &geometryPrototype, const Attribut m_lightInstance = m_session->createLightInstance( m_geometryPrototype ? m_geometryPrototype->id() : riley::GeometryPrototypeId(), material ? material->id() : riley::MaterialId(), m_lightShader->id(), { 0, nullptr }, IdentityTransform(), - mergedAttributes( attributes->instanceAttributes(), m_extraAttributes ) + mergedAttributes( attributes->lightInstanceAttributes(), m_extraAttributes ) ); } @@ -226,7 +226,7 @@ bool Light::attributes( const IECoreScenePreview::Renderer::AttributesInterface return true; } - const RtParamList allAttributes = mergedAttributes( renderManAttributes->instanceAttributes(), m_extraAttributes ); + const RtParamList allAttributes = mergedAttributes( renderManAttributes->lightInstanceAttributes(), m_extraAttributes ); const Material *material = renderManAttributes->lightMaterial(); const riley::LightInstanceResult result = m_session->modifyLightInstance( @@ -357,7 +357,7 @@ void Light::updateLinking( RtUString memberships, RtUString shadowSubset ) lightShaderId = &lightShader->id(); } - const RtParamList allAttributes = mergedAttributes( m_attributes->instanceAttributes(), m_extraAttributes ); + const RtParamList allAttributes = mergedAttributes( m_attributes->lightInstanceAttributes(), m_extraAttributes ); const riley::LightInstanceResult result = m_session->modifyLightInstance( m_lightInstance, /* material = */ nullptr, diff --git a/src/IECoreRenderMan/Renderer.cpp b/src/IECoreRenderMan/Renderer.cpp index ae322d8e8a..43abd040f4 100644 --- a/src/IECoreRenderMan/Renderer.cpp +++ b/src/IECoreRenderMan/Renderer.cpp @@ -50,6 +50,7 @@ #include "PointInstancerCache.h" #include "Session.h" #include "Transform.h" +#include "USDMeshLight.h" #include "Volume.h" #include "GafferScene/Private/IECoreScenePreview/Renderer.h" @@ -150,24 +151,40 @@ class RenderManRenderer final : public IECoreScenePreview::Renderer auto typedAttributes = static_cast( attributes ); - ConstGeometryPrototypePtr geometryPrototype; + ConstGeometryPrototypePtr lightGeometryPrototype; + ConstGeometryPrototypePtr surfaceGeometryPrototype; if( objectSamples.size() ) { if( auto mesh = runTimeCast( objectSamples[0].get() ) ) { + if( typedAttributes->isUSDMeshLight() ) + { + surfaceGeometryPrototype = m_geometryPrototypeCache->get( objectSamples, times, typedAttributes, /* messageContext = */ name ); + if( !surfaceGeometryPrototype ) + { + return nullptr; + } + } + // RenderMan refuses to share mesh prototypes between GeometryInstances and // LightInstances, so we insert some blind data to give the mesh geometry // a different hash, causing the GeometryPrototypeCache to create a prototype // that won't be used by `Renderer::object()`. ObjectSamples uniquefiedObjectSamples = objectSamples; MeshPrimitivePtr meshCopy = mesh->copy(); + meshCopy->blindData()->writable().insert( g_forMeshLightBlindData ); uniquefiedObjectSamples[0] = meshCopy; - geometryPrototype = m_geometryPrototypeCache->get( uniquefiedObjectSamples, times, typedAttributes, /* messageContext = */ name ); + lightGeometryPrototype = m_geometryPrototypeCache->get( uniquefiedObjectSamples, times, typedAttributes, /* messageContext = */ name ); + + if( typedAttributes->isUSDMeshLight() ) + { + return new IECoreRenderMan::USDMeshLight( name, lightGeometryPrototype, surfaceGeometryPrototype, typedAttributes, m_materialCache.get(), m_lightLinker.get(), m_session ); + } } } - return new IECoreRenderMan::Light( geometryPrototype, typedAttributes, m_materialCache.get(), m_lightLinker.get(), m_session ); + return new IECoreRenderMan::Light( lightGeometryPrototype, typedAttributes, m_materialCache.get(), m_lightLinker.get(), m_session ); } ObjectInterfacePtr lightFilter( const std::string &name, const ObjectSamples &samples, const SampleTimes ×, const AttributesInterface *attributes ) override diff --git a/src/IECoreRenderMan/ShaderNetworkAlgo.cpp b/src/IECoreRenderMan/ShaderNetworkAlgo.cpp index 008a732fb2..24d7a7454b 100644 --- a/src/IECoreRenderMan/ShaderNetworkAlgo.cpp +++ b/src/IECoreRenderMan/ShaderNetworkAlgo.cpp @@ -892,6 +892,48 @@ ShaderNetworkPtr preprocessedNetwork( const IECoreScene::ShaderNetwork *shaderNe return result; } +template +T *attributeCast( const IECore::RunTimeTyped *v, const IECore::InternedString &name ) +{ + if( !v ) + { + return nullptr; + } + + T *t = IECore::runTimeCast( v ); + if( t ) + { + return t; + } + + IECore::msg( IECore::Msg::Warning, "IECoreRenderMan::ShaderNetworkAlgo", fmt::format( "Expected {} but got {} for attribute \"{}\".", T::staticTypeName(), v->typeName(), name.c_str() ) ); + return nullptr; +} + +template +const T *attribute( const CompoundObject::ObjectMap &attributes, IECore::InternedString name ) +{ + auto it = attributes.find( name ); + if( it == attributes.end() ) + { + return nullptr; + } + + return attributeCast( it->second.get(), name ); +} + +pair shaderNetworkAttribute( const CompoundObject::ObjectMap &attributes, const vector &attributeNames ) +{ + for( const auto &name : attributeNames ) + { + if( const auto *shaderNetwork = attribute( attributes, name ) ) + { + return { name, shaderNetwork }; + } + } + return { InternedString(), nullptr }; +} + } // namespace std::vector IECoreRenderMan::ShaderNetworkAlgo::convert( const IECoreScene::ShaderNetwork *network ) @@ -1021,8 +1063,11 @@ const InternedString g_diffuseParameter( "diffuse" ); const InternedString g_diffuseColorParameter( "diffuseColor" ); const InternedString g_diffuseDoubleSidedParameter( "diffuseDoubleSided" ); const InternedString g_diffuseGainParameter( "diffuseGain") ; +const InternedString g_emissionColorParameter( "emissionColor" ); const InternedString g_emissionFocusParameter( "emissionFocus" ); const InternedString g_emissionFocusTintParameter( "emissionFocusTint" ); +const InternedString g_emissiveColorParameter( "emissiveColor" ); +const InternedString g_emitColorParameter( "emitColor" ); const InternedString g_enableColorTemperatureParameter( "enableColorTemperature" ); const InternedString g_enableShadowsParameter( "enableShadows" ); const InternedString g_enableTemperatureParameter( "enableTemperature" ); @@ -1074,6 +1119,7 @@ const InternedString g_specularIorParameter( "specularIor" ); const InternedString g_specularModelTypeParameter( "specularModelType" ); const InternedString g_specularRoughnessParameter( "specularRoughness" ); const InternedString g_temperatureParameter( "temperature" ); +const InternedString g_textureColorParameter( "textureColor" ); const InternedString g_textureFileParameter( "texture:file" ); const InternedString g_textureFormatParameter( "texture:format" ); const InternedString g_typeParameter( "type" ); @@ -1084,6 +1130,9 @@ const InternedString g_widthParameter( "width" ); const std::string g_renderManLightNamespace( "ri:light:" ); +const InternedString g_lightAttributeName( "light" ); +const vector g_surfaceAttributeNames = { "ri:surface", "surface" }; + const std::vector g_pxrSurfaceParameters = { g_diffuseGainParameter, g_diffuseColorParameter, @@ -1208,6 +1257,51 @@ void replaceUSDShader( ShaderNetwork *network, InternedString handle, ShaderPtr } } +std::pair surfaceGlowParameters( const IECoreScene::ShaderNetwork *shaderNetwork ) +{ + ShaderNetwork::Parameter glowColorParameter; + ShaderNetwork::Parameter glowColorInput; + if( !shaderNetwork ) + { + return { glowColorParameter, glowColorInput }; + } + + for( const auto &[handle, shader] : shaderNetwork->shaders() ) + { + if( + shader->getName() == "PxrSurface" || + shader->getName() == "PxrLayerSurface" || + shader->getName() == "PxrMarschnerHair" + ) + { + glowColorParameter = { handle, g_glowColorParameter }; + break; + } + else if( shader->getName() == "LamaEmission" ) + { + glowColorParameter = { handle, g_emissionColorParameter }; + break; + } + else if( shader->getName() == "PxrDisney" || shader->getName() == "PxrConstant" ) + { + glowColorParameter = { handle, g_emitColorParameter }; + break; + } + else if( shader->getName() == "UsdPreviewSurface" ) + { + glowColorParameter = { handle, g_emissiveColorParameter }; + break; + } + } + + if( glowColorParameter ) + { + glowColorInput = shaderNetwork->input( glowColorParameter ); + } + + return { glowColorParameter, glowColorInput }; +} + } // namespace void IECoreRenderMan::ShaderNetworkAlgo::convertUSDShaders( ShaderNetwork *shaderNetwork ) @@ -1367,6 +1461,61 @@ M44f IECoreRenderMan::ShaderNetworkAlgo::usdLightTransform( const Shader *lightS return M44f(); } +ConstCompoundObjectPtr IECoreRenderMan::ShaderNetworkAlgo::convertUSDMeshLightAttributes( const CompoundObject *attributes ) +{ + const auto *lightNetwork = attribute( attributes->members(), g_lightAttributeName ); + if( !lightNetwork ) + { + return attributes; + } + + const Shader *outputShader = lightNetwork->outputShader(); + if( !outputShader || outputShader->getName() != "MeshLight" ) + { + return attributes; + } + + CompoundObjectPtr result = attributes->copy(); + + ShaderNetworkPtr newLightShaderNetwork = lightNetwork->copy(); + const ShaderNetwork *surfaceNetwork = shaderNetworkAttribute( attributes->members(), g_surfaceAttributeNames ).second; + + const auto &[glowColorParameter, glowColorInput] = surfaceGlowParameters( surfaceNetwork ); + + ShaderNetwork::Parameter lightOutputParameter = lightNetwork->getOutput(); + const Shader *lightOutputShader = lightNetwork->outputShader(); + + ShaderPtr newLightShader = new Shader( "PxrMeshLight", "ri:light" ); + transferUSDLightParameters( newLightShaderNetwork.get(), lightOutputParameter.shader, lightOutputShader, newLightShader.get() ); + transferUSDParameter( newLightShaderNetwork.get(), lightOutputParameter.shader, lightOutputShader, g_normalizeParameter, newLightShader.get(), g_areaNormalizeParameter, false ); + + // RenderMan's PxrMeshLight has a separate parameter `textureColor` we can use for the surface + // glow. It takes care of the tinting for us so we only need to take care of transferring the + // glow color and input network, if any. + if( glowColorParameter ) + { + const Color3f c = parameterValue( surfaceNetwork->getShader( glowColorParameter.shader ), glowColorParameter.name, Color3f( 0.f ) ); + newLightShader->parameters()[g_textureColorParameter] = new Color3fData( c ); + } + + if( glowColorInput ) + { + ShaderNetworkPtr glowNetwork = surfaceNetwork->copy(); + glowNetwork->setOutput( glowColorInput ); + IECoreScene::ShaderNetworkAlgo::removeUnusedShaders( glowNetwork.get() ); + ShaderNetwork::Parameter newGlowInput = IECoreScene::ShaderNetworkAlgo::addShaders( newLightShaderNetwork.get(), glowNetwork.get(), /* connections = */ true ); + + newLightShaderNetwork->addConnection( { newGlowInput, { lightOutputParameter.shader, g_textureColorParameter } } ); + } + + replaceUSDShader( newLightShaderNetwork.get(), lightOutputParameter.shader, std::move( newLightShader ) ); + IECoreScene::ShaderNetworkAlgo::removeUnusedShaders( newLightShaderNetwork.get() ); + + result->members()[g_lightAttributeName] = std::move( newLightShaderNetwork ); + + return result; +} + ////////////////////////////////////////////////////////////////////////// // `ShaderNetworkAlgo::evaluateVStructConditional()` implementation ////////////////////////////////////////////////////////////////////////// diff --git a/src/IECoreRenderMan/USDMeshLight.cpp b/src/IECoreRenderMan/USDMeshLight.cpp new file mode 100644 index 0000000000..30ed1bef45 --- /dev/null +++ b/src/IECoreRenderMan/USDMeshLight.cpp @@ -0,0 +1,82 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2026, Cinesite VFX Ltd. 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 John Haddon nor the names of +// any other contributors to this software 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. +// +////////////////////////////////////////////////////////////////////////// + +#include "USDMeshLight.h" + +#include "Light.h" +#include "Object.h" + +using namespace std; +using namespace Imath; +using namespace IECore; +using namespace IECoreRenderMan; + +USDMeshLight::USDMeshLight( const std::string &name, const ConstGeometryPrototypePtr &lightGeometryPrototype, const ConstGeometryPrototypePtr &surfaceGeometryPrototype, const Attributes *attributes, MaterialCache *materialCache, LightLinker *lightLinker, Session *session ) : + Light( lightGeometryPrototype, attributes, materialCache, lightLinker, session ) +{ + m_surface = new Object( name, surfaceGeometryPrototype, attributes, lightLinker, session ); +} + +USDMeshLight::~USDMeshLight() +{ +} + +void USDMeshLight::transform( const IECoreScenePreview::Renderer::TransformSamples &samples, const IECoreScenePreview::Renderer::SampleTimes × ) +{ + Light::transform( samples, times ); + m_surface->transform( samples, times ); +} + +bool USDMeshLight::attributes( const IECoreScenePreview::Renderer::AttributesInterface *attributes ) +{ + return Light::attributes( attributes ) && m_surface->attributes( attributes ); +} + +void USDMeshLight::link( const IECore::InternedString &type, const IECoreScenePreview::Renderer::ConstObjectSetPtr &objects ) +{ + Light::link( type, objects ); + m_surface->link( type, objects ); +} + +void USDMeshLight::assignID( uint32_t id ) +{ + m_surface->assignID( id ); +} + +void USDMeshLight::assignInstanceID( uint32_t id ) +{ + // \todo : This will be needed once our RenderMan backend supports encapsulated instancers +} diff --git a/src/IECoreRenderMan/USDMeshLight.h b/src/IECoreRenderMan/USDMeshLight.h new file mode 100644 index 0000000000..4cac6e8959 --- /dev/null +++ b/src/IECoreRenderMan/USDMeshLight.h @@ -0,0 +1,76 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2026, Cinesite VFX Ltd. 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 John Haddon nor the names of +// any other contributors to this software 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. +// +////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "GafferScene/Private/IECoreScenePreview/Renderer.h" + +#include "Attributes.h" +#include "GeometryPrototypeCache.h" +#include "Light.h" +#include "LightLinker.h" +#include "MaterialCache.h" +#include "Session.h" + +#include "Riley.h" + +namespace IECoreRenderMan +{ + +class USDMeshLight : public Light +{ + + public : + + USDMeshLight( const std::string &name, const ConstGeometryPrototypePtr &lightGeometryPrototype, const ConstGeometryPrototypePtr &surfaceGeometryPrototype, const Attributes *attributes, MaterialCache *materialCache, LightLinker *lightLinker, Session *session ); + ~USDMeshLight() override; + + // ObjectInterface overrides + // ========================= + + void transform( const IECoreScenePreview::Renderer::TransformSamples &samples, const IECoreScenePreview::Renderer::SampleTimes × ) override; + bool attributes( const IECoreScenePreview::Renderer::AttributesInterface *attributes ) override; + void link( const IECore::InternedString &type, const IECoreScenePreview::Renderer::ConstObjectSetPtr &objects ) override; + void assignID( uint32_t id ) override; + void assignInstanceID( uint32_t id ) override; + + private : + + IECoreScenePreview::Renderer::ObjectInterfacePtr m_surface; + +}; + +} // namespace IECoreRenderMan diff --git a/src/IECoreRenderManModule/IECoreRenderManModule.cpp b/src/IECoreRenderManModule/IECoreRenderManModule.cpp index a4d02424e0..5dead1160c 100644 --- a/src/IECoreRenderManModule/IECoreRenderManModule.cpp +++ b/src/IECoreRenderManModule/IECoreRenderManModule.cpp @@ -36,6 +36,8 @@ #include "boost/python.hpp" +#include "IECorePython/ScopedGILRelease.h" + #include "IECoreRenderMan/ShaderNetworkAlgo.h" #include "prmanapi.h" @@ -70,6 +72,13 @@ ShaderNetworkAlgo::VStructAction evaluateVStructConditionalWrapper( const std::s ); } +IECore::CompoundObjectPtr convertUSDMeshLightAttributesWrapper( const IECore::CompoundObject &attributes, bool copy ) +{ + IECorePython::ScopedGILRelease r; + IECore::ConstCompoundObjectPtr result = ShaderNetworkAlgo::convertUSDMeshLightAttributes( &attributes ); + return copy ? result->copy() : boost::const_pointer_cast( result ); +} + } // namespace BOOST_PYTHON_MODULE( _IECoreRenderMan ) @@ -85,6 +94,8 @@ BOOST_PYTHON_MODULE( _IECoreRenderMan ) def( "convertUSDShaders", &ShaderNetworkAlgo::convertUSDShaders ); def( "usdLightTransform", &ShaderNetworkAlgo::usdLightTransform ); + def( "convertUSDMeshLightAttributes", &convertUSDMeshLightAttributesWrapper, ( arg_( "_copy" ) = true ) ); + { scope s = class_( "VStructAction" ) .def_readonly( "type", &ShaderNetworkAlgo::VStructAction::type ) From 3880a1227394a9d314cd8954787f4fd94be4d41f Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Fri, 7 Aug 2026 12:18:54 -0400 Subject: [PATCH 09/11] 3Delight : Add support for USD mesh lights --- Changes.md | 2 +- include/IECoreDelight/ShaderNetworkAlgo.h | 3 + python/IECoreDelightTest/RendererTest.py | 91 ++++++ .../ShaderNetworkAlgoTest.py | 308 ++++++++++++++++++ src/IECoreDelight/Renderer.cpp | 289 +++++++++++++--- src/IECoreDelight/ShaderNetworkAlgo.cpp | 192 +++++++++++ .../IECoreDelightModule.cpp | 15 + 7 files changed, 862 insertions(+), 38 deletions(-) diff --git a/Changes.md b/Changes.md index b43fc8a4a2..af98da7525 100644 --- a/Changes.md +++ b/Changes.md @@ -11,7 +11,7 @@ Features - Faster rendering. - USDMeshLight : - Added node to add necessary attributes to geometry to convert to a USDMeshLight. - - Added Arnold and RenderMan rendering. + - Added Arnold, RenderMan and 3Delight rendering. Improvements ------------ diff --git a/include/IECoreDelight/ShaderNetworkAlgo.h b/include/IECoreDelight/ShaderNetworkAlgo.h index 204ae1f549..9ebf858648 100644 --- a/include/IECoreDelight/ShaderNetworkAlgo.h +++ b/include/IECoreDelight/ShaderNetworkAlgo.h @@ -61,6 +61,9 @@ void updateLightGeometry( const IECoreScene::ShaderNetwork *shaderNetwork, NSICo /// tests. IECOREDELIGHT_API void convertUSDShaders( IECoreScene::ShaderNetwork *shaderNetwork ); +/// Returns a modified set of attributes conforming to the USDMeshLight specification. +IECOREDELIGHT_API IECore::ConstCompoundObjectPtr convertUSDMeshLightAttributes( const IECore::CompoundObject *attributes ); + } // namespace ShaderNetworkAlgo } // namespace IECoreDelight \ No newline at end of file diff --git a/python/IECoreDelightTest/RendererTest.py b/python/IECoreDelightTest/RendererTest.py index 66a70702bd..f0b7ca2f55 100644 --- a/python/IECoreDelightTest/RendererTest.py +++ b/python/IECoreDelightTest/RendererTest.py @@ -1547,6 +1547,97 @@ def __cylinderMesh( length, radius ) : return { "P": p, "P.indices": pIndices, "N": n, "N.indices": nIndices } + def testUSDMeshLight( self ) : + + for surfaceShader in [ None, IECoreScene.Shader( "dlPrincipled", "osl:surface", {} ) ] : + + with self.subTest( surfaceShader = surfaceShader ) : + + r = GafferScene.Private.IECoreScenePreview.Renderer.create( + "3Delight", + GafferScene.Private.IECoreScenePreview.Renderer.RenderType.SceneDescription, + str( self.temporaryDirectory() / "test.nsia" ), + ) + + attributes = IECore.CompoundObject( + { + "light" : IECoreScene.ShaderNetwork( + { + "lightHandle" : IECoreScene.Shader( "MeshLight", "light", { "exposure" : 2.0 } ) + }, + output = "lightHandle" + ) + } + ) + if surfaceShader is not None : + attributes["osl:surface"] = IECoreScene.ShaderNetwork( + { "surfaceHandle" : surfaceShader }, + output = "surfaceHandle", + ) + + r.light( + "testMeshLight", + IECoreScene.MeshPrimitive.createBox( imath.Box3f( imath.V3f( -0.5 ), imath.V3f( 0.5 ) ) ), + r.attributes( attributes ) + ).transform( imath.M44f().translate( imath.V3f( 1.0, 2.0, 3.0 ) ) * imath.M44f().rotate( IECore.degreesToRadians( imath.V3f( 10.0, 20.0, 30.0 ) ) ) ) + + r.render() + + del r + + nsi = self.__parseDict( self.temporaryDirectory() / "test.nsia" ) + + cubeProperties = { + "P" : [ imath.V3f( -0.5, -0.5, -0.5 ), imath.V3f( 0.5, -0.5, -0.5 ), imath.V3f( 0.5, 0.5, -0.5 ), imath.V3f( -0.5, 0.5, -0.5 ), imath.V3f( 0.5, -0.5, 0.5 ), imath.V3f( 0.5, 0.5, 0.5 ), imath.V3f( -0.5, -0.5, 0.5 ), imath.V3f( -0.5, 0.5, 0.5 ) ], + "P.indices" : [ 3, 2, 1, 0, 1, 2, 5, 4, 4, 5, 7, 6, 6, 7, 3, 0, 2, 3, 7, 5, 0, 1, 4, 6 ], + "N" : [ imath.V3f( 0, 0, 1 ), imath.V3f( 0, 0, -1 ), imath.V3f( 0, 1, 0 ), imath.V3f( 0, -1, 0 ), imath.V3f( 1, 0, 0 ), imath.V3f( -1, 0, 0 ) ], + "N.indices" : [ 1, 1, 1, 1, 4, 4, 4, 4, 0, 0, 0, 0, 5, 5, 5, 5, 2, 2, 2, 2, 3, 3, 3, 3 ], + } + + self.__assertLightSettings( + nsi, + [ + ( + "MeshLight:light", + imath.V3f( 1.0, 2.0, 3.0 ), + imath.V3f( 10.0, 20.0, 30.0 ), + "mesh", + cubeProperties, + "areaLight.oso", + { "exposure" : 2.0 }, + { "exposure" : 2.0 }, + ) + ] + ) + + # Not a light, but we can make use of the assertion nontheless + self.__assertLightSettings( + nsi, + [ + ( + "MeshLight:surface", + imath.V3f( 1.0, 2.0, 3.0 ), + imath.V3f( 10.0, 20.0, 30.0 ), + "mesh", + cubeProperties, + "Constant.oso" if surfaceShader is None else surfaceShader.name + ".oso", + {}, + {}, + ) + ] + ) + + for k, v in nsi.items() : + if k.startswith( "attributes:" ) and not k.endswith( ":usdMeshLight" ) : + self.assertEqual( v["visibility.shadow"], 0 ) + if k.startswith( "attributes:" ) and k.endswith( ":usdMeshLight" ) : + self.assertEqual( v["visibility.camera"], 0 ) + self.assertEqual( v["visibility.reflection"], 0 ) + self.assertEqual( v["visibility.refraction"], 0 ) + self.assertEqual( v["visibility.shadow"], 1 ) + self.assertEqual( v["visibility.specular"], 0 ) + + def testOutputLayerNames( self ) : renderer = GafferScene.Private.IECoreScenePreview.Renderer.create( diff --git a/python/IECoreDelightTest/ShaderNetworkAlgoTest.py b/python/IECoreDelightTest/ShaderNetworkAlgoTest.py index 2e1b828a9d..ea028e536c 100644 --- a/python/IECoreDelightTest/ShaderNetworkAlgoTest.py +++ b/python/IECoreDelightTest/ShaderNetworkAlgoTest.py @@ -554,3 +554,311 @@ def testConvertUSDUVTextureUDIM( self ) : self.assertEqual( texture.name, "__usd/__usdUVTexture" ) self.assertEqual( texture.parameters["file"].value, "test.UDIM.png" ) self.assertEqual( texture.parameters["file_meta_colorspace"].value, "sRGB" ) + + def testUSDMeshLight( self ) : + + attributes = IECore.CompoundObject( + { + "light" : IECoreScene.ShaderNetwork( + shaders = { "light" : IECoreScene.Shader( "MeshLight", "light" ) }, + output = "light" + ) + } + ) + + modifiedAttributes = IECoreDelight.ShaderNetworkAlgo.convertUSDMeshLightAttributes( attributes ) + lightNetwork = modifiedAttributes["light"] + + self.assertEqual( len( lightNetwork.shaders() ), 1 ) + shader = lightNetwork.getShader( "light" ) + self.assertIsNotNone( shader ) + self.assertEqual( shader.name, "areaLight" ) + self.assertEqual( shader.type, "osl:light" ) + self.assertFalse( lightNetwork.input( ( "light", "i_color" ) ) ) + + incandescenceParameter = "incandescence" + for shaderName in [ + "_3DelightGlass", + "_3DelightMaterial", + "anisotropic", + "blinn", + "dl3DelightMaterial", + "dlConstant", + "dlGlass", + "dlPrincipled", + "dlStandard", + "dlSubstance", + "dlToon", + "lambert", + "material3Delight", + "material3DelightGlass", + ] : + with self.subTest( shaderName = shaderName ) : + + # Surface color only, light color only (no color inputs) + + attributes = IECore.CompoundObject( + { + "light" : IECoreScene.ShaderNetwork( + shaders = { + "light" : IECoreScene.Shader( + "MeshLight", "light", + { "color" : imath.Color3f( 0.1, 0.2, 0.3 ), "intensity" : 2.0, "exposure" : 3.0 } + ) + }, + output = "light" + ), + "surface" : IECoreScene.ShaderNetwork( + shaders = { + "surface" : IECoreScene.Shader( + shaderName, "osl:surface", + { incandescenceParameter : imath.Color3f( 0.4, 0.5, 0.6 ) } + ), + }, + output = "surface" + ) + } + ) + + originalSurfaceNetwork = attributes["surface"].copy() + modifiedAttributes = IECoreDelight.ShaderNetworkAlgo.convertUSDMeshLightAttributes( attributes ) + lightNetwork = modifiedAttributes["light"] + surfaceNetwork = modifiedAttributes["surface"] + + self.assertEqual( surfaceNetwork, originalSurfaceNetwork ) + + self.assertEqual( len( lightNetwork.shaders() ), 1 ) + light = lightNetwork.getShader( "light" ) + self.assertIsNotNone( light ) + self.assertEqual( light.name, "areaLight" ) + self.assertEqual( light.type, "osl:light" ) + for i in range( 0, 3 ) : + self.assertAlmostEqual( light.parameters["i_color"].value[i], imath.Color3f( 0.1 * 0.4, 0.2 * 0.5, 0.3 * 0.6 )[i] ) + self.assertEqual( light.parameters["intensity"].value, 2.0 ) + self.assertEqual( light.parameters["exposure"].value, 3.0 ) + self.assertFalse( lightNetwork.input( ( "light", "i_color" ) ) ) + + # Surface with color input, light with color only + + for index, lightColor in enumerate( [ imath.Color3f( 0.0 ), imath.Color3f( 0.0, 0.5, 1.0 ), imath.Color3f( 1.0 ) ] ) : + + with self.subTest( lightColor = lightColor ) : + + attributes = IECore.CompoundObject ( + { + "light" : IECoreScene.ShaderNetwork( + shaders = { "light" : IECoreScene.Shader( "MeshLight", "light", { "color" : lightColor } ) }, + output = "light" + ), + "surface" : IECoreScene.ShaderNetwork( + shaders = { + "layered" : IECoreScene.Shader( "dlLayeredMaterial", "osl:surface" ), + "surface" : IECoreScene.Shader( + shaderName, "osl:surface", + { incandescenceParameter : imath.Color3f( 0.4, 0.5, 0.6 ) } + ), + "correct" : IECoreScene.Shader( + "dlColorCorrection", "osl:surface", + { "gain" : imath.Color3f( 2.0 ) } + ), + "texture" : IECoreScene.Shader( + "dlTexture", "osl:surface", + { "textureFile" : "testFile.tx" } + ), + }, + connections = [ + ( ( "texture", "outColor" ), ( "correct", "input" ) ), + ( ( "correct", "outColor" ), ( "surface", incandescenceParameter ) ), + ( ( "surface", "outColor" ), ( "layered", "i_color" ) ), + ], + output = "layered" + ) + } + ) + + originalSurfaceNetwork = attributes["surface"].copy() + modifiedAttributes = IECoreDelight.ShaderNetworkAlgo.convertUSDMeshLightAttributes( attributes ) + lightNetwork = modifiedAttributes["light"] + surfaceNetwork = modifiedAttributes["surface"] + + self.assertEqual( surfaceNetwork, originalSurfaceNetwork ) + + self.assertEqual( + len( lightNetwork.shaders() ), + [ + 1, # Light color is 0, so no color input needed + 4, # Shaders from the light, the surface color inputs and a tint + 3, # Same as above but no tint needed for white light + ][index] + ) + + light = lightNetwork.getShader( "light" ) + self.assertIsNotNone( light ) + self.assertEqual( light.name, "areaLight" ) + self.assertEqual( light.type, "osl:light" ) + for i in range( 0, 3 ) : + self.assertAlmostEqual( light.parameters["i_color"].value[i], ( lightColor * imath.Color3f( 0.4, 0.5, 0.6 ) )[i] ) + + if lightColor != imath.Color3f( 0 ) : + correct = lightNetwork.getShader( "correct" ) + self.assertIsNotNone( correct ) + self.assertEqual( correct.parameters["gain"].value, imath.Color3f( 2 ) ) + + texture = lightNetwork.getShader( "texture" ) + self.assertIsNotNone( texture ) + self.assertEqual( texture.parameters["textureFile"].value, "testFile.tx" ) + + if lightColor != imath.Color3f( 1 ) : + tint = lightNetwork.getShader( "tint" ) + self.assertIsNotNone ( tint ) + self.assertEqual( tint.parameters["b"].value, lightColor ) + self.assertEqual( lightNetwork.input( ( "light", "i_color" ) ), ( "tint", "out" ) ) + self.assertEqual( lightNetwork.input( ( "tint", "a" ) ), ( "correct", "outColor" ) ) + else : + self.assertEqual( lightNetwork.input( ( "light", "i_color" ) ), ( "correct", "outColor" ) ) + + self.assertEqual( lightNetwork.input( ( "correct", "input" ) ), ( "texture", "outColor" ) ) + + # Surface color only, light with color input + + for index, surfaceEmitColor in enumerate( [ imath.Color3f( 0.0 ), imath.Color3f( 0.0, 0.5, 1.0 ), imath.Color3f( 1.0 ) ] ) : + + with self.subTest( surfaceEmitColor = surfaceEmitColor ) : + + attributes = IECore.CompoundObject( + { + "light" : IECoreScene.ShaderNetwork( + shaders = { + "light" : IECoreScene.Shader( "MeshLight", "light", { "color" : imath.Color3f( 0.1, 0.2, 0.3 ) } ), + "correct" : IECoreScene.Shader( "dlColorCorrection", "osl:surface", { "gain" : imath.Color3f( 2 ) } ), + "texture" : IECoreScene.Shader( "dlTexture", "osl:surface", { "textureFile" : "testFile.tx" } ), + }, + connections = [ + ( ( "texture", "outColor" ), ( "correct", "input" ) ), + ( ( "correct", "outColor" ), ( "light", "color" ) ), + ], + output = "light" + ), + "surface" : IECoreScene.ShaderNetwork( + shaders = { + "surface" : IECoreScene.Shader( shaderName, "osl:surface", { incandescenceParameter : surfaceEmitColor } ), + }, + output = "surface" + ) + } + ) + + originalLightNetwork = attributes["light"].copy() + originalSurfaceNetwork = attributes["surface"].copy() + modifiedAttributes = IECoreDelight.ShaderNetworkAlgo.convertUSDMeshLightAttributes( attributes ) + lightNetwork = modifiedAttributes["light"] + surfaceNetwork = modifiedAttributes["surface"] + + self.assertEqual( surfaceNetwork, originalSurfaceNetwork ) + + self.assertEqual( + len( lightNetwork.shaders() ), + [ + 1, # No surface color to transfer so no input needed + len( originalLightNetwork.shaders() ) + 1, # Original shaders plus tint + len( originalLightNetwork.shaders() ), # Full white surface glow, no tint needed + ][index] + ) + + light = lightNetwork.getShader( "light" ) + self.assertIsNotNone( light ) + self.assertEqual( light.name, "areaLight" ) + self.assertEqual( light.type, "osl:light" ) + for i in range( 0, 3 ) : + self.assertAlmostEqual( light.parameters["i_color"].value[i], ( imath.Color3f( 0.1, 0.2, 0.3 ) * surfaceEmitColor )[i] ) + + if surfaceEmitColor != imath.Color3f( 0.0 ) : + correct = lightNetwork.getShader( "correct" ) + self.assertIsNotNone( correct ) + self.assertEqual( correct.parameters["gain"].value, imath.Color3f( 2 ) ) + + texture = lightNetwork.getShader( "texture" ) + self.assertIsNotNone( texture ) + self.assertEqual( texture.parameters["textureFile"].value, "testFile.tx" ) + + if surfaceEmitColor == imath.Color3f( 1 ) : + self.assertEqual( lightNetwork.input( ( "light", "i_color" ) ), ( "correct", "outColor" ) ) + else : + tint = lightNetwork.getShader( "tint" ) + self.assertIsNotNone ( tint ) + self.assertEqual( tint.parameters["a"].value, surfaceEmitColor ) + self.assertEqual( lightNetwork.input( ( "light", "i_color" ) ), ( "tint", "out" ) ) + self.assertEqual( lightNetwork.input( ( "tint", "b" ) ), ( "correct", "outColor" ) ) + self.assertEqual( lightNetwork.input( ( "correct", "input" ) ), ( "texture", "outColor" ) ) + + # Light and surface with color inputs + + attributes = IECore.CompoundObject( + { + "light" : IECoreScene.ShaderNetwork( + shaders = { + "light" : IECoreScene.Shader( "MeshLight", "light", { "color" : imath.Color3f( 0.1, 0.2, 0.3 ) } ), + "lightTexture" : IECoreScene.Shader( "dlTexture", "osl:surface", { "textureFile" : "lightTestFile.tx" } ), + }, + connections = [ ( ( "lightTexture", "outColor" ), ( "light", "color" ) ) ], + output = "light" + ), + "surface" : IECoreScene.ShaderNetwork( + shaders = { + "surface" : IECoreScene.Shader( + shaderName, "osl:surface", + { incandescenceParameter : imath.Color3f( 0.4, 0.5, 0.6 ) } + ), + "correct" : IECoreScene.Shader( + "dlColorCorrection", "osl:surface", + { "gain" : imath.Color3f( 2 ) } + ), + "texture" : IECoreScene.Shader( + "dlTexture", "osl:surface", + { "textureFile" : "testFile.tx" } + ), + }, + connections = [ + ( ( "texture", "outColor" ), ( "correct", "input" ) ), + ( ( "correct", "outColor" ), ( "surface", incandescenceParameter ) ), + ], + output = "surface" + ) + } + ) + + originalSurfaceNetwork = attributes["surface"].copy() + modifiedAttributes = IECoreDelight.ShaderNetworkAlgo.convertUSDMeshLightAttributes( attributes ) + lightNetwork = modifiedAttributes["light"] + surfaceNetwork = modifiedAttributes["surface"] + + self.assertEqual( surfaceNetwork, originalSurfaceNetwork ) + + self.assertEqual( len( lightNetwork.shaders() ), 5 ) + + light = lightNetwork.getShader( "light" ) + self.assertIsNotNone( light ) + self.assertEqual( light.name, "areaLight" ) + self.assertEqual( light.type, "osl:light" ) + for i in range( 0, 3 ) : + self.assertAlmostEqual( light.parameters["i_color"].value[i], (imath.Color3f( 0.1, 0.2, 0.3 ) * imath.Color3f( 0.4, 0.5, 0.6 ) )[i] ) + + correct = lightNetwork.getShader( "correct" ) + self.assertIsNotNone( correct ) + self.assertEqual( correct.parameters["gain"].value, imath.Color3f( 2 ) ) + + texture = lightNetwork.getShader( "texture" ) + self.assertIsNotNone( texture ) + self.assertEqual( texture.parameters["textureFile"].value, "testFile.tx" ) + + lightTexture = lightNetwork.getShader( "lightTexture" ) + self.assertIsNotNone( lightTexture ) + self.assertEqual( lightTexture.parameters["textureFile"].value, "lightTestFile.tx" ) + + tint = lightNetwork.getShader( "tint" ) + self.assertIsNotNone( tint ) + + self.assertEqual( lightNetwork.input( ( "light", "i_color" ) ), ( "tint", "out" ) ) + self.assertEqual( lightNetwork.input( ( "tint", "a" ) ), ( "correct", "outColor" ) ) + self.assertEqual( lightNetwork.input( ( "correct", "input" ) ), ( "texture", "outColor" ) ) + self.assertEqual( lightNetwork.input( ( "tint", "b" ) ), ( "lightTexture", "outColor" ) ) diff --git a/src/IECoreDelight/Renderer.cpp b/src/IECoreDelight/Renderer.cpp index 42c717182a..d496248a2e 100644 --- a/src/IECoreDelight/Renderer.cpp +++ b/src/IECoreDelight/Renderer.cpp @@ -678,11 +678,19 @@ namespace // surfaces), we support "light" attributes as well for compatibility with // other renderers and some specific workflows in Gaffer. std::array g_surfaceShaderAttributeNames = { "osl:light", "light", "osl:surface", "surface" }; +std::array g_USDMeshLightAttributeNames = { "light", "osl:light" }; +std::array g_USDMeshLightSurfaceShaderAttributeNames = { "osl:surface", "surface" }; std::array g_volumeShaderAttributeNames = { "osl:volume", "volume" }; std::array g_displacementShaderAttributeNames = { "osl:displacement", "displacement" }; const InternedString g_USDLightAttributeName = "light"; const InternedString g_USDSurfaceAttributeName = "surface"; +const InternedString g_visibilityCameraAttributeName = "dl:visibility.camera"; +const InternedString g_visibilityReflectionAttributeName = "dl:visibility.reflection"; +const InternedString g_visibilityRefractionAttributeName = "dl:visibility.refraction"; +const InternedString g_visibilityShadowAttributeName = "dl:visibility.shadow"; +const InternedString g_visibilitySpecularAttributeName = "dl:visibility.specular"; + const IECore::InternedString g_setsAttributeName( "sets" ); const IECore::InternedString g_lightMuteAttributeName( "light:mute" ); @@ -695,12 +703,47 @@ class DelightAttributes : public IECoreScenePreview::Renderer::AttributesInterfa : m_handle( context, "attributes:" + attributes->Object::hash().toString(), ownership, "attributes", {} ), m_lightMute( false ), m_hash( attributes->Object::hash() ) { - for( const auto &attributeName : g_surfaceShaderAttributeNames ) + bool isUSDMeshLight = false; + for( const auto &attributeName : g_USDMeshLightAttributeNames ) { - m_surfaceShader = shader(attributeName, attributes, shaderCache ); - if( m_surfaceShader ) + if( auto usdLightNetwork = attributes->member( attributeName ) ) { - break; + if( const Shader *shader = usdLightNetwork->outputShader() ) + { + if( shader->getName() == "MeshLight" ) + { + m_USDMeshLightHandle = DelightHandle( context, "attributes:" + attributes->Object::hash().toString() + ":usdMeshLight", ownership, "attributes", {} ); + isUSDMeshLight = true; + break; + } + } + } + } + + ConstCompoundObjectPtr modifiedAttributes = IECoreDelight::ShaderNetworkAlgo::convertUSDMeshLightAttributes( attributes ); + attributes = modifiedAttributes.get(); + + if( !isUSDMeshLight ) + { + for( const auto &attributeName : g_surfaceShaderAttributeNames ) + { + m_surfaceShader = shader(attributeName, attributes, shaderCache ); + if( m_surfaceShader ) + { + break; + } + } + } + else + { + m_USDMeshLightShader = shader(g_USDLightAttributeName, attributes, shaderCache ); + for( const auto &attributeName : g_USDMeshLightSurfaceShaderAttributeNames ) + { + m_surfaceShader = shader(attributeName, attributes, shaderCache ); + if( m_surfaceShader ) + { + break; + } } } @@ -730,7 +773,8 @@ class DelightAttributes : public IECoreScenePreview::Renderer::AttributesInterfa } } - ParameterList params; + ParameterList surfaceParams; + ParameterList usdMeshLightParams; for( const auto &m : attributes->members() ) { if( m.first == g_setsAttributeName ) @@ -747,7 +791,29 @@ class DelightAttributes : public IECoreScenePreview::Renderer::AttributesInterfa { if( const Data *d = reportedCast( m.second.get(), "attribute", m.first ) ) { - params.add( m.first.c_str() + 3, d, true ); + if( this->isUSDMeshLight() ) + { + // USDMeshLight requires some attributes to be set to specific values + if( m.first != g_visibilityShadowAttributeName ) + { + surfaceParams.add( m.first.c_str() + 3, d, true ); + } + + if( + m.first != g_visibilityCameraAttributeName && + m.first != g_visibilityReflectionAttributeName && + m.first != g_visibilityRefractionAttributeName && + m.first != g_visibilityShadowAttributeName && + m.first != g_visibilitySpecularAttributeName + ) + { + usdMeshLightParams.add( m.first.c_str() + 3, d, true ); + } + } + else + { + surfaceParams.add( m.first.c_str() + 3, d, true ); + } } } else if( boost::starts_with( m.first.string(), "render:" ) ) @@ -758,7 +824,8 @@ class DelightAttributes : public IECoreScenePreview::Renderer::AttributesInterfa { if( const Data *d = reportedCast( m.second.get(), "attribute", m.first ) ) { - params.add( m.first.c_str(), d, true ); + surfaceParams.add( m.first.c_str(), d, true ); + usdMeshLightParams.add( m.first.c_str(), d, true ); } } else if( boost::contains( m.first.string(), ":" ) || m.first == g_USDLightAttributeName || m.first == g_USDSurfaceAttributeName ) @@ -772,7 +839,40 @@ class DelightAttributes : public IECoreScenePreview::Renderer::AttributesInterfa } } - NSISetAttribute( m_handle.context(), m_handle.name(), params.size(), params.data() ); + if( this->isUSDMeshLight() ) + { + static BoolDataPtr g_trueData = new BoolData( true ); + static BoolDataPtr g_falseData = new BoolData( false ); + + surfaceParams.add( g_visibilityShadowAttributeName.c_str() + 3, g_falseData.get(), true ); + + usdMeshLightParams.add( g_visibilityCameraAttributeName.c_str() + 3, g_falseData.get(), true ); + usdMeshLightParams.add( g_visibilityReflectionAttributeName.c_str() + 3, g_falseData.get(), true ); + usdMeshLightParams.add( g_visibilityRefractionAttributeName.c_str() + 3, g_falseData.get(), true ); + usdMeshLightParams.add( g_visibilityShadowAttributeName.c_str() + 3, g_trueData.get(), true ); + usdMeshLightParams.add( g_visibilitySpecularAttributeName.c_str() + 3, g_falseData.get(), true ); + + NSISetAttribute( m_USDMeshLightHandle.context(), m_USDMeshLightHandle.name(), usdMeshLightParams.size(), usdMeshLightParams.data() ); + + NSIConnect( + context, + m_USDMeshLightShader->handle().name(), "", + m_USDMeshLightHandle.name(), "surfaceshader", + 0, nullptr + ); + + if( m_displacementShader ) + { + NSIConnect( + context, + m_displacementShader->handle().name(), "", + m_USDMeshLightHandle.name(), "displacementshader", + 0, nullptr + ); + } + } + + NSISetAttribute( m_handle.context(), m_handle.name(), surfaceParams.size(), surfaceParams.data() ); if( !m_surfaceShader ) { @@ -817,14 +917,19 @@ class DelightAttributes : public IECoreScenePreview::Renderer::AttributesInterfa const ShaderNetwork *usdLightShader() const { - return m_usdLightShader.get(); + return !m_USDMeshLightShader ? m_usdLightShader.get() : nullptr; } - const DelightHandle &handle() const + const DelightHandle &surfaceHandle() const { return m_handle; } + const DelightHandle &lightHandle() const + { + return isUSDMeshLight() ? m_USDMeshLightHandle : m_handle; + } + bool lightMute() const { return m_lightMute; @@ -835,6 +940,11 @@ class DelightAttributes : public IECoreScenePreview::Renderer::AttributesInterfa return m_hash; } + bool isUSDMeshLight() const + { + return m_USDMeshLightShader != nullptr; + } + private : static ConstDelightShaderPtr shader( const IECore::InternedString &name, const IECore::CompoundObject *attributes, ShaderCache *shaderCache ) @@ -850,9 +960,11 @@ class DelightAttributes : public IECoreScenePreview::Renderer::AttributesInterfa } DelightHandle m_handle; + DelightHandle m_USDMeshLightHandle; ConstDelightShaderPtr m_surfaceShader; ConstDelightShaderPtr m_volumeShader; ConstDelightShaderPtr m_displacementShader; + ConstDelightShaderPtr m_USDMeshLightShader; ConstShaderNetworkPtr m_usdLightShader; bool m_lightMute; @@ -1090,6 +1202,40 @@ class DelightObject: public IECoreScenePreview::Renderer::ObjectInterface bool attributes( const IECoreScenePreview::Renderer::AttributesInterface *attributes ) override { + return attributesInternal( + attributes, + []( const DelightAttributes *a ) -> const DelightHandle & + { + return a->surfaceHandle(); + } + ); + } + + void link( const IECore::InternedString &type, const IECoreScenePreview::Renderer::ConstObjectSetPtr &objects ) override + { + } + + void assignID( uint32_t id ) override + { + assignIDInternal( id, "cortexID" ); + } + + void assignInstanceID( uint32_t instanceID ) override + { + // This isn't actually used yet, but it will be ready to go if we add support for encapsulation + // to our 3delight backend so we need to deal with encapsulated instancers. + assignIDInternal( instanceID, "cortexInstanceID" ); + } + + protected : + + template + bool attributesInternal( + const IECoreScenePreview::Renderer::AttributesInterface *attributes, + HandleFunctor &&handleFunctor // Signature : const DelightHandle &functor( const DelightAttributes * ) + ) + { + auto castAttributes = static_cast( attributes ); if( m_attributes ) { if( attributes == m_attributes ) @@ -1097,29 +1243,34 @@ class DelightObject: public IECoreScenePreview::Renderer::ObjectInterface return true; } + if( m_attributes->isUSDMeshLight() != castAttributes->isUSDMeshLight() ) + { + return false; + } + NSIDisconnect( m_transformHandle.context(), - m_attributes->handle().name(), "", + handleFunctor( m_attributes.get() ).name(), "", m_transformHandle.name(), "geometryattributes" ); NSIDisconnect( m_transformHandle.context(), - m_attributes->handle().name(), "", + handleFunctor( m_attributes.get() ).name(), "", m_transformHandle.name(), "shaderattributes" ); } - m_attributes = static_cast( attributes ); + m_attributes = castAttributes; NSIConnect( m_transformHandle.context(), - m_attributes->handle().name(), "", + handleFunctor( m_attributes.get() ).name(), "", m_transformHandle.name(), "geometryattributes", 0, nullptr ); NSIConnect( m_transformHandle.context(), - m_attributes->handle().name(), "", + handleFunctor( m_attributes.get() ).name(), "", m_transformHandle.name(), "shaderattributes", 0, nullptr @@ -1128,24 +1279,6 @@ class DelightObject: public IECoreScenePreview::Renderer::ObjectInterface return true; } - void link( const IECore::InternedString &type, const IECoreScenePreview::Renderer::ConstObjectSetPtr &objects ) override - { - } - - void assignID( uint32_t id ) override - { - assignIDInternal( id, "cortexID" ); - } - - void assignInstanceID( uint32_t instanceID ) override - { - // This isn't actually used yet, but it will be ready to go if we add support for encapsulation - // to our 3delight backend so we need to deal with encapsulated instancers. - assignIDInternal( instanceID, "cortexInstanceID" ); - } - - protected : - const DelightHandle m_transformHandle; // We keep a reference to the prototype and attributes so that they // remain alive for at least as long as the object does. @@ -1211,7 +1344,19 @@ class DelightLight : public DelightObject bool attributes( const IECoreScenePreview::Renderer::AttributesInterface *attributes ) override { const bool wasMuted = m_attributes && m_attributes->lightMute(); - DelightObject::attributes( attributes ); + + if( + !attributesInternal( + attributes, + []( const DelightAttributes *a ) -> const DelightHandle & + { + return a->lightHandle(); + } + ) + ) + { + return false; + } if( wasMuted && !m_attributes->lightMute() ) { @@ -1434,14 +1579,14 @@ class PointInstancerCache : public IECore::RefCounted NSIConnect( m_context, - typedAttributes->handle().name(), "", + typedAttributes->surfaceHandle().name(), "", transformHandle.c_str(), "geometryattributes", 0, nullptr ); NSIConnect( m_context, - typedAttributes->handle().name(), "", + typedAttributes->surfaceHandle().name(), "", transformHandle.c_str(), "shaderattributes", 0, nullptr @@ -1531,6 +1676,67 @@ class DelightInstancerObject : public DelightObject } // namespace +////////////////////////////////////////////////////////////////////////// +// DelightUSDMeshLight +////////////////////////////////////////////////////////////////////////// + + +namespace +{ + +class DelightUSDMeshLight : public DelightLight +{ + public : + + DelightUSDMeshLight( NSIContext_t context, const std::string &name, DelightHandleSharedPtr prototype, DelightHandle::Ownership ownership ) : + DelightLight( context, name + ":light", prototype, ownership ) + { + m_surface = new DelightObject( context, name + ":surface", prototype, ownership ); + } + + void transform( const IECoreScenePreview::Renderer::TransformSamples &samples, const IECoreScenePreview::Renderer::SampleTimes × ) override + { + DelightLight::transform( samples, times ); + m_surface->transform( samples, times ); + } + bool attributes( const IECoreScenePreview::Renderer::AttributesInterface *attributes ) override + { + const std::optional wasUSDMeshLight = m_attributes ? m_attributes->isUSDMeshLight() : std::optional(); + + bool result = DelightLight::attributes( attributes ); + + if( !result || ( wasUSDMeshLight.has_value() && *wasUSDMeshLight != m_attributes->isUSDMeshLight() ) ) + { + return false; + } + + return m_surface->attributes( attributes ); + } + void link( const IECore::InternedString &type, const IECoreScenePreview::Renderer::ConstObjectSetPtr &objects ) override + { + DelightLight::link( type, objects ); + m_surface->link( type, objects ); + } + void assignID( uint32_t id ) override + { + // The mesh light itself is not visible to camera, so it does not get and ID assigned. + m_surface->assignID( id ); + } + void assignInstanceID( uint32_t id ) override + { + // This isn't actually used yet, but it will be ready to go if we add support for encapsulation + // to our 3delight backend so we need to deal with encapsulated instancers. + m_surface->assignInstanceID( id ); + } + + private : + + IECoreScenePreview::Renderer::ObjectInterfacePtr m_surface; + +}; + +} // namespace + ////////////////////////////////////////////////////////////////////////// // DelightRenderer ////////////////////////////////////////////////////////////////////////// @@ -1836,7 +2042,16 @@ class DelightRenderer final : public IECoreScenePreview::Renderer prototype = m_prototypeCache->get( objectSamples, times ); } - ObjectInterfacePtr result = new DelightLight( m_context, name, prototype, ownership() ); + auto castAttributes = static_cast( attributes ); + ObjectInterfacePtr result; + if( castAttributes->isUSDMeshLight() ) + { + result = new DelightUSDMeshLight( m_context, name, prototype, ownership() ); + } + else + { + result = new DelightLight( m_context, name, prototype, ownership() ); + } result->attributes( attributes ); return result; diff --git a/src/IECoreDelight/ShaderNetworkAlgo.cpp b/src/IECoreDelight/ShaderNetworkAlgo.cpp index 12264e58e2..c83ce7a904 100644 --- a/src/IECoreDelight/ShaderNetworkAlgo.cpp +++ b/src/IECoreDelight/ShaderNetworkAlgo.cpp @@ -339,6 +339,48 @@ T parameterValue( const Shader *shader, InternedString parameterName, const T &d return defaultValue; } +template +T *attributeCast( const IECore::RunTimeTyped *v, const IECore::InternedString &name ) +{ + if( !v ) + { + return nullptr; + } + + T *t = IECore::runTimeCast( v ); + if( t ) + { + return t; + } + + IECore::msg( IECore::Msg::Warning, "IECoreDelight::ShaderNetworkAlgo", fmt::format( "Expected {} but got {} for attribute \"{}\".", T::staticTypeName(), v->typeName(), name.c_str() ) ); + return nullptr; +} + +template +const T *attribute( const CompoundObject::ObjectMap &attributes, IECore::InternedString name ) +{ + auto it = attributes.find( name ); + if( it == attributes.end() ) + { + return nullptr; + } + + return attributeCast( it->second.get(), name ); +} + +std::pair shaderNetworkAttribute( const CompoundObject::ObjectMap &attributes, const std::vector &attributeNames ) +{ + for( const auto &name : attributeNames ) + { + if( const auto *shaderNetwork = attribute( attributes, name ) ) + { + return { name, shaderNetwork }; + } + } + return { InternedString(), nullptr }; +} + ////////////////////////////////////////////////////////////////////////// // USD conversion code ////////////////////////////////////////////////////////////////////////// @@ -437,6 +479,7 @@ const InternedString g_fileParameter( "file" ); const InternedString g_fileMetaColorSpaceParameter( "file_meta_colorspace" ); const InternedString g_gParameter( "g" ); const InternedString g_heightParameter( "height" ); +const InternedString g_incandescenceParameter( "incandescence" ); const InternedString g_inParameter( "in" ); const InternedString g_input1Parameter( "input1" ); const InternedString g_input2XParameter( "input2X" ); @@ -456,6 +499,7 @@ const InternedString g_multiplyOutputParameter( "out" ); const InternedString g_nameParameter( "name" ); const InternedString g_normalParameter( "normal" ); const InternedString g_normalizeParameter( "normalize" ); +const InternedString g_normalizeAreaParameter( "normalize_area" ); const InternedString g_opacityParameter( "opacity" ); const InternedString g_opacityThresholdParameter( "opacityThreshold" ); const InternedString g_outParameter( "out" ); @@ -500,6 +544,10 @@ const InternedString g_dlNormalizeParameter( "normalize_area" ); const InternedString g_dlSpecularParameter( "reflection_contribution" ); const InternedString g_dlTextureFileParameter( "textureFile" ); +const InternedString g_emptyString( "" ); +const std::vector g_lightAttributeNames = { "osl:light", "light" }; +const std::vector g_surfaceAttributeNames = { "osl:surface", "surface" }; + const float g_defaultAngle = 0.53f; const float g_defaultLength = 1.f; const float g_defaultWidth = 1.f; @@ -778,6 +826,47 @@ void convertUSDUVTextures( ShaderNetwork *network ) } } +std::pair surfaceGlowParameters( const IECoreScene::ShaderNetwork *shaderNetwork ) +{ + ShaderNetwork::Parameter incandescenceParameter; + ShaderNetwork::Parameter incandescenceInput; + if( !shaderNetwork ) + { + return { incandescenceParameter, incandescenceInput }; + } + + for( const auto &[handle, shader] : shaderNetwork->shaders() ) + { + if( + shader->getName() == "_3DelightGlass" || + shader->getName() == "_3DelightMaterial" || + shader->getName() == "anisotropic" || + shader->getName() == "blinn" || + shader->getName() == "dl3DelightMaterial" || + shader->getName() == "dlConstant" || + shader->getName() == "dlGlass" || + shader->getName() == "dlPrincipled" || + shader->getName() == "dlStandard" || + shader->getName() == "dlSubstance" || + shader->getName() == "dlToon" || + shader->getName() == "lambert" || + shader->getName() == "material3Delight" || + shader->getName() == "material3DelightGlass" + ) + { + incandescenceParameter = { handle, g_incandescenceParameter }; + break; + } + } + + if( incandescenceParameter ) + { + incandescenceInput = shaderNetwork->input( incandescenceParameter ); + } + + return { incandescenceParameter, incandescenceInput }; +} + } // namespace namespace IECoreDelight @@ -1017,6 +1106,109 @@ void convertUSDShaders( ShaderNetwork *shaderNetwork ) } } +ConstCompoundObjectPtr convertUSDMeshLightAttributes( const CompoundObject *attributes ) +{ + const auto &[lightAttribute, lightNetwork] = shaderNetworkAttribute( attributes->members(), g_lightAttributeNames ); + if( !lightNetwork ) + { + return attributes; + } + + const Shader *outputShader = lightNetwork->outputShader(); + if( !outputShader || outputShader->getName() != "MeshLight" ) + { + return attributes; + } + + CompoundObjectPtr result = attributes->copy(); + + ShaderNetworkPtr newLightShaderNetwork = lightNetwork->copy(); + const ShaderNetwork *surfaceNetwork = shaderNetworkAttribute( attributes->members(), g_surfaceAttributeNames ).second; + + const auto &[incandescenceParameter, incandescenceInput] = surfaceGlowParameters( surfaceNetwork ); + + ShaderNetwork::Parameter lightOutputParameter = lightNetwork->getOutput(); + const Shader *lightOutputShader = lightNetwork->outputShader(); + + ShaderPtr newLightShader = new Shader( "areaLight", "osl:light" ); + transferUSDLightParameters( newLightShaderNetwork.get(), lightOutputParameter.shader, lightOutputShader, newLightShader.get() ); + transferUSDParameter( newLightShaderNetwork.get(), lightOutputParameter.shader, lightOutputShader, g_normalizeParameter, newLightShader.get(), g_normalizeAreaParameter, false ); + + // The potential light inputs are in the first row of this matrix. + // The potential surface inputs are in the first column. + // The cells are the resulting mesh light color / input. + // C = light color x surface color. If 0 or 1 in parenthesis, it means it's known to be that value. + // TINT = A multiply shader combining the surface and light colors. + // Light / Emission Tex = the texture is connected directly without tint. + // | LightColor 0 | LightColor 0-1 | LightColor 1 | LightColor Textured + // EmissionColor 0 | C(0) | C(0) | C(0) | C(0) + // EmissionColor 0-1 | C(0) | C | C | TINT + // EmissionColor 1 | C(0) | C | C(1) | Light Tex + // EmissionColor Textured | C(0) | TINT | Emission Tex | TINT + + const Color3f lightColor = parameterValue( newLightShader.get(), g_dlColorParameter, Color3f( 1.f ) ); + const Color3f emissionColor = incandescenceParameter ? parameterValue( surfaceNetwork->getShader( incandescenceParameter.shader ), incandescenceParameter.name, Color3f( 0.f ) ) : Color3f( 0.f ); + if( incandescenceParameter ) + { + newLightShader->parameters()[g_dlColorParameter] = new Color3fData( emissionColor * lightColor ); + } + + InternedString tintHandle; + const ShaderNetwork::Parameter meshLightColorParameter = { lightOutputParameter.shader, g_colorParameter }; + const ShaderNetwork::Parameter meshLightColorInput = lightNetwork->input( meshLightColorParameter ); + const ShaderNetwork::Parameter dlMeshLightColorParameter = { lightOutputParameter.shader, g_dlColorParameter }; + // Remove the input to the light color. We will add it back later if needed. + removeInput( newLightShaderNetwork.get(), meshLightColorParameter ); + + if( incandescenceInput && ( lightColor != Color3f( 0.f ) || meshLightColorInput ) ) + { + ShaderNetworkPtr glowNetwork = surfaceNetwork->copy(); + glowNetwork->setOutput( incandescenceInput ); + IECoreScene::ShaderNetworkAlgo::removeUnusedShaders( glowNetwork.get() ); + ShaderNetwork::Parameter newGlowColorInput = IECoreScene::ShaderNetworkAlgo::addShaders( newLightShaderNetwork.get(), glowNetwork.get(), /* connections = */ true ); + + if( lightColor != Color3f( 1.f ) || meshLightColorInput ) + { + ShaderPtr tintShader = new Shader( "Maths/MultiplyColor", "osl:shader", { { "b", new Color3fData( lightColor ) } } ); + tintHandle = newLightShaderNetwork->addShader( InternedString( "tint" ), std::move( tintShader ) ); + + newLightShaderNetwork->addConnection( { newGlowColorInput, { tintHandle, "a" } } ); + newLightShaderNetwork->addConnection( { { tintHandle, "out" }, dlMeshLightColorParameter } ); + } + else + { + newLightShaderNetwork->addConnection( { newGlowColorInput, dlMeshLightColorParameter } ); + } + } + + if( meshLightColorInput && ( emissionColor != Color3f( 0.f ) || incandescenceInput ) ) + { + if( emissionColor != Color3f( 1.f ) || incandescenceInput ) + { + if( tintHandle == g_emptyString ) + { + ShaderPtr tintShader = new Shader( "Maths/MultiplyColor", "osl:shader", { { "a", new Color3fData( emissionColor ) } } ); + tintHandle = newLightShaderNetwork->addShader( InternedString( "tint" ), std::move( tintShader ) ); + + newLightShaderNetwork->addConnection( { { tintHandle, "out" }, dlMeshLightColorParameter } ); + } + + newLightShaderNetwork->addConnection( { meshLightColorInput, { tintHandle, "b" } } ); + } + else + { + newLightShaderNetwork->addConnection( { meshLightColorInput, dlMeshLightColorParameter } ); + } + } + + replaceUSDShader( newLightShaderNetwork.get(), lightOutputParameter.shader, std::move( newLightShader ) ); + IECoreScene::ShaderNetworkAlgo::removeUnusedShaders( newLightShaderNetwork.get() ); + + result->members()[lightAttribute] = std::move( newLightShaderNetwork ); + + return result; +} + ShaderNetworkPtr preprocessedNetwork( const ShaderNetwork *shaderNetwork ) { ShaderNetworkPtr result = shaderNetwork->copy(); diff --git a/src/IECoreDelightModule/IECoreDelightModule.cpp b/src/IECoreDelightModule/IECoreDelightModule.cpp index 2eb01482cc..3c6a7df02b 100644 --- a/src/IECoreDelightModule/IECoreDelightModule.cpp +++ b/src/IECoreDelightModule/IECoreDelightModule.cpp @@ -36,11 +36,25 @@ #include "boost/python.hpp" +#include "IECorePython/ScopedGILRelease.h" + #include "IECoreDelight/ShaderNetworkAlgo.h" using namespace boost::python; using namespace IECoreDelight; +namespace +{ + +IECore::CompoundObjectPtr convertUSDMeshLightAttributesWrapper( const IECore::CompoundObject &attributes, bool copy ) +{ + IECorePython::ScopedGILRelease r; + IECore::ConstCompoundObjectPtr result = ShaderNetworkAlgo::convertUSDMeshLightAttributes( &attributes ); + return copy ? result->copy() : boost::const_pointer_cast( result ); +} + +} + BOOST_PYTHON_MODULE( _IECoreDelight ) { @@ -49,5 +63,6 @@ BOOST_PYTHON_MODULE( _IECoreDelight ) scope shaderNetworkAlgoScope( shaderNetworkAlgoModule ); def( "convertUSDShaders", &ShaderNetworkAlgo::convertUSDShaders ); + def( "convertUSDMeshLightAttributes", &convertUSDMeshLightAttributesWrapper, ( arg_( "_copy" ) = true ) ); } From 99ca7422ed15c5969c49419f93f89e256b92059a Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Wed, 12 Aug 2026 15:30:03 -0400 Subject: [PATCH 10/11] Cycles : Add support for USD mesh lights --- Changes.md | 2 +- .../IECoreCyclesPreview/ShaderNetworkAlgo.h | 3 + .../ShaderNetworkAlgoTest.py | 359 ++++++++++++++++++ .../IECoreCyclesPreview/Renderer.cpp | 35 +- .../IECoreCyclesPreview/ShaderNetworkAlgo.cpp | 285 +++++++++++++- src/GafferCyclesModule/GafferCyclesModule.cpp | 13 + 6 files changed, 686 insertions(+), 11 deletions(-) diff --git a/Changes.md b/Changes.md index af98da7525..da7d0190c2 100644 --- a/Changes.md +++ b/Changes.md @@ -11,7 +11,7 @@ Features - Faster rendering. - USDMeshLight : - Added node to add necessary attributes to geometry to convert to a USDMeshLight. - - Added Arnold, RenderMan and 3Delight rendering. + - Added Arnold, RenderMan, 3Delight and Cycles rendering. Improvements ------------ diff --git a/include/GafferCycles/IECoreCyclesPreview/ShaderNetworkAlgo.h b/include/GafferCycles/IECoreCyclesPreview/ShaderNetworkAlgo.h index e6c69ec1e0..43e57a06ab 100644 --- a/include/GafferCycles/IECoreCyclesPreview/ShaderNetworkAlgo.h +++ b/include/GafferCycles/IECoreCyclesPreview/ShaderNetworkAlgo.h @@ -96,6 +96,9 @@ IECORECYCLES_API IECoreScene::ShaderNetworkPtr convertLightShader( const IECoreS /// calling `convertLight()` and `convertLightShader()`. IECORECYCLES_API void convertUSDShaders( IECoreScene::ShaderNetwork *shaderNetwork ); +/// Returns a modified set of attributes conforming to the USDMeshLight specification. +IECORECYCLES_API IECore::ConstCompoundObjectPtr convertUSDMeshLightAttributes( const IECore::CompoundObject *attributes ); + } // namespace ShaderNetworkAlgo } // namespace IECoreCycles diff --git a/python/GafferCyclesTest/IECoreCyclesPreviewTest/ShaderNetworkAlgoTest.py b/python/GafferCyclesTest/IECoreCyclesPreviewTest/ShaderNetworkAlgoTest.py index 0554ed32fe..be773d705f 100644 --- a/python/GafferCyclesTest/IECoreCyclesPreviewTest/ShaderNetworkAlgoTest.py +++ b/python/GafferCyclesTest/IECoreCyclesPreviewTest/ShaderNetworkAlgoTest.py @@ -864,6 +864,365 @@ def testConvertUSDPrimvarReader( self ) : self.assertEqual( len( reader.parameters ), 1 ) self.assertEqual( reader.parameters["attribute"].value, "test" ) + def testUSDMeshLight( self ) : + + attributes = IECore.CompoundObject( + { + "light" : IECoreScene.ShaderNetwork( + shaders = { "light" : IECoreScene.Shader( "MeshLight", "light" ) }, + output = "light" + ) + } + ) + + modifiedAttributes = IECoreCycles.ShaderNetworkAlgo.convertUSDMeshLightAttributes( attributes ) + + self.assertNotIn( "light", modifiedAttributes ) + self.assertIn( "cycles:surface", modifiedAttributes ) + + mixShader = IECoreScene.Shader( "mix_closure", "cycles:surface" ) + mixShader.blindData()["__USDRayVisibility"] = ( 1 << 11 ) - 1 # `PATH_RAY_ALL_VISIBILITY` value from cycles/include/kernel/types.h + meshLightSurface = IECoreScene.ShaderNetwork( + shaders = { + "mixShader" : mixShader, + "lightPath" : IECoreScene.Shader( "light_path", "shader" ), + "emission" : IECoreScene.Shader( "emission", "shader", { "color" : IECore.Color3fData( imath.Color3f( 1.0 ) ), "strength" : IECore.FloatData( 1.0 ) } ), + "geometry" : IECoreScene.Shader( "geometry", "surface" ), + "vectorMath" : IECoreScene.Shader( "vector_math", "shader", { "math_type" : IECore.StringData( "dot_product" ) } ), + }, + connections = [ + ( ( "geometry", "normal" ), ( "vectorMath", "vector1" ) ), + ( ( "geometry", "incoming" ), ( "vectorMath", "vector2" ) ), + ( ( "vectorMath", "value" ), ( "mixShader", "closure1" ) ), + ( ( "emission", "emission" ), ( "mixShader", "closure2" ) ), + ( ( "lightPath", "is_diffuse_ray" ), ( "mixShader", "fac" ) ), + ], + output = ( "mixShader", "closure" ), + ) + + self.assertEqual( modifiedAttributes["cycles:surface"], meshLightSurface ) + + for shaderName, emissionColorParameter in [ + ( "principled_bsdf", "emission_color" ), + ( "emission", "color" ), + ( "UsdPreviewSurface", "emissiveColor" ), + ] : + + with self.subTest( shaderName = shaderName ) : + # Surface color only, light color only (no color inputs) + + attributes = IECore.CompoundObject( + { + "light" : IECoreScene.ShaderNetwork( + shaders = { + "light" : IECoreScene.Shader( + "MeshLight", "light", + { "color" : imath.Color3f( 0.125, 0.25, 0.375 ), "intensity" : 2.0, "exposure" : 3.0 } + ) + }, + output = "light" + ), + "cycles:surface" : IECoreScene.ShaderNetwork( + shaders = { + "surface" : IECoreScene.Shader( + shaderName, "cycles:surface", + { emissionColorParameter : imath.Color3f( 0.5, 0.625, 0.75 ) } ), + }, + output = "surface" + ), + } + ) + + modifiedAttributes = IECoreCycles.ShaderNetworkAlgo.convertUSDMeshLightAttributes( attributes ) + + self.assertNotIn( "light", modifiedAttributes ) + self.assertIn( "cycles:surface", modifiedAttributes ) + + meshLightSurface = IECoreScene.ShaderNetwork( + shaders = { + "surface" : IECoreScene.Shader( shaderName, "cycles:surface", { emissionColorParameter : IECore.Color3fData( imath.Color3f( 0.5, 0.625, 0.75 ) ) } ), + "mixShader" : mixShader, + "lightPath" : IECoreScene.Shader( "light_path", "shader" ), + "emission" : IECoreScene.Shader( "emission", "shader", { "color" : IECore.Color3fData( imath.Color3f( 0.125 * 0.5, 0.25 * 0.625, 0.375 * 0.75) ), "strength" : IECore.FloatData( 2.0 * pow( 2.0, 3.0 ) ) } ), + }, + connections = [ + ( ( "surface", "" ), ( "mixShader", "closure1" ) ), + ( ( "emission", "emission" ), ( "mixShader", "closure2" ) ), + ( ( "lightPath", "is_diffuse_ray" ), ( "mixShader", "fac" ) ), + ], + output = ( "mixShader", "closure" ), + ) + + self.assertEqual( modifiedAttributes["cycles:surface"], meshLightSurface ) + + # Surface with color input, black mesh light emission + + def surfaceWithTexture( lightColor ) : + return IECore.CompoundObject( + { + "light" : IECoreScene.ShaderNetwork( + shaders = { "light" : IECoreScene.Shader( "MeshLight", "light", { "color" : lightColor } ) }, + output = "light" + ), + "cycles:surface" : IECoreScene.ShaderNetwork( + shaders = { + "mixClosure" : IECoreScene.Shader( "mix_closure", "cycles:surface", { "fac" : 0.5 } ), + "surface" : IECoreScene.Shader( shaderName, "cycles:surface", { emissionColorParameter : imath.Color3f( 0.5, 0.625, 0.75 ) } ), + "hsv" : IECoreScene.Shader( "hsv", "shader", { "saturation" : 0.5, "value" : 0.25 } ), + "texture" : IECoreScene.Shader( "image_texture", "shader", { "filename" : "test.tx" } ), + }, + output = ( "mixClosure", "closure" ), + connections = [ + ( ( "texture", "color" ), ( "hsv", "color" ) ), + ( ( "hsv", "color" ), ( "surface", emissionColorParameter ) ), + ( ( "surface", "BSDF" ), ( "mixClosure", "closure1" ) ), + ] + ), + } + ) + + modifiedAttributes = IECoreCycles.ShaderNetworkAlgo.convertUSDMeshLightAttributes( surfaceWithTexture( imath.Color3f( 0.0 ) ) ) + meshLightSurface = IECoreScene.ShaderNetwork( + shaders = { + "mixClosure" : IECoreScene.Shader( "mix_closure", "cycles:surface", { "fac" : 0.5 } ), + "surface" : IECoreScene.Shader( shaderName, "cycles:surface", { emissionColorParameter : imath.Color3f( 0.5, 0.625, 0.75 ) } ), + "hsv" : IECoreScene.Shader( "hsv", "shader", { "saturation" : 0.5, "value" : 0.25 } ), + "texture" : IECoreScene.Shader( "image_texture", "shader", { "filename" : "test.tx" } ), + "mixShader" : mixShader, + "lightPath" : IECoreScene.Shader( "light_path", "shader" ), + "emission" : IECoreScene.Shader( "emission", "shader", { "color" : IECore.Color3fData( imath.Color3f( 0.0 ) ), "strength" : IECore.FloatData( 1.0 ) } ), + }, + output = ( "mixShader", "closure" ), + connections = [ + ( ( "texture", "color" ), ( "hsv", "color" ) ), + ( ( "hsv", "color" ), ( "surface", emissionColorParameter ) ), + ( ( "surface", "BSDF" ), ( "mixClosure", "closure1" ) ), + ( ( "mixClosure", "closure" ), ( "mixShader", "closure1" ) ), + ( ( "emission", "emission" ), ( "mixShader", "closure2" ) ), + ( ( "lightPath", "is_diffuse_ray" ), ( "mixShader", "fac" ) ), + ] + ) + + self.assertEqual( modifiedAttributes["cycles:surface"], meshLightSurface ) + + # Surface with color input, white mesh light emission + + modifiedAttributes = IECoreCycles.ShaderNetworkAlgo.convertUSDMeshLightAttributes( surfaceWithTexture( imath.Color3f( 1.0 ) ) ) + meshLightSurface = IECoreScene.ShaderNetwork( + shaders = { + "mixClosure" : IECoreScene.Shader( "mix_closure", "cycles:surface", { "fac" : 0.5 } ), + "surface" : IECoreScene.Shader( shaderName, "cycles:surface", { emissionColorParameter : imath.Color3f( 0.5, 0.625, 0.75 ) } ), + "hsv" : IECoreScene.Shader( "hsv", "shader", { "saturation" : 0.5, "value" : 0.25 } ), + "texture" : IECoreScene.Shader( "image_texture", "shader", { "filename" : "test.tx" } ), + "mixShader" : mixShader, + "lightPath" : IECoreScene.Shader( "light_path", "shader" ), + "emission" : IECoreScene.Shader( "emission", "shader", { "color" : IECore.Color3fData( imath.Color3f( 0.5, 0.625, 0.75 ) ), "strength" : IECore.FloatData( 1.0 ) } ), + }, + output = ( "mixShader", "closure" ), + connections = [ + ( ( "texture", "color" ), ( "hsv", "color" ) ), + ( ( "hsv", "color" ), ( "surface", emissionColorParameter ) ), + ( ( "surface", "BSDF" ), ( "mixClosure", "closure1" ) ), + ( ( "mixClosure", "closure" ), ( "mixShader", "closure1" ) ), + ( ( "emission", "emission" ), ( "mixShader", "closure2" ) ), + ( ( "lightPath", "is_diffuse_ray" ), ( "mixShader", "fac" ) ), + ( ( "hsv", "color" ), ( "emission", "color" ) ), + ] + ) + + self.assertEqual( modifiedAttributes["cycles:surface"], meshLightSurface ) + + # Surface with color input, colored mesh light emission + + modifiedAttributes = IECoreCycles.ShaderNetworkAlgo.convertUSDMeshLightAttributes( surfaceWithTexture( imath.Color3f( 0.125, 0.25, 0.375 ) ) ) + meshLightSurface = IECoreScene.ShaderNetwork( + shaders = { + "mixClosure" : IECoreScene.Shader( "mix_closure", "cycles:surface", { "fac" : 0.5 } ), + "surface" : IECoreScene.Shader( shaderName, "cycles:surface", { emissionColorParameter : imath.Color3f( 0.5, 0.625, 0.75 ) } ), + "hsv" : IECoreScene.Shader( "hsv", "shader", { "saturation" : 0.5, "value" : 0.25 } ), + "texture" : IECoreScene.Shader( "image_texture", "shader", { "filename" : "test.tx" } ), + "mixShader" : mixShader, + "lightPath" : IECoreScene.Shader( "light_path", "shader" ), + "emission" : IECoreScene.Shader( "emission", "shader", { "color" : IECore.Color3fData( imath.Color3f( 0.125 * 0.5, 0.25 * 0.625, 0.375 * 0.75 ) ), "strength" : IECore.FloatData( 1.0 ) } ), + "tint" : IECoreScene.Shader( "vector_math", "shader", { "vector1" : imath.Color3f( 0.5, 0.625, 0.75 ), "vector2" : imath.Color3f( 0.125, 0.25, 0.375 ), "math_type" : "multiply" } ), + }, + output = ( "mixShader", "closure" ), + connections = [ + ( ( "texture", "color" ), ( "hsv", "color" ) ), + ( ( "hsv", "color" ), ( "surface", emissionColorParameter ) ), + ( ( "surface", "BSDF" ), ( "mixClosure", "closure1" ) ), + ( ( "mixClosure", "closure" ), ( "mixShader", "closure1" ) ), + ( ( "emission", "emission" ), ( "mixShader", "closure2" ) ), + ( ( "lightPath", "is_diffuse_ray" ), ( "mixShader", "fac" ) ), + ( ( "hsv", "color" ), ( "tint", "vector1" ) ), + ( ( "tint", "vector" ), ( "emission", "color" ) ), + ] + ) + + self.assertEqual( modifiedAttributes["cycles:surface"], meshLightSurface ) + + # Surface with black emission, light with color input + + def lightWithTexture( surfaceColor ) : + return IECore.CompoundObject( + { + "light" : IECoreScene.ShaderNetwork( + shaders = { + "light" : IECoreScene.Shader( "MeshLight", "light", { "color" : imath.Color3f( 0.125, 0.25, 0.375 ) } ), + "hsv" : IECoreScene.Shader( "hsv", "shader", { "saturation" : 0.5, "value" : 0.25 } ), + "texture" : IECoreScene.Shader( "image_texture", "shader", { "filename" : "test.tx" } ), + }, + output = "light", + connections = [ + ( ( "texture", "color" ), ( "hsv", "color" ) ), + ( ( "hsv", "color" ), ( "light", "color" ) ), + ] + ), + "cycles:surface" : IECoreScene.ShaderNetwork( + shaders = { + "mixClosure" : IECoreScene.Shader( "mix_closure", "cycles:surface", { "fac" : 0.5 } ), + "surface" : IECoreScene.Shader( shaderName, "cycles:surface", { emissionColorParameter : surfaceColor } ), + }, + output = ( "mixClosure", "closure" ), + connections = [ + ( ( "surface", "BSDF" ), ( "mixClosure", "closure1" ) ), + ] + ), + } + ) + + modifiedAttributes = IECoreCycles.ShaderNetworkAlgo.convertUSDMeshLightAttributes( lightWithTexture( imath.Color3f( 0.0 ) ) ) + meshLightSurface = IECoreScene.ShaderNetwork( + shaders = { + "mixClosure" : IECoreScene.Shader( "mix_closure", "cycles:surface", { "fac" : 0.5 } ), + "surface" : IECoreScene.Shader( shaderName, "cycles:surface", { emissionColorParameter : imath.Color3f( 0.0 ) } ), + "mixShader" : mixShader, + "lightPath" : IECoreScene.Shader( "light_path", "shader" ), + "emission" : IECoreScene.Shader( "emission", "shader", { "color" : IECore.Color3fData( imath.Color3f( 0.0 ) ), "strength" : IECore.FloatData( 1.0 ) } ), + }, + output = ( "mixShader", "closure" ), + connections = [ + ( ( "surface", "BSDF" ), ( "mixClosure", "closure1" ) ), + ( ( "mixClosure", "closure" ), ( "mixShader", "closure1" ) ), + ( ( "emission", "emission" ), ( "mixShader", "closure2" ) ), + ( ( "lightPath", "is_diffuse_ray" ), ( "mixShader", "fac" ) ), + ] + ) + + self.assertEqual( modifiedAttributes["cycles:surface"], meshLightSurface ) + + # Surface with white emission, light with color input + + modifiedAttributes = IECoreCycles.ShaderNetworkAlgo.convertUSDMeshLightAttributes( lightWithTexture( imath.Color3f( 1.0 ) ) ) + meshLightSurface = IECoreScene.ShaderNetwork( + shaders = { + "mixClosure" : IECoreScene.Shader( "mix_closure", "cycles:surface", { "fac" : 0.5 } ), + "surface" : IECoreScene.Shader( shaderName, "cycles:surface", { emissionColorParameter : imath.Color3f( 1.0 ) } ), + "hsv" : IECoreScene.Shader( "hsv", "shader", { "saturation" : 0.5, "value" : 0.25 } ), + "texture" : IECoreScene.Shader( "image_texture", "shader", { "filename" : "test.tx" } ), + "mixShader" : mixShader, + "lightPath" : IECoreScene.Shader( "light_path", "shader" ), + "emission" : IECoreScene.Shader( "emission", "shader", { "color" : IECore.Color3fData( imath.Color3f( 0.125, 0.25, 0.375 ) ), "strength" : IECore.FloatData( 1.0 ) } ), + }, + output = ( "mixShader", "closure" ), + connections = [ + ( ( "texture", "color" ), ( "hsv", "color" ) ), + ( ( "hsv", "color" ), ( "emission", "color" ) ), + ( ( "surface", "BSDF" ), ( "mixClosure", "closure1" ) ), + ( ( "mixClosure", "closure" ), ( "mixShader", "closure1" ) ), + ( ( "emission", "emission" ), ( "mixShader", "closure2" ) ), + ( ( "lightPath", "is_diffuse_ray" ), ( "mixShader", "fac" ) ), + ] + ) + + self.assertEqual( modifiedAttributes["cycles:surface"], meshLightSurface ) + + # Surface with colored emission, light with color input + + modifiedAttributes = IECoreCycles.ShaderNetworkAlgo.convertUSDMeshLightAttributes( lightWithTexture( imath.Color3f( 0.5, 0.625, 0.75) ) ) + meshLightSurface = IECoreScene.ShaderNetwork( + shaders = { + "mixClosure" : IECoreScene.Shader( "mix_closure", "cycles:surface", { "fac" : 0.5 } ), + "surface" : IECoreScene.Shader( shaderName, "cycles:surface", { emissionColorParameter : imath.Color3f( 0.5, 0.625, 0.75 ) } ), + "hsv" : IECoreScene.Shader( "hsv", "shader", { "saturation" : 0.5, "value" : 0.25 } ), + "texture" : IECoreScene.Shader( "image_texture", "shader", { "filename" : "test.tx" } ), + "mixShader" : mixShader, + "lightPath" : IECoreScene.Shader( "light_path", "shader" ), + "emission" : IECoreScene.Shader( "emission", "shader", { "color" : IECore.Color3fData( imath.Color3f( 0.125 * 0.5, 0.25 * 0.625, 0.375 * 0.75 ) ), "strength" : IECore.FloatData( 1.0 ) } ), + "tint" : IECoreScene.Shader( "vector_math", "shader", { "vector1" : imath.Color3f( 0.5, 0.625, 0.75 ), "vector2" : imath.Color3f( 0.125, 0.25, 0.375 ), "math_type" : "multiply" } ), + }, + output = ( "mixShader", "closure" ), + connections = [ + ( ( "texture", "color" ), ( "hsv", "color" ) ), + ( ( "surface", "BSDF" ), ( "mixClosure", "closure1" ) ), + ( ( "mixClosure", "closure" ), ( "mixShader", "closure1" ) ), + ( ( "emission", "emission" ), ( "mixShader", "closure2" ) ), + ( ( "lightPath", "is_diffuse_ray" ), ( "mixShader", "fac" ) ), + ( ( "hsv", "color" ), ( "tint", "vector2" ) ), + ( ( "tint", "vector" ), ( "emission", "color" ) ), + ] + ) + + self.assertEqual( modifiedAttributes["cycles:surface"], meshLightSurface ) + + # Surface with color input, light with color input + + sourceNetwork = IECore.CompoundObject( + { + "light" : IECoreScene.ShaderNetwork( + shaders = { + "light" : IECoreScene.Shader( "MeshLight", "light", { "color" : imath.Color3f( 0.125, 0.25, 0.375 ) } ), + "lightTexture" : IECoreScene.Shader( "image_texture", "shader", { "filename" : "lightTest.tx" } ), + }, + output = "light", + connections = [ ( ( "lightTexture", "color" ), ( "light", "color" ) ), ] + ), + "cycles:surface" : IECoreScene.ShaderNetwork( + shaders = { + "mixClosure" : IECoreScene.Shader( "mix_closure", "cycles:surface", { "fac" : 0.5 } ), + "surface" : IECoreScene.Shader( shaderName, "cycles:surface", { emissionColorParameter : imath.Color3f( 0.5, 0.625, 0.75 ) } ), + "hsv" : IECoreScene.Shader( "hsv", "shader", { "saturation" : 0.5, "value" : 0.25 } ), + "texture" : IECoreScene.Shader( "image_texture", "shader", { "filename" : "test.tx" } ), + }, + output = ( "mixClosure", "closure" ), + connections = [ + ( ( "texture", "color" ), ( "hsv", "color" ) ), + ( ( "hsv", "color" ), ( "surface", emissionColorParameter ) ), + ( ( "surface", "BSDF" ), ( "mixClosure", "closure1" ) ), + ] + ), + } + ) + + modifiedAttributes = IECoreCycles.ShaderNetworkAlgo.convertUSDMeshLightAttributes( sourceNetwork ) + meshLightSurface = IECoreScene.ShaderNetwork( + shaders = { + "mixClosure" : IECoreScene.Shader( "mix_closure", "cycles:surface", { "fac" : 0.5 } ), + "surface" : IECoreScene.Shader( shaderName, "cycles:surface", { emissionColorParameter : imath.Color3f( 0.5, 0.625, 0.75 ) } ), + "hsv" : IECoreScene.Shader( "hsv", "shader", { "saturation" : 0.5, "value" : 0.25 } ), + "texture" : IECoreScene.Shader( "image_texture", "shader", { "filename" : "test.tx" } ), + "mixShader" : mixShader, + "lightPath" : IECoreScene.Shader( "light_path", "shader" ), + "emission" : IECoreScene.Shader( "emission", "shader", { "color" : IECore.Color3fData( imath.Color3f( 0.5 * .125, 0.625 * 0.25, 0.75 * 0.375 ) ), "strength" : IECore.FloatData( 1.0 ) } ), + "lightTexture" : IECoreScene.Shader( "image_texture", "shader", { "filename" : "lightTest.tx" } ), + "tint" : IECoreScene.Shader( "vector_math", "shader", { "vector1" : imath.Color3f( 0.5, 0.625, 0.75 ), "vector2" : imath.Color3f( 0.125, 0.25, 0.375 ), "math_type" : "multiply" } ), + }, + output = ( "mixShader", "closure" ), + connections = [ + ( ( "texture", "color" ), ( "hsv", "color" ) ), + ( ( "hsv", "color" ), ( "surface", emissionColorParameter ) ), + ( ( "surface", "BSDF" ), ( "mixClosure", "closure1" ) ), + ( ( "mixClosure", "closure" ), ( "mixShader", "closure1" ) ), + ( ( "emission", "emission" ), ( "mixShader", "closure2" ) ), + ( ( "lightPath", "is_diffuse_ray" ), ( "mixShader", "fac" ) ), + ( ( "hsv", "color" ), ( "tint", "vector1" ) ), + ( ( "lightTexture", "color" ), ( "tint", "vector2" ) ), + ( ( "tint", "vector" ), ( "emission", "color" ) ), + ] + ) + + self.assertEqual( modifiedAttributes["cycles:surface"], meshLightSurface ) + def __assertShadersEqual( self, shader1, shader2, message = None ) : self.assertEqual( shader1.name, shader2.name, message ) diff --git a/src/GafferCycles/IECoreCyclesPreview/Renderer.cpp b/src/GafferCycles/IECoreCyclesPreview/Renderer.cpp index afdcf00472..9653eb7842 100644 --- a/src/GafferCycles/IECoreCyclesPreview/Renderer.cpp +++ b/src/GafferCycles/IECoreCyclesPreview/Renderer.cpp @@ -868,8 +868,19 @@ class CyclesAttributes : public IECoreScenePreview::Renderer::AttributesInterfac m_assetName( "" ), m_lightGroup( "" ), m_isCausticsCaster( false ), - m_isCausticsReceiver( false ) + m_isCausticsReceiver( false ), + m_isUSDMeshLight( false ) { + if( auto light = attributes->member( g_lightAttributeName ) ) + { + if( light->outputShader() && light->outputShader()->getName() == "MeshLight" ) + { + m_isUSDMeshLight = true; + } + } + ConstCompoundObjectPtr modifiedAttributes = ShaderNetworkAlgo::convertUSDMeshLightAttributes( attributes ); + attributes = modifiedAttributes.get(); + updateVisibility( g_cameraVisibilityAttributeName, (int)ccl::PATH_RAY_CAMERA, attributes ); updateVisibility( g_diffuseVisibilityAttributeName, (int)ccl::PATH_RAY_DIFFUSE, attributes ); updateVisibility( g_glossyVisibilityAttributeName, (int)ccl::PATH_RAY_GLOSSY, attributes ); @@ -1215,6 +1226,11 @@ class CyclesAttributes : public IECoreScenePreview::Renderer::AttributesInterfac return m_volume.clipping ? m_volume.clipping.value() : 0.001f; } + bool isUSDMeshLight() const + { + return m_isUSDMeshLight; + } + private : void updateVisibility( const IECore::InternedString &name, int rayType, const IECore::CompoundObject *attributes ) @@ -1367,6 +1383,7 @@ class CyclesAttributes : public IECoreScenePreview::Renderer::AttributesInterfac bool m_isCausticsCaster; bool m_isCausticsReceiver; bool m_muteLight; + bool m_isUSDMeshLight; using CustomAttributes = ccl::vector; CustomAttributes m_custom; @@ -2557,7 +2574,21 @@ class CyclesRenderer final : public IECoreScenePreview::Renderer const IECore::MessageHandler::Scope s( m_messageHandler.get() ); acquireSession(); - ObjectInterfacePtr result = new CyclesLight( m_scene, name, m_nodeDeleter.get() ); + auto typedAttributes = static_cast( attributes ); + ObjectInterfacePtr result; + if( typedAttributes->isUSDMeshLight() ) + { + SharedGeometryPtr geometry = m_geometryCache->get( samples, times, attributes, name ); + if( !geometry ) + { + return nullptr; + } + result = new CyclesObject( m_scene, geometry, name, frame(), &m_lightLinker, m_nodeDeleter.get() ); + } + else + { + result = new CyclesLight( m_scene, name, m_nodeDeleter.get() ); + } result->attributes( attributes ); return result; } diff --git a/src/GafferCycles/IECoreCyclesPreview/ShaderNetworkAlgo.cpp b/src/GafferCycles/IECoreCyclesPreview/ShaderNetworkAlgo.cpp index 570191e45e..e48617eb56 100644 --- a/src/GafferCycles/IECoreCyclesPreview/ShaderNetworkAlgo.cpp +++ b/src/GafferCycles/IECoreCyclesPreview/ShaderNetworkAlgo.cpp @@ -830,6 +830,8 @@ const InternedString g_BSDFParameter( "BSDF" ); const InternedString g_castShadowParameter( "cast_shadow" ); const InternedString g_clearcoatParameter( "clearcoat" ); const InternedString g_clearcoatRoughnessParameter( "clearcoatRoughness" ); +const InternedString g_closure1Parameter( "closure1" ); +const InternedString g_closure2Parameter( "closure2" ); const InternedString g_coatRoughnessParameter( "coat_roughness" ); const InternedString g_coatWeightParameter( "coat_weight" ); const InternedString g_colorParameter( "color" ); @@ -873,6 +875,7 @@ const InternedString g_resultParameter( "result" ); const InternedString g_rgbParameter( "rgb" ); const InternedString g_rotationParameter( "rotation" ); const InternedString g_scaleParameter( "scale" ); +const InternedString g_strengthParameter( "strength" ); const InternedString g_roughnessParameter( "roughness" ); const InternedString g_shadowEnableParameter( "shadow:enable" ); const InternedString g_shapingConeAngleParameter( "shaping:cone:angle" ); @@ -914,9 +917,49 @@ const InternedString g_wrapSParameter( "wrapS" ); const InternedString g_wrapTParameter( "wrapT" ); const InternedString g_USDRayVisibilityBlindDataKey( "__USDRayVisibility" ); +const InternedString g_closureOutput( "closure" ); +const InternedString g_emissionOutput( "emission" ); +const InternedString g_isDiffuseRayOutput( "is_diffuse_ray" ); +const InternedString g_vectorOutput( "vector" ); + const string g_cyclesNamespace( "cycles:" ); -void transferUSDLightParameters( ShaderNetwork *network, InternedString shaderHandle, const Shader *usdShader, Shader *shader ) +const InternedString g_lightAttributeName( "light" ); + +const std::vector g_surfaceShaderAttributeNames = { + "cycles:surface", + "osl:surface", + "osl:shader", + "surface" +}; + +/// \todo This is copied from `IECoreCyclesPreview/Renderer`. Should +/// it be shared in some way? +IECoreScene::ConstShaderNetworkPtr g_facingRatio = []() { + + ShaderNetworkPtr result = new ShaderNetwork; + + const InternedString geometryHandle = result->addShader( + "geometry", new Shader( "geometry" ) + ); + const InternedString vectorMathHandle = result->addShader( + "vectorMath", new Shader( + "vector_math", "shader", + { + { "math_type", new StringData( "dot_product" ) } + } + ) + ); + + result->addConnection( { { geometryHandle, "normal" }, { vectorMathHandle, "vector1" } } ); + result->addConnection( { { geometryHandle, "incoming" }, { vectorMathHandle, "vector2" } } ); + result->setOutput( { vectorMathHandle, "value" } ); + + return result; + +} (); + +void transferCommonUSDLightParameters( const Shader *usdShader, Shader *shader ) { Color3f color = parameterValue( usdShader, g_colorParameter, Color3f( 1 ) ); if( parameterValue( usdShader, g_enableColorTemperatureParameter, false ) ) @@ -925,11 +968,6 @@ void transferUSDLightParameters( ShaderNetwork *network, InternedString shaderHa } shader->parameters()[g_colorParameter] = new Color3fData( color ); - transferUSDParameter( network, shaderHandle, usdShader, g_exposureParameter, shader, g_exposureParameter, 0.0f ); - transferUSDParameter( network, shaderHandle, usdShader, g_intensityParameter, shader, g_intensityParameter, 1.0f ); - transferUSDParameter( network, shaderHandle, usdShader, g_normalizeParameter, shader, g_normalizeParameter, false ); - transferUSDParameter( network, shaderHandle, usdShader, g_shadowEnableParameter, shader, g_castShadowParameter, true ); - int visibility = (int)ccl::PATH_RAY_ALL_VISIBILITY; if( parameterValue( usdShader, g_diffuseParameter, 1.0f ) == 0.0f ) { @@ -941,8 +979,6 @@ void transferUSDLightParameters( ShaderNetwork *network, InternedString shaderHa } shader->blindData()->writable()[g_USDRayVisibilityBlindDataKey] = new IntData( visibility ); - shader->parameters()[g_useMISParameter] = new BoolData( true ); - for( const auto &[name, value] : usdShader->parameters() ) { if( boost::starts_with( name.string(), g_cyclesNamespace ) ) @@ -952,6 +988,18 @@ void transferUSDLightParameters( ShaderNetwork *network, InternedString shaderHa } } +void transferUSDLightParameters( ShaderNetwork *network, InternedString shaderHandle, const Shader *usdShader, Shader *shader ) +{ + transferCommonUSDLightParameters( usdShader, shader ); + + transferUSDParameter( network, shaderHandle, usdShader, g_exposureParameter, shader, g_exposureParameter, 0.0f ); + transferUSDParameter( network, shaderHandle, usdShader, g_intensityParameter, shader, g_intensityParameter, 1.0f ); + transferUSDParameter( network, shaderHandle, usdShader, g_normalizeParameter, shader, g_normalizeParameter, false ); + transferUSDParameter( network, shaderHandle, usdShader, g_shadowEnableParameter, shader, g_castShadowParameter, true ); + + shader->parameters()[g_useMISParameter] = new BoolData( true ); +} + void transferUSDShapingParameters( ShaderNetwork *network, InternedString shaderHandle, const Shader *usdShader, Shader *shader ) { if( auto d = usdShader->parametersData()->member( g_shapingConeAngleParameter ) ) @@ -1230,6 +1278,86 @@ void convertUSDUVTextures( ShaderNetwork *network ) } } +template +T *reportedCast( const IECore::RunTimeTyped *v, const char *type, const IECore::InternedString &name ) +{ + if( !v ) + { + return nullptr; + } + + T *t = IECore::runTimeCast( v ); + if( t ) + { + return t; + } + + IECore::msg( IECore::Msg::Warning, "IECoreCycles::ShaderNetworkAlgo", fmt::format( "Expected {} but got {} for {} \"{}\".", T::staticTypeName(), v->typeName(), type, name.c_str() ) ); + return nullptr; +} + +template +const T *attribute( const IECore::CompoundObject::ObjectMap &attributes, IECore::InternedString name ) +{ + auto it = attributes.find( name ); + if( it == attributes.end() ) + { + return nullptr; + } + + return reportedCast( it->second.get(), "attribute", name ); +} + +using ShaderNetworkAttributePair = pair; +ShaderNetworkAttributePair shaderNetworkAttribute( const vector &attributeNames, const IECore::CompoundObject::ObjectMap &attributes ) +{ + for( const auto &name : attributeNames ) + { + if( const auto *shaderNetwork = attribute( attributes, name ) ) + { + return { name, shaderNetwork }; + } + } + return { IECore::InternedString(), nullptr }; +} + +std::pair surfaceGlowParameters( const ShaderNetwork *shaderNetwork ) +{ + ShaderNetwork::Parameter emissionColorParameter; + ShaderNetwork::Parameter emissionColorInput; + + if( !shaderNetwork ) + { + return { emissionColorParameter, emissionColorInput }; + } + + for( const auto &[handle, shader] : shaderNetwork->shaders() ) + { + if( shader->getName() == "principled_bsdf" ) + { + emissionColorParameter = { handle, "emission_color" }; + break; + } + else if( shader->getName() == "emission" ) + { + emissionColorParameter = { handle, "color" }; + break; + } + else if( shader->getName() == "UsdPreviewSurface" ) + { + emissionColorParameter = { handle, g_emissiveColorParameter }; + break; + } + } + + if( emissionColorParameter ) + { + emissionColorInput = shaderNetwork->input( emissionColorParameter ); + } + + return { emissionColorParameter, emissionColorInput }; +} + } // namespace void IECoreCycles::ShaderNetworkAlgo::convertUSDShaders( ShaderNetwork *shaderNetwork ) @@ -1430,3 +1558,144 @@ void IECoreCycles::ShaderNetworkAlgo::convertUSDShaders( ShaderNetwork *shaderNe IECoreScene::ShaderNetworkAlgo::removeUnusedShaders( shaderNetwork ); } + +ConstCompoundObjectPtr IECoreCycles::ShaderNetworkAlgo::convertUSDMeshLightAttributes( const CompoundObject *attributes ) +{ + const auto *lightNetwork = attribute( attributes->members(), g_lightAttributeName ); + if( !lightNetwork ) + { + return attributes; + } + + const Shader *lightShader = lightNetwork->outputShader(); + if( !lightShader || lightShader->getName() != "MeshLight" ) + { + return attributes; + } + + CompoundObjectPtr result = attributes->copy(); + + // Get the surface shader or create a default shader if no surface shader is present. + const auto [surfaceAttribute, surfaceNetwork] = [&attributes, &result] + { + const auto attr = shaderNetworkAttribute( g_surfaceShaderAttributeNames, attributes->members() ); + if( !attr.second ) + { + const InternedString surfaceAttribute = g_surfaceShaderAttributeNames.front(); + result->members()[surfaceAttribute] = g_facingRatio->copy(); + return ShaderNetworkAttributePair{ surfaceAttribute, result->member( surfaceAttribute ) }; + } + return attr; + }(); + + ShaderNetworkPtr newSurfaceNetwork = surfaceNetwork->copy(); + + ShaderPtr emissionShader = new Shader( "emission", "shader" ); + transferCommonUSDLightParameters( lightShader, emissionShader.get() ); + + // `constantLightStrength()` includes `color`, which we handle separately, so we do our own calculation. + const float intensity = parameterValue( lightShader->parameters(), g_intensityParameter, 1.f ); + const float exposure = parameterValue( lightShader->parameters(), g_exposureParameter, 0.f ); + emissionShader->parameters()[g_strengthParameter] = new FloatData( intensity * powf( 2.f, exposure ) ); + + DataPtr visibilityData; + auto visibilityIterator = emissionShader->blindData()->readable().find( g_USDRayVisibilityBlindDataKey ); + if( visibilityIterator != emissionShader->blindData()->readable().end() ) + { + visibilityData = visibilityIterator->second; + emissionShader->blindData()->writable().erase( g_USDRayVisibilityBlindDataKey ); + } + + const Color3f lightColor = parameterValue( emissionShader.get(), g_colorParameter, Color3f( 1.f ) ); + + const auto &[emissionColorParameter, emissionColorInput] = surfaceGlowParameters( newSurfaceNetwork.get() ); + const Color3f emissionColor = emissionColorParameter ? parameterValue( surfaceNetwork->getShader( emissionColorParameter.shader ), emissionColorParameter.name, Color3f( 0.f ) ) : Color3f( 0.f ); + if( emissionColorParameter ) + { + emissionShader->parameters()[g_colorParameter] = new Color3fData( lightColor * emissionColor ); + } + + const InternedString emissionShaderHandle = newSurfaceNetwork->addShader( InternedString( "emission" ), std::move( emissionShader ) ); + + ShaderPtr lightPathShader = new Shader( "light_path", "shader" ); + const InternedString lightPathShaderHandle = newSurfaceNetwork->addShader( InternedString( "lightPath" ), std::move( lightPathShader ) ); + + ShaderPtr mixShader = new Shader( "mix_closure", "cycles:surface" ); + if( visibilityData ) + { + mixShader->blindData()->writable()[g_USDRayVisibilityBlindDataKey] = visibilityData; + } + const InternedString mixShaderHandle = newSurfaceNetwork->addShader( InternedString( "mixShader" ), std::move( mixShader ) ); + + const ShaderNetwork::Parameter originalOutputParameter = newSurfaceNetwork->getOutput(); + + newSurfaceNetwork->setOutput( { mixShaderHandle, g_closureOutput } ); + newSurfaceNetwork->addConnection( { { lightPathShaderHandle, g_isDiffuseRayOutput }, { mixShaderHandle, g_facParameter } } ); + newSurfaceNetwork->addConnection( { originalOutputParameter, { mixShaderHandle, g_closure1Parameter } } ); + newSurfaceNetwork->addConnection( { { emissionShaderHandle, g_emissionOutput }, { mixShaderHandle, g_closure2Parameter } } ); + + InternedString tintHandle; + ShaderNetwork::Parameter lightOutputParameter = lightNetwork->getOutput(); + const ShaderNetwork::Parameter meshLightColorParameter = { lightOutputParameter.shader, g_colorParameter }; + const ShaderNetwork::Parameter meshLightColorInput = lightNetwork->input( meshLightColorParameter ); + + if( emissionColorInput && ( lightColor != Color3f( 0.f ) || meshLightColorInput ) ) + { + ShaderNetwork::Parameter textureDestination = { emissionShaderHandle, g_colorParameter }; + if( lightColor != Color3f( 1.f ) || meshLightColorInput ) + { + ShaderPtr tintShader = new Shader( + "vector_math", + "shader", + { + { g_vector1Parameter, new Color3fData( emissionColor ) }, + { g_vector2Parameter, new Color3fData( lightColor ) }, + { g_mathTypeParameter, new StringData( "multiply" ) } + } + ); + tintHandle = newSurfaceNetwork->addShader( InternedString( "tint" ), std::move( tintShader ) ); + newSurfaceNetwork->addConnection( { { tintHandle, g_vectorOutput }, { emissionShaderHandle, g_colorParameter } } ); + textureDestination = { tintHandle, g_vector1Parameter }; + } + newSurfaceNetwork->addConnection( { emissionColorInput, textureDestination } ); + } + if( meshLightColorInput && ( emissionColor != Color3f( 0.f ) || emissionColorInput ) ) + { + ShaderNetworkPtr meshLightColorNetwork = lightNetwork->copy(); + meshLightColorNetwork->setOutput( meshLightColorInput ); + IECoreScene::ShaderNetworkAlgo::removeUnusedShaders( meshLightColorNetwork.get() ); + ShaderNetwork::Parameter newLightColorInput = IECoreScene::ShaderNetworkAlgo::addShaders( newSurfaceNetwork.get(), meshLightColorNetwork.get(), /* connections = */ true ); + + ShaderNetwork::Parameter textureDestination = { emissionShaderHandle, g_colorParameter }; + if( emissionColor != Color3f( 1.f ) || emissionColorInput ) + { + if( tintHandle == g_empty ) + { + ShaderPtr tintShader = new Shader( + "vector_math", + "shader", + { + { g_vector1Parameter, new Color3fData( emissionColor ) }, + { g_vector2Parameter, new Color3fData( lightColor ) }, + { g_mathTypeParameter, new StringData( "multiply" ) } + } + ); + tintHandle = newSurfaceNetwork->addShader( InternedString( "tint" ), std::move( tintShader ) ); + newSurfaceNetwork->addConnection( { { tintHandle, g_vectorOutput }, { emissionShaderHandle, g_colorParameter } } ); + } + textureDestination = { tintHandle, g_vector2Parameter }; + } + + if( !newSurfaceNetwork->input( textureDestination ) ) + { + newSurfaceNetwork->addConnection( { newLightColorInput, textureDestination } ); + } + } + + IECoreScene::ShaderNetworkAlgo::removeUnusedShaders( newSurfaceNetwork.get() ); + + result->members()[surfaceAttribute] = std::move( newSurfaceNetwork ); + result->members().erase( g_lightAttributeName ); + + return result; +} diff --git a/src/GafferCyclesModule/GafferCyclesModule.cpp b/src/GafferCyclesModule/GafferCyclesModule.cpp index 6df849cc1f..078318a838 100644 --- a/src/GafferCyclesModule/GafferCyclesModule.cpp +++ b/src/GafferCyclesModule/GafferCyclesModule.cpp @@ -54,6 +54,18 @@ using namespace GafferBindings; using namespace GafferDispatchBindings; using namespace GafferCycles; +namespace +{ + +IECore::CompoundObjectPtr convertUSDMeshLightAttributesWrapper( const IECore::CompoundObject &attributes, bool copy ) +{ + IECorePython::ScopedGILRelease r; + IECore::ConstCompoundObjectPtr result = IECoreCycles::ShaderNetworkAlgo::convertUSDMeshLightAttributes( &attributes ); + return copy ? result->copy() : boost::const_pointer_cast( result ); +} + +} // namespace + BOOST_PYTHON_MODULE( _GafferCycles ) { @@ -88,6 +100,7 @@ BOOST_PYTHON_MODULE( _GafferCycles ) scope shaderNetworkAlgoScope( shaderNetworkAlgoModule ); def( "convertUSDShaders", &IECoreCycles::ShaderNetworkAlgo::convertUSDShaders ); + def( "convertUSDMeshLightAttributes", &convertUSDMeshLightAttributesWrapper, ( arg_( "_copy" ) = true ) ); } } From c76841ad07822107a216498575427c3d1deb77b3 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Fri, 7 Aug 2026 15:30:38 -0400 Subject: [PATCH 11/11] GafferArnoldShaderTest : Fix test condition --- python/GafferArnoldTest/ArnoldShaderTest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/GafferArnoldTest/ArnoldShaderTest.py b/python/GafferArnoldTest/ArnoldShaderTest.py index dbc7e03ff4..04cbedf9f4 100644 --- a/python/GafferArnoldTest/ArnoldShaderTest.py +++ b/python/GafferArnoldTest/ArnoldShaderTest.py @@ -443,7 +443,7 @@ def testMeshLight( self ) : self.assertEqual( n["type"].getValue(), "ai:light" ) self.assertTrue( "exposure" in n["parameters"] ) - self.assertTrue( n["out"].typeId(), Gaffer.Plug.staticTypeId() ) + self.assertEqual( n["out"].typeId(), Gaffer.Plug.staticTypeId() ) def testColorParameterMetadata( self ) :