Takram's atmosphere
Flares and chaff under @takram/three-atmosphere's sky: the flames in its light units, the smoke and the chaff lit by its sun and sky.
The in-the-sun example under
@takram/three-atmosphere's
sky. Hold the chaff and it streams out behind the fighter, lit by the
sunlight and skylight the atmosphere works out three kilometres up. Bring the
sun down below the horizon and fire the flares: the camera opens up for the
dimmer sky, and the same flames come up brighter against it.
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. <Countermeasures> finds the volume pass through its
<VolumePassContext>, as the plumes, the vapor and the contrails do, and
draws its flames, smoke, chaff and glare in it, over the atmosphere's
backdrop. The point lights stay in the aircraft, lighting the scene.
The pass has no depth of its own: each of them reads the opaque scene's from
pass.backdrop instead, so the airframe still hides what is behind it.
Without React, give backdrop to the Countermeasures and put its glare,
trail, smoke and clouds in pass.scene.
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⁻⁵
<Countermeasures
look={{
exposure,
sunDirection, // towards the sun, world space
sunColor: lighting.sun,
sunIntensity: 1,
skyColor: lighting.sky,
skyIntensity: 1,
}}
/>The sun and the sky are taken as the contrails take them, from takram's
getSplitScalarIlluminance at the aircraft:
- The sun is its
direct: the solar irradiance through the air above. It lights the smoke through its phase function, and the chaff by its glints and its flash at the sun's reflection. - The sky is the mean of the dome above, Eh / π from its horizontal illuminance, and the sunlit ground below: the light that falls on the smoke and the chaff from every side.
Both are already in the atmosphere's units, so their intensities stay at 1. The flames are in candelas, the exposure turning them into the same units.
The air in front of it
The pass's contents get no aerial perspective from the atmosphere. The
countermeasures put their own haze in front of them, 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 hundred metres keep over 95 % of the
light. Takram's clear air three kilometres up, about 0.01 per kilometre in
green, is nearly twenty times thinner;
give visibilityM to match it if the scene is seen from far off.
The glitter is counted pixel by pixel, in the pass's pixels: at its half resolution it is coarser, with the same mean.
import { type FC, type RefObject, useMemo, useRef, useState } from "react";
import {
AtmosphereStage,
LUMINANCE_SCALE,
useAtmosphereLighting,
} from "./atmosphere";
import {
ChaffControls,
ChaffingFighter,
type Daylight,
type Settings,
START_SETTINGS,
VIEWS,
} from "./chaff-in-the-sun";
import { Aim } from "./stage";
// The fighter's flares and chaff under takram's sky. The flames are drawn in
// the atmosphere's units, the smoke and the chaff lit by the sunlight and
// skylight it works out at the aircraft, and all of it in core's volume
// pass, so the sky is drawn behind it and the airframe in front
// Three kilometres up, as the daylight example
const ALTITUDE_M = 3000;
/**
* The fighter, in the atmosphere's light.
* @param props The settings, the chaff held, the flares fired, and where to
* say what it is doing
* @returns The model and its countermeasures, once there is light to draw
* them in
*/
const Fighter: FC<{
settings: Settings;
held: boolean;
flares: number;
readout: RefObject<HTMLSpanElement | null>;
}> = ({ settings, held, flares, readout }) => {
const lighting = useAtmosphereLighting();
// One cd/m² is the atmosphere's luminance scale on screen; the sun as an
// irradiance and the sky as a radiance are already in its units
const daylight = useMemo<Daylight | undefined>(() => {
if (!lighting) return undefined;
const [r, g, b] = lighting.sun;
return {
look: {
exposure: LUMINANCE_SCALE,
sunDirection: lighting.sunDirection,
sunColor: lighting.sun,
sunIntensity: 1,
skyColor: lighting.sky,
skyIntensity: 1,
},
lux: (0.2126 * r + 0.7152 * g + 0.0722 * b) / LUMINANCE_SCALE,
};
}, [lighting]);
if (!daylight) return null;
return (
<ChaffingFighter
settings={settings}
held={held}
flares={flares}
readout={readout}
daylight={daylight}
/>
);
};
/**
* The fighter's flares and chaff in the atmosphere, the sun yours.
* @returns The example
*/
export const AtmosphereCountermeasures: FC = () => {
const [settings, set_settings] = useState<Settings>(START_SETTINGS);
const [held, set_held] = useState(false);
const [flares, set_flares] = useState(0);
const readout = useRef<HTMLSpanElement>(null);
const change = (next: Partial<Settings>) =>
set_settings((previous) => ({ ...previous, ...next }));
const { camera, target } = VIEWS[settings.view];
// The fighter flies west, -Z, with north +X: its bearing from the nose,
// round to the left, as an azimuth from north towards east
const bearing = (settings.bearing_deg * Math.PI) / 180;
const azimuth_deg =
(Math.atan2(-Math.cos(bearing), -Math.sin(bearing)) * 180) / Math.PI;
return (
<AtmosphereStage
sunElevationDeg={settings.elevation_deg}
sunAzimuthDeg={azimuth_deg}
altitudeM={ALTITUDE_M}
camera={[...camera]}
target={[...target]}
fov={45}
overlay={
<>
<ChaffControls
settings={settings}
change={change}
hold={set_held}
lowest_deg={-4}
>
<button
type="button"
onClick={() => set_flares((count) => count + 1)}
>
Flares
</button>
</ChaffControls>
<span ref={readout} className="example-note" />
</>
}
>
<Aim camera={[...camera]} target={[...target]} />
<Fighter
settings={settings}
held={held}
flares={flares}
readout={readout}
/>
</AtmosphereStage>
);
};
export default AtmosphereCountermeasures;