From a7749212eff5b3ee4e1fdbed97e0210f523e03ab Mon Sep 17 00:00:00 2001 From: Anders Langlands Date: Wed, 25 Oct 2023 10:21:50 +1300 Subject: [PATCH] Add explanation of UsdLux quantities and behavior Adds more detailed and precise explanations of expected behavior for UsdLux lights to UsdLux docs. --- pxr/usd/usdLux/distantLight.h | 8 +- pxr/usd/usdLux/generatedSchema.usda | 399 ++++++++++++++++++++++++++-- pxr/usd/usdLux/lightAPI.h | 211 ++++++++++++++- pxr/usd/usdLux/overview.dox | 57 ++++ pxr/usd/usdLux/schema.usda | 395 +++++++++++++++++++++++++-- pxr/usd/usdLux/shapingAPI.h | 150 ++++++++++- 6 files changed, 1159 insertions(+), 61 deletions(-) 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..6c7513a6b5 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,183 @@ 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 is + centered at theta = pi / 180 degrees, so theta = pi is always unaltered, + regardless of the angleScale. The scaling amount is `1 + angleScale`. + This has the effect that negative values (greater than -1.0) decrease + the sampled IES theta, while positive values increase the sampled IES + theta. + + Specifically, this factor is applied: + +
+ profileScale = 1 + angleScale + + 𝛳ies = (𝛳light - 𝜋) / profileScale + 𝜋 + + 𝛳ies = clamp(𝛳ies, 0, 𝜋) +
+ + ...where 𝛳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. + + Values below -1.0 are clipped to -1.0. + """ ) 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 +1028,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..40424eea47 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,33 @@ 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 is + centered at theta = pi / 180 degrees, so theta = pi is always unaltered, + regardless of the angleScale. The scaling amount is `1 + angleScale`. + This has the effect that negative values (greater than -1.0) decrease + the sampled IES theta, while positive values increase the sampled IES + theta. + + Specifically, this factor is applied: + +
+ profileScale = 1 + angleScale + + 𝛳ies = (𝛳light - 𝜋) / profileScale + 𝜋 + + 𝛳ies = clamp(𝛳ies, 0, 𝜋) +
+ + ...where 𝛳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. + + Values below -1.0 are clipped to -1.0. + """ customData = { token apiName = "shaping:ies:angleScale" } @@ -530,7 +857,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 +1028,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..4a8a9715eb 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,33 @@ 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 is + /// centered at theta = pi / 180 degrees, so theta = pi is always unaltered, + /// regardless of the angleScale. The scaling amount is `1 + angleScale`. + /// This has the effect that negative values (greater than -1.0) decrease + /// the sampled IES theta, while positive values increase the sampled IES + /// theta. + /// + /// Specifically, this factor is applied: + /// + ///
+ /// profileScale = 1 + angleScale + /// + /// 𝛳ies = (𝛳light - 𝜋) / profileScale + 𝜋 + /// + /// 𝛳ies = clamp(𝛳ies, 0, 𝜋) + ///
+ /// + /// ...where 𝛳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. + /// + /// Values below -1.0 are clipped to -1.0. + /// /// /// | || /// | -- | -- | @@ -295,6 +425,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. + /// /// /// | || /// | -- | -- |