···41414242### 1. Port the physical model (reuse, don't reinvent)
43434444-- [ ] Port constants: Rayleigh/Mie/ozone coefficients, scale heights,
4444+- [x] Port constants: Rayleigh/Mie/ozone coefficients, scale heights,
4545 ground/atmosphere radii, `groundAlbedo` โ same as `gradient.ts` /
4646 the `common` shader tab.
4747- [ ] Port `getScatteringValues` (density + extinction at a given altitude).
4848-- [ ] Port the phase functions (`rayleighPhase`, `miePhase`).
4949-- [ ] Port `rayIntersectSphere` / `intersectSphere`.
4848+- [x] Port the phase functions (`rayleighPhase`, `miePhase`).
4949+- [x] Port `rayIntersectSphere` / `intersectSphere`.
5050- [ ] Decide: **do you need a full LUT texture, or a direct function call?**
5151 Since you're only evaluating a handful of directions per frame (not a
5252 whole screen), you likely don't need GPU textures at all โ a plain
+62
src/apply/colour.ts
···11+import { Colordx, colordx } from "@colordx/core"
22+33+// constants from Hillaire, section 4 table 1
44+const RAYLEIGH_SCATTER = [5.802e-6, 13.558e-6, 33.1e-6] // rgb
55+const MIE_SCATTER = 3.996e-6
66+const MIE_ABSORB = 4.44e-6
77+const OZONE_SCATTER = 0
88+const OZONE_ABSORB = [0.65e-6, 1.881e-6, 0.085e-6] // rgb
99+1010+const GROUND_ALBEDO = 0.3
1111+1212+const RAYLEIGH_SCALE_HEIGHT = 8e3 // metres
1313+const MIE_SCALE_HEIGHT = 1.2e3 // metres
1414+1515+const GROUND_RADIUS = 6_360e3 // radius of the earth, metres
1616+const TOP_RADIUS = 6_460e3 // radius of the atmosphere, metres
1717+1818+type Vec3 = [number, number, number];
1919+2020+function dot(v1: Vec3, v2: Vec3) {
2121+ return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2];
2222+}
2323+2424+// ACES tonemapper (Knarkowicz)
2525+function aces(colour: Colordx): Colordx {
2626+ Object.keys(colour.toRgb()).forEach((_key, colour) => {
2727+ const n = colour * (2.51 * colour + 0.03)
2828+ const d = colour * (2.43 * colour + 0.59) + 0.14
2929+ return Math.max(0, Math.min(1, n / d))
3030+ })
3131+ return colordx(colour)
3232+}
3333+3434+function rayleighPhase(angle: number): number {
3535+ return (3 * (1 + Math.cos(angle) ** 2)) / (16 * Math.PI);
3636+}
3737+3838+function miePhase(angle: number): number {
3939+ const g = 0.8;
4040+ const scale = 3 / (8 * Math.PI);
4141+ const num = (1 - g ** 2) * (1 + Math.cos(angle) ** 2);
4242+ const denom =
4343+ (2 + g ** 2) * (1 + g ** 2 - 2 * g * Math.cos(angle)) ** (3 / 2);
4444+ return (scale * num) / denom;
4545+}
4646+4747+// From "5.3.2 Intersecting Ray or Segment Against Sphere" (Real-Time Collision Detection)
4848+function intersectSphere(rayOrigin: Vec3, direction: Vec3, radius: number): number | null {
4949+ // Sphere center is at origin (= 0), so rayOrigin - sphereCenter = rayOrigin
5050+ const b = dot(rayOrigin, direction);
5151+ const c = dot(rayOrigin, rayOrigin) - (radius ** 2);
5252+ const discr = (b ** 2) - c;
5353+ if (discr < 0)
5454+ // Ray misses sphere
5555+ return null;
5656+ const t = -b - Math.sqrt(discr);
5757+ if (t < 0)
5858+ // Ray inside sphere; Use far discriminant
5959+ return -b + Math.sqrt(discr);
6060+ return t;
6161+}
6262+