diff --git a/pxr/usd/usdLux/distantLight.h b/pxr/usd/usdLux/distantLight.h
index d89d005cca..651f33517d 100644
--- a/pxr/usd/usdLux/distantLight.h
+++ b/pxr/usd/usdLux/distantLight.h
@@ -136,10 +136,16 @@ class UsdLuxDistantLight : public UsdLuxNonboundableLightBase
// --------------------------------------------------------------------- //
// ANGLE
// --------------------------------------------------------------------- //
- /// Angular size of the light in degrees.
+ /// Angular diameter of the light in degrees.
/// As an example, the Sun is approximately 0.53 degrees as seen from Earth.
/// Higher values broaden the light and therefore soften shadow edges.
///
+ /// This value is assumed to be in the range `0 <= angle < 360`, and will
+ /// be clipped to this range. Note that this implies that we can have a
+ /// distant light emitting from more than a hemispherical area of light
+ /// if angle > 180. While this is valid, it is possible that for large
+ /// angles a DomeLight may provide better performance.
+ ///
///
/// | ||
/// | -- | -- |
diff --git a/pxr/usd/usdLux/generatedSchema.usda b/pxr/usd/usdLux/generatedSchema.usda
index 3b05417fe7..79477a0d2a 100644
--- a/pxr/usd/usdLux/generatedSchema.usda
+++ b/pxr/usd/usdLux/generatedSchema.usda
@@ -8,13 +8,48 @@ class "LightAPI" (
customData = {
token[] apiSchemaOverridePropertyNames = ["collection:lightLink:includeRoot", "collection:shadowLink:includeRoot"]
}
- doc = """API schema that imparts the quality of being a light onto a prim.
+ doc = '''API schema that imparts the quality of being a light onto a prim.
A light is any prim that has this schema applied to it. This is true
regardless of whether LightAPI is included as a built-in API of the prim
type (e.g. RectLight or DistantLight) or is applied directly to a Gprim
that should be treated as a light.
+ Quantities and Units
+
+ Most renderers consuming OpenUSD today are RGB renderers, rather than
+ spectral. Units in RGB renderers are tricky to define as each of the red,
+ green and blue channels transported by the renderer represents the
+ convolution of a spectral exposure distribution, e.g. CIE Illuminant D65,
+ with a sensor response function, e.g. CIE 1931 𝓍̅. Thus the main quantity
+ in an RGB renderer is neither radiance nor luminance, but "integrated
+ radiance" or "tristimulus weight".
+
+ The emission of a default light with `intensity` 1 and `color` [1, 1, 1] is
+ an Illuminant D spectral distribution with chromaticity matching the
+ rendering color space white point, normalized such that a ray normally
+ incident upon the sensor with EV0 exposure settings will generate a pixel
+ value of [1, 1, 1] in the rendering color space.
+
+ Given the above definition, that means that the luminance of said default
+ light will be 1 *nit (cd∕m²)* and its emission spectral radiance
+ distribution is easily computed by appropriate normalization.
+
+ For brevity, the term *emission* will be used in the documentation to mean
+ "emitted spectral radiance" or "emitted integrated radiance/tristimulus
+ weight", as appropriate.
+
+ The method of "uplifting" an RGB color to a spectral distribution is
+ unspecified other than that it should round-trip under the rendering
+ illuminant to the limits of numerical accuracy.
+
+ Note that some color spaces, most notably ACES, define their white points
+ by chromaticity coordinates that do not exactly line up to any value of a
+ standard illuminant. Because we do not define the method of uplift beyond
+ the round-tripping requirement, we discourage the use of such color spaces
+ as the rendering color space, and instead encourage the use of color spaces
+ whose white point has a well-defined spectral representation, such as D65.
+
Linking
Lights can be linked to geometry. Linking controls which geometry
@@ -31,7 +66,7 @@ class "LightAPI" (
include the desired objects. These are complementary approaches
that may each be preferable depending on the scenario and how
to best express the intent of the light setup.
- """
+ '''
)
{
uniform bool collection:lightLink:includeRoot = 1
@@ -39,7 +74,20 @@ class "LightAPI" (
color3f inputs:color = (1, 1, 1) (
displayGroup = "Basic"
displayName = "Color"
- doc = "The color of emitted light, in energy-linear terms."
+ doc = """The color of emitted light, in the rendering color space.
+
+ This color is just multiplied with the emission:
+
+
+ LColor = LScalar ⋅ color
+
+
+ In the case of a spectral renderer, this color should be uplifted such
+ that it round-trips to within the limit of numerical accuracy under the
+ rendering illuminant. We recommend the use of a rendering color space
+ well defined in terms of a Illuminant D illuminant, to avoid unspecified
+ uplift. See: \\ref usdLux_quantities
+ """
)
float inputs:colorTemperature = 6500 (
displayGroup = "Basic"
@@ -50,7 +98,18 @@ class "LightAPI" (
is from 1000 to 10000. Only takes effect when
enableColorTemperature is set to true. When active, the
computed result multiplies against the color attribute.
- See UsdLuxBlackbodyTemperatureAsRgb()."""
+ See UsdLuxBlackbodyTemperatureAsRgb().
+
+ This is always calculated as an RGB color using a D65 white point,
+ regardless of the rendering color space, normalized such that the
+ default value of 6500 will always result in white, and then should be
+ transformed to the rendering color space.
+
+ Spectral renderers should do the same and then uplift the resulting
+ color after multiplying with the `color` attribute. We recommend the
+ use of a rendering color space well defined in terms of a Illuminant D
+ illuminant, to avoid unspecified uplift. See: \\ref usdLux_quantities
+ """
)
float inputs:diffuse = 1 (
displayGroup = "Refine"
@@ -66,22 +125,158 @@ class "LightAPI" (
float inputs:exposure = 0 (
displayGroup = "Basic"
displayName = "Exposure"
- doc = """Scales the power of the light exponentially as a power
+ doc = """Scales the brightness of the light exponentially as a power
of 2 (similar to an F-stop control over exposure). The result
- is multiplied against the intensity."""
+ is multiplied against the intensity:
+
+
+ LScalar = LScalar ⋅ 2exposure
+
+
+ Normatively, the lights' emission is in units of spectral radiance
+ normalized such that a directly visible light with `intensity` 1 and
+ `exposure` 0 normally incident upon the sensor plane will generate a
+ pixel value of [1, 1, 1] in an RGB renderer, and thus have a luminance
+ of 1 nit (cd∕m²). A light with `intensity` 1 and `exposure` 2 would
+ therefore have a luminance of 4 nits.
+ """
)
float inputs:intensity = 1 (
displayGroup = "Basic"
displayName = "Intensity"
- doc = "Scales the power of the light linearly."
+ doc = """Scales the brightness of the light linearly.
+
+ Expresses the \"base\", unmultiplied luminance emitted (L) of the light,
+ in nits (cd∕m²):
+
+
+ LScalar = intensity
+
+
+ Normatively, the lights' emission is in units of spectral radiance
+ normalized such that a directly visible light with `intensity` 1 and
+ `exposure` 0 normally incident upon the sensor plane will generate a
+ pixel value of [1, 1, 1] in an RGB renderer, and thus have a luminance
+ of 1 nit. A light with `intensity` 2 and `exposure` 0 would therefore
+ have a luminance of 2 nits.
+ """
)
bool inputs:normalize = 0 (
displayGroup = "Advanced"
displayName = "Normalize Power"
- doc = """Normalizes power by the surface area of the light.
- This makes it easier to independently adjust the power and shape
- of the light, by causing the power to not vary with the area or
- angular size of the light."""
+ doc = """Normalizes the emission such that the power of the light
+ remains constant while altering the size of the light, by dividing the
+ luminance by the world-space surface area of the light.
+
+ This makes it easier to independently adjust the brightness and size
+ of the light, by causing the total illumination provided by a light to
+ not vary with the area or angular size of the light.
+
+ Mathematically, this means that the luminance of the light will be
+ divided by a factor representing the \"size\" of the light:
+
+
+ LScalar = LScalar / sizeFactor
+
+
+ ...where `sizeFactor` = 1 if `normalize` is off, and is calculated
+ depending on the family of the light as described below if `normalize`
+ is on.
+
+ ### DomeLight / PortalLight:
+
+ For a dome light (and its henchman, the PortalLight), this attribute is
+ ignored:
+
+
+ sizeFactordome = 1
+
+
+ ### Area Lights:
+
+ For an area light, the `sizeFactor` is the surface area (in world
+ space) of the shape of the light, including any scaling applied to the
+ light by its transform stack. This includes the boundable light types
+ which have a calculable surface area:
+
+ - MeshLightAPI
+ - DiskLight
+ - RectLight
+ - SphereLight
+ - CylinderLight
+ - (deprecated) GeometryLight
+
+
+ sizeFactorarea = worldSpaceSurfaceArea(light)
+
+
+ ### DistantLight:
+
+ For distant lights, we first define 𝛳max as:
+
+
+ 𝛳max = clamp(toRadians(distantLightAngle) / 2, 0, 𝜋)
+
+
+ Then we use the following formula:
+
+ * if 𝛳max = 0:
+
+ sizeFactordistant = 1
+
+
+ * if 0 < 𝛳max ≤ 𝜋 / 2:
+
+ sizeFactordistant = sin²𝛳max ⋅ 𝜋
+
+
+ * if 𝜋 / 2 < 𝛳max ≤ 𝜋:
+
+ sizeFactordistant =
+ (2 - sin²𝛳max) ⋅ 𝜋
+
+
+ This formula is used because it satisfies the following two properties:
+
+ 1. When normalize is enabled, the received illuminance from this light
+ on a surface normal to the light's primary direction is held constant
+ when angle changes, and the \"intensity\" property becomes a measure of
+ the illuminance, expressed in lux, for a light with 0 exposure.
+
+ 2. If we assume that our distant light is an approximation for a \"very
+ far\" sphere light (like the sun), then (for
+ *0 < 𝛳max ≤ 𝜋/2*) this definition agrees with the
+ definition used for area lights - ie, the total power of this distant
+ sphere light is constant when the \"size\" (ie, angle) changes, and our
+ sizeFactor is proportional to the total surface area of this sphere.
+
+ ### Other Lights
+
+ The above taxonomy describes behavior for all built-in light types.
+ (Note that the above is based on schema *family* - ie, `DomeLight_1`
+ follows the rules for a `DomeLight`, and ignores `normalize`.)
+
+ Lights from other third-party plugins / schemas must document their
+ expected behavior with regards to normalize. However, some general
+ guidelines are:
+
+ - Lights that either inherit from or are strongly associated with one of
+ the built-in types should follow the behavior of the built-in type
+ they inherit/resemble; ie, a renderer-specific \"MyRendererRectLight\"
+ should have its size factor be its world-space surface area
+ - Lights that are boundable and have a calcuable surface area should
+ follow the rules for an Area Light, and have their sizeFactor be their
+ world-space surface area
+ - Lights that are non-boundable and/or have no way to concretely or even
+ \"intuitively\" associate them with a \"size\" will ignore this attribe
+ (and always set sizeFactor = 1)
+
+ Lights that don't clearly meet any of the above criteria may either
+ ignore the normalize attribute or try to implement support using
+ whatever hueristic seems to make sense - for instance,
+ MyMandelbulbLight might use a sizeFactor equal to the world-space
+ surface area of a sphere which \"roughly\" bounds it.
+ """
)
float inputs:specular = 1 (
displayGroup = "Refine"
@@ -366,47 +561,224 @@ class "ShapingAPI" (
float inputs:shaping:cone:angle = 90 (
displayGroup = "Shaping"
displayName = "Cone Angle"
- doc = """Angular limit off the primary axis to restrict the
- light spread."""
+ doc = '''Angular limit off the primary axis to restrict the light
+ spread, in degrees.
+
+ Light emissions at angles off the primary axis greater than this are
+ guaranteed to be zero, ie:
+
+
+
+ 𝛳offAxis = acos(lightAxis • emissionDir)
+
+ 𝛳cutoff = toRadians(coneAngle)
+
+
+ 𝛳offAxis > 𝛳cutoff
+ ⟹ LScalar = 0
+
+
+
+ For angles < coneAngle, behavior is determined by shaping:cone:softness
+ - see below. But at the default of coneSoftness = 0, the luminance is
+ unaltered if the emissionOffAxisAngle <= coneAngle, so the coneAngle
+ functions as a hard binary "off" toggle for all angles > coneAngle.
+ '''
)
float inputs:shaping:cone:softness = 0 (
displayGroup = "Shaping"
displayName = "Cone Softness"
- doc = """Controls the cutoff softness for cone angle.
- TODO: clarify semantics"""
+ doc = '''Controls the cutoff softness for cone angle.
+
+ At the default of coneSoftness = 0, the luminance is unaltered if the
+ emissionOffAxisAngle <= coneAngle, and 0 if
+ emissionOffAxisAngle > coneAngle, so in this situation the coneAngle
+ functions as a hard binary "off" toggle for all angles > coneAngle.
+
+ For coneSoftness in the range (0, 1], it defines the proportion of the
+ non-cutoff angles over which the luminance is smoothly interpolated from
+ 0 to 1. Mathematically:
+
+
+ 𝛳offAxis = acos(lightAxis • emissionDir)
+
+ 𝛳cutoff = toRadians(coneAngle)
+
+ 𝛳smoothStart = lerp(coneSoftness, 𝛳cutoff, 0)
+
+ LScalar = LScalar ⋅
+ (1 - smoothStep(𝛳offAxis,
+ 𝛳smoothStart,
+ 𝛳cutoff)
+
+
+ Values outside of the [0, 1] range are clamped to the range.
+ '''
)
float inputs:shaping:focus = 0 (
displayGroup = "Shaping"
displayName = "Emission Focus"
doc = """A control to shape the spread of light. Higher focus
values pull light towards the center and narrow the spread.
- Implemented as an off-axis cosine power exponent.
- TODO: clarify semantics"""
+
+ This is implemented as a multiplication with the absolute value of the
+ dot product between the light's surface normal and the emission
+ direction, raised to the power `focus`. See `inputs:shaping:focusTint`
+ for the complete formula - but if we assume a default `focusTint` of
+ pure black, then that formula simplifies to:
+
+
+ focusFactor = |emissionDirection • lightNormal|focus
+
+ LColor = focusFactor ⋅ LColor
+
+
+ Values < 0 are ignored
+ """
)
color3f inputs:shaping:focusTint = (0, 0, 0) (
displayGroup = "Shaping"
displayName = "Emission Focus Tint"
doc = """Off-axis color tint. This tints the emission in the
falloff region. The default tint is black.
- TODO: clarify semantics"""
+
+ This is implemented as a linear interpolation between `focusTint` and
+ white, by the factor computed from the focus attribute, in other words:
+
+
+ focusFactor = |emissionDirection • lightNormal|focus
+
+ focusColor = lerp(focusFactor, focusTint, [1, 1, 1])
+
+ LColor =
+ componentwiseMultiply(focusColor, LColor)
+
+
+ Note that this implies that a focusTint of pure white will disable
+ focus.
+ """
)
float inputs:shaping:ies:angleScale = 0 (
displayGroup = "Shaping"
displayName = "Profile Scale"
doc = """Rescales the angular distribution of the IES profile.
- TODO: clarify semantics"""
+
+ Applies a scaling factor to the latitudinal theta/vertical polar
+ coordinate before sampling the ies profile, to shift the samples more
+ toward the \"top\" or \"bottom\" of the profile. The scaling origin varies
+ depending on whether `angleScale` is positive or negative. If it is
+ positive, the scaling origin is theta = 0; if it is negative, the
+ scaling origin is theta = pi (180 degrees). Values where
+ |angleScale| < 1 will \"shrink\" the angular range in which the
+ iesProfile is applied, while values where |angleScale| > 1 will
+ \"grow\" the angular range to which the iesProfile is mapped.
+
+ If 𝛳light is the latitudinal theta polar
+ coordinate of the emission direction in the light's local space, and
+ 𝛳ies is the value that will be used when
+ actually sampling the profile, then the exact formula is:
+
+ * if angleScale > 0:
+
+ 𝛳ies = 𝛳light / angleScale
+
+
+ * if angleScale = 0:
+
+ 𝛳ies = 𝛳light
+
+
+ * if angleScale < 0:
+
+ 𝛳ies = (𝛳light - π) / -angleScale
+
+
+ Usage guidelines for artists / lighting TDs:
+ --------------------------------------------
+
+ **if you have an ies profile for a spotlight aimed \"down\":**
+
+ - you should use a positive angleScale (> 0)
+ - values where 0 < angleScale < 1 will narrow the spotlight beam
+ - values where angleScale > 1 will broaden the spotlight beam
+ - ie, if the original ies profile is a downward spotlight with
+ a total cone angle of 60°, then angleScale = .5 will narrow it to
+ have a cone angle of 30°, and an angleScale of 1.5 will broaden it
+ to have a cone angle of 90°
+
+ **if you have an ies profile for a spotlight aimed \"up\":**
+
+ - you should use a negative angleScale (< 0)
+ - values where -1 < angleScale < 0 will narrow the spotlight beam
+ - values where angleScale < -1 will broaden the spotlight beam
+ - ie, if the original ies profile is an upward spotlight with
+ a total cone angle of 60°, then angleScale = -.5 will narrow it to
+ have a cone angle of 30°, and an angleScale of -1.5 will broaden
+ it to have a cone angle of 90°
+
+ **if you have an ies profile that's isn't clearly \"aimed\" in a single
+ direction, OR it's aimed in a direction other than straight up or
+ down:**
+
+ - applying angleScale will alter the vertical angle mapping for your
+ ies light, but it may be difficult to have a clear intuitive sense
+ of how varying the angleScale will affect the shape of your light
+
+ If you violate the above rules (ie, use a negative angleScale for a
+ spotlight aimed down), then angleScale will still alter the vertical-
+ angle mapping, but in more non-intuitive ways (ie, broadening /
+ narrowing may seem inverted, and the ies profile may seem to \"translate\"
+ through the vertical angles, rather than uniformly scale).
+ """
)
asset inputs:shaping:ies:file (
displayGroup = "Shaping"
displayName = "IES Profile"
doc = """An IES (Illumination Engineering Society) light
- profile describing the angular distribution of light."""
+ profile describing the angular distribution of light.
+
+ For full details on the .ies file format, see the full specification,
+ ANSI/IES LM-63-19:
+
+ https://store.ies.org/product/lm-63-19-approved-method-ies-standard-file-format-for-the-electronic-transfer-of-photometric-data-and-related-information/
+
+ The luminous intensity values in the ies profile are sampled using
+ the emission direction in the light's local space (after a possible
+ transformtion by a non-zero shaping:ies:angleScale, see below). The
+ sampled value is then potentially normalized by the overal power of the
+ profile if shaping:ies:normalize is enabled, and then used as a scaling
+ factor on the returned luminance:
+
+
+
+ 𝛳light, 𝜙 =
+ toPolarCoordinates(emissionDirectionInLightSpace)
+
+ 𝛳ies = applyAngleScale(𝛳light, angleScale)
+
+ iesSample = sampleIES(iesFile, 𝛳ies, 𝜙)
+
+ iesNormalize ⟹ iesSample = iesSample ⋅ iesProfilePower(iesFile)
+
+ LColor = iesSample ⋅ LColor
+
+
+ See `inputs:shaping:ies:angleScale` for a description of
+ `applyAngleScale`, and `inputs:shaping:ies:normalize` for how
+ `iesProfilePower` is calculated.
+ """
)
bool inputs:shaping:ies:normalize = 0 (
displayGroup = "Shaping"
displayName = "Profile Normalization"
doc = """Normalizes the IES profile so that it affects the shaping
- of the light while preserving the overall energy output."""
+ of the light while preserving the overall energy output.
+
+ The sampled luminous intensity is scaled by the overall power of the
+ ies profile if this is on, where the total power is calculated by
+ integrating the luminous intensity over all solid angle patches
+ defined in the profile.
+ """
)
}
@@ -697,14 +1069,34 @@ class DistantLight "DistantLight" (
float inputs:angle = 0.53 (
displayGroup = "Basic"
displayName = "Angle Extent"
- doc = """Angular size of the light in degrees.
+ doc = """Angular diameter of the light in degrees.
As an example, the Sun is approximately 0.53 degrees as seen from Earth.
Higher values broaden the light and therefore soften shadow edges.
+
+ This value is assumed to be in the range `0 <= angle < 360`, and will
+ be clipped to this range. Note that this implies that we can have a
+ distant light emitting from more than a hemispherical area of light
+ if angle > 180. While this is valid, it is possible that for large
+ angles a DomeLight may provide better performance.
"""
)
float inputs:intensity = 50000 (
- doc = """Scales the emission of the light linearly.
- The DistantLight has a high default intensity to approximate the Sun."""
+ doc = """Scales the brightness of the light linearly.
+
+ Expresses the \"base\", unmultiplied luminance emitted (L) of the light,
+ in nits (cd∕m²):
+
+
+ LScalar = intensity
+
+
+ Normatively, the lights' emission is in units of spectral radiance
+ normalized such that a directly visible light with `intensity` 1 and
+ `exposure` 0 normally incident upon the sensor plane will generate a
+ pixel value of [1, 1, 1] in an RGB renderer, and thus have a luminance
+ of 1 nit. A light with `intensity` 2 and `exposure` 0 would therefore
+ have a luminance of 2 nits.
+ """
)
uniform token light:shaderId = "DistantLight"
rel proxyPrim (
diff --git a/pxr/usd/usdLux/lightAPI.h b/pxr/usd/usdLux/lightAPI.h
index 691aee1f25..271b9f0a00 100644
--- a/pxr/usd/usdLux/lightAPI.h
+++ b/pxr/usd/usdLux/lightAPI.h
@@ -46,6 +46,41 @@ class SdfAssetPath;
/// type (e.g. RectLight or DistantLight) or is applied directly to a Gprim
/// that should be treated as a light.
///
+/// Quantities and Units
+///
+/// Most renderers consuming OpenUSD today are RGB renderers, rather than
+/// spectral. Units in RGB renderers are tricky to define as each of the red,
+/// green and blue channels transported by the renderer represents the
+/// convolution of a spectral exposure distribution, e.g. CIE Illuminant D65,
+/// with a sensor response function, e.g. CIE 1931 𝓍̅. Thus the main quantity
+/// in an RGB renderer is neither radiance nor luminance, but "integrated
+/// radiance" or "tristimulus weight".
+///
+/// The emission of a default light with `intensity` 1 and `color` [1, 1, 1] is
+/// an Illuminant D spectral distribution with chromaticity matching the
+/// rendering color space white point, normalized such that a ray normally
+/// incident upon the sensor with EV0 exposure settings will generate a pixel
+/// value of [1, 1, 1] in the rendering color space.
+///
+/// Given the above definition, that means that the luminance of said default
+/// light will be 1 *nit (cd∕m²)* and its emission spectral radiance
+/// distribution is easily computed by appropriate normalization.
+///
+/// For brevity, the term *emission* will be used in the documentation to mean
+/// "emitted spectral radiance" or "emitted integrated radiance/tristimulus
+/// weight", as appropriate.
+///
+/// The method of "uplifting" an RGB color to a spectral distribution is
+/// unspecified other than that it should round-trip under the rendering
+/// illuminant to the limits of numerical accuracy.
+///
+/// Note that some color spaces, most notably ACES, define their white points
+/// by chromaticity coordinates that do not exactly line up to any value of a
+/// standard illuminant. Because we do not define the method of uplift beyond
+/// the round-tripping requirement, we discourage the use of such color spaces
+/// as the rendering color space, and instead encourage the use of color spaces
+/// whose white point has a well-defined spectral representation, such as D65.
+///
/// Linking
///
/// Lights can be linked to geometry. Linking controls which geometry
@@ -268,7 +303,22 @@ class UsdLuxLightAPI : public UsdAPISchemaBase
// --------------------------------------------------------------------- //
// INTENSITY
// --------------------------------------------------------------------- //
- /// Scales the power of the light linearly.
+ /// Scales the brightness of the light linearly.
+ ///
+ /// Expresses the "base", unmultiplied luminance emitted (L) of the light,
+ /// in nits (cd∕m²):
+ ///
+ ///
+ /// LScalar = intensity
+ ///
+ ///
+ /// Normatively, the lights' emission is in units of spectral radiance
+ /// normalized such that a directly visible light with `intensity` 1 and
+ /// `exposure` 0 normally incident upon the sensor plane will generate a
+ /// pixel value of [1, 1, 1] in an RGB renderer, and thus have a luminance
+ /// of 1 nit. A light with `intensity` 2 and `exposure` 0 would therefore
+ /// have a luminance of 2 nits.
+ ///
///
/// | ||
/// | -- | -- |
@@ -290,9 +340,21 @@ class UsdLuxLightAPI : public UsdAPISchemaBase
// --------------------------------------------------------------------- //
// EXPOSURE
// --------------------------------------------------------------------- //
- /// Scales the power of the light exponentially as a power
+ /// Scales the brightness of the light exponentially as a power
/// of 2 (similar to an F-stop control over exposure). The result
- /// is multiplied against the intensity.
+ /// is multiplied against the intensity:
+ ///
+ ///
+ /// LScalar = LScalar ⋅ 2exposure
+ ///
+ ///
+ /// Normatively, the lights' emission is in units of spectral radiance
+ /// normalized such that a directly visible light with `intensity` 1 and
+ /// `exposure` 0 normally incident upon the sensor plane will generate a
+ /// pixel value of [1, 1, 1] in an RGB renderer, and thus have a luminance
+ /// of 1 nit (cd∕m²). A light with `intensity` 1 and `exposure` 2 would
+ /// therefore have a luminance of 4 nits.
+ ///
///
/// | ||
/// | -- | -- |
@@ -360,10 +422,119 @@ class UsdLuxLightAPI : public UsdAPISchemaBase
// --------------------------------------------------------------------- //
// NORMALIZE
// --------------------------------------------------------------------- //
- /// Normalizes power by the surface area of the light.
- /// This makes it easier to independently adjust the power and shape
- /// of the light, by causing the power to not vary with the area or
- /// angular size of the light.
+ /// Normalizes the emission such that the power of the light
+ /// remains constant while altering the size of the light, by dividing the
+ /// luminance by the world-space surface area of the light.
+ ///
+ /// This makes it easier to independently adjust the brightness and size
+ /// of the light, by causing the total illumination provided by a light to
+ /// not vary with the area or angular size of the light.
+ ///
+ /// Mathematically, this means that the luminance of the light will be
+ /// divided by a factor representing the "size" of the light:
+ ///
+ ///
+ /// LScalar = LScalar / sizeFactor
+ ///
+ ///
+ /// ...where `sizeFactor` = 1 if `normalize` is off, and is calculated
+ /// depending on the family of the light as described below if `normalize`
+ /// is on.
+ ///
+ /// ### DomeLight / PortalLight:
+ ///
+ /// For a dome light (and its henchman, the PortalLight), this attribute is
+ /// ignored:
+ ///
+ ///
+ /// sizeFactordome = 1
+ ///
+ ///
+ /// ### Area Lights:
+ ///
+ /// For an area light, the `sizeFactor` is the surface area (in world
+ /// space) of the shape of the light, including any scaling applied to the
+ /// light by its transform stack. This includes the boundable light types
+ /// which have a calculable surface area:
+ ///
+ /// - MeshLightAPI
+ /// - DiskLight
+ /// - RectLight
+ /// - SphereLight
+ /// - CylinderLight
+ /// - (deprecated) GeometryLight
+ ///
+ ///
+ /// sizeFactorarea = worldSpaceSurfaceArea(light)
+ ///
+ ///
+ /// ### DistantLight:
+ ///
+ /// For distant lights, we first define 𝛳max as:
+ ///
+ ///
+ /// 𝛳max = clamp(toRadians(distantLightAngle) / 2, 0, 𝜋)
+ ///
+ ///
+ /// Then we use the following formula:
+ ///
+ /// * if 𝛳max = 0:
+ ///
+ /// sizeFactordistant = 1
+ ///
+ ///
+ /// * if 0 < 𝛳max ≤ 𝜋 / 2:
+ ///
+ /// sizeFactordistant = sin²𝛳max ⋅ 𝜋
+ ///
+ ///
+ /// * if 𝜋 / 2 < 𝛳max ≤ 𝜋:
+ ///
+ /// sizeFactordistant =
+ /// (2 - sin²𝛳max) ⋅ 𝜋
+ ///
+ ///
+ /// This formula is used because it satisfies the following two properties:
+ ///
+ /// 1. When normalize is enabled, the received illuminance from this light
+ /// on a surface normal to the light's primary direction is held constant
+ /// when angle changes, and the "intensity" property becomes a measure of
+ /// the illuminance, expressed in lux, for a light with 0 exposure.
+ ///
+ /// 2. If we assume that our distant light is an approximation for a "very
+ /// far" sphere light (like the sun), then (for
+ /// *0 < 𝛳max ≤ 𝜋/2*) this definition agrees with the
+ /// definition used for area lights - ie, the total power of this distant
+ /// sphere light is constant when the "size" (ie, angle) changes, and our
+ /// sizeFactor is proportional to the total surface area of this sphere.
+ ///
+ /// ### Other Lights
+ ///
+ /// The above taxonomy describes behavior for all built-in light types.
+ /// (Note that the above is based on schema *family* - ie, `DomeLight_1`
+ /// follows the rules for a `DomeLight`, and ignores `normalize`.)
+ ///
+ /// Lights from other third-party plugins / schemas must document their
+ /// expected behavior with regards to normalize. However, some general
+ /// guidelines are:
+ ///
+ /// - Lights that either inherit from or are strongly associated with one of
+ /// the built-in types should follow the behavior of the built-in type
+ /// they inherit/resemble; ie, a renderer-specific "MyRendererRectLight"
+ /// should have its size factor be its world-space surface area
+ /// - Lights that are boundable and have a calcuable surface area should
+ /// follow the rules for an Area Light, and have their sizeFactor be their
+ /// world-space surface area
+ /// - Lights that are non-boundable and/or have no way to concretely or even
+ /// "intuitively" associate them with a "size" will ignore this attribe
+ /// (and always set sizeFactor = 1)
+ ///
+ /// Lights that don't clearly meet any of the above criteria may either
+ /// ignore the normalize attribute or try to implement support using
+ /// whatever hueristic seems to make sense - for instance,
+ /// MyMandelbulbLight might use a sizeFactor equal to the world-space
+ /// surface area of a sphere which "roughly" bounds it.
+ ///
///
/// | ||
/// | -- | -- |
@@ -385,7 +556,20 @@ class UsdLuxLightAPI : public UsdAPISchemaBase
// --------------------------------------------------------------------- //
// COLOR
// --------------------------------------------------------------------- //
- /// The color of emitted light, in energy-linear terms.
+ /// The color of emitted light, in the rendering color space.
+ ///
+ /// This color is just multiplied with the emission:
+ ///
+ ///
+ /// LColor = LScalar ⋅ color
+ ///
+ ///
+ /// In the case of a spectral renderer, this color should be uplifted such
+ /// that it round-trips to within the limit of numerical accuracy under the
+ /// rendering illuminant. We recommend the use of a rendering color space
+ /// well defined in terms of a Illuminant D illuminant, to avoid unspecified
+ /// uplift. See: \ref usdLux_quantities
+ ///
///
/// | ||
/// | -- | -- |
@@ -436,6 +620,17 @@ class UsdLuxLightAPI : public UsdAPISchemaBase
/// enableColorTemperature is set to true. When active, the
/// computed result multiplies against the color attribute.
/// See UsdLuxBlackbodyTemperatureAsRgb().
+ ///
+ /// This is always calculated as an RGB color using a D65 white point,
+ /// regardless of the rendering color space, normalized such that the
+ /// default value of 6500 will always result in white, and then should be
+ /// transformed to the rendering color space.
+ ///
+ /// Spectral renderers should do the same and then uplift the resulting
+ /// color after multiplying with the `color` attribute. We recommend the
+ /// use of a rendering color space well defined in terms of a Illuminant D
+ /// illuminant, to avoid unspecified uplift. See: \ref usdLux_quantities
+ ///
///
/// | ||
/// | -- | -- |
diff --git a/pxr/usd/usdLux/overview.dox b/pxr/usd/usdLux/overview.dox
index 542e5b8544..c15a0eec75 100644
--- a/pxr/usd/usdLux/overview.dox
+++ b/pxr/usd/usdLux/overview.dox
@@ -101,6 +101,55 @@ constraints, such as to attach lights to moving geometry. This is
consistent with USD's policy of storing the computed result of rigging,
not the rigging itself.
+\subsection usdLux_quantities Quantities and Units
+
+Most renderers consuming OpenUSD today are RGB renderers, rather than spectral.
+Units in RGB renderers are tricky to define as each of the red, green and blue
+channels transported by the renderer represents the convolution of a spectral
+exposure distribution, e.g. CIE Illuminant D65, with a sensor response function,
+e.g. CIE 1931 𝓍̅. Thus the main quantity in an RGB renderer is neither radiance
+nor luminance, but "integrated radiance" or "tristimulus weight".
+
+The emission of a default light with `intensity` 1 and `color` [1, 1, 1] is an
+Illuminant D spectral distribution with chromaticity matching the rendering
+color space white point, normalized such that a ray normally incident upon the
+sensor with EV0 exposure settings will generate a pixel value of [1, 1, 1] in
+the rendering color space.
+
+Given the above definition, that means that the luminance of said default light
+will be 1 *nit (cd∕m²)* and its emission spectral radiance distribution is
+easily computed by appropriate normalization.
+
+For brevity, the term *emission* will be used in the documentation to mean
+"emitted spectral radiance" or "emitted integrated radiance/tristimulus weight",
+as appropriate.
+
+The method of "uplifting" an RGB color to a spectral distribution is unspecified
+other than that it should round-trip under the rendering illuminant to the
+limits of numerical accuracy.
+
+Note that some color spaces, most notably ACES, define their white points by
+chromaticity coordinates that do not exactly line up to any value of a standard
+illuminant. Because we do not define the method of uplift beyond the round-
+tripping requirement, we discourage the use of such color spaces as the
+rendering color space, and instead encourage the use of color spaces whose white
+point has a well-defined spectral representation, such as D65.
+
+\subsubsection usdLux_quantities_exposure Exposure
+
+In order to generate an image, a camera effectively integrates irradiance at a patch
+on the sensor over time to get exposure, which is converted to an electrical signal.
+The default behaviour of many renderers is to ignore this, which as we have seen means
+that 1 nit incident on the sensor results in an output signal of 1.0.
+
+1 nit is a tiny amount of light, equivalent by definition to a single candle viewed at a distance. Thus
+using physically plausible values for light brightnesses will result in blown out images
+unless exposure is taken into account. A full physical camera model would be a good
+future addition to OpenUSD, but there is already a means of communicating sensor
+exposure via `UsdGeomCamera::GetExposureAttr()` and it is expected that all renderers
+implementing UsdLux must respect the value of that attribute by scaling the rendered
+image by `pow(2, exposure)`.
+
\subsection usdLux_Behavior Properties & Behavior
Colors specified in attributes are in energy-linear terms,
@@ -113,6 +162,14 @@ that do not measure the visible solid angle are expected to provide an
approximation, such as inverse-square falloff. Further artistic control
over attenuation can be modelled as light filters.
+The behavior of all attributes is in UsdLuxLightAPI is documented on their `Get()`
+methods. All attributes are expected to be supported by all light types, with
+the effect of each attribute applying multiplicatively in sequence.
+
+The example hdEmbree delegate provides a reference implementation, as well as a
+series of unit tests and reference images for renderers to check their own
+implementation against.
+
More complex behaviors are provided via a set of API classes.
These behaviors are common and well-defined but may not be supported
in all rendering environments. These API classes provide
diff --git a/pxr/usd/usdLux/schema.usda b/pxr/usd/usdLux/schema.usda
index 1efa467182..a4daa0a3b9 100644
--- a/pxr/usd/usdLux/schema.usda
+++ b/pxr/usd/usdLux/schema.usda
@@ -53,6 +53,41 @@ class "LightAPI" (
type (e.g. RectLight or DistantLight) or is applied directly to a Gprim
that should be treated as a light.
+ Quantities and Units
+
+ Most renderers consuming OpenUSD today are RGB renderers, rather than
+ spectral. Units in RGB renderers are tricky to define as each of the red,
+ green and blue channels transported by the renderer represents the
+ convolution of a spectral exposure distribution, e.g. CIE Illuminant D65,
+ with a sensor response function, e.g. CIE 1931 𝓍̅. Thus the main quantity
+ in an RGB renderer is neither radiance nor luminance, but "integrated
+ radiance" or "tristimulus weight".
+
+ The emission of a default light with `intensity` 1 and `color` [1, 1, 1] is
+ an Illuminant D spectral distribution with chromaticity matching the
+ rendering color space white point, normalized such that a ray normally
+ incident upon the sensor with EV0 exposure settings will generate a pixel
+ value of [1, 1, 1] in the rendering color space.
+
+ Given the above definition, that means that the luminance of said default
+ light will be 1 *nit (cd∕m²)* and its emission spectral radiance
+ distribution is easily computed by appropriate normalization.
+
+ For brevity, the term *emission* will be used in the documentation to mean
+ "emitted spectral radiance" or "emitted integrated radiance/tristimulus
+ weight", as appropriate.
+
+ The method of "uplifting" an RGB color to a spectral distribution is
+ unspecified other than that it should round-trip under the rendering
+ illuminant to the limits of numerical accuracy.
+
+ Note that some color spaces, most notably ACES, define their white points
+ by chromaticity coordinates that do not exactly line up to any value of a
+ standard illuminant. Because we do not define the method of uplift beyond
+ the round-tripping requirement, we discourage the use of such color spaces
+ as the rendering color space, and instead encourage the use of color spaces
+ whose white point has a well-defined spectral representation, such as D65.
+
Linking
Lights can be linked to geometry. Linking controls which geometry
@@ -152,7 +187,22 @@ class "LightAPI" (
float inputs:intensity = 1 (
displayGroup = "Basic"
displayName = "Intensity"
- doc = """Scales the power of the light linearly."""
+ doc = """Scales the brightness of the light linearly.
+
+ Expresses the "base", unmultiplied luminance emitted (L) of the light,
+ in nits (cd∕m²):
+
+
+ LScalar = intensity
+
+
+ Normatively, the lights' emission is in units of spectral radiance
+ normalized such that a directly visible light with `intensity` 1 and
+ `exposure` 0 normally incident upon the sensor plane will generate a
+ pixel value of [1, 1, 1] in an RGB renderer, and thus have a luminance
+ of 1 nit. A light with `intensity` 2 and `exposure` 0 would therefore
+ have a luminance of 2 nits.
+ """
customData = {
token apiName = "intensity"
}
@@ -160,9 +210,21 @@ class "LightAPI" (
float inputs:exposure = 0 (
displayGroup = "Basic"
displayName = "Exposure"
- doc = """Scales the power of the light exponentially as a power
+ doc = """Scales the brightness of the light exponentially as a power
of 2 (similar to an F-stop control over exposure). The result
- is multiplied against the intensity."""
+ is multiplied against the intensity:
+
+
+ LScalar = LScalar ⋅ 2exposure
+
+
+ Normatively, the lights' emission is in units of spectral radiance
+ normalized such that a directly visible light with `intensity` 1 and
+ `exposure` 0 normally incident upon the sensor plane will generate a
+ pixel value of [1, 1, 1] in an RGB renderer, and thus have a luminance
+ of 1 nit (cd∕m²). A light with `intensity` 1 and `exposure` 2 would
+ therefore have a luminance of 4 nits.
+ """
customData = {
token apiName = "exposure"
}
@@ -188,10 +250,119 @@ class "LightAPI" (
bool inputs:normalize = false (
displayGroup = "Advanced"
displayName = "Normalize Power"
- doc = """Normalizes power by the surface area of the light.
- This makes it easier to independently adjust the power and shape
- of the light, by causing the power to not vary with the area or
- angular size of the light."""
+ doc = """Normalizes the emission such that the power of the light
+ remains constant while altering the size of the light, by dividing the
+ luminance by the world-space surface area of the light.
+
+ This makes it easier to independently adjust the brightness and size
+ of the light, by causing the total illumination provided by a light to
+ not vary with the area or angular size of the light.
+
+ Mathematically, this means that the luminance of the light will be
+ divided by a factor representing the "size" of the light:
+
+
+ LScalar = LScalar / sizeFactor
+
+
+ ...where `sizeFactor` = 1 if `normalize` is off, and is calculated
+ depending on the family of the light as described below if `normalize`
+ is on.
+
+ ### DomeLight / PortalLight:
+
+ For a dome light (and its henchman, the PortalLight), this attribute is
+ ignored:
+
+
+ sizeFactordome = 1
+
+
+ ### Area Lights:
+
+ For an area light, the `sizeFactor` is the surface area (in world
+ space) of the shape of the light, including any scaling applied to the
+ light by its transform stack. This includes the boundable light types
+ which have a calculable surface area:
+
+ - MeshLightAPI
+ - DiskLight
+ - RectLight
+ - SphereLight
+ - CylinderLight
+ - (deprecated) GeometryLight
+
+
+ sizeFactorarea = worldSpaceSurfaceArea(light)
+
+
+ ### DistantLight:
+
+ For distant lights, we first define 𝛳max as:
+
+
+ 𝛳max = clamp(toRadians(distantLightAngle) / 2, 0, 𝜋)
+
+
+ Then we use the following formula:
+
+ * if 𝛳max = 0:
+
+ sizeFactordistant = 1
+
+
+ * if 0 < 𝛳max ≤ 𝜋 / 2:
+
+ sizeFactordistant = sin²𝛳max ⋅ 𝜋
+
+
+ * if 𝜋 / 2 < 𝛳max ≤ 𝜋:
+
+ sizeFactordistant =
+ (2 - sin²𝛳max) ⋅ 𝜋
+
+
+ This formula is used because it satisfies the following two properties:
+
+ 1. When normalize is enabled, the received illuminance from this light
+ on a surface normal to the light's primary direction is held constant
+ when angle changes, and the "intensity" property becomes a measure of
+ the illuminance, expressed in lux, for a light with 0 exposure.
+
+ 2. If we assume that our distant light is an approximation for a "very
+ far" sphere light (like the sun), then (for
+ *0 < 𝛳max ≤ 𝜋/2*) this definition agrees with the
+ definition used for area lights - ie, the total power of this distant
+ sphere light is constant when the "size" (ie, angle) changes, and our
+ sizeFactor is proportional to the total surface area of this sphere.
+
+ ### Other Lights
+
+ The above taxonomy describes behavior for all built-in light types.
+ (Note that the above is based on schema *family* - ie, `DomeLight_1`
+ follows the rules for a `DomeLight`, and ignores `normalize`.)
+
+ Lights from other third-party plugins / schemas must document their
+ expected behavior with regards to normalize. However, some general
+ guidelines are:
+
+ - Lights that either inherit from or are strongly associated with one of
+ the built-in types should follow the behavior of the built-in type
+ they inherit/resemble; ie, a renderer-specific "MyRendererRectLight"
+ should have its size factor be its world-space surface area
+ - Lights that are boundable and have a calcuable surface area should
+ follow the rules for an Area Light, and have their sizeFactor be their
+ world-space surface area
+ - Lights that are non-boundable and/or have no way to concretely or even
+ "intuitively" associate them with a "size" will ignore this attribe
+ (and always set sizeFactor = 1)
+
+ Lights that don't clearly meet any of the above criteria may either
+ ignore the normalize attribute or try to implement support using
+ whatever hueristic seems to make sense - for instance,
+ MyMandelbulbLight might use a sizeFactor equal to the world-space
+ surface area of a sphere which "roughly" bounds it.
+ """
customData = {
token apiName = "normalize"
}
@@ -199,7 +370,20 @@ class "LightAPI" (
color3f inputs:color = (1, 1, 1) (
displayGroup = "Basic"
displayName = "Color"
- doc = """The color of emitted light, in energy-linear terms."""
+ doc = """The color of emitted light, in the rendering color space.
+
+ This color is just multiplied with the emission:
+
+
+ LColor = LScalar ⋅ color
+
+
+ In the case of a spectral renderer, this color should be uplifted such
+ that it round-trips to within the limit of numerical accuracy under the
+ rendering illuminant. We recommend the use of a rendering color space
+ well defined in terms of a Illuminant D illuminant, to avoid unspecified
+ uplift. See: \\ref usdLux_quantities
+ """
customData = {
token apiName = "color"
}
@@ -221,7 +405,18 @@ class "LightAPI" (
is from 1000 to 10000. Only takes effect when
enableColorTemperature is set to true. When active, the
computed result multiplies against the color attribute.
- See UsdLuxBlackbodyTemperatureAsRgb()."""
+ See UsdLuxBlackbodyTemperatureAsRgb().
+
+ This is always calculated as an RGB color using a D65 white point,
+ regardless of the rendering color space, normalized such that the
+ default value of 6500 will always result in white, and then should be
+ transformed to the rendering color space.
+
+ Spectral renderers should do the same and then uplift the resulting
+ color after multiplying with the `color` attribute. We recommend the
+ use of a rendering color space well defined in terms of a Illuminant D
+ illuminant, to avoid unspecified uplift. See: \\ref usdLux_quantities
+ """
customData = {
token apiName = "colorTemperature"
}
@@ -474,8 +669,21 @@ class "ShapingAPI" (
displayName = "Emission Focus"
doc = """A control to shape the spread of light. Higher focus
values pull light towards the center and narrow the spread.
- Implemented as an off-axis cosine power exponent.
- TODO: clarify semantics"""
+
+ This is implemented as a multiplication with the absolute value of the
+ dot product between the light's surface normal and the emission
+ direction, raised to the power `focus`. See `inputs:shaping:focusTint`
+ for the complete formula - but if we assume a default `focusTint` of
+ pure black, then that formula simplifies to:
+
+
+ focusFactor = |emissionDirection • lightNormal|focus
+
+ LColor = focusFactor ⋅ LColor
+
+
+ Values < 0 are ignored
+ """
customData = {
token apiName = "shaping:focus"
}
@@ -485,7 +693,23 @@ class "ShapingAPI" (
displayName = "Emission Focus Tint"
doc = """Off-axis color tint. This tints the emission in the
falloff region. The default tint is black.
- TODO: clarify semantics"""
+
+ This is implemented as a linear interpolation between `focusTint` and
+ white, by the factor computed from the focus attribute, in other words:
+
+
+ focusFactor = |emissionDirection • lightNormal|focus
+
+ focusColor = lerp(focusFactor, focusTint, [1, 1, 1])
+
+ LColor =
+ componentwiseMultiply(focusColor, LColor)
+
+
+ Note that this implies that a focusTint of pure white will disable
+ focus.
+ """
+
customData = {
token apiName = "shaping:focusTint"
}
@@ -493,8 +717,29 @@ class "ShapingAPI" (
float inputs:shaping:cone:angle = 90 (
displayGroup = "Shaping"
displayName = "Cone Angle"
- doc = """Angular limit off the primary axis to restrict the
- light spread."""
+ doc = """Angular limit off the primary axis to restrict the light
+ spread, in degrees.
+
+ Light emissions at angles off the primary axis greater than this are
+ guaranteed to be zero, ie:
+
+
+
+ 𝛳offAxis = acos(lightAxis • emissionDir)
+
+ 𝛳cutoff = toRadians(coneAngle)
+
+
+ 𝛳offAxis > 𝛳cutoff
+ ⟹ LScalar = 0
+
+
+
+ For angles < coneAngle, behavior is determined by shaping:cone:softness
+ - see below. But at the default of coneSoftness = 0, the luminance is
+ unaltered if the emissionOffAxisAngle <= coneAngle, so the coneAngle
+ functions as a hard binary "off" toggle for all angles > coneAngle.
+ """
customData = {
token apiName = "shaping:cone:angle"
}
@@ -503,7 +748,32 @@ class "ShapingAPI" (
displayGroup = "Shaping"
displayName = "Cone Softness"
doc = """Controls the cutoff softness for cone angle.
- TODO: clarify semantics"""
+
+ At the default of coneSoftness = 0, the luminance is unaltered if the
+ emissionOffAxisAngle <= coneAngle, and 0 if
+ emissionOffAxisAngle > coneAngle, so in this situation the coneAngle
+ functions as a hard binary "off" toggle for all angles > coneAngle.
+
+ For coneSoftness in the range (0, 1], it defines the proportion of the
+ non-cutoff angles over which the luminance is smoothly interpolated from
+ 0 to 1. Mathematically:
+
+
+ 𝛳offAxis = acos(lightAxis • emissionDir)
+
+ 𝛳cutoff = toRadians(coneAngle)
+
+ 𝛳smoothStart = lerp(coneSoftness, 𝛳cutoff, 0)
+
+ LScalar = LScalar ⋅
+ (1 - smoothStep(𝛳offAxis,
+ 𝛳smoothStart,
+ 𝛳cutoff)
+
+
+ Values outside of the [0, 1] range are clamped to the range.
+ """
+
customData = {
token apiName = "shaping:cone:softness"
}
@@ -512,7 +782,38 @@ class "ShapingAPI" (
displayGroup = "Shaping"
displayName = "IES Profile"
doc = """An IES (Illumination Engineering Society) light
- profile describing the angular distribution of light."""
+ profile describing the angular distribution of light.
+
+ For full details on the .ies file format, see the full specification,
+ ANSI/IES LM-63-19:
+
+ https://store.ies.org/product/lm-63-19-approved-method-ies-standard-file-format-for-the-electronic-transfer-of-photometric-data-and-related-information/
+
+ The luminous intensity values in the ies profile are sampled using
+ the emission direction in the light's local space (after a possible
+ transformtion by a non-zero shaping:ies:angleScale, see below). The
+ sampled value is then potentially normalized by the overal power of the
+ profile if shaping:ies:normalize is enabled, and then used as a scaling
+ factor on the returned luminance:
+
+
+
+ 𝛳light, 𝜙 =
+ toPolarCoordinates(emissionDirectionInLightSpace)
+
+ 𝛳ies = applyAngleScale(𝛳light, angleScale)
+
+ iesSample = sampleIES(iesFile, 𝛳ies, 𝜙)
+
+ iesNormalize ⟹ iesSample = iesSample ⋅ iesProfilePower(iesFile)
+
+ LColor = iesSample ⋅ LColor
+
+
+ See `inputs:shaping:ies:angleScale` for a description of
+ `applyAngleScale`, and `inputs:shaping:ies:normalize` for how
+ `iesProfilePower` is calculated.
+ """
customData = {
token apiName = "shaping:ies:file"
}
@@ -521,7 +822,74 @@ class "ShapingAPI" (
displayGroup = "Shaping"
displayName = "Profile Scale"
doc = """Rescales the angular distribution of the IES profile.
- TODO: clarify semantics"""
+
+ Applies a scaling factor to the latitudinal theta/vertical polar
+ coordinate before sampling the ies profile, to shift the samples more
+ toward the "top" or "bottom" of the profile. The scaling origin varies
+ depending on whether `angleScale` is positive or negative. If it is
+ positive, the scaling origin is theta = 0; if it is negative, the
+ scaling origin is theta = pi (180 degrees). Values where
+ |angleScale| < 1 will "shrink" the angular range in which the
+ iesProfile is applied, while values where |angleScale| > 1 will
+ "grow" the angular range to which the iesProfile is mapped.
+
+ If 𝛳light is the latitudinal theta polar
+ coordinate of the emission direction in the light's local space, and
+ 𝛳ies is the value that will be used when
+ actually sampling the profile, then the exact formula is:
+
+ * if angleScale > 0:
+
+ 𝛳ies = 𝛳light / angleScale
+
+
+ * if angleScale = 0:
+
+ 𝛳ies = 𝛳light
+
+
+ * if angleScale < 0:
+
+ 𝛳ies = (𝛳light - π) / -angleScale
+
+
+ Usage guidelines for artists / lighting TDs:
+ --------------------------------------------
+
+ **if you have an ies profile for a spotlight aimed "down":**
+
+ - you should use a positive angleScale (> 0)
+ - values where 0 < angleScale < 1 will narrow the spotlight beam
+ - values where angleScale > 1 will broaden the spotlight beam
+ - ie, if the original ies profile is a downward spotlight with
+ a total cone angle of 60°, then angleScale = .5 will narrow it to
+ have a cone angle of 30°, and an angleScale of 1.5 will broaden it
+ to have a cone angle of 90°
+
+ **if you have an ies profile for a spotlight aimed "up":**
+
+ - you should use a negative angleScale (< 0)
+ - values where -1 < angleScale < 0 will narrow the spotlight beam
+ - values where angleScale < -1 will broaden the spotlight beam
+ - ie, if the original ies profile is an upward spotlight with
+ a total cone angle of 60°, then angleScale = -.5 will narrow it to
+ have a cone angle of 30°, and an angleScale of -1.5 will broaden
+ it to have a cone angle of 90°
+
+ **if you have an ies profile that's isn't clearly "aimed" in a single
+ direction, OR it's aimed in a direction other than straight up or
+ down:**
+
+ - applying angleScale will alter the vertical angle mapping for your
+ ies light, but it may be difficult to have a clear intuitive sense
+ of how varying the angleScale will affect the shape of your light
+
+ If you violate the above rules (ie, use a negative angleScale for a
+ spotlight aimed down), then angleScale will still alter the vertical-
+ angle mapping, but in more non-intuitive ways (ie, broadening /
+ narrowing may seem inverted, and the ies profile may seem to "translate"
+ through the vertical angles, rather than uniformly scale).
+ """
customData = {
token apiName = "shaping:ies:angleScale"
}
@@ -530,7 +898,13 @@ class "ShapingAPI" (
displayGroup = "Shaping"
displayName = "Profile Normalization"
doc = """Normalizes the IES profile so that it affects the shaping
- of the light while preserving the overall energy output."""
+ of the light while preserving the overall energy output.
+
+ The sampled luminous intensity is scaled by the overall power of the
+ ies profile if this is on, where the total power is calculated by
+ integrating the luminous intensity over all solid angle patches
+ defined in the profile.
+ """
customData = {
token apiName = "shaping:ies:normalize"
}
@@ -695,17 +1069,37 @@ class DistantLight "DistantLight" (
float inputs:angle = 0.53 (
displayGroup = "Basic"
displayName = "Angle Extent"
- doc = """Angular size of the light in degrees.
+ doc = """Angular diameter of the light in degrees.
As an example, the Sun is approximately 0.53 degrees as seen from Earth.
Higher values broaden the light and therefore soften shadow edges.
+
+ This value is assumed to be in the range `0 <= angle < 360`, and will
+ be clipped to this range. Note that this implies that we can have a
+ distant light emitting from more than a hemispherical area of light
+ if angle > 180. While this is valid, it is possible that for large
+ angles a DomeLight may provide better performance.
"""
customData = {
token apiName = "angle"
}
)
float inputs:intensity = 50000 (
- doc = """Scales the emission of the light linearly.
- The DistantLight has a high default intensity to approximate the Sun."""
+ doc = """Scales the brightness of the light linearly.
+
+ Expresses the "base", unmultiplied luminance emitted (L) of the light,
+ in nits (cd∕m²):
+
+
+ LScalar = intensity
+
+
+ Normatively, the lights' emission is in units of spectral radiance
+ normalized such that a directly visible light with `intensity` 1 and
+ `exposure` 0 normally incident upon the sensor plane will generate a
+ pixel value of [1, 1, 1] in an RGB renderer, and thus have a luminance
+ of 1 nit. A light with `intensity` 2 and `exposure` 0 would therefore
+ have a luminance of 2 nits.
+ """
customData = {
token apiName = "intensity"
bool apiSchemaOverride = true
diff --git a/pxr/usd/usdLux/shapingAPI.h b/pxr/usd/usdLux/shapingAPI.h
index 5c7304eac1..2b31856064 100644
--- a/pxr/usd/usdLux/shapingAPI.h
+++ b/pxr/usd/usdLux/shapingAPI.h
@@ -154,8 +154,21 @@ class UsdLuxShapingAPI : public UsdAPISchemaBase
// --------------------------------------------------------------------- //
/// A control to shape the spread of light. Higher focus
/// values pull light towards the center and narrow the spread.
- /// Implemented as an off-axis cosine power exponent.
- /// TODO: clarify semantics
+ ///
+ /// This is implemented as a multiplication with the absolute value of the
+ /// dot product between the light's surface normal and the emission
+ /// direction, raised to the power `focus`. See `inputs:shaping:focusTint`
+ /// for the complete formula - but if we assume a default `focusTint` of
+ /// pure black, then that formula simplifies to:
+ ///
+ ///
+ /// focusFactor = |emissionDirection • lightNormal|focus
+ ///
+ /// LColor = focusFactor ⋅ LColor
+ ///
+ ///
+ /// Values < 0 are ignored
+ ///
///
/// | ||
/// | -- | -- |
@@ -179,7 +192,22 @@ class UsdLuxShapingAPI : public UsdAPISchemaBase
// --------------------------------------------------------------------- //
/// Off-axis color tint. This tints the emission in the
/// falloff region. The default tint is black.
- /// TODO: clarify semantics
+ ///
+ /// This is implemented as a linear interpolation between `focusTint` and
+ /// white, by the factor computed from the focus attribute, in other words:
+ ///
+ ///
+ /// focusFactor = |emissionDirection • lightNormal|focus
+ ///
+ /// focusColor = lerp(focusFactor, focusTint, [1, 1, 1])
+ ///
+ /// LColor =
+ /// componentwiseMultiply(focusColor, LColor)
+ ///
+ ///
+ /// Note that this implies that a focusTint of pure white will disable
+ /// focus.
+ ///
///
/// | ||
/// | -- | -- |
@@ -201,8 +229,29 @@ class UsdLuxShapingAPI : public UsdAPISchemaBase
// --------------------------------------------------------------------- //
// SHAPING:CONE:ANGLE
// --------------------------------------------------------------------- //
- /// Angular limit off the primary axis to restrict the
- /// light spread.
+ /// Angular limit off the primary axis to restrict the light
+ /// spread, in degrees.
+ ///
+ /// Light emissions at angles off the primary axis greater than this are
+ /// guaranteed to be zero, ie:
+ ///
+ ///
+ ///
+ /// 𝛳offAxis = acos(lightAxis • emissionDir)
+ ///
+ /// 𝛳cutoff = toRadians(coneAngle)
+ ///
+ ///
+ /// 𝛳offAxis > 𝛳cutoff
+ /// ⟹ LScalar = 0
+ ///
+ ///
+ ///
+ /// For angles < coneAngle, behavior is determined by shaping:cone:softness
+ /// - see below. But at the default of coneSoftness = 0, the luminance is
+ /// unaltered if the emissionOffAxisAngle <= coneAngle, so the coneAngle
+ /// functions as a hard binary "off" toggle for all angles > coneAngle.
+ ///
///
/// | ||
/// | -- | -- |
@@ -225,7 +274,31 @@ class UsdLuxShapingAPI : public UsdAPISchemaBase
// SHAPING:CONE:SOFTNESS
// --------------------------------------------------------------------- //
/// Controls the cutoff softness for cone angle.
- /// TODO: clarify semantics
+ ///
+ /// At the default of coneSoftness = 0, the luminance is unaltered if the
+ /// emissionOffAxisAngle <= coneAngle, and 0 if
+ /// emissionOffAxisAngle > coneAngle, so in this situation the coneAngle
+ /// functions as a hard binary "off" toggle for all angles > coneAngle.
+ ///
+ /// For coneSoftness in the range (0, 1], it defines the proportion of the
+ /// non-cutoff angles over which the luminance is smoothly interpolated from
+ /// 0 to 1. Mathematically:
+ ///
+ ///
+ /// 𝛳offAxis = acos(lightAxis • emissionDir)
+ ///
+ /// 𝛳cutoff = toRadians(coneAngle)
+ ///
+ /// 𝛳smoothStart = lerp(coneSoftness, 𝛳cutoff, 0)
+ ///
+ /// LScalar = LScalar ⋅
+ /// (1 - smoothStep(𝛳offAxis,
+ /// 𝛳smoothStart,
+ /// 𝛳cutoff)
+ ///
+ ///
+ /// Values outside of the [0, 1] range are clamped to the range.
+ ///
///
/// | ||
/// | -- | -- |
@@ -249,6 +322,37 @@ class UsdLuxShapingAPI : public UsdAPISchemaBase
// --------------------------------------------------------------------- //
/// An IES (Illumination Engineering Society) light
/// profile describing the angular distribution of light.
+ ///
+ /// For full details on the .ies file format, see the full specification,
+ /// ANSI/IES LM-63-19:
+ ///
+ /// https://store.ies.org/product/lm-63-19-approved-method-ies-standard-file-format-for-the-electronic-transfer-of-photometric-data-and-related-information/
+ ///
+ /// The luminous intensity values in the ies profile are sampled using
+ /// the emission direction in the light's local space (after a possible
+ /// transformtion by a non-zero shaping:ies:angleScale, see below). The
+ /// sampled value is then potentially normalized by the overal power of the
+ /// profile if shaping:ies:normalize is enabled, and then used as a scaling
+ /// factor on the returned luminance:
+ ///
+ ///
+ ///
+ /// 𝛳light, 𝜙 =
+ /// toPolarCoordinates(emissionDirectionInLightSpace)
+ ///
+ /// 𝛳ies = applyAngleScale(𝛳light, angleScale)
+ ///
+ /// iesSample = sampleIES(iesFile, 𝛳ies, 𝜙)
+ ///
+ /// iesNormalize ⟹ iesSample = iesSample ⋅ iesProfilePower(iesFile)
+ ///
+ /// LColor = iesSample ⋅ LColor
+ ///
+ ///
+ /// See `inputs:shaping:ies:angleScale` for a description of
+ /// `applyAngleScale`, and `inputs:shaping:ies:normalize` for how
+ /// `iesProfilePower` is calculated.
+ ///
///
/// | ||
/// | -- | -- |
@@ -271,7 +375,74 @@ class UsdLuxShapingAPI : public UsdAPISchemaBase
// SHAPING:IES:ANGLESCALE
// --------------------------------------------------------------------- //
/// Rescales the angular distribution of the IES profile.
- /// TODO: clarify semantics
+ ///
+ /// Applies a scaling factor to the latitudinal theta/vertical polar
+ /// coordinate before sampling the ies profile, to shift the samples more
+ /// toward the "top" or "bottom" of the profile. The scaling origin varies
+ /// depending on whether `angleScale` is positive or negative. If it is
+ /// positive, the scaling origin is theta = 0; if it is negative, the
+ /// scaling origin is theta = pi (180 degrees). Values where
+ /// |angleScale| < 1 will "shrink" the angular range in which the
+ /// iesProfile is applied, while values where |angleScale| > 1 will
+ /// "grow" the angular range to which the iesProfile is mapped.
+ ///
+ /// If 𝛳light is the latitudinal theta polar
+ /// coordinate of the emission direction in the light's local space, and
+ /// 𝛳ies is the value that will be used when
+ /// actually sampling the profile, then the exact formula is:
+ ///
+ /// * if angleScale > 0:
+ ///
+ /// 𝛳ies = 𝛳light / angleScale
+ ///
+ ///
+ /// * if angleScale = 0:
+ ///
+ /// 𝛳ies = 𝛳light
+ ///
+ ///
+ /// * if angleScale < 0:
+ ///
+ /// 𝛳ies = (𝛳light - π) / -angleScale
+ ///
+ ///
+ /// Usage guidelines for artists / lighting TDs:
+ /// --------------------------------------------
+ ///
+ /// **if you have an ies profile for a spotlight aimed "down":**
+ ///
+ /// - you should use a positive angleScale (> 0)
+ /// - values where 0 < angleScale < 1 will narrow the spotlight beam
+ /// - values where angleScale > 1 will broaden the spotlight beam
+ /// - ie, if the original ies profile is a downward spotlight with
+ /// a total cone angle of 60°, then angleScale = .5 will narrow it to
+ /// have a cone angle of 30°, and an angleScale of 1.5 will broaden it
+ /// to have a cone angle of 90°
+ ///
+ /// **if you have an ies profile for a spotlight aimed "up":**
+ ///
+ /// - you should use a negative angleScale (< 0)
+ /// - values where -1 < angleScale < 0 will narrow the spotlight beam
+ /// - values where angleScale < -1 will broaden the spotlight beam
+ /// - ie, if the original ies profile is an upward spotlight with
+ /// a total cone angle of 60°, then angleScale = -.5 will narrow it to
+ /// have a cone angle of 30°, and an angleScale of -1.5 will broaden
+ /// it to have a cone angle of 90°
+ ///
+ /// **if you have an ies profile that's isn't clearly "aimed" in a single
+ /// direction, OR it's aimed in a direction other than straight up or
+ /// down:**
+ ///
+ /// - applying angleScale will alter the vertical angle mapping for your
+ /// ies light, but it may be difficult to have a clear intuitive sense
+ /// of how varying the angleScale will affect the shape of your light
+ ///
+ /// If you violate the above rules (ie, use a negative angleScale for a
+ /// spotlight aimed down), then angleScale will still alter the vertical-
+ /// angle mapping, but in more non-intuitive ways (ie, broadening /
+ /// narrowing may seem inverted, and the ies profile may seem to "translate"
+ /// through the vertical angles, rather than uniformly scale).
+ ///
///
/// | ||
/// | -- | -- |
@@ -295,6 +466,12 @@ class UsdLuxShapingAPI : public UsdAPISchemaBase
// --------------------------------------------------------------------- //
/// Normalizes the IES profile so that it affects the shaping
/// of the light while preserving the overall energy output.
+ ///
+ /// The sampled luminous intensity is scaled by the overall power of the
+ /// ies profile if this is on, where the total power is calculated by
+ /// integrating the luminous intensity over all solid angle patches
+ /// defined in the profile.
+ ///
///
/// | ||
/// | -- | -- |