Files
benilla-octo/crates/benilla-world/src/view.rs
T
SamandClaude Fable 5 8eea16157c benilla-world: the engine becomes its own crate
- the world engine splits from the game into a benilla-world crate — 112 files, and the wall is ratcheted to zero names of the game inside the engine
- the engine's public doorway is counted and gated at 32 items, and the render materials, shaders and asset cache move down to benilla-assets
- the game's files leave the engine's asset root — game:// is its own space — and a spike binary boots the engine with no game attached
- weather rides along: snowfall emission is denominated in the reference's frame cap, and a long teleport retires the stranded precipitation field instead of leaving it behind

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:13:44 +07:00

232 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! **The view** — who is looking, with what optics, and how far the detailed world is drawn.
//!
//! Three things, one owner. [`WorldCamera`] marks *the* camera the scene is rendered through;
//! [`CAM_NEAR`] and [`CAM_FOVY`] are its optics; [`ViewDistance`] is the faithful `farclip`.
//!
//! The marker and the optics were in `player::camera` until decision 1160's stage zero, and that
//! was the single largest edge across the engine/game line: **26 engine files** — terrain
//! streaming, the portal PVS, sun follow, picking, every effect sim — reached into the *player
//! controller* to ask which camera to read. None of them care about a player; they care about the
//! viewer, which a world editor and a serverless viewer have without one. `farclip` itself was
//! promoted out of the debug panel earlier, for the same reason at a smaller scale: it was a
//! `ModelDebug` field doing double duty, so it read as a debug knob rather than as config, and
//! subsystems each kept their own idea of it.
//!
//! Read by: the hard far-clip **wall** (terrain/model/liquid/WDL/particle shaders, pushed as
//! `fog_params.w` by `lighting::apply_wow_lighting`), the per-object **cull**
//! (`debug_panel::apply_model_visibility`), the particle **draw-set gate**
//! (`particles::sim::simulate_particles`) — both through [`within_farclip`] — and
//! — once `terrain_stream` is split — the tile
//! **stream radius** (which should derive its coverage from `farclip`). Live-editable via the debug
//! panel slider (the same A/B lever as before, now writing this resource).
use bevy::prelude::*;
/// **The viewer's own body**, as the world needs it — 1160's wire (a), the half that is not about
/// where to stream.
///
/// Three engine lanes read the game's avatar for the same three facts and behind the *same*
/// predicate (`active && !detached`): the WMO interior probe wants the eye's world point, the
/// water foam wants a wading body, the precipitation slab wants the commanded planar speed its
/// tilt keys on. None of them wants a `Player` — they want a body that may or may not be there.
///
/// `None` on [`Self::at`] means exactly what each of those sites used to spell out by hand: no
/// live avatar, or an eye that has been detached from it. A program with no avatar at all leaves
/// this defaulted and every lane takes its no-body branch, which is what the world viewer needs.
#[derive(Resource, Clone, Copy)]
pub struct Viewer {
/// The avatar's position in **Bevy** space, when one is live and the eye is on it.
pub at: Option<Vec3>,
/// Its last-streamed CMovement flags (`MOVEMENTFLAGS`, cached at `unit+0x9e8`). `0` with no
/// body. Read through [`Self::translating`] / [`Self::turning`] rather than masked at each
/// site — the bit values are the wire's, and one restatement of them is one too many already.
pub move_flags: u32,
/// The **commanded** planar speed in yd/s (`[[player+0x118]+0x84]`): exactly zero with no
/// direction key held, live rather than a decayed measurement.
pub planar_speed: f32,
/// Its collision cylinder height in yards — the foam ring's radius input.
pub height: f32,
/// The **first-person feather**: how opaque the viewer's own body is as the camera zooms into
/// it. `1.0` normally, ramping to `0.0` at full zoom-in. A property of the eye's relationship
/// to the body, which is why it rides here and not on the body.
pub self_fade: f32,
/// **Drunkenness**, `0.0..=1.0` — `PLAYER_BYTES_3` byte 1 clamped at 100. The full-screen haze
/// is a property of the eye, not of any body in the scene.
pub drunk: f32,
/// Is the viewer a **ghost**? While the flag is up the active `LightParams` slot is 4 — the
/// death profile, applied instantly (decision 0308 §7, byte-verified `death-light.md`).
pub ghost: bool,
/// Is a loading cover over the world right now, so the viewer has not actually *seen* anything
/// yet? The appear ramp arms on this falling edge — the faithful trigger is "the player can
/// see the entity", and the residency proxy goes true well before the cover drops now that
/// the clear waits for the whole scene (decision 0737).
pub world_covered: bool,
}
impl Default for Viewer {
/// No body, and **nothing covering the world** — the defaults a program with no game boots
/// with. `self_fade` is `1.0`: absent an eye-to-body relationship, nothing is feathered.
fn default() -> Self {
Self {
at: None,
move_flags: 0,
planar_speed: 0.0,
height: 0.0,
self_fade: 1.0,
drunk: 0.0,
ghost: false,
world_covered: false,
}
}
}
impl Viewer {
/// Is the body **translating**? The four direction bits (`& 0xf`) — the same test the
/// reference's water-ripple driver runs (`0x5fa760`).
pub(crate) fn translating(&self) -> bool {
self.move_flags & 0xf != 0
}
/// Is it **turning in place**? The two keyboard turn bits (`& 0x30`). Strafe slides without
/// turning and is covered by [`Self::translating`]; a mouse-look body-step sets no flag at all.
pub(crate) fn turning(&self) -> bool {
self.move_flags & 0x30 != 0
}
}
/// View distance in yards. `farclip` = WoW's `farclip` CVar — the projection far plane for the detailed
/// world; geometry beyond it is clipped per-pixel (the wall) and the WDL horizon fills in beyond.
/// Default **777** (the vanilla max-view clamp `[177, 777]`; matches the reference `Config.wtf`).
#[derive(Resource, Clone, Copy)]
pub struct ViewDistance {
pub farclip: f32,
}
/// The settable range of [`ViewDistance::farclip`], shared by the debug-panel slider and the
/// `$WOW_FARCLIP` env knob so the two can't drift. The vanilla `farclip` CVar clamp is `[177, 777]`
/// (validate callback `0x688d40`, wow-re `terrain.md` "Camera-distance CVars"; its default is 350, ours
/// is the max) — the upper end here runs past that ONLY as an A/B lever against the old
/// "draw everything in the tile window" look.
pub const FARCLIP_RANGE: std::ops::RangeInclusive<f32> = 177.0..=1200.0;
impl Default for ViewDistance {
/// `$WOW_FARCLIP` (yd, clamped to [`FARCLIP_RANGE`]) overrides the 777 default. The panel slider is
/// the live lever, but a headless capture has no hands — and a horizon or fog report almost always
/// arrives with the director's slider somewhere other than the default (the 0684 gap was invisible
/// at 777 and glaring at 320), so reproducing one must not need a human. Read once at startup, like
/// the other capture-side knobs.
fn default() -> Self {
let farclip = std::env::var("WOW_FARCLIP")
.ok()
.and_then(|v| v.parse::<f32>().ok())
.map_or(777.0, |v| {
v.clamp(*FARCLIP_RANGE.start(), *FARCLIP_RANGE.end())
});
Self { farclip }
}
}
/// Is a world bounding sphere inside the far-clip wall — i.e. does the detailed world still draw it?
///
/// **The one spelling of "is it nearer than `farclip`", shared by every CPU-side consumer.** The test is
/// planar depth along the camera-forward axis (`(center − eye)·fwd`) of the sphere's NEAREST point, which
/// is deliberately the *same coordinate* the per-pixel wall uses in the shaders (`terrain.wgsl` /
/// `wow_model.wgsl` / `wow_effect.wgsl` all discard on eye-Z past `fog_params.w`). Agreeing on the
/// coordinate is what makes an object straddling the boundary **dissolve** through it instead of popping
/// when its origin crosses.
///
/// Radial distance would be the obvious alternative and it is wrong: it disagrees with the wall off-axis,
/// so a wide object at the edge of the frame pops while its pixels were still being drawn.
///
/// ## Why this is not the camera's far plane
/// The world camera's projection far is ~3000 yd — far *beyond* `farclip` on purpose, so the coarse WDL
/// horizon can draw behind the wall. So the frustum's own far plane is **not** the reference's far plane,
/// and a `Frustum::intersects_sphere(.., intersect_far = true)` is not a substitute for this test. In the
/// reference there is one projection far plane at `farclip` and it bounds the detailed world; here that
/// bound is this function plus the shaders' per-pixel discard, and nothing else.
pub fn within_farclip(
farclip: f32,
cam_pos: Vec3,
cam_fwd: Vec3,
center: Vec3,
radius: f32,
) -> bool {
(center - cam_pos).dot(cam_fwd) - radius <= farclip
}
/// Marks **the world camera** — the one flying the scene. Every "where is the viewer" consumer
/// (terrain streaming, PVS, sun follow, sound listener, picking, the capture pin, …) filters on this,
/// NOT on `Camera3d`: since the portrait booths (the portrait booths) there are multiple `Camera3d`s,
/// and a bare `With<WorldCamera>` query silently reads (or writes!) an off-screen booth camera — exactly
/// how the capture pin once yanked the booths to the scenario eye and blanked every portrait.
#[derive(Component)]
pub struct WorldCamera;
/// The camera **near-plane** distance (yd) — the reference's own **1/9**, hardcoded in its camera
/// ctor (`0x50a6c0`: `+0x38 = 0x3de38e39`; the `nearclip` console cvar stores to a global with zero
/// readers — dead plumbing; wow-re `water-frame-straddle` §4d). Shared by the projection (the camera spawn)
/// and the self-avatar fade's `nearclip` ([`crate::model_fade::self_model_fade_alpha`]) so the model
/// finishes fading exactly as the near plane would begin to slice it — the reference couples the
/// two the same way (`cam+0x38 ≈ 0.1`, set per frame in the driver `0x511bc0`).
///
/// It was 1.0 from 0062 to 0905 "for depth precision" — a rationale that predates knowing the
/// pipeline: the projection is `perspective_infinite_reverse_rh` on a float depth buffer
/// ([`crate::capture::depth_probe`]'s tests draw with the real one), where `depth = near/z` makes
/// relative precision — and our ULP-relative bias ladder ([`crate::sky_order`]) — independent of
/// the near value. The small near is what keeps the whole waterline-crossing band (the corner-min
/// submersion probe, `liquid::detect_submersion`) inches tall instead of a yard.
pub const CAM_NEAR: f32 = 1.0 / 9.0;
/// The camera's vertical field of view (radians) — one constant shared by the projection
/// (the camera spawn) and every consumer that needs the near rectangle's true shape. 45°, the value the
/// projection has always used (Bevy's `PerspectiveProjection` default, ≈ the reference's 44.1° —
/// [`crate::sun`]'s projection note); naming it here just stops the consumers drifting apart.
pub const CAM_FOVY: f32 = std::f32::consts::FRAC_PI_4;
#[cfg(test)]
mod tests {
use super::*;
/// The wall is planar depth along camera-forward, measured to the sphere's nearest point.
#[test]
fn planar_depth_of_the_nearest_point() {
let eye = Vec3::ZERO;
let fwd = Vec3::NEG_Z; // Bevy's camera looks down −Z
let at = |d: f32, r: f32| within_farclip(777.0, eye, fwd, Vec3::new(0.0, 0.0, -d), r);
assert!(at(700.0, 0.0));
assert!(at(777.0, 0.0)); // exactly at the wall still draws (the shader discards past it)
assert!(!at(778.0, 0.0));
// A big object straddling the wall stays in: its near side is still inside, and the
// per-pixel wall dissolves the far half. This is the no-pop property.
assert!(at(800.0, 30.0));
assert!(!at(900.0, 30.0));
}
/// Off-axis is where radial distance and the shader's eye-Z part company — the wall is a PLANE,
/// so a point 700 yd forward and 700 yd sideways (radially ~990) is still inside it.
#[test]
fn the_wall_is_a_plane_not_a_sphere() {
let eye = Vec3::ZERO;
let fwd = Vec3::NEG_Z;
let off = Vec3::new(700.0, 0.0, -700.0);
assert!(off.length() > 777.0, "radially outside");
assert!(
within_farclip(777.0, eye, fwd, off, 0.0),
"but inside the planar wall — must match the shader, which discards on eye-Z"
);
}
/// Behind the camera is trivially inside the wall (negative depth); the lateral frustum planes,
/// not this test, are what reject it. Pinned so nobody "fixes" this into an abs().
#[test]
fn behind_the_camera_is_not_this_tests_job() {
let eye = Vec3::ZERO;
assert!(within_farclip(
777.0,
eye,
Vec3::NEG_Z,
Vec3::new(0.0, 0.0, 5000.0),
0.0
));
}
}