diff --git a/Changes.md b/Changes.md
index d9545fc515c..e4bdc4a0024 100644
--- a/Changes.md
+++ b/Changes.md
@@ -5,6 +5,7 @@ Features
--------
- AttributeEditor, LightEditor, RenderPassEditor : Added drag and drop editing. Edits can be created or updated by dropping a value into a cell. Cells representing a set expression or string array can be modified by holding Shift to append to an existing edit, or Control may be held to remove from an existing edit.
+- USD : Added Cycles-specific light parameters to USD Lux lights.
Fixes
-----
diff --git a/src/GafferCycles/IECoreCyclesPreview/ShaderNetworkAlgo.cpp b/src/GafferCycles/IECoreCyclesPreview/ShaderNetworkAlgo.cpp
index 69e58fedd4a..1103b68bbb6 100644
--- a/src/GafferCycles/IECoreCyclesPreview/ShaderNetworkAlgo.cpp
+++ b/src/GafferCycles/IECoreCyclesPreview/ShaderNetworkAlgo.cpp
@@ -946,6 +946,8 @@ const InternedString g_widthParameter( "width" );
const InternedString g_wrapSParameter( "wrapS" );
const InternedString g_wrapTParameter( "wrapT" );
+const string g_cyclesNamespace( "cycles:" );
+
void transferUSDLightParameters( ShaderNetwork *network, InternedString shaderHandle, const Shader *usdShader, Shader *shader )
{
Color3f color = parameterValue( usdShader, g_colorParameter, Color3f( 1 ) );
@@ -967,6 +969,14 @@ void transferUSDLightParameters( ShaderNetwork *network, InternedString shaderHa
shader->parameters()[g_useGlossyParameter] = new BoolData( specular > 0.0f );
shader->parameters()[g_useMISParameter] = new BoolData( true );
+
+ for( const auto &[name, value] : usdShader->parameters() )
+ {
+ if( boost::starts_with( name.string(), g_cyclesNamespace ) )
+ {
+ shader->parameters()[name.string().substr(g_cyclesNamespace.size())] = value;
+ }
+ }
}
void transferUSDShapingParameters( ShaderNetwork *network, InternedString shaderHandle, const Shader *usdShader, Shader *shader )
diff --git a/startup/GafferUSD/cyclesLights.py b/startup/GafferUSD/cyclesLights.py
new file mode 100644
index 00000000000..8c0664affc5
--- /dev/null
+++ b/startup/GafferUSD/cyclesLights.py
@@ -0,0 +1,53 @@
+##########################################################################
+#
+# Copyright (c) 2025, Alex Fuller. 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 pathlib
+
+from pxr import Plug
+
+# Register a USD plugin that adds Cycles-specific auto-apply schemas for
+# UsdLux lights. We deliberately don't add this to the `PXR_PLUGINPATH_NAME`
+# search path because we don't want it to be loaded in any third-party
+# applications that Gaffer might launch as subprocessses. So instead we
+# register it manually with `RegisterPlugins`. See `GafferCycles.usda`
+# for more details.
+
+try :
+ import GafferCycles
+ Plug.Registry().RegisterPlugins( str( pathlib.Path( GafferCycles.__file__ ).parents[2] / "plugin" / "GafferCycles" / "plugInfo.json" ) )
+except ImportError :
+ # GafferCycles not available
+ pass
diff --git a/startup/gui/lightEditor.py b/startup/gui/lightEditor.py
index 584070579ec..f6c992a86f9 100644
--- a/startup/gui/lightEditor.py
+++ b/startup/gui/lightEditor.py
@@ -40,6 +40,44 @@
import Gaffer
import GafferSceneUI
+# UsdLux lights
+
+Gaffer.Metadata.registerValue( GafferSceneUI.LightEditor.Settings, "attribute", "preset:USD", "light" )
+
+GafferSceneUI.LightEditor.registerParameter( "light", "color" )
+GafferSceneUI.LightEditor.registerParameter( "light", "intensity" )
+GafferSceneUI.LightEditor.registerParameter( "light", "exposure" )
+GafferSceneUI.LightEditor.registerParameter( "light", "colorTemperature" )
+GafferSceneUI.LightEditor.registerParameter( "light", "enableColorTemperature" )
+GafferSceneUI.LightEditor.registerParameter( "light", "normalize" )
+GafferSceneUI.LightEditor.registerParameter( "light", "diffuse" )
+GafferSceneUI.LightEditor.registerParameter( "light", "specular" )
+
+GafferSceneUI.LightEditor.registerParameter( "light", "width", "Geometry" )
+GafferSceneUI.LightEditor.registerParameter( "light", "height", "Geometry" )
+GafferSceneUI.LightEditor.registerParameter( "light", "radius", "Geometry" )
+GafferSceneUI.LightEditor.registerParameter( "light", "treatAsPoint", "Geometry" )
+GafferSceneUI.LightEditor.registerParameter( "light", "length", "Geometry" )
+GafferSceneUI.LightEditor.registerParameter( "light", "treatAsLine", "Geometry" )
+GafferSceneUI.LightEditor.registerParameter( "light", "angle", "Geometry" )
+
+GafferSceneUI.LightEditor.registerParameter( "light", "texture:file", "Texture" )
+GafferSceneUI.LightEditor.registerParameter( "light", "texture:format", "Texture" )
+
+GafferSceneUI.LightEditor.registerParameter( "light", "shaping:cone:angle", "Shaping" )
+GafferSceneUI.LightEditor.registerParameter( "light", "shaping:cone:softness", "Shaping" )
+GafferSceneUI.LightEditor.registerParameter( "light", "shaping:focus", "Shaping" )
+GafferSceneUI.LightEditor.registerParameter( "light", "shaping:focusTint", "Shaping" )
+GafferSceneUI.LightEditor.registerParameter( "light", "shaping:ies:file", "Shaping" )
+GafferSceneUI.LightEditor.registerParameter( "light", "shaping:ies:angleScale", "Shaping" )
+GafferSceneUI.LightEditor.registerParameter( "light", "shaping:ies:normalize", "Shaping" )
+
+GafferSceneUI.LightEditor.registerParameter( "light", "shadow:enable", "Shadow" )
+GafferSceneUI.LightEditor.registerParameter( "light", "shadow:color", "Shadow" )
+GafferSceneUI.LightEditor.registerParameter( "light", "shadow:distance", "Shadow" )
+GafferSceneUI.LightEditor.registerParameter( "light", "shadow:falloff", "Shadow" )
+GafferSceneUI.LightEditor.registerParameter( "light", "shadow:falloffGamma", "Shadow" )
+
if os.environ.get( "CYCLES_ROOT" ) and os.environ.get( "GAFFERCYCLES_HIDE_UI", "" ) != "1" :
Gaffer.Metadata.registerValue( GafferSceneUI.LightEditor.Settings, "attribute", "preset:Cycles", "cycles:light" )
@@ -65,6 +103,18 @@
Gaffer.Metadata.registerValue( GafferSceneUI.LightEditor.Settings, "attribute", "userDefault", "cycles:light" )
+ # Register Cycles-specific parameters for USD lights.
+ for parameter in [
+ "lightgroup",
+ "use_mis", "use_camera", "use_diffuse", "use_glossy", "use_transmission", "use_scatter", "use_caustics",
+ "spread", "map_resolution", "max_bounces"
+ ] :
+ GafferSceneUI.LightEditor.registerParameter(
+ "light", f"cycles:{parameter}", "Cycles",
+ columnName = parameter.replace( "cycles:", "" )
+ )
+
+
with IECore.IgnoredExceptions( ImportError ) :
# This import appears unused, but it is intentional; it prevents us from
@@ -92,44 +142,6 @@
Gaffer.Metadata.registerValue( GafferSceneUI.LightEditor.Settings, "attribute", "userDefault", "osl:light" )
-# UsdLux lights
-
-Gaffer.Metadata.registerValue( GafferSceneUI.LightEditor.Settings, "attribute", "preset:USD", "light" )
-
-GafferSceneUI.LightEditor.registerParameter( "light", "color" )
-GafferSceneUI.LightEditor.registerParameter( "light", "intensity" )
-GafferSceneUI.LightEditor.registerParameter( "light", "exposure" )
-GafferSceneUI.LightEditor.registerParameter( "light", "colorTemperature" )
-GafferSceneUI.LightEditor.registerParameter( "light", "enableColorTemperature" )
-GafferSceneUI.LightEditor.registerParameter( "light", "normalize" )
-GafferSceneUI.LightEditor.registerParameter( "light", "diffuse" )
-GafferSceneUI.LightEditor.registerParameter( "light", "specular" )
-
-GafferSceneUI.LightEditor.registerParameter( "light", "width", "Geometry" )
-GafferSceneUI.LightEditor.registerParameter( "light", "height", "Geometry" )
-GafferSceneUI.LightEditor.registerParameter( "light", "radius", "Geometry" )
-GafferSceneUI.LightEditor.registerParameter( "light", "treatAsPoint", "Geometry" )
-GafferSceneUI.LightEditor.registerParameter( "light", "length", "Geometry" )
-GafferSceneUI.LightEditor.registerParameter( "light", "treatAsLine", "Geometry" )
-GafferSceneUI.LightEditor.registerParameter( "light", "angle", "Geometry" )
-
-GafferSceneUI.LightEditor.registerParameter( "light", "texture:file", "Texture" )
-GafferSceneUI.LightEditor.registerParameter( "light", "texture:format", "Texture" )
-
-GafferSceneUI.LightEditor.registerParameter( "light", "shaping:cone:angle", "Shaping" )
-GafferSceneUI.LightEditor.registerParameter( "light", "shaping:cone:softness", "Shaping" )
-GafferSceneUI.LightEditor.registerParameter( "light", "shaping:focus", "Shaping" )
-GafferSceneUI.LightEditor.registerParameter( "light", "shaping:focusTint", "Shaping" )
-GafferSceneUI.LightEditor.registerParameter( "light", "shaping:ies:file", "Shaping" )
-GafferSceneUI.LightEditor.registerParameter( "light", "shaping:ies:angleScale", "Shaping" )
-GafferSceneUI.LightEditor.registerParameter( "light", "shaping:ies:normalize", "Shaping" )
-
-GafferSceneUI.LightEditor.registerParameter( "light", "shadow:enable", "Shadow" )
-GafferSceneUI.LightEditor.registerParameter( "light", "shadow:color", "Shadow" )
-GafferSceneUI.LightEditor.registerParameter( "light", "shadow:distance", "Shadow" )
-GafferSceneUI.LightEditor.registerParameter( "light", "shadow:falloff", "Shadow" )
-GafferSceneUI.LightEditor.registerParameter( "light", "shadow:falloffGamma", "Shadow" )
-
# Arnold lights
with IECore.IgnoredExceptions( ImportError ) :
diff --git a/startup/gui/usd.py b/startup/gui/usd.py
index 8b0de997aaf..3b864938328 100644
--- a/startup/gui/usd.py
+++ b/startup/gui/usd.py
@@ -35,6 +35,7 @@
#
##########################################################################
+import os
import Gaffer
import GafferUSD
@@ -54,3 +55,15 @@
"samples", "volume_samples", "resolution"
] ) :
Gaffer.Metadata.registerValue( GafferUSD.USDLight, f"parameters.arnold:{parameter}", "layout:index", 1000 + i )
+
+# Change Cycles ordering.
+for i, parameter in enumerate( [
+ "lightgroup",
+ "use_mis", "use_camera", "use_diffuse", "use_glossy", "use_transmission", "use_scatter", "use_caustics",
+ "spread", "map_resolution", "max_bounces"
+] ) :
+ Gaffer.Metadata.registerValue( GafferUSD.USDLight, f"parameters.cycles:{parameter}", "layout:index", 2000 + i )
+
+# Only show the Cycles parameters if Cycles exists and not hidden
+Gaffer.Metadata.registerValue( GafferUSD.USDLight, "parameters", "layout:activator:cyclesUIEnabled", lambda x : os.environ.get( "CYCLES_ROOT" ) and os.environ.get( "GAFFERCYCLES_HIDE_UI", "" ) != "1" )
+Gaffer.Metadata.registerValue( GafferUSD.USDLight, "parameters.cycles:*", "layout:visibilityActivator", "cyclesUIEnabled" )
diff --git a/usdSchemas/GafferCycles.usda b/usdSchemas/GafferCycles.usda
new file mode 100644
index 00000000000..ae0d00ac3ed
--- /dev/null
+++ b/usdSchemas/GafferCycles.usda
@@ -0,0 +1,130 @@
+#usda 1.0
+(
+ subLayers = [
+ @usdLux/schema.usda@,
+ @usd/schema.usda@
+ ]
+)
+
+over "GLOBAL" (
+ customData = {
+ string libraryName = "GafferCycles"
+ bool skipCodeGeneration = 1
+ bool useLiteralIdentifier = 1
+ }
+)
+{
+}
+
+# Here we define a bunch of codeless auto-apply API schemas for extending the
+# standard UsdLux lights with inputs specific to Cycles. This approach is
+# modelled on the one used by UsdRiPxr to add RenderMan-specific inputs, and
+# we believe is the one Pixar intends everyone to use.
+
+class "GafferCyclesLightAPI" (
+ customData = {
+ token[] apiSchemaAutoApplyTo = [ "DistantLight", "DiskLight", "DomeLight", "RectLight", "SphereLight" ]
+ string apiSchemaType = "singleApply"
+ string className = "GafferCyclesLightAPI"
+ }
+ inherits =
+)
+{
+
+ string inputs:cycles:lightgroup = "" (
+ displayGroup = "Basic"
+ displayName = "Light Group (Cycles)"
+ )
+
+ bool inputs:cycles:use_mis = true (
+ displayGroup = "Refine"
+ displayName = "MIS (Cycles)"
+ )
+
+ bool inputs:cycles:use_camera = true (
+ displayGroup = "Refine"
+ displayName = "Camera (Cycles)"
+ )
+
+ bool inputs:cycles:use_diffuse = true (
+ displayGroup = "Refine"
+ displayName = "Diffuse (Cycles)"
+ )
+
+ bool inputs:cycles:use_glossy = true (
+ displayGroup = "Refine"
+ displayName = "Glossy (Cycles)"
+ )
+
+ bool inputs:cycles:use_transmission = true (
+ displayGroup = "Refine"
+ displayName = "Transmission (Cycles)"
+ )
+
+ bool inputs:cycles:use_scatter = true (
+ displayGroup = "Refine"
+ displayName = "Volume Scatter (Cycles)"
+ )
+
+ bool inputs:cycles:use_caustics = false (
+ displayGroup = "Refine"
+ displayName = "Shadow Caustics (Cycles)"
+ )
+
+ int inputs:cycles:max_bounces = 1024 (
+ displayGroup = "Refine"
+ displayName = "Max Bounces (Cycles)"
+ )
+
+}
+
+class "GafferCyclesDiskLightAPI" (
+ customData = {
+ token[] apiSchemaAutoApplyTo = ["DiskLight"]
+ string apiSchemaType = "singleApply"
+ string className = "GafferCyclesDiskLightAPI"
+ }
+ inherits =
+)
+{
+
+ float inputs:cycles:spread = 180.0 (
+ displayGroup = "Geometry"
+ displayName = "Spread (Cycles)"
+ )
+
+}
+
+class "GafferCyclesQuadLightAPI" (
+ customData = {
+ token[] apiSchemaAutoApplyTo = ["RectLight"]
+ string apiSchemaType = "singleApply"
+ string className = "GafferCyclesQuadLightAPI"
+ }
+ inherits =
+)
+{
+
+ float inputs:cycles:spread = 180.0 (
+ displayGroup = "Geometry"
+ displayName = "Spread (Cycles)"
+ )
+
+}
+
+class "GafferCyclesBackgroundLightAPI" (
+ customData = {
+ token[] apiSchemaAutoApplyTo = ["DomeLight"]
+ string apiSchemaType = "singleApply"
+ string className = "GafferCyclesBackgroundLightAPI"
+ }
+ inherits =
+)
+{
+
+ int inputs:cycles:map_resolution = 1024 (
+ displayGroup = "Sampling"
+ displayName = "Map Resolution (Cycles)"
+ )
+
+}