aeronautic

Takram's atmosphere

Contrails lit by the sunlight and skylight @takram/three-atmosphere works out at the aircraft's altitude.

atmosphere-contrails.tsx
Starting…

A contrail is white because its ice scatters, so under @takram/three-atmosphere it should scatter the atmosphere's own light. Bring the sun down: the sunlight through more air reddens and dims, the sky darkens, and the trails follow both, at the exposure the sky meters. Change the altitude and the sky is the one at that height, as the trails are.

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. <Contrails> finds the volume pass through its <VolumePassContext>, as the plumes and the vapor do, and draws over the atmosphere's backdrop.

The light at the aircraft

The trails take the light as the vapor does: the sun as an irradiance, scattered through the crystals' phase function, and the sky as a radiance, the same from every direction. Takram's getSplitScalarIlluminance gives both at the aircraft, from the tables the sky is drawn with.

  • The sun is its direct: the solar irradiance through the air above.
  • The sky is the mean of the dome above, Eh / π from its horizontal illuminance, and the sunlit ground below.
<Contrails
  airframe={airframe}
  look={{
    sunDirection, // towards the sun, world space
    sunColor: lighting.sun,
    sunIntensity: 1,
    skyColor: lighting.sky,
    skyIntensity: 1,
  }}
/>

Both are already in the atmosphere's units, so the look's exposure stays at 1 and the camera photographs the trails with the sky.

The light is read once, at the aircraft, for the whole trail. A trail sinks with its wake, at most some 100 m behind the docs' fighter, and the light a few hundred metres lower is the same to well under 1 %.

The air in front of it

A trail is kilometres long, so the air between it and the camera is worth working out. The trails get no aerial perspective of their own: at the tropopause, with takram's defaults, the air's extinction is about 3.6 × 10⁻³ per kilometre in green and 8.4 × 10⁻³ in blue (Rayleigh at e−11/8 of its sea-level 1.36 × 10⁻², almost no aerosol above its 1.2 km scale height, and a little ozone). Five kilometres of it, the far end of the example's trail, keep 98 % of the light in green and 96 % in blue. At 6 km, the bottom of the altitude slider, they keep 97 % and 93 %.

Seen from the ground, through tens of kilometres of the denser air below, the trails would need it.

atmosphere-contrails.tsx
import type { ContrailLook } from "@aeronautic/contrails";
import { type FC, type RefObject, useMemo, useRef, useState } from "react";
import { AtmosphereStage, useAtmosphereLighting } from "./atmosphere";
import {
  ContrailsControls,
  ContrailsFighter,
  type Settings,
  START_SETTINGS,
} from "./contrails";

// The fighter's contrails under takram's sky. The ice is lit by the sunlight
// and the skylight the atmosphere works out at the aircraft's altitude, so
// the trails redden and dim with the sun as the sky does

// Low enough to colour the light, high enough to light the trails: the sun
// round the horizon where the stage's own sun is, ahead of the camera and to
// its left, so the crystals scatter it forwards
const SUN_DEG = 20;
const SUN_AZIMUTH_DEG = 135;

/**
 * The fighter and its trails, in the atmosphere's light.
 * @param props The settings, and where to say what the trails are doing
 * @returns The model and its trails, once there is light to draw them in
 */
const Fighter: FC<{
  settings: Settings;
  readout: RefObject<HTMLSpanElement | null>;
}> = ({ settings, readout }) => {
  const lighting = useAtmosphereLighting();

  // The sun as an irradiance and the sky as a radiance, as the trails take
  // them, both already in the atmosphere's units
  const look = useMemo<Partial<ContrailLook> | undefined>(
    () =>
      lighting
        ? {
            sunDirection: lighting.sunDirection,
            sunColor: lighting.sun,
            sunIntensity: 1,
            skyColor: lighting.sky,
            skyIntensity: 1,
          }
        : undefined,
    [lighting],
  );

  if (!look) return null;

  return <ContrailsFighter settings={settings} readout={readout} look={look} />;
};

/**
 * The fighter's contrails in the atmosphere, the sun and the day yours.
 * @returns The example
 */
export const AtmosphereContrails: FC = () => {
  const [settings, set_settings] = useState<Settings>(START_SETTINGS);
  const [sun_deg, set_sun_deg] = useState(SUN_DEG);
  const readout = useRef<HTMLSpanElement>(null);

  const change = (next: Partial<Settings>) =>
    set_settings((previous) => ({ ...previous, ...next }));

  return (
    <AtmosphereStage
      sunElevationDeg={sun_deg}
      sunAzimuthDeg={SUN_AZIMUTH_DEG}
      // The sky is the one at the altitude the trails are flown at
      altitudeM={settings.altitude_m}
      camera={[44, 6, -76]}
      target={[0, -2, 34]}
      fov={16}
      overlay={
        <>
          <ContrailsControls settings={settings} change={change}>
            <label>
              sun {sun_deg}°
              <input
                type="range"
                min={-2}
                max={90}
                step={0.5}
                value={sun_deg}
                onChange={(event) => set_sun_deg(event.target.valueAsNumber)}
              />
            </label>
          </ContrailsControls>
          <span ref={readout} className="example-note" />
        </>
      }
    >
      <Fighter settings={settings} readout={readout} />
    </AtmosphereStage>
  );
};

export default AtmosphereContrails;

On this page