Add light softness: spotlight edge softness for physically-based rendering.

The new spotlight attribute softness (real in [0, 1], default 0) is the
fraction of the cone, measured inward from the cutoff, over which
intensity falls to zero. It is used by physically-based lighting models;
the Phong model's corresponding knob remains exponent.

The filament renderer previously hardcoded the inner cone angle to 0,
making the entire beam penumbra: the shader attenuates by the squared
smoothstep ((cos(theta) - cos(outer)) / (cos(inner) - cos(outer)))^2, so
a cutoff-25 spot delivered its rated candela only exactly on-axis and
about a third of it averaged over the light pool, with the deficit
shrinking as the cutoff widens. The inner angle is now
(1 - softness) * cutoff, so at the default the light delivers its full
intensity everywhere inside the cone and illuminance follows E = I/d^2
independent of the cutoff. Setting softness to 1 reproduces the previous
appearance exactly (verified bit-identical), which is the migration path
for models tuned against the old behavior.

The filament light type also changes from FOCUSED_SPOT to SPOT. With
intensity given in candela and the cone set at build time the two types
produce identical output (FOCUSED_SPOT's power-conserving rescale only
applies when the cone changes after the intensity is set), but SPOT
guarantees that candela never rescales with cone angle should the cone
ever become runtime-editable.

Verified with headless renders under a linear tone mapper against an
equal-candela point light at cutoffs 25/45/80: softness 0 gives
spot/point luminance ratio 1.000 at all sampled angles inside the cone;
softness 0.2 is flat over the inner 80% of the cone; softness 1 matches
the previous renderer with zero linear-pixel difference. XML round-trip
and the [0, 1] compile-time check verified. Introspect and wasm bindings
regenerated.

PiperOrigin-RevId: 959334706
Change-Id: I0f0729781899880de1729ea9b3d8c055d715a025
This commit is contained in:
Yuval Tassa
2026-08-04 18:11:03 -07:00
committed by Copybara-Service
parent 92261d9095
commit f9a00bd5b5
27 changed files with 118 additions and 15 deletions
+2
View File
@@ -1495,6 +1495,7 @@
<xs:attribute name="range" type="xs:float" default="10"/>
<xs:attribute name="attenuation" type="float3" default="1 0 0"/>
<xs:attribute name="cutoff" type="xs:float" default="45"/>
<xs:attribute name="softness" type="xs:float" default="0"/>
<xs:attribute name="exponent" type="xs:float" default="10"/>
<xs:attribute name="ambient" type="float3"/>
<xs:attribute name="diffuse" type="float3" default="0.7 0.7 0.7"/>
@@ -2273,6 +2274,7 @@
<xs:attribute name="range" type="xs:float" default="10"/>
<xs:attribute name="attenuation" type="float3" default="1 0 0"/>
<xs:attribute name="cutoff" type="xs:float" default="45"/>
<xs:attribute name="softness" type="xs:float" default="0"/>
<xs:attribute name="exponent" type="xs:float" default="10"/>
<xs:attribute name="ambient" type="float3"/>
<xs:attribute name="diffuse" type="float3" default="0.7 0.7 0.7"/>
+1
View File
@@ -394,6 +394,7 @@ static const mjXDefaultEntry kDefaults_mjsLight[] = {
{"range", (int)offsetof(mjsLight, range), 1, 1, 1, 0, {10.0}},
{"attenuation", (int)offsetof(mjsLight, attenuation), 1, 3, 3, 0, {1.0, 0.0, 0.0}},
{"cutoff", (int)offsetof(mjsLight, cutoff), 1, 1, 1, 0, {45.0}},
{"softness", (int)offsetof(mjsLight, softness), 1, 1, 1, 0, {0.0}},
{"exponent", (int)offsetof(mjsLight, exponent), 1, 1, 1, 0, {10.0}},
{"ambient", (int)offsetof(mjsLight, ambient), 1, 3, 0, 0, {0}},
{"diffuse", (int)offsetof(mjsLight, diffuse), 1, 3, 3, 0, {0.7, 0.7, 0.7}},
+1
View File
@@ -426,6 +426,7 @@ inline constexpr mjXAttr kLightAttrs[] = {
{"range", mjXAttr::kFloat, 1, true, false, false, false, (int)offsetof(mjsLight, range)},
{"attenuation", mjXAttr::kFloat, 3, true, false, false, false, (int)offsetof(mjsLight, attenuation)},
{"cutoff", mjXAttr::kFloat, 1, true, false, false, false, (int)offsetof(mjsLight, cutoff)},
{"softness", mjXAttr::kFloat, 1, true, false, false, false, (int)offsetof(mjsLight, softness)},
{"exponent", mjXAttr::kFloat, 1, true, false, false, false, (int)offsetof(mjsLight, exponent)},
{"ambient", mjXAttr::kFloat, 3, true, false, false, false, (int)offsetof(mjsLight, ambient)},
{"diffuse", mjXAttr::kFloat, 3, true, false, false, false, (int)offsetof(mjsLight, diffuse)},
+4 -4
View File
@@ -92,8 +92,8 @@ std::vector<const char*> MJCF[] = {
"axisangle", "xyaxes", "zaxis", "euler", "mode", "focal", "focalpixel", "principal",
"principalpixel", "sensorsize", "user"},
{"light", "?", "directional", "type", "castshadow", "active", "pos", "dir", "bulbradius",
"intensity", "range", "attenuation", "cutoff", "exponent", "ambient", "diffuse",
"specular", "mode"},
"intensity", "range", "attenuation", "cutoff", "softness", "exponent", "ambient",
"diffuse", "specular", "mode"},
{"pair", "?", "condim", "friction", "solref", "solreffriction", "solimp", "gap", "margin",
"adhesion"},
{"equality", "?", "active", "solref", "solimp"},
@@ -204,8 +204,8 @@ std::vector<const char*> MJCF[] = {
"pos", "quat", "axisangle", "xyaxes", "zaxis", "euler", "mode", "target", "focal",
"focalpixel", "principal", "principalpixel", "sensorsize", "user"},
{"light", "*", "name", "class", "directional", "type", "castshadow", "active", "pos",
"dir", "bulbradius", "intensity", "range", "attenuation", "cutoff", "exponent",
"ambient", "diffuse", "specular", "mode", "target", "texture"},
"dir", "bulbradius", "intensity", "range", "attenuation", "cutoff", "softness",
"exponent", "ambient", "diffuse", "specular", "mode", "target", "texture"},
{"plugin", "*", "plugin", "instance"},
{"<"},
{"config", "*", "key", "value"},
+1
View File
@@ -1117,6 +1117,7 @@ element light : mjsLight {
range : float = 10
attenuation : float[3] = {1, 0, 0}
cutoff : float = 45
softness : float = 0
exponent : float = 10
ambient : float[3]
diffuse : float[3] = {0.7, 0.7, 0.7}