Takram's atmosphere
Position lights, strobes and landing lights under @takram/three-atmosphere's sky, in its light units.
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.
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;