physical-lights
physical-lights
PHYSICAL LIGHTS — a one-scene port of three.js `webgl_lights_physical`, kept faithful on purpose: it is the acceptance test for the point light's inverse-square decay, Reinhard tone mapping and the bump/roughness/metalness texture slots.
Read the rig's notes
THE SCALE FACTOR IS 1000. The example is authored in METRES (its own comment
says so: "Brick cube is 50 cm in size"); the authoring unit here is the
MILLIMETRE (packages/core/units.ts). So every length below is the example's
number x 1000 — and exactly ONE parameter carries its own factor, because it
is not a length:
light intensity x 1000^2 = x 1e6 decay=2 is inverse-square in WORLD
units, so a 1000x longer distance is
1e6 less irradiance. Cancel it on the
light. (Contrast the rect-area-light
port, which needed no rescale at all:
area-light irradiance is a solid
angle, and a solid angle is
scale-invariant.)
EVERYTHING ELSE IS SCALE-FREE, bumpScale included — and that one is worth
stating because it looks like it should not be. `perturbNormalArb` divides
the height gradient by dFdx(surf_pos), which would shrink the bump as the
scene grows — except three NORMALIZES that derivative
(`vec3 vSigmaX = normalize( dFdx( surf_pos.xyz ) )`), so the world scale
divides straight back out. bumpScale is the example's 1. Authored at 1000 it
does not deepen the grain, it destroys the normals: the floor crawls with
speckle and the brick faces read as a fine wire grid.
emissiveIntensity x 1 a radiance, not a falloff.
hemisphere x 1 ambient irradiance does not decay.
THE NUMBERS, DERIVED
bulbPower = keys[4] = 400 lm (the example's default)
PointLight.power sets intensity = power / 4pi = 31.830988 cd
x 1e6 = 31830988.6 <- authored
emissiveIntensity = intensity / 0.02^2
= 31.830988 / 0.0004 = 79577.47 <- authored
hemiIrradiance = keys[0] = 0.0001 lx
exposure = 0.68^5 = 0.14539336
THE BULB'S BOB IS EXACT, NOT APPROXIMATED
bulbLight.position.y = cos(Date.now() * 0.0005) * 0.75 + 1.25
is a cosine of period 2pi / 0.0005 ms = 12.566 s, swinging 2000 -> 500 -> 2000
mm. easeInOutSine IS a raised cosine, so three keyframes at 0 / 6.283 / 12.566
reproduce the curve exactly — no sampling, no drift. The example runs off the
wall clock and therefore starts at an arbitrary phase; a reel is deterministic,
so this one starts at the top of the swing.
THE BULB MESH IS A SIBLING, NOT A CHILD
The example parents the emissive sphere to the light (`bulbLight.add(mesh)`).
A promo light takes no children, so the sphere is its own primitive carrying a
duplicate copy of the same position.y track — and the copy is only identical
because the sphere authors NO `position` attr. A node's transform keyframes
are RELATIVE to its base attribute (xml-scene resolveRelativeTransformKeyframes:
base + value), a light's are absolute; give the sphere `position="0 2000 0"`
as well and its track resolves to 4000/2500/4000 and it flies off the light.
It must NOT cast a shadow: the
light sits at its centre, so a shadow-casting bulb would put the entire scene
in shadow. The example gets that free (castShadow defaults to false); here it
is authored, and the shadow camera's 500mm near plane clips the 20mm sphere
out of the depth cube as a second line of defence.
TEXTURE SLOTS ARE AUTHORED PER PRIMITIVE, NOT IN <materials>
A <materials> definition's params go straight to MeshStandardMaterial, which
would receive `bump_map` as a STRING. Named definitions carry PBR knobs; the
slots must ride on the attachment. Hence the three brick cubes repeat their
material block rather than sharing one name.
NOT PORTED (there is no WebGL in any of it): the #info credit line, the
lil-gui panel, the Stats meter, and OrbitControls — a reel has an authored
camera, and this one holds the example's opening view.
ASSETS REQUIRED in assets/images/ (from three.js examples/textures/):
hardwood2_diffuse.jpg hardwood2_bump.jpg hardwood2_roughness.jpg
brick_diffuse.jpg brick_bump.jpg
earth_atmos_2048.jpg earth_specular_2048.jpg
THIS PORT ADDED THREE THINGS TO THE ENGINE (promo-runtime.ts), all of which
the reel default is free to ignore:
TONE_MAPPINGS the complete three table, Reinhard included. It used
to hold Linear/ACESFilmic/None and fall back to
ACESFilmic — so an unmapped name was not an error,
it was a silently different picture.
SHADOW_MAP_TYPES `renderer shadow_map_type=basic`. PCFSoft stays the
default; a port needs three's own PCFShadowMap or
the shadow edges do not match.
cast_shadow / per-primitive opt-out of the blanket both-flags-on.
receive_shadow Required here, not cosmetic — see the floor and the
bulb mesh below.- Topic
- lights
- Scenes
- 2
- Kind
- reference rig
//
PHYSICAL LIGHTS — a one-scene port of three.js `webgl_lights_physical`,
kept faithful on purpose: it is the acceptance test for the point light's
inverse-square decay, Reinhard tone mapping and the bump/roughness/metalness
texture slots.
THE SCALE FACTOR IS 1000. The example is authored in METRES (its own comment
says so: "Brick cube is 50 cm in size"); the authoring unit here is the
MILLIMETRE (packages/core/units.ts). So every length below is the example's
number x 1000 — and exactly ONE parameter carries its own factor, because it
is not a length:
light intensity x 1000^2 = x 1e6 decay=2 is inverse-square in WORLD
units, so a 1000x longer distance is
1e6 less irradiance. Cancel it on the
light. (Contrast the rect-area-light
port, which needed no rescale at all:
area-light irradiance is a solid
angle, and a solid angle is
scale-invariant.)
EVERYTHING ELSE IS SCALE-FREE, bumpScale included — and that one is worth
stating because it looks like it should not be. `perturbNormalArb` divides
the height gradient by dFdx(surf_pos), which would shrink the bump as the
scene grows — except three NORMALIZES that derivative
(`vec3 vSigmaX = normalize( dFdx( surf_pos.xyz ) )`), so the world scale
divides straight back out. bumpScale is the example's 1. Authored at 1000 it
does not deepen the grain, it destroys the normals: the floor crawls with
speckle and the brick faces read as a fine wire grid.
emissiveIntensity x 1 a radiance, not a falloff.
hemisphere x 1 ambient irradiance does not decay.
THE NUMBERS, DERIVED
bulbPower = keys[4] = 400 lm (the example's default)
PointLight.power sets intensity = power / 4pi = 31.830988 cd
x 1e6 = 31830988.6 <- authored
emissiveIntensity = intensity / 0.02^2
= 31.830988 / 0.0004 = 79577.47 <- authored
hemiIrradiance = keys[0] = 0.0001 lx
exposure = 0.68^5 = 0.14539336
THE BULB'S BOB IS EXACT, NOT APPROXIMATED
bulbLight.position.y = cos(Date.now() * 0.0005) * 0.75 + 1.25
is a cosine of period 2pi / 0.0005 ms = 12.566 s, swinging 2000 -> 500 -> 2000
mm. easeInOutSine IS a raised cosine, so three keyframes at 0 / 6.283 / 12.566
reproduce the curve exactly — no sampling, no drift. The example runs off the
wall clock and therefore starts at an arbitrary phase; a reel is deterministic,
so this one starts at the top of the swing.
THE BULB MESH IS A SIBLING, NOT A CHILD
The example parents the emissive sphere to the light (`bulbLight.add(mesh)`).
A promo light takes no children, so the sphere is its own primitive carrying a
duplicate copy of the same position.y track — and the copy is only identical
because the sphere authors NO `position` attr. A node's transform keyframes
are RELATIVE to its base attribute (xml-scene resolveRelativeTransformKeyframes:
base + value), a light's are absolute; give the sphere `position="0 2000 0"`
as well and its track resolves to 4000/2500/4000 and it flies off the light.
It must NOT cast a shadow: the
light sits at its centre, so a shadow-casting bulb would put the entire scene
in shadow. The example gets that free (castShadow defaults to false); here it
is authored, and the shadow camera's 500mm near plane clips the 20mm sphere
out of the depth cube as a second line of defence.
TEXTURE SLOTS ARE AUTHORED PER PRIMITIVE, NOT IN <materials>
A <materials> definition's params go str