Comments in benilla-world: one line of fact each, no history, no record numbers (#121)
Part of #41: the comments of `benilla-world`, Rust and WGSL, are rewritten to the rule in `docs/METHOD.md`, which is what the code does and the reference fact behind it, in a line. 24,226 comment lines become 8,863. - **The mechanical strip.** The citations that were only a private record number go first, and each edit is shown to be a pure removal. - **Wrong facts corrected.** The rewrite corrects the comments that stated wrong facts. Among them: - `0x6b62e0` is the WMO group's liquid renderer, not a doodad drawer. - The reference creates a WMO's props once, when a group enters its streaming window (`0x698720` → `0x695aa0`). It then admits them each frame the portal walk visits the group (`0x685d70` → `0x6838f0`). The old text had them created per visible group. - The liquid query's comments now state the reference's order: the map-object leg first, then the ADT (`0x69b6d0`). They no longer call the Outdoors/Inside split a match. - The interior probe runs from the character, not the camera. - The reference lights ground clutter. The old comment said it draws unlit. - The model-particle "pool order" deviation was wrong. Each instance is its own mesh, depth-sorted every frame. - The reference's vertical field of view follows the aspect ratio (44.1° at 16:9). The comment now gives it beside our fixed 45°. - The effect shader's far wall is the live `farclip`, 177 to 777 yd, not a fixed 777 yd. - Dozens more turned up on the way: renamed or deleted items, docs sitting on the wrong item, stale counts and links. - **Deviation labels.** "Deviation:" now marks only a choice with a reason. An unbuilt behaviour says so plainly. - **Verification.** Every file strips to identical code, and `cargo fmt --check` is clean. Five second readers compared every file old against new, restored the facts the rewrite had dropped, and checked each new citation against the reference. Defects the rewrite surfaced outside the comments are filed as their own issues (#106, #114, #115, #116). The comments here state what the code does. String literals are untouched; they are #92. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d69d09dfce
commit
fdfd8d39ec
@@ -1,52 +1,20 @@
|
||||
//! The cloud coverage kernel — a faithful port of the reference's procedural cloud field.
|
||||
//!
|
||||
//! The real client maintains a scrolling 128×128 byte tile of cloud coverage (the SkyManager
|
||||
//! working-set `0xce98e8`), regenerated in 32-row bands at ~10 Hz by the coverage-noise kernel
|
||||
//! (`WoW.exe 0x6cffc0`): 4-octave
|
||||
//! toroidal value noise (lacunarity 2, persistence 0.5) → `rawByte`, thresholded by the authored
|
||||
//! Light.dbc cloud density `C` (`T = trunc((1−C)·255)`, `0x6d0970`) and shaped through a fixed
|
||||
//! 256-byte tone curve (`0x6d0900`, gamma 0.96 — frozen here as [`CURVE`]). One field serves every
|
||||
//! consumer: the glare occlusion `occ1` samples the same tile the visible layer renders from
|
||||
//! (`0x6cfa90`).
|
||||
//!
|
||||
//! The noise lattice is keyed by three axes: tile row (`row_key`, advancing `freq` per row), tile
|
||||
//! column (`col_key`, advancing `freq` per column), and time (the u16 `phase`, bumped once per full
|
||||
//! tile wrap ≈ 0.4 s — its high byte picks the permutation slice pair, its low byte the fade
|
||||
//! weight between them). High key bytes select lattice cells through the permutation table; low
|
||||
//! bytes index the raised-cosine [`fade`] table as interpolation weights. Band regeneration at an
|
||||
//! unchanged `phase`/`T` is idempotent (keys derive from absolute tile coordinates), so the field
|
||||
//! only *moves* when the phase advances or the authored density changes — exactly the reference's
|
||||
//! slow cloud drift.
|
||||
//!
|
||||
//! Each regen fire ends with the **color pass** (`0x6cfb00` — diffed
|
||||
//! bit-exact): the coverage bytes become RGBA texels (gradient + sun-aligned glow, alpha = the
|
||||
//! coverage byte), and *that image* is what the dome uploads — the reference binds its color
|
||||
//! buffer zero-copy to the gx texture (`0x58ac70`). The glow's per-cell surface normal comes
|
||||
//! from the octave-2 derivative leg (`[cfg+0x68]`, written by `0x6cffc0`, read by `0x6cfb00`).
|
||||
//!
|
||||
//! Deviations from the bytes, all in never-hit or non-visual domains (recorded in the decision
|
||||
//! record): the `acos` argument is clamped to ±1 (the reference NaNs above ~70° elevation, a
|
||||
//! domain its sun/moon never reach); LUT reads wrap toroidally instead of running off the flat
|
||||
//! heap at the measure-zero `u == 1.0` edge; the gradient table uses the reference's MSVC-LCG
|
||||
//! *formula* with a fixed seed (the reference seed is process-random and not visually
|
||||
//! load-bearing — any 256 uniform values in [−1, 1] are equivalent, `0x6d0c90`);
|
||||
//! and the color buffer seeds alpha-0 instead of `0xFFFFFFFF` (no white flash before the first
|
||||
//! build).
|
||||
//! The reference's procedural cloud field (`0xce98e8`): a scrolling 128×128 byte tile of 4-octave
|
||||
//! toroidal value noise, thresholded by the Light.dbc cloud density, shaped by a fixed tone curve
|
||||
//! (`0x6d0900`) and colored into the texels the dome draws. The glare samples the same tile.
|
||||
|
||||
use bevy::math::Vec3;
|
||||
|
||||
use super::tables::{fade_table, gradient_table, CURVE, PERM};
|
||||
|
||||
/// Tile side at `SkyCloudLOD 0` (`cols = 128 << LOD`; the CVar clamps to [0,1] and defaults to 0 —
|
||||
/// `0x6d1d60`). We implement LOD 0.
|
||||
/// Tile side at `SkyCloudLOD` 0, its default (`128 << LOD`, `0x6d1d60`); only LOD 0 is built.
|
||||
pub const COLS: usize = 128;
|
||||
/// `log2(COLS)` — the row-pitch shift the sampler uses (`[cfg+0x20]`).
|
||||
/// `log2(COLS)`, the sampler's row-pitch shift (`[cfg+0x20]`).
|
||||
pub const SHIFT: u32 = 7;
|
||||
/// Rows regenerated per fire (`[cfg+0x14]`, default 32).
|
||||
/// Rows regenerated per fire (`[cfg+0x14]`).
|
||||
pub const ROWS_PER_TICK: usize = 32;
|
||||
/// Octave count (`[cfg+0x28]`, constant 4).
|
||||
/// Octaves (`[cfg+0x28]`).
|
||||
pub const OCTAVES: usize = 4;
|
||||
/// Regen countdown reset (`0x8115b4` = 0.1 s) — the ~10 Hz cadence.
|
||||
/// Regen countdown reset in seconds (`0x8115b4`): a 10 Hz cadence.
|
||||
pub const REGEN_PERIOD: f32 = 0.1;
|
||||
/// Per-octave lattice frequencies, LOD 0 row of the base table `0x86f3dc` (`(16 >> LOD) << oct`).
|
||||
const BASE_FREQ: [u16; OCTAVES] = [16, 32, 64, 128];
|
||||
@@ -56,26 +24,18 @@ fn perm(i: u32) -> u32 {
|
||||
u32::from(PERM[(i & 0xff) as usize])
|
||||
}
|
||||
|
||||
/// Per-octave lattice walk state — the 0x54-byte stack record of `0x6cffc0`, kept as named fields.
|
||||
/// Per-octave lattice walk state: `0x6cffc0`'s 0x54-byte stack record as named fields.
|
||||
struct Octave {
|
||||
/// Lattice frequency (`base_table[LOD·5 + oct]`; the record's `B+8`/`B+0xa` key delta).
|
||||
freq: u16,
|
||||
/// Row axis key (`B+4`): starts at `scroll·freq`, advances `freq` per row. High byte = row
|
||||
/// lattice cell, low byte = row fade index.
|
||||
/// Row key (`B+4`), from `scroll·freq`, `+freq` per row: high byte the cell, low byte the fade.
|
||||
row_key: u16,
|
||||
/// Column axis key (`B+2`): re-seeded to `phase` each row, advances `freq` per column. High
|
||||
/// byte = column lattice cell (`uVar14`), low byte = column fade index.
|
||||
col_key: u16,
|
||||
/// Octave amplitude `1 / 2^oct` (`B+0x1a`).
|
||||
amp: f32,
|
||||
/// Per-row lattice corner seeds (`B+0x1e..0x2a`): row cell hashed through the phase-selected
|
||||
/// permutation slices (`x*` = current time slice, `y*` = next).
|
||||
/// Row corner seeds (`B+0x1e..0x2a`): `x*` through the current time slice, `y*` the next.
|
||||
x0: u32,
|
||||
x1: u32,
|
||||
y0: u32,
|
||||
y1: u32,
|
||||
/// Cached corner gradients + deltas for the current column cell (`B+0x2e..0x4a`), rebuilt
|
||||
/// lazily when the column cell changes (`B+0x4c` cache key).
|
||||
g00: f32,
|
||||
g00d: f32,
|
||||
g10: f32,
|
||||
@@ -87,36 +47,27 @@ struct Octave {
|
||||
cached: u32,
|
||||
}
|
||||
|
||||
/// The cloud coverage field: the byte tile, the colored texture, and the regen state.
|
||||
/// The cloud coverage field: the byte tile, its colored texels and the regen state.
|
||||
pub struct CloudKernel {
|
||||
/// The coverage byte tile (`[cfg+0x44]`), `COLS²` row-major. `byte/255 = R ∈ [0,1]`.
|
||||
/// Coverage bytes (`[cfg+0x44]`), `COLS²` row-major; `byte/255` is the coverage `R`.
|
||||
tile: Vec<u8>,
|
||||
/// The float accumulation tile (`[cfg+0x50]`).
|
||||
accum: Vec<f32>,
|
||||
/// The per-cell shape derivative pairs (`[cfg+0x68]`, 2 f32/cell) — written by the octave-2
|
||||
/// leg, consumed by the color pass as the glow surface normal `S = (dx, dy, 1)`
|
||||
/// (`0x6cffc0` writes it, `0x6cfb00` reads it).
|
||||
/// Per-cell slopes from the octave-2 leg (`[cfg+0x68]`), the glow normal `(dx, dy, 1)`.
|
||||
deriv: Vec<[f32; 2]>,
|
||||
/// The previous-row scratch (`[cfg+0x5c]`, `COLS` f32) feeding the row derivative.
|
||||
prevrow: Vec<f32>,
|
||||
/// The colored RGBA texels (`[cfg+0x38]`, 4 B/cell: gradient+glow RGB, alpha = the coverage
|
||||
/// byte) — **this is what the reference uploads** (`0x58ac70` binds `[cfg+0x38]` zero-copy).
|
||||
/// Colored texels (`[cfg+0x38]`), the buffer the reference binds as its texture (`0x58ac70`).
|
||||
rgba: Vec<[u8; 4]>,
|
||||
/// Scroll position — the tile row the next band starts at (`[cfg+0x18]`).
|
||||
/// The tile row the next band starts at (`[cfg+0x18]`).
|
||||
scroll: usize,
|
||||
/// Noise-space phase (`[cfg+0xa0]`, u16) — +1 per full tile wrap; the time axis.
|
||||
/// The noise's time axis (`[cfg+0xa0]`), +1 per full tile wrap.
|
||||
phase: u16,
|
||||
/// Regen countdown (`[cfg+0xb0]`); starts expired so the first tick fires.
|
||||
countdown: f32,
|
||||
/// The gradient table (`0xce92d8`) — 256 × f32 in [−1, 1], built once (see [`gradient_table`]).
|
||||
gradient: [f32; 256],
|
||||
/// The fade table (`0xce8dd8`) — `0.5·(1 − cos(iπ/256))`, built once.
|
||||
fade: [f32; 256],
|
||||
}
|
||||
|
||||
/// Per-fire inputs to the color pass — `0x6cfb00`'s per-frame setup, resolved by the caller from
|
||||
/// [`crate::lighting::WowLighting`]: the three Light.dbc cloud palette rows, the weather blend,
|
||||
/// and the glow body (sun by day / moon by night, with its 8-key day envelope).
|
||||
/// The color pass inputs (`0x6cfb00`'s per-frame setup), from [`crate::lighting::WowLighting`].
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub struct CloudFrame {
|
||||
/// Sun-glow palette (IntBand sub-10), sRGB 0..1.
|
||||
@@ -125,12 +76,11 @@ pub struct CloudFrame {
|
||||
pub(crate) slope: [f32; 3],
|
||||
/// Gradient base (IntBand sub-12).
|
||||
pub(crate) gbase: [f32; 3],
|
||||
/// The weather storm blend `bcc` — feeds the glow z-bias (`bcc·192 + 64`) and the dim
|
||||
/// (`1 − 0.75·bcc`).
|
||||
/// The weather storm blend, which sets the glow's z-bias and dim.
|
||||
pub(crate) bcc: f32,
|
||||
/// Camera→body direction of the glow body (Bevy frame).
|
||||
/// Camera→glow body direction (Bevy frame): the sun by day, the moon by night.
|
||||
pub(crate) glow_dir: Vec3,
|
||||
/// The glow day-envelope factor (`0xce9ab8` track — `daynight::cloud_glow_track`).
|
||||
/// The glow's day envelope, the reference's static track at `0xce9ab8`.
|
||||
pub(crate) glow_track: f32,
|
||||
}
|
||||
|
||||
@@ -141,8 +91,8 @@ impl Default for CloudKernel {
|
||||
accum: vec![0.0; COLS * COLS],
|
||||
deriv: vec![[0.0; 2]; COLS * COLS],
|
||||
prevrow: vec![0.0; COLS],
|
||||
// The reference inits the color buffer 0xFFFFFFFF; we keep alpha 0 until the first
|
||||
// build so an unprimed dome can never flash white (cosmetic-only deviation).
|
||||
// Deviation: the reference starts this buffer at 0xFFFFFFFF; alpha 0 keeps a dome
|
||||
// drawn before the first build from flashing white.
|
||||
rgba: vec![[255, 255, 255, 0]; COLS * COLS],
|
||||
scroll: 0,
|
||||
phase: 0,
|
||||
@@ -154,9 +104,7 @@ impl Default for CloudKernel {
|
||||
}
|
||||
|
||||
impl CloudKernel {
|
||||
/// Advance the countdown and regenerate one 32-row band when it expires (`0x6cffc0`'s
|
||||
/// self-throttle: fire when the decremented countdown ≤ 0, reset to 0.1). Returns whether the
|
||||
/// tile changed.
|
||||
/// Regenerates one band when the countdown runs out (`0x6cffc0`); true if the tile changed.
|
||||
pub fn tick(&mut self, dt: f32, density: f32, frame: &CloudFrame) -> bool {
|
||||
self.countdown -= dt;
|
||||
if self.countdown > 0.0 {
|
||||
@@ -167,33 +115,28 @@ impl CloudKernel {
|
||||
true
|
||||
}
|
||||
|
||||
/// Full-tile rebuild (`0x6cff90`): regenerate every row at once — the reference runs this on
|
||||
/// discontinuities (init / zone / LOD change) by forcing the row count to `cols`.
|
||||
/// Regenerates every row (`0x6cff90`), as the reference does on init, zone and LOD changes.
|
||||
pub fn rebuild(&mut self, density: f32, frame: &CloudFrame) {
|
||||
self.scroll = 0;
|
||||
self.regen(density, COLS, frame);
|
||||
self.countdown = REGEN_PERIOD;
|
||||
}
|
||||
|
||||
/// Re-run the color pass over the whole tile without touching the coverage — the frozen
|
||||
/// capture clock's path when only the palette/sun/weather inputs moved.
|
||||
/// Re-runs the color pass over the whole tile without touching coverage, for the frozen clock.
|
||||
pub fn recolor(&mut self, frame: &CloudFrame) {
|
||||
self.color_band(0, COLS, frame);
|
||||
}
|
||||
|
||||
/// One regeneration fire: `rows` tile rows starting at `self.scroll` (noise + quantize +
|
||||
/// the color pass, the reference's `0x6cffc0` → `0x6cfb00` order), then the scroll/phase
|
||||
/// advance. The body is the `0x6cffc0` transcription in named-field form.
|
||||
/// One fire of `0x6cffc0`: noise, quantize and color `rows` rows from `scroll`, then advance.
|
||||
fn regen(&mut self, density: f32, rows: usize, frame: &CloudFrame) {
|
||||
// Threshold refresh (`0x6d0970`, called at the top of every fire): T = trunc((1−C)·255).
|
||||
// The reference does not clamp C; authored bands stay in [0,1] and we clamp for safety.
|
||||
// Threshold (`0x6d0970`): the reference does not clamp; authored densities stay in [0, 1].
|
||||
let threshold = ((1.0 - density.clamp(0.0, 1.0)) * 255.0) as i32;
|
||||
|
||||
// Phase-selected permutation slice pair: the time axis. `seed` = phase HIGH byte.
|
||||
// The time axis: the phase's high byte picks the permutation slice pair.
|
||||
let seed = u32::from(self.phase >> 8);
|
||||
let slice_a = perm(seed); // local_18 — current time slice
|
||||
let slice_b = perm(seed + 1); // local_1c — next time slice
|
||||
let fade_t = f64::from(self.fade[(self.phase & 0xff) as usize]); // f_b — time fraction
|
||||
let slice_a = perm(seed); // current time slice
|
||||
let slice_b = perm(seed + 1); // next time slice
|
||||
let fade_t = f64::from(self.fade[(self.phase & 0xff) as usize]); // time fraction
|
||||
|
||||
let scroll = self.scroll;
|
||||
let mut oct: Vec<Octave> = (0..OCTAVES)
|
||||
@@ -201,8 +144,7 @@ impl CloudKernel {
|
||||
let freq = BASE_FREQ[c];
|
||||
Octave {
|
||||
freq,
|
||||
// B+4 init: `(u16)(tile_x · key)` — absolute-row keyed, which is what makes
|
||||
// band regeneration idempotent at a fixed phase.
|
||||
// Keyed by the absolute row, so a band regenerates identically at one phase.
|
||||
row_key: (scroll as u32).wrapping_mul(u32::from(freq)) as u16,
|
||||
col_key: 0,
|
||||
amp: 1.0 / (1u32 << c) as f32,
|
||||
@@ -223,15 +165,12 @@ impl CloudKernel {
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Float-tile zero-clear over the band.
|
||||
for v in &mut self.accum[scroll * COLS..(scroll + rows).min(COLS) * COLS] {
|
||||
*v = 0.0;
|
||||
}
|
||||
|
||||
for row in 0..rows {
|
||||
let base = (scroll + row) * COLS;
|
||||
// Per-row lattice setup: hash the row cell (row_key high byte) through the two time
|
||||
// slices; re-seed the column walk from the phase; invalidate the corner cache.
|
||||
for o in oct.iter_mut() {
|
||||
let bv = u32::from(o.row_key >> 8);
|
||||
o.x0 = perm(slice_a + bv);
|
||||
@@ -241,14 +180,13 @@ impl CloudKernel {
|
||||
o.col_key = self.phase;
|
||||
o.cached = u32::MAX;
|
||||
}
|
||||
// fVar4 — the previous column's third-octave partial sum (reset per row: the
|
||||
// `fld 0.0` before the column loop).
|
||||
// The previous column's three-octave partial sum, reset per row.
|
||||
let mut prev_accum = 0.0f32;
|
||||
for col in 0..COLS {
|
||||
let cell = base + col;
|
||||
for (oi, o) in oct.iter_mut().enumerate() {
|
||||
let fade_row = f64::from(self.fade[(o.row_key & 0xff) as usize]); // f_a
|
||||
let cz = u32::from(o.col_key >> 8); // uVar14 — column lattice cell
|
||||
let fade_row = f64::from(self.fade[(o.row_key & 0xff) as usize]);
|
||||
let cz = u32::from(o.col_key >> 8); // column lattice cell
|
||||
if cz != o.cached {
|
||||
o.cached = cz;
|
||||
let g = |s: u32| self.gradient[perm(s) as usize];
|
||||
@@ -262,8 +200,7 @@ impl CloudKernel {
|
||||
o.g11 = g(s3);
|
||||
o.g11d = g(s3 + 1) - o.g11;
|
||||
}
|
||||
// The fade-interpolated bilinear + time lerp, mirroring the binary's f64
|
||||
// in-register chain with its one f32 round-trip (`v8`).
|
||||
// The reference's f64 chain, with its one f32 round trip (`v8`).
|
||||
let fx = f64::from(self.fade[(o.col_key & 0xff) as usize]);
|
||||
let v7 = fx * f64::from(o.g00d) + f64::from(o.g00);
|
||||
let v8 = f64::from((fx * f64::from(o.g01d) + f64::from(o.g01)) as f32);
|
||||
@@ -273,10 +210,7 @@ impl CloudKernel {
|
||||
let acc = f64::from(self.accum[cell]);
|
||||
let stored = (((v8b - v7b) * fade_t + v7b) * f64::from(o.amp) + acc) as f32;
|
||||
self.accum[cell] = stored;
|
||||
// The octave-2 derivative leg (`0x6cffc0`, the `local_10 == 2` branch): the
|
||||
// column/row slopes of the three-octave partial sum, into the pair buffer the
|
||||
// color pass reads as the glow surface normal. `scale = 1 << (shift − 7)` — 1
|
||||
// at LOD 0.
|
||||
// Octave 2 stores the running sum's slopes: the color pass's glow normal.
|
||||
if oi == 2 {
|
||||
let scale = f64::from((1i32 << ((SHIFT - 7) & 0x1f)) as f32);
|
||||
self.deriv[cell][0] =
|
||||
@@ -288,24 +222,21 @@ impl CloudKernel {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Per-row key advance: walks the row axis one `freq` step.
|
||||
for o in oct.iter_mut() {
|
||||
o.row_key = o.row_key.wrapping_add(o.freq);
|
||||
}
|
||||
}
|
||||
|
||||
// Palette-quantize the band into the byte tile: the binary's float-bits pack
|
||||
// (`fmul 64; fadd 128; fadd 512; fstp` then bits `>> 14`), threshold, tone curve.
|
||||
// Quantize by the reference's float-bits pack, then threshold and tone curve.
|
||||
for cell in scroll * COLS..(scroll + rows).min(COLS) * COLS {
|
||||
let q = ((f64::from(self.accum[cell]) * 64.0 + 128.0 + 512.0) as f32).to_bits();
|
||||
let idx = ((q >> 14) & 0xff) as i32 - threshold;
|
||||
self.tile[cell] = if idx >= 0 { CURVE[idx as usize] } else { 0 };
|
||||
}
|
||||
|
||||
// The color pass over the same band (`0x6cfb00`, called before the scroll advance).
|
||||
// The color pass (`0x6cfb00`) runs before the scroll advance.
|
||||
self.color_band(scroll, rows, frame);
|
||||
|
||||
// Scroll advance with wrap: the wrap bumps the noise-space phase — the time axis moves.
|
||||
self.scroll += rows;
|
||||
if self.scroll >= COLS {
|
||||
self.phase = self.phase.wrapping_add(1);
|
||||
@@ -313,20 +244,15 @@ impl CloudKernel {
|
||||
}
|
||||
}
|
||||
|
||||
/// The color pass — the exact `0x6cfb00` per-cell algorithm (diffed
|
||||
/// bit-exact): per cell `t` = the coverage byte — `t == 0` copies the previous
|
||||
/// cell's RGB with alpha 0 (the filtering-friendly hole fill); else the gradient
|
||||
/// `slope·p + gbase` with `p = (((255−t)>>1) + 64)/255`, plus the sun-aligned glow
|
||||
/// `sun·(cosθ·intensity)` where `cosθ` aligns the cell→body vector (tile-cell units, z =
|
||||
/// `bcc·192 + 64`) against the cell's shape normal `(dx, dy, 1)` through the binary's integer
|
||||
/// fast-inverse-sqrt. Channels clamp at 1 and pack `floor(ch·255)`; alpha = `t`.
|
||||
/// The color pass (`0x6cfb00`): a hole copies the previous cell's RGB at alpha 0; a covered
|
||||
/// cell is a gradient in its coverage plus a glow by the angle to its slope normal.
|
||||
fn color_band(&mut self, start: usize, rows: usize, frame: &CloudFrame) {
|
||||
let f = f64::from;
|
||||
let z_bias = (f(frame.bcc) * 192.0 + 64.0) as f32;
|
||||
let intensity = (f(frame.glow_track) * (1.0 - f(frame.bcc) * 0.75)) as f32;
|
||||
let body = body_cells(frame.glow_dir);
|
||||
for row in start..(start + rows).min(COLS) {
|
||||
let row_base = row as f32; // [ebp-0x84] = f18 + row — the absolute tile row
|
||||
let row_base = row as f32; // the absolute tile row
|
||||
for col in 0..COLS {
|
||||
let g = row * COLS + col;
|
||||
let t = self.tile[g];
|
||||
@@ -337,7 +263,7 @@ impl CloudKernel {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// The angle byte: n = (((255 − t) >> 1) + 0x40) & 0xff ∈ [64, 191].
|
||||
// The angle byte, in [64, 191].
|
||||
let n = u32::from((255u8.wrapping_sub(t) >> 1).wrapping_add(0x40));
|
||||
let p = f(INV_255) * f64::from(n);
|
||||
let mut ch = [
|
||||
@@ -346,8 +272,7 @@ impl CloudKernel {
|
||||
(f(frame.slope[2]) * p + f(frame.gbase[2])) as f32,
|
||||
];
|
||||
if let Some((su, sv)) = body {
|
||||
// V = (Su − col, Sv − row, z_bias); S = (dx, dy, 1). Accumulation and
|
||||
// product order per the bytes (±1-ulp load-bearing in the diff).
|
||||
// The reference's sum and product order: a one-ulp change moves output bytes.
|
||||
let vx = (f(su) - f64::from(col as u32)) as f32;
|
||||
let vy = (f(sv) - f(row_base)) as f32;
|
||||
let vz = z_bias;
|
||||
@@ -373,26 +298,23 @@ impl CloudKernel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sample the coverage `R ∈ [0,1]` toward a camera-relative offset `d` (Bevy frame, +Y up) —
|
||||
/// the reference's `FUN_006cfa90`: project onto the tile, read the byte. `d` is the
|
||||
/// un-normalized body offset — the glare samples at its 12-unit sky point, so the zenith
|
||||
/// shift contributes `cos45°/|d|`.
|
||||
/// The coverage in [0, 1] toward camera-relative `d` (`0x6cfa90`). `d` is not normalized: the
|
||||
/// glare samples at its 12-unit sky point, so the zenith shift weighs `cos45°/|d|`.
|
||||
pub fn coverage(&self, d: Vec3) -> f32 {
|
||||
let Some((u, v)) = project_cells(d) else {
|
||||
return f32::from(self.tile[(COLS / 2) * COLS + COLS / 2]) / 255.0;
|
||||
};
|
||||
let (col, row) = (u as i32, v as i32); // _ftol
|
||||
// Toroidal mask (the reference reads the flat heap unchecked; `u == 1.0` is measure-zero).
|
||||
let (col, row) = (u as i32, v as i32);
|
||||
// Deviation: the index wraps where the reference reads past the tile, only at `u == 1.0`.
|
||||
let cell = ((row as usize & (COLS - 1)) << SHIFT) + (col as usize & (COLS - 1));
|
||||
f32::from(self.tile[cell]) / 255.0
|
||||
}
|
||||
|
||||
/// The colored RGBA texels for the visible-layer texture upload (`0x58ac70`'s zero-copy bind).
|
||||
/// The colored texels the visible layer uploads (`0x58ac70`).
|
||||
pub fn rgba(&self) -> &[[u8; 4]] {
|
||||
&self.rgba
|
||||
}
|
||||
|
||||
/// Raw coverage bytes (tests).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn tile(&self) -> &[u8] {
|
||||
&self.tile
|
||||
@@ -404,10 +326,8 @@ impl CloudKernel {
|
||||
}
|
||||
}
|
||||
|
||||
/// The azimuthal tile projection (`FUN_006cf870`): camera-relative offset → fractional tile cell
|
||||
/// `(col, row)`. LUT centre = zenith, radius grows with the angle off a `+cos(π/4)`-shifted
|
||||
/// zenith axis, saturating at 45° (the rim; below-horizon directions clamp there). `None` for a
|
||||
/// degenerate zero offset.
|
||||
/// The azimuthal tile projection (`0x6cf870`): the zenith at the centre, the radius growing with
|
||||
/// the angle off a `+cos(π/4)`-shifted axis up to the 45° rim, where lower directions clamp.
|
||||
fn project_cells(d: Vec3) -> Option<(f32, f32)> {
|
||||
let len = f64::from(d.length());
|
||||
if len < 1e-6 {
|
||||
@@ -415,8 +335,7 @@ fn project_cells(d: Vec3) -> Option<(f32, f32)> {
|
||||
}
|
||||
let quarter_pi = f64::from(std::f32::consts::FRAC_PI_4);
|
||||
let c = f64::from(d.y) + f64::from(std::f32::consts::FRAC_PI_4.cos());
|
||||
// The reference feeds `c/len` to acos unclamped and NaNs above ~70° elevation — a domain
|
||||
// its bodies never reach; we clamp (recorded deviation).
|
||||
// Deviation: clamped; the reference NaNs above about 70° elevation, where its bodies never go.
|
||||
let theta = (c / len).clamp(-1.0, 1.0).acos();
|
||||
let phase = theta.min(quarter_pi) / quarter_pi * 0.5;
|
||||
let hyp = (f64::from(d.x) * f64::from(d.x) + f64::from(d.z) * f64::from(d.z)).sqrt();
|
||||
@@ -435,17 +354,15 @@ fn project_cells(d: Vec3) -> Option<(f32, f32)> {
|
||||
))
|
||||
}
|
||||
|
||||
/// The glow body's tile cell (`0x6cfb00` setup, steps 1–2): intersect the camera→body ray with
|
||||
/// the unit sky dome (`0x6cf9c0` — the `−cos(π/4)`-shifted sphere, larger
|
||||
/// quadratic root) and project the hit point onto the tile. `None` on the degenerate no-root /
|
||||
/// zero-direction case.
|
||||
/// The glow body's tile cell (`0x6cfb00` setup): the camera→body ray's far hit on the unit dome
|
||||
/// shifted by `−cos(π/4)` (`0x6cf9c0`), projected onto the tile.
|
||||
fn body_cells(dir: Vec3) -> Option<(f32, f32)> {
|
||||
let f = f64::from;
|
||||
// k = −cos(0.25·π), the quarter-arc constant both functions open with.
|
||||
// k = −cos(π/4), the dome's shift.
|
||||
let k = -(f(0.25f32) * f(std::f32::consts::PI)).cos();
|
||||
let a = (f(dir.x) * f(dir.x) + f(dir.y) * f(dir.y) + f(dir.z) * f(dir.z)) as f32;
|
||||
let b = {
|
||||
// `fchs; fmul v.z; fadd st,st` — (−k·up) doubled (the reference's z-up ↔ our y-up).
|
||||
// −k·up, doubled; the reference's up is z, ours is y.
|
||||
let nk = -k * f(dir.y);
|
||||
(nk + nk) as f32
|
||||
};
|
||||
@@ -459,8 +376,7 @@ fn body_cells(dir: Vec3) -> Option<(f32, f32)> {
|
||||
project_cells(hit)
|
||||
}
|
||||
|
||||
/// The cmath quadratic solver (`0x454f40`): larger root of `a·t² + b·t + c = 0`, Vieta-stable
|
||||
/// exactly as the binary rounds it; `None` on the no-root leg (`b² ≤ 4ac` or unordered).
|
||||
/// The larger root of `a·t² + b·t + c = 0`, rounded as the reference's solver does (`0x454f40`).
|
||||
fn quadratic_larger_root(a: f32, b: f32, c: f32) -> Option<f32> {
|
||||
let f = f64::from;
|
||||
let g = f(a) * f(c) * 4.0;
|
||||
@@ -482,33 +398,26 @@ fn quadratic_larger_root(a: f32, b: f32, c: f32) -> Option<f32> {
|
||||
})
|
||||
}
|
||||
|
||||
/// `1/255` as the binary stores it (`0x8026c8` = `0x3b808081`).
|
||||
/// `1/255` as the reference stores it (`0x8026c8`).
|
||||
const INV_255: f32 = f32::from_bits(0x3b80_8081);
|
||||
|
||||
/// The binary's integer fast-inverse-sqrt leaf (`0x456330`): a one-shot seed, NO Newton step —
|
||||
/// deterministic bit-manipulation, so ported verbatim (the approximation error is part of the
|
||||
/// reference's glow shape).
|
||||
/// The reference's inverse-sqrt seed (`0x456330`), no Newton step: its error shapes the glow.
|
||||
fn fisr(x: f32) -> f32 {
|
||||
f32::from_bits(0x5f39_97bbu32.wrapping_sub((x.to_bits() >> 1) & 0x3fff_ffff))
|
||||
}
|
||||
|
||||
/// Pack one color channel to a byte exactly as the binary does (`0x6cfef6..0x6cff44`): clamp
|
||||
/// `≤ 1.0` (no lower clamp), then the `bits(ch·255 + 512) >> 14` trick — `floor(ch·255)` after
|
||||
/// f32 rounding.
|
||||
/// Packs a channel as the reference does (`0x6cfef6..0x6cff44`): clamped above only, floored.
|
||||
fn pack_channel(ch: f64) -> u8 {
|
||||
let clamped = if ch < 1.0 { ch } else { 1.0 };
|
||||
(((clamped * 255.0 + 512.0) as f32).to_bits() >> 14) as u8
|
||||
}
|
||||
|
||||
/// Sun glare cloud occlusion (`occ1_sun = 1 − R`, `0x6cf7b0`): a cloud over the sun dims the
|
||||
/// flare linearly with coverage.
|
||||
/// The sun glare's cloud occlusion (`0x6cf7b0`): the flare dims linearly with coverage.
|
||||
pub fn occ1_sun(r: f32) -> f32 {
|
||||
1.0 - r
|
||||
}
|
||||
|
||||
/// Moon glare cloud occlusion — the tent `1 − |2(R − 0.5)|` (`0x6cf7d0`): zero at R=0 *and* R=1.
|
||||
/// The reference's moon halo is a thin-cloud effect — off in a perfectly clear patch of sky,
|
||||
/// blooming when a wisp crosses the moon.
|
||||
/// The moon glare's cloud occlusion (`0x6cf7d0`): a tent, so the halo blooms only in thin cloud.
|
||||
pub fn occ1_moon(r: f32) -> f32 {
|
||||
1.0 - (2.0 * (r - 0.5)).abs()
|
||||
}
|
||||
@@ -517,8 +426,6 @@ pub fn occ1_moon(r: f32) -> f32 {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A daytime frame: mid-grey slope over a dark base, warm glow, clear weather, the sun high
|
||||
/// in the +X sky at full envelope.
|
||||
fn frame() -> CloudFrame {
|
||||
CloudFrame {
|
||||
sun: [1.0, 0.78, 0.54],
|
||||
@@ -530,9 +437,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear sky (C=0 ⇒ T=255): every cell quantizes below the threshold ⇒ R = 0 everywhere ⇒
|
||||
/// occ1_sun = 1 (full flare), occ1_moon = 0 (no halo). The colored
|
||||
/// texels are all alpha 0 (the t==0 hole fill).
|
||||
/// Density 0 puts the threshold at 255, so every cell quantizes to 0.
|
||||
#[test]
|
||||
fn clear_sky_is_empty_coverage() {
|
||||
let mut k = CloudKernel::default();
|
||||
@@ -545,11 +450,7 @@ mod tests {
|
||||
assert_eq!(occ1_moon(0.0), 0.0);
|
||||
}
|
||||
|
||||
/// Full density (C=1 ⇒ T=0): every raw byte clears the threshold, so the whole sky reads as
|
||||
/// heavy cover through the tone curve's high region — a storm's overcast field, not a
|
||||
/// scattered one. Regeneration is deterministic. Intermediate density (C=0.6, the reference's
|
||||
/// own init threshold) cuts the threshold through the noise distribution: a real mix of
|
||||
/// clear (0) and covered cells — the scattered-cloud texture.
|
||||
/// Density 1 is overcast; 0.6 gives the reference's init threshold, 101, and scattered cloud.
|
||||
#[test]
|
||||
fn density_shapes_the_field_and_regen_is_deterministic() {
|
||||
let mut a = CloudKernel::default();
|
||||
@@ -558,8 +459,7 @@ mod tests {
|
||||
b.rebuild(1.0, &frame());
|
||||
assert_eq!(a.tile(), b.tile());
|
||||
assert_eq!(a.rgba(), b.rgba());
|
||||
// Overcast: no cell clears (the noise's whole range sits above T=0), and the curve pushes
|
||||
// the bulk high (measured mean ≈ 242, min 135 — the accumulator tail through the toe).
|
||||
// Measured: mean about 242, minimum 135.
|
||||
assert!(
|
||||
a.tile().iter().all(|&v| v > 0),
|
||||
"overcast leaves no clear cell"
|
||||
@@ -576,19 +476,14 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Band regeneration is keyed by absolute tile coordinates: four 32-row ticks at a fixed
|
||||
/// phase reproduce exactly the tile a full rebuild computes at that phase. This pins the
|
||||
/// toroidal walk (row_key seeding, per-row advance, quantize windows) against the reference
|
||||
/// structure — a wrong scroll seed or window shears the field between bands. The colored
|
||||
/// texels agree from row 1 (row 0's row-derivative reads the persistent prev-row scratch,
|
||||
/// which legitimately differs between a fresh full pass and a scrolled one — the reference's
|
||||
/// own post-rebuild wart, gone by the next wrap).
|
||||
/// Row 0's texels differ: its row slope reads the previous-row scratch, which a fresh rebuild
|
||||
/// and a scrolled band see differently, as in the reference.
|
||||
#[test]
|
||||
fn incremental_bands_tile_the_full_field() {
|
||||
let mut inc = CloudKernel::default();
|
||||
inc.rebuild(0.6, &frame()); // ends with phase bumped to 1, scroll 0
|
||||
inc.rebuild(0.6, &frame()); // leaves phase 1, scroll 0
|
||||
for _ in 0..4 {
|
||||
inc.tick(1.0, 0.6, &frame()); // four band fires cover the whole tile at phase 1
|
||||
inc.tick(1.0, 0.6, &frame());
|
||||
}
|
||||
let mut full = CloudKernel::default();
|
||||
full.set_phase(1);
|
||||
@@ -597,10 +492,6 @@ mod tests {
|
||||
assert_eq!(inc.rgba()[COLS..], full.rgba()[COLS..]);
|
||||
}
|
||||
|
||||
/// The color pass (`0x6cfb00`): a covered cell away from the glow carries the pure gradient
|
||||
/// bytes (`floor((slope·p + gbase)·255)`, `p = (((255−t)>>1)+64)/255`) with alpha = t; a
|
||||
/// cell whose body-alignment is positive gains the sun term; a hole copies its left
|
||||
/// neighbour's RGB at alpha 0.
|
||||
#[test]
|
||||
fn color_pass_matches_the_byte_math() {
|
||||
let mut k = CloudKernel::default();
|
||||
@@ -619,7 +510,6 @@ mod tests {
|
||||
assert_eq!(px[1], want(f.slope[1], f.gbase[1]));
|
||||
assert_eq!(px[2], want(f.slope[2], f.gbase[2]));
|
||||
}
|
||||
// Glow on: texels toward the sun brighten, none darken below the gradient base.
|
||||
let lit = frame();
|
||||
let mut kl = CloudKernel::default();
|
||||
kl.rebuild(1.0, &lit);
|
||||
@@ -633,8 +523,6 @@ mod tests {
|
||||
assert!(kl.rgba().iter().zip(k.rgba()).all(|(a, b)| a[0] >= b[0]));
|
||||
}
|
||||
|
||||
/// The hole fill: force a hole next to a covered cell and recolor — the hole copies its left
|
||||
/// neighbour's RGB with alpha 0 (the reference's filtering-friendly early-out).
|
||||
#[test]
|
||||
fn holes_copy_the_left_neighbour_rgb() {
|
||||
let mut k = CloudKernel::default();
|
||||
@@ -649,32 +537,26 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The integer fast-inverse-sqrt seed (`0x456330`): the exact bit formula, ~3% accurate.
|
||||
#[test]
|
||||
fn fisr_is_the_binary_seed() {
|
||||
assert_eq!(fisr(1.0).to_bits(), 0x3f79_97bb);
|
||||
assert!((fisr(4.0) - 0.5).abs() < 0.02);
|
||||
}
|
||||
|
||||
/// The azimuthal projection: zenith reads the tile centre; a low direction lands on the rim
|
||||
/// ring at its azimuth; below-horizon clamps to the same rim radius.
|
||||
#[test]
|
||||
fn sampler_projects_zenith_to_centre_and_horizon_to_rim() {
|
||||
let mut k = CloudKernel::default();
|
||||
k.tile.fill(0);
|
||||
let mid = COLS / 2;
|
||||
k.tile[mid * COLS + mid] = 255;
|
||||
// Straight up: phase 0 ⇒ the centre cell.
|
||||
assert_eq!(k.coverage(Vec3::new(0.0, 12.0, 0.0)), 1.0);
|
||||
// A horizontal +X direction: phase clamps to 0.5 ⇒ col = COLS (wraps to 0), row = mid.
|
||||
// Horizontal +X clamps to the rim: col = COLS, which wraps to 0, row = mid.
|
||||
k.tile[mid * COLS] = 51;
|
||||
let r = k.coverage(Vec3::new(12.0, 0.0, 0.0));
|
||||
assert!((r - 0.2).abs() < 1e-3, "rim read {r}");
|
||||
// Below the horizon: same clamp, same cell.
|
||||
assert_eq!(k.coverage(Vec3::new(12.0, -4.0, 0.0)), r);
|
||||
}
|
||||
|
||||
/// The moon tent peaks at half coverage and vanishes at both extremes (`0x6cf7d0`).
|
||||
#[test]
|
||||
fn moon_tent_shape() {
|
||||
assert_eq!(occ1_moon(0.5), 1.0);
|
||||
|
||||
Reference in New Issue
Block a user