# Solar Flares

Solar Flares is PowerShot's WebGPU spectral sunlight flare — a scene-linear
camera-optics pass. It deliberately does not
use Three.js `Lensflare`, a chain of circles, RGB channel offsets, or bloom as a
substitute for all flare phenomena.

## What is simulated

The shipped `Tronnier Heliar 100 mm` profile contains:

- 16 strongest two-reflection ghost paths selected from 28 valid surface pairs.
- Three coating-aware transport wavelengths at 475, 550, and 650 nm.
- 21 source-incidence bins from 0° through 30°.
- A 13×13 entrance-pupil grid per path, wavelength, and incidence bin.
- Sensor position, aperture-plane position, housing radius, thin-film
  throughput, and finite-area pupil-to-sensor flux concentration.
- A seven-blade Fraunhofer PSF generated from an energy-normalized complex-pupil FFT.
- Five-wavelength diffraction integration at 430, 480, 530, 590, and 650 nm.
- Integration over the projected 0.533° solar disc: nine source directions for
  the ghost transport and thirteen taps inside the diffraction pattern. The
  disc radius is derived from the active lens focal length rather than a fixed
  blur kernel.
- A compact full-resolution source-glare response with a clipped white core,
  cool scatter halo, uneven short streaks, and a small offset cyan lobe. The
  aperture star remains in the FFT pass instead of being redrawn analytically.
- Fixed low-order pupil error, minute edge variation, and weak surface defects
  keep the lens signature stable while avoiding a mathematically perfect star.
- Three broad analytic veiling-glare lobes.

The shipped half-float transfer atlas is 2.6 MiB;
`npm run generate:flare-atlas:max` rebuilds the 9.84 MiB calibration-grade
version (24 paths × 31 angles × 17² grid), and `--paths/--angles/--grid`
override the tiers individually. Throughput and flux are
log2-encoded in the asset to retain the energy range of good anti-reflection
coatings, then decoded before additive HDR accumulation. Flux density is
computed from finite pupil-cell and mapped sensor-triangle areas rather than a
point derivative, so unresolved caustics cannot become single-vertex energy
spikes. Incomplete density control volumes are reconstructed from the nearest
fully supported pupil cell and renormalized to the traced pupil-area energy.
Invalid boundary rays retain a zero coverage flag but carry an extrapolated edge
transform, preventing clipped triangles from stretching across the frame.

Atlas sensor millimetres and diffraction extents are transformed through the
active perspective projection. For strict reproduction, make the camera field
of view match the profile's 100 mm focal length on a 36 × 24 mm gate. A
different perspective field of view remains angularly registered, but is a
virtual remapping rather than a claim that the prescription changed focal
length. Orthographic cameras are rejected.

## Correct pipeline order

```text
scene + sky in scene-linear HDR
→ solar visibility
→ spectral internal-reflection ghosts
→ spectral aperture diffraction
→ veiling glare
→ sensor bloom / halation
→ exposure
→ tone mapping or film print response
→ output colour conversion
```

Do not add the flare after tone mapping. Display white no longer contains the
radiometric range needed to drive a credible optical response.

To feed the result into the digital PowerShot ISP:

```js
const scenePass = pass(scene, camera);
const optical = solarFlarePass(scenePass, flare, {
  camera,
  sun,
  depthTexture: scenePass.getTexture("depth"),
});

powershot.setInputEncoding("linear");
renderPipeline.outputNode = powerShotPass(optical, powershot);
```

Film follows the same ordering, but `FilmPipeline` remains the only display
transform and receives linear input.

## Sun contract

`sun` can be a `THREE.DirectionalLight` or a descriptor:

```js
const sun = {
  // World-space direction from the camera toward the source.
  direction: new THREE.Vector3(0.2, 0.35, -1).normalize(),
  color: new THREE.Color(1.0, 0.94, 0.82),
  sourceRadiance: 4.0,
  angularDiameterDeg: 0.533,
};
```

For a directional light, PowerShot derives the source direction from
`light.position - light.target.position`. `sourceRadiance` is scene-relative
because Three.js applications do not share one absolute radiometric exposure
scale. Set `light.userData.solarFlareRadiance` when its lighting intensity
and desired camera-source radiance use different calibrations.

On-screen visibility samples nine points over the finite solar disc against the
scene depth texture. Visibility is temporally smoothed, with a slower response
for broad veiling glare than for sharp ghosts. Screen depth cannot determine
whether an off-screen sun is hidden, so applications with large occluders should
provide a synchronous camera-to-sun test:

```js
flare.setVisibilityProvider(({ camera, direction }) => {
  return cameraToSunIsClear(camera.position, direction) ? 1 : 0;
});
```

## Main controls

```js
flare.setStrength({
  strength: 1,
  ghosts: 1,
  ghostRadianceScale: 20,
  diffraction: 1,
  glare: 1,
  veiling: 0.06,
});

flare.setAperture({
  fNumber: 8,
  blades: 7,
  roundness: 0.08,
  rotation: 0, // radians, fixed relative to the sensor
});
```

The flare remains active outside the frame until the lens hood acceptance curve
rejects it. `flare.settings.hoodAngleDeg` controls the fade beyond the atlas's
30° incidence limit. Ghosts render at half resolution by default; diffraction
keeps full pixel density inside a tightly bounded window around the PSF quad
and the veil renders at one-sixteenth resolution. The source-glare response is
evaluated in the composite, so it
costs no render target and stays stable at any resolution. Its response curve
sits between the starburst and the slow veil so the halo dims quickly behind an
occluder while keeping a little forward scatter. Set `ghostResolutionScale: 1`
at construction for calibration captures. Aperture, housing, and pupil-coverage
edges use derivative-based pixel coverage. The prefiltered diffraction FFT is
treated as a continuous energy density; scaled half-float storage retains its
faint tail, and f-stop changes preserve the expected inverse-square integrated
aperture energy while widening the pattern.

`ghostRadianceScale` calibrates the atlas' physical relative throughput to the
application's scene-relative HDR radiance convention. It does not alter path
positions, dispersion, aperture clipping, or the ranking of reflection paths.

## Accuracy boundary

The optical geometry is traced from a real historical prescription, and the
runtime atlas is deterministic. The coating is a physically evaluated
single-layer quarter-wave model. It is not a claim to reproduce a specific
commercial lens coating, sensor cover glass, dust pattern, or proprietary
camera response exactly. Match a particular lens by calibrating path strengths,
coating curves, veiling intensity, source radiance, and the downstream sensor
model against photographs from that lens.

The prescription is a classic public Tronnier Heliar design. The surface table
was digitized from OpenLensFlare's `heliar-tronnier.xml` sample
(BSD-2-Clause, © István Csoba), which follows the same historical design used
by Hullin et al. 2011. The atlas generator in
`tools/generate-solar-flare-atlas.mjs` recomputes the shipped asset from the
human-readable prescription in `src/solar-flare-profile.js`; no third-party
binary data ships with PowerShot.
