Skip to content
Docs
Open Promoeyes
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