aeronautic

Takram's atmosphere

Position lights, strobes and landing lights under @takram/three-atmosphere's sky, in its light units.

atmosphere-lights.tsx
Starting…

The fighter on its approach, 300 m up with the gear down, under @takram/three-atmosphere's sky, every light on. The sun starts 4° below the horizon, in civil twilight. Raise it and the camera stops down for the brighter sky, so the same lights fade into it. Lower it and they stand out as the dusk deepens.

The setup, the volume pass with the aerial perspective as its backdrop and the scene's place on the earth, is on core's page.

In the volume pass

The atmosphere's aerial perspective draws the sky wherever the opaque scene left nothing, so whatever is drawn over the sky in the scene itself is drawn over again. <Lights> finds the volume pass through its <VolumePassContext> and draws its beams and its glare there, over the atmosphere's backdrop. The spot and point lights stay in the aircraft, lighting the scene.

The pass has no depth of its own: the beams and the glare read the opaque scene's depth from pass.backdrop instead, so the airframe still hides the lights behind it. Without React, give backdrop to the Lights and put its volumes in pass.scene: the glare goes in volumes too.

The light units

The look's exposure is the scene's value for 1 cd/m². The atmosphere draws one unit on screen as about 75 700 cd/m², so under it the exposure is its luminanceScale, whatever the camera meters:

import { AtmosphereParameters } from "@takram/three-atmosphere/webgpu";

const exposure = new AtmosphereParameters().luminanceScale; // ≈ 1.3 × 10⁻⁵

<Lights look={{ exposure }} />;

The lights are in candelas, the exposure turning them into the atmosphere's units. The camera's exposure is the atmosphere's: its meter reads the sunlight and the skylight at the aircraft and opens up as the sun goes down, as a photographer's would.

The air in front of them

The pass's contents get no aerial perspective from the atmosphere. The lights dim and redden through their own air, from the air's visibility: at its default 23 km, Koschmieder's 3.912 / V is 0.17 per kilometre, and the example's few tens of metres keep all but a fraction of a percent of the light. Takram's air at 300 m is clearer than that. Set visibilityM to match it if the lights are seen from kilometres off. The beams scatter in the same air, so they show more in a mist than on a clear night.

atmosphere-lights.tsx
import type { LightsSwitches } from "@aeronautic/lights";
import { Lights } from "@aeronautic/lights/react";
import { type FC, useMemo, useState } from "react";
import type { Object3D } from "three";
import { AtmosphereStage, LUMINANCE_SCALE } from "./atmosphere";
import { useFighter, useLensesUnlit } from "./fighter";

// The fighter on its approach at dusk, under takram's sky, every light on.
// The lights are in candelas, drawn at the atmosphere's luminance scale, so
// they hold their own against the sky as the camera opens up for the dusk;
// their glare and their beams are in core's volume pass, over the sky

// On the approach, some 300 m up, gear down
const ALTITUDE_M = 300;

// Civil twilight ends with the sun 6° below the horizon
const SUN_DEG = -4;

const SWITCHES: Record<keyof LightsSwitches, string> = {
  navigation: "position",
  anticollision: "strobes",
  landing: "landing",
  taxi: "taxi",
};

/**
 * The fighter, its lights on its own nodes, in the atmosphere's units.
 * @param props The switches
 * @returns The model and its lights
 */
const Fighter: FC<{ switches: LightsSwitches }> = ({ switches }) => {
  const { scene, nodes } = useFighter();

  useLensesUnlit(scene);

  // The anchors the model gives each light, as on the apron
  const airframe = useMemo(() => {
    const node = (name: string) => nodes[name] as Object3D;

    return {
      navigation: [
        { side: "left", at: node("LIGHT_Nav_L_Anchor") },
        { side: "right", at: node("LIGHT_Nav_R_Anchor") },
        { side: "aft", at: node("LIGHT_Nav_Tail_C_Anchor") },
      ],
      anticollision: [
        { at: node("LIGHT_Strobe_Top_C_Anchor"), color: "white" },
        { at: node("LIGHT_Strobe_Bottom_C_Anchor"), color: "red", phaseS: 0.5 },
      ],
      beams: [
        { at: node("LIGHT_Landing_C_Anchor"), lamp: "landing", onGear: true },
        { at: node("LIGHT_Taxi_C_Anchor"), lamp: "taxi", onGear: true },
      ],
    } as const;
  }, [nodes]);

  // One cd/m² is the atmosphere's luminance scale on screen
  const look = useMemo(() => ({ exposure: LUMINANCE_SCALE }), []);
  const air = useMemo(() => ({ altitudeM: ALTITUDE_M }), []);

  return (
    <group>
      <primitive object={scene} />
      <Lights airframe={airframe} switches={switches} air={air} look={look} />
    </group>
  );
};

/**
 * The fighter's lights in the atmosphere, the sun yours.
 * @returns The example
 */
export const AtmosphereLights: FC = () => {
  const [sun_deg, set_sun_deg] = useState(SUN_DEG);
  const [switches, set_switches] = useState<LightsSwitches>({
    navigation: true,
    anticollision: true,
    landing: true,
    taxi: true,
  });

  return (
    <AtmosphereStage
      sunElevationDeg={sun_deg}
      altitudeM={ALTITUDE_M}
      camera={[-26, 6, -20]}
      target={[0, -1, 0]}
      fov={40}
      overlay={
        <div className="example-controls">
          <label>
            sun {sun_deg}°
            <input
              type="range"
              min={-10}
              max={20}
              step={0.5}
              value={sun_deg}
              onChange={(event) => set_sun_deg(event.target.valueAsNumber)}
            />
          </label>
          {(Object.keys(SWITCHES) as (keyof LightsSwitches)[]).map((key) => (
            <label key={key}>
              {SWITCHES[key]}
              <input
                type="checkbox"
                checked={switches[key]}
                onChange={(event) =>
                  set_switches((previous) => ({
                    ...previous,
                    [key]: event.target.checked,
                  }))
                }
              />
            </label>
          ))}
        </div>
      }
    >
      <Fighter switches={switches} />
    </AtmosphereStage>
  );
};

export default AtmosphereLights;

On this page