New leaf crate v2/crates/ruview-offaxis implementing ADR-324's projection
core from the published math only (Kooima 2008 generalized perspective
projection; Casiez 2012 one-euro filter) — no code from the unlicensed
prior-art repo. Dependency-free native core; wasm-bindgen surface gated to
wasm32 (cdylib+rlib, ~54 KB wasm after bindgen).
- projection: Screen (3 corners, any orientation) + off_axis() -> typed
errors, never NaN matrices; column-major f64 (three.js Matrix4 layout).
Tests pin the defining invariants: screen corners -> NDC corners for a
grid of eye positions and tilted screens; screen-plane points are
eye-invariant; centered eye reduces to the symmetric frustum; near/far
map to NDC -1/+1.
- filter: one-euro with injected timestamps (no clock in crate);
monotonicity/convergence/NaN-rejection tests.
- rf: field-peak extraction mirroring field_localize.rs constants
(X_SCALE 0.6, Z_SCALE 0.5, PEAK_THRESHOLD 0.35) and the Tier B
coarse-parallax stage (deadband, gain, hard clamp) so over-claiming is
impossible at the API level. No accuracy numbers asserted.
- wasm: OffAxisCamera + RfParallax bindgen classes; per-frame updates hold
last good state instead of throwing.
- benches (criterion): full Tier B frame ~598 ns; argmax scan optimized
-18%/-33% (20x20/100x100). MEASURED table + reproducer in README.
- examples/three.js/demos/07-off-axis-window.html: demo with SYNTHETIC
mouse simulator and labeled RF Tier B mode ('coarse body parallax - not
head tracking'), physical calibration panel, /ws/sensing input, and a
build-instructions overlay when the local pkg/ output is missing
(generated artifacts stay uncommitted; .gitignore entry added).
- Validated 23 unit tests + doctest, clippy clean, wasm32 release build,
Node smoke test of the bindgen output, and a headless-Chromium run of
the demo (engine load, eye response, mode labels, ws failure path).
- ADR-324: header + section 2.5 amendment recording the Rust/WASM core.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01BU3NcEgTpAVvu5QGtw4czT
7.3 KiB
ruview-offaxis
Clean-room off-axis (head-coupled) perspective projection in Rust, with a wasm-bindgen surface for browser demos. Implements ADR-324.
The screen becomes a window: given the physical screen's corners and the viewer's eye position, the crate produces the asymmetric frustum and screen-aligned view matrix that keep the screen plane fixed while everything behind and in front of it moves with true parallax.
Clean-room statement
ADR-324 records that the prior-art repository (icurtis1/off-axis-sneaker)
publishes no license. No code, assets, or derived text from it appear in
this crate. The implementation follows the published math only:
- Robert Kooima, Generalized Perspective Projection (2008) — the
pa/pb/pcscreen-corner frustum formulation. - Casiez, Roussel & Vogel, 1€ Filter (CHI 2012) — adaptive tracking-noise smoothing.
What's in the crate
| Module | Contents |
|---|---|
projection |
Screen (3 corners, any orientation), off_axis() → asymmetric projection + screen-aligned view matrix (column-major f64, the three.js Matrix4.elements layout). All failure modes are typed errors — never NaN matrices. |
filter |
OneEuro / OneEuro3 one-euro filter. Timestamps are injected; the crate never reads a clock. |
rf |
field_peak() — strongest-cell extraction for /ws/sensing signal_field grids, mirroring the sensing server's field_localize.rs constants (X_SCALE 0.6, Z_SCALE 0.5, PEAK_THRESHOLD 0.35). CoarseParallax — the Tier B stage: deadband + gain + hard clamp + one-euro. ScreenCalibration — physical screen (cm) + normalized-head → metric eye mapping (Tier A input hook). |
wasm (wasm32 only) |
OffAxisCamera and RfParallax wasm-bindgen classes. |
The native core is dependency-free; wasm-bindgen is pulled only when
compiling for wasm32.
Honesty contract (repo rule — read before demoing)
- A single-link CSI field peak is a representation of field energy, not
metric localization and never a head position (see the caveat in
wifi-densepose-sensing-server/src/field_localize.rs). The Tier B path is therefore coarse body parallax by construction: deadbanded, gain-limited, hard-clamped. Do not present it as head tracking; the demo labels it on screen at all times. - Numeric defaults (gains, deadbands, filter cutoffs) are interaction-design
choices —
CLAIMED, not measured performance. - The benchmark numbers below are
MEASUREDwith the stated reproducer on the stated machine; re-run locally before relying on them.
Native quick start
use ruview_offaxis::{off_axis, Screen, Vec3};
// 60 cm × 34 cm screen centered at the origin; eye 65 cm out, 10 cm right.
let screen = Screen::centered(0.60, 0.34)?;
let oa = off_axis(&screen, Vec3::new(0.10, 0.0, 0.65), 0.05, 100.0)?;
let mvp: [f64; 16] = oa.view_projection(); // column-major, GL/three.js layout
# Ok::<(), ruview_offaxis::OffAxisError>(())
Key invariant (unit-tested for a grid of eye positions and for tilted
screens): the physical screen corners always project exactly to the NDC
corners — pa→(−1,−1), pb→(1,−1), pc→(−1,1), pd→(1,1) — and points on
the screen plane are eye-invariant. That is the mathematical definition of
"the screen is a window".
Building the WASM package
Generated artifacts are not committed (repo rule). Build once:
cd v2
rustup target add wasm32-unknown-unknown
cargo build -p ruview-offaxis --target wasm32-unknown-unknown --release
# Install the matching CLI once: cargo install wasm-bindgen-cli --version 0.2.114
wasm-bindgen --target web --out-dir crates/ruview-offaxis/pkg \
target/wasm32-unknown-unknown/release/ruview_offaxis.wasm
Output: pkg/ruview_offaxis.js + pkg/ruview_offaxis_bg.wasm
(≈54 KB wasm, MEASURED for this crate at wasm-bindgen 0.2.114; wasm-opt -Oz
can shrink it further if you have binaryen). The demo at
examples/three.js/demos/07-off-axis-window.html loads this path directly —
build, then open the demo. A Node smoke test of the same flow lives in the
PR's validation notes.
three.js integration (the whole wiring)
import init, { OffAxisCamera, RfParallax } from './pkg/ruview_offaxis.js';
await init();
// Physical calibration in cm — measure your actual screen.
const cam = new OffAxisCamera(60, 34, 65, 0.05, 100.0);
cam.set_filter(1.2, 0.4); // one-euro: min_cutoff Hz, beta
const camera = new THREE.PerspectiveCamera();
camera.matrixAutoUpdate = false; // WASM owns every matrix
const view = new THREE.Matrix4();
function onFrame(eyeX, eyeY, eyeZ) { // metres, screen space
cam.update_eye(eyeX, eyeY, eyeZ, performance.now() / 1000);
camera.projectionMatrix.fromArray(cam.projection());
camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert();
view.fromArray(cam.view());
camera.matrixWorld.copy(view).invert();
camera.matrixWorldInverse.copy(view);
}
Scene convention: the screen plane is z = 0; content behind the screen has
z < 0; content with z > 0 "pops out". Do not update camera.aspect
on resize — the frustum is determined by the physical screen, not the
viewport.
Input tiers (ADR-324)
- Tier A (fine tracker) — feed any head tracker through
cam.update_normalized(nx, ny, depthScale, lateralRangeM, t);nx/nyare normalized image coordinates from whatever fine tracker the host runs (which stays entirely in the browser). RF adds presence gating around it. - Tier B (RF only, labeled) —
RfParallax.update(Float32Array, nx, nz, t)with/ws/sensingsignal_fieldvalues, thenrf.eye()→cam.update_eye(...). Keep the on-screen "coarse body parallax — not head tracking" label; the clamps in the Rust core bound the excursion but the label is what keeps the demo honest.
Benchmarks
MEASURED — reproducer: cd v2 && cargo bench -p ruview-offaxis.
Environment for the numbers below: Linux x86_64 container (shared/virtualized
CPU), rustc 1.89.0, criterion 0.5, 2026-08-16. Treat them as order-of-
magnitude; re-run on your hardware.
| Benchmark | Time (median) |
|---|---|
off_axis_projection (frustum + view build) |
~76 ns |
view_projection combined (4×4 multiply) |
~27 ns |
one_euro3_step (3-axis filter step) |
~60 ns |
field_peak_20x20 (live grid size) |
~488 ns |
field_peak_100x100 |
~12.3 µs |
tier_b_full_frame_20x20 (scan → parallax → projection) |
~598 ns |
The full Tier B per-frame path costs well under a microsecond — under 0.01% of a 60 Hz frame budget. The argmax scan was the only hot spot found; it was rewritten branch-light for a measured −18% (20×20) / −33% (100×100) before these numbers were taken. End-to-end motion-to-photon latency (RF capture → render) has not been measured and is dominated by the sensing pipeline, not this crate; no figure is claimed.
Validation
cd v2
cargo test -p ruview-offaxis # 23 unit tests + doctest
cargo clippy -p ruview-offaxis --all-targets # zero warnings
cargo build -p ruview-offaxis --target wasm32-unknown-unknown --release
References
- ADR-324 — decision record, tier definitions, licensing analysis
docs/adr/ADR-282-*— L0–L5 evidence ladder (labels used above)v2/crates/wifi-densepose-sensing-server/src/field_localize.rs— the field-peak honesty caveat this crate inherits