The field of view follows the aspect; a missing model draws the cube (#2930)

Three gaps:

- The world camera's vertical field of view was a fixed 45°. 1.12.1's camera holds π/2 (`0x50a707`), and its projection takes the vertical half-angle as fov / (2·√(aspect² + 1)) (`0x5c3cc0`). That gives 54.0° at 4:3, 47.7° at 16:10, 44.1° at 16:9 and 35.0° at 21:9. benilla now derives it from the viewport's aspect and re-derives it on a resize. The spyglass keeps its 6× narrowing.
- A unit, item, effect or doodad whose model file is missing drew nothing. `CM2Scene::CreateModel` (`0x707350`), which every lane creates through, opens `Spells\ErrorCube` instead (`0x70737f`). benilla's asset readers now do the same for a missing `.m2`.
- `view::msaa_from_env` was dead code that defaulted to 4x, and is deleted.

Tests: the field of view at each aspect, and `CreatureDisplayInfo` 4 (`CrystalSpider.mdx`, absent) loading the cube.

Fixes #124
Fixes #330
Fixes #129

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-11-01 05:00:44 +07:00
committed by GitHub
parent 36e41ecf42
commit e6ed2a2a9d
11 changed files with 280 additions and 71 deletions
@@ -574,6 +574,61 @@ fn model_dir(model_path: &str) -> &str {
mod tests {
use super::*;
/// `CreatureDisplayInfo` 4 names `CrystalSpider.mdx`, which does not ship: the model load
/// stands `Spells\ErrorCube.mdx` (`0x707350`, `0x70737f`) rather than drawing nothing.
#[test]
fn a_display_whose_model_file_is_missing_resolves_to_the_error_cube() {
use benilla_assets::{
m2_url, register_asset_loaders, register_mpq_source, M2Model, ERROR_CUBE,
};
let data = benilla_formats::wow_data_or_skip!();
let mut chain = benilla_formats::open_chain(&data).expect("open chain");
let catalog = benilla_formats::load_creature_catalog(&mut chain).expect("creatures");
let path = catalog.model(4).expect("display 4").model_path;
assert!(
path.to_ascii_lowercase().contains("crystalspider"),
"{path}"
);
let mut app = App::new();
app.add_plugins(MinimalPlugins);
register_mpq_source(&mut app, &data).expect("register mpq source");
app.add_plugins(bevy::asset::AssetPlugin::default());
app.init_asset::<Image>();
app.init_asset::<Mesh>();
app.init_asset::<bevy::mesh::skinning::SkinnedMeshInverseBindposes>();
app.init_asset::<bevy::animation::AnimationClip>();
app.init_asset::<bevy::animation::graph::AnimationGraph>();
register_asset_loaders(&mut app);
let server = app.world().resource::<AssetServer>().clone();
let display = new_creature_display(&catalog, 4, &server);
assert!(display.names_a_model());
let ModelHandle::M2(handle) = display.handle else {
panic!("display 4 names an M2");
};
let cube: Handle<M2Model> = server.load(m2_url(ERROR_CUBE));
let shape = |app: &App, h: &Handle<M2Model>| {
app.world().resource::<Assets<M2Model>>().get(h).map(|m| {
m.submeshes
.iter()
.map(|s| s.geometry.positions.len())
.collect::<Vec<_>>()
})
};
let mut got = None;
for _ in 0..3000 {
app.update();
if let (Some(a), Some(b)) = (shape(&app, &handle), shape(&app, &cube)) {
got = Some((a, b));
break;
}
std::thread::sleep(std::time::Duration::from_millis(2));
}
let (spider, cube) = got.expect("both load");
assert!(!cube.is_empty(), "the cube has geometry");
assert_eq!(spider, cube, "the missing model is the cube");
}
/// Both kinds carry the same empty `parts` once built, so only the handle tells a display that
/// named no model from one whose model draws nothing.
#[test]
+18 -21
View File
@@ -6,15 +6,14 @@
//! `0x8` makes `SetCameraView` return early), with `[camera+0x40] = n × π/180`; removal restores
//! `π/2` and unlocks. The Ornate Spyglass (item 5507, spell 12883) passes 15, a 6× zoom.
//!
//! The ratio is applied, not the degrees: `[camera+0x40]` defaults to 90°, while our [`CAM_FOVY`]
//! is the effective vertical field of view, 45° against the reference's measured 44.1°.
//! The ratio is applied to `[camera+0x40]`, which defaults to 90°; the world projection then turns
//! it into the vertical field of view by the frame's aspect (`0x5c3cc0`, [`benilla_world::view`]).
use bevy::camera::{PerspectiveProjection, Projection};
use bevy::prelude::*;
use crate::net::{ObjectStore, SelfPlayer};
use crate::ui_action::Spells;
use benilla_world::view::{WorldCamera, CAM_FOVY};
use benilla_world::view::FovZoom;
/// `0x4c`, the aura name the reference's effect walk matches.
const SPELL_AURA_FAR_SIGHT: u32 = 76;
@@ -43,7 +42,7 @@ pub(super) fn apply_scoped_view(
self_q: Query<&ObjectStore, With<SelfPlayer>>,
spells: Option<Res<Spells>>,
mut scoped: ResMut<ScopedView>,
mut projections: Query<&mut Projection, With<WorldCamera>>,
mut fov_zoom: ResMut<FovZoom>,
) {
let catalog = spells.as_ref().map(|s| &s.catalog);
scoped.zoom = self_q
@@ -64,14 +63,10 @@ pub(super) fn apply_scoped_view(
.filter(|&deg| deg > 0.0)
.map(|deg| deg / REFERENCE_DEFAULT_DEGREES);
let Ok(mut projection) = projections.single_mut() else {
return;
};
let want = CAM_FOVY * scoped.zoom.unwrap_or(1.0);
if let Projection::Perspective(PerspectiveProjection { fov, .. }) = &mut *projection {
if (*fov - want).abs() > f32::EPSILON {
*fov = want;
}
// `stamp_world_fov` turns it into the field of view at the frame's aspect.
let want = scoped.zoom.unwrap_or(1.0);
if fov_zoom.0 != want {
fov_zoom.0 = want;
}
}
@@ -81,25 +76,27 @@ mod tests {
#[test]
fn the_spyglass_narrows_our_own_field_of_view_six_fold() {
use benilla_world::view::world_fovy;
let scoped = ScopedView {
zoom: Some(15.0 / REFERENCE_DEFAULT_DEGREES),
};
let fov = CAM_FOVY * scoped.zoom.unwrap();
let normal = world_fovy(16.0 / 9.0);
let fov = normal * scoped.zoom.unwrap();
assert!(
(fov - CAM_FOVY / 6.0).abs() < 1e-6,
"the spyglass is a 6x zoom off whatever our normal FOV is, not a literal 15 degrees"
(fov - normal / 6.0).abs() < 1e-6,
"the spyglass is a 6x zoom off whatever the normal FOV is, not a literal 15 degrees"
);
assert!(
(fov.to_degrees() - 7.5).abs() < 1e-4,
"…which lands at 7.5 degrees vertical for our 45 degree default, not 15"
(fov.to_degrees() - 44.1 / 6.0).abs() < 0.02,
"…which lands at a sixth of the 16:9 field of view, not 15"
);
assert!(scoped.active());
let none = ScopedView { zoom: None };
assert_eq!(
CAM_FOVY * none.zoom.unwrap_or(1.0),
CAM_FOVY,
"unscoped restores OUR default, never the reference's stored 90"
none.zoom.unwrap_or(1.0),
1.0,
"unscoped restores the reference's 90, never a stored default"
);
assert!(!none.active());
}
+3 -2
View File
@@ -10,7 +10,7 @@ use benilla_assets::coords::wow_to_bevy;
use benilla_assets::{RenderConfig, WorldAssets};
use benilla_world::terrain_stream::SPAWN_XY;
use benilla_world::view::{WorldCamera, CAM_FAR, CAM_FOVY, NEARCLIP_DEFAULT};
use benilla_world::view::{world_fovy, WorldCamera, CAM_FAR, NEARCLIP_DEFAULT};
use super::{
CameraControl, FlyCam, MoveSpeed, Player, PlayerCapsule, CAM_DIST_DEFAULT, CAPSULE_HEIGHT,
@@ -102,7 +102,8 @@ pub(super) fn setup_player(
// Frame zero only: `view::stamp_near_clip` re-stamps the live `nearclip` every frame,
// as `0x511bc0` overwrites the reference camera's `[cam+0x38]`.
near: NEARCLIP_DEFAULT,
fov: CAM_FOVY,
// Frame zero only: `view::stamp_world_fov` derives it from the viewport's aspect.
fov: world_fovy(16.0 / 9.0),
..default()
}),
// A linear `Rgba16Float` target without tonemapping: the shaders light in clamped gamma
+1 -1
View File
@@ -290,7 +290,7 @@ const SORTED_LEAKS: &[(&str, &str)] = &[
),
("terrain_stream::point_light", "CLOSE/absorb"),
("terrain_stream::spawn_model_entities", "CLOSE/move-engine"),
("view::CAM_FOVY", "CLOSE/absorb"),
("view::FovZoom", "CLOSE/absorb"),
("view::FARCLIP_RANGE", "CLOSE/absorb"),
("view::NEARCLIP_DEFAULT", "CLOSE/absorb"),
("wmo_portal::UnitWmoRoom", "CLOSE/absorb"),
+68 -3
View File
@@ -152,9 +152,44 @@ impl MpqAssetReader {
}
}
/// `CM2Scene::CreateModel` (`0x707350`) opens [`ERROR_CUBE`] when the requested model fails to
/// open (`0x70737f`), for every lane that creates through it (units `0x613e43`, GameObjects
/// `0x5f7bde`, items, spell effects, `Model:SetModel` `0x76cd01`). So a model path whose file is
/// missing reads the cube's bytes, from the archives with open flag 0; any other miss, and the
/// cube's own, stays a miss.
fn or_error_cube(
path: &Path,
read: Result<Vec<u8>, AssetReaderError>,
cube: impl FnOnce(&str) -> Result<Option<Vec<u8>>, AssetReaderError>,
) -> Result<Vec<u8>, AssetReaderError> {
let Err(AssetReaderError::NotFound(_)) = &read else {
return read;
};
let is_m2 = path
.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("m2"));
let Some(file) = benilla_formats::m2_file_name(ERROR_CUBE) else {
return read;
};
if !is_m2
|| path
.to_str()
.is_some_and(|p| p.eq_ignore_ascii_case(&file.replace('\\', "/")))
{
return read;
}
match cube(&file) {
Ok(Some(bytes)) => Ok(bytes),
_ => read,
}
}
impl AssetReader for MpqAssetReader {
async fn read<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
Ok(VecReader::new(self.read_bytes(path)?))
let bytes = or_error_cube(path, self.read_bytes(path), |cube| {
self.read_candidate(cube)
})?;
Ok(VecReader::new(bytes))
}
async fn read_meta<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
@@ -193,7 +228,7 @@ impl AssetReader for LooseAssetReader {
async fn read<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
// The URL holds `/` where the model's name held `\` ([`m2_url`] writes every separator
// as `/`), so it is turned back for the index.
let bytes = first_candidate(path, |name| {
let found = first_candidate(path, |name| {
let name = name.replace('/', "\\");
match self.loose.find(&name, LooseOpen::Index) {
// A loose file that will not open is a miss, with no turn to the chain
@@ -201,7 +236,8 @@ impl AssetReader for LooseAssetReader {
Some(file) => Ok(file.read()),
None => self.mpq.read_candidate(&name),
}
})?;
});
let bytes = or_error_cube(path, found, |cube| self.mpq.read_candidate(cube))?;
Ok(VecReader::new(bytes))
}
@@ -382,6 +418,35 @@ mod tests {
use bevy::camera::primitives::MeshAabb;
use bevy::tasks::block_on;
/// `0x70737f`: a model that fails to open stands the error cube; nothing else does.
#[test]
fn a_missing_model_reads_the_error_cube() {
let missing = || Err(AssetReaderError::NotFound("x".into()));
let cube = |name: &str| {
assert_eq!(name, "spells\\errorcube.m2");
Ok(Some(vec![7u8]))
};
let m2 = Path::new("creature/crystalspider/crystalspider.m2");
assert_eq!(or_error_cube(m2, missing(), cube).unwrap(), vec![7u8]);
assert_eq!(
or_error_cube(m2, Ok(vec![1u8]), cube).unwrap(),
vec![1u8],
"a model that opens is its own"
);
assert!(
or_error_cube(Path::new("creature/x/x.blp"), missing(), cube).is_err(),
"a texture miss is not a model miss"
);
assert!(
or_error_cube(Path::new("spells/errorcube.m2"), missing(), cube).is_err(),
"no cube behind the cube"
);
assert!(
or_error_cube(m2, missing(), |_| Ok(None)).is_err(),
"an install without the cube keeps the miss"
);
}
/// `0x706a50`'s extension rule: the last dot's `.mdx`/`.mdl` in any case is `.m2`, `.m2`
/// stands, and anything else stands the error cube from the archives (`0x70737f`).
#[test]
+15 -7
View File
@@ -715,9 +715,10 @@ mod tests {
#[test]
fn the_cull_never_clips_a_mote_that_is_on_screen() {
for aspect in [4.0 / 3.0, 16.0 / 10.0, 16.0 / 9.0, 21.0 / 9.0, 32.0 / 9.0] {
let (tx, ty) = cull_limits(crate::view::CAM_FOVY, aspect);
let frustum_x = (crate::view::CAM_FOVY * 0.5).tan() * aspect;
let frustum_y = (crate::view::CAM_FOVY * 0.5).tan();
let fovy = crate::view::world_fovy(aspect);
let (tx, ty) = cull_limits(fovy, aspect);
let frustum_x = (fovy * 0.5).tan() * aspect;
let frustum_y = (fovy * 0.5).tan();
assert!(
tx >= frustum_x,
"aspect {aspect}: cull {tx} < frustum {frustum_x}"
@@ -728,10 +729,17 @@ mod tests {
);
}
// At the aspects the reference ran at, the limit is its 90° cone exactly.
assert_eq!(cull_limits(crate::view::CAM_FOVY, 4.0 / 3.0), (1.0, 1.0));
assert_eq!(cull_limits(crate::view::CAM_FOVY, 16.0 / 9.0), (1.0, 1.0));
let (tx, ty) = cull_limits(crate::view::CAM_FOVY, 32.0 / 9.0);
assert!(tx > 1.0 && ty == 1.0);
assert_eq!(
cull_limits(crate::view::world_fovy(4.0 / 3.0), 4.0 / 3.0),
(1.0, 1.0)
);
assert_eq!(
cull_limits(crate::view::world_fovy(16.0 / 9.0), 16.0 / 9.0),
(1.0, 1.0)
);
let (tx, ty) = cull_limits(crate::view::world_fovy(32.0 / 9.0), 32.0 / 9.0);
// The reference's relation keeps the frustum inside its cone at any aspect.
assert_eq!((tx, ty), (1.0, 1.0));
}
#[test]
+2 -1
View File
@@ -6,7 +6,8 @@
//! LightIntBand sub-9, alpha 0xFF) tints every disc and glare but moon02, which has no colour
//! writer; the white moon's teal rim is the dome's night bands (sub-3..6) through its feathered
//! edge. Every body takes the sky's far-depth pin. The bodies are placed in plain world space, so
//! they project through our camera's 45° fovy, where the reference's is 44.1° at 16:9.
//! they project through the world camera's fovy, which follows the aspect as the reference's does
//! (`0x5c3cc0`).
use bevy::pbr::MaterialPlugin;
use bevy::prelude::*;
+106 -24
View File
@@ -1,4 +1,4 @@
//! The view: the camera the scene renders through ([`WorldCamera`]), its optic ([`CAM_FOVY`]),
//! The view: the camera the scene renders through ([`WorldCamera`]), its optic ([`world_fovy`]),
//! and how far the detailed world draws ([`ViewDistance`], the reference's `nearclip` and
//! `farclip` pair). `farclip` bounds the per-pixel wall, the CPU cull ([`within_farclip`]) and
//! the terrain residency window (`terrain_stream::window`), which derives its reach from it as the
@@ -289,24 +289,6 @@ pub fn within_farclip(
#[derive(Component)]
pub struct WorldCamera;
/// The `$WOW_MSAA` knob as a Bevy `Msaa` level, 4x when unset. `8` falls back to 4x:
/// `Rgba16Float` is only guaranteed `[1, 4]` samples, and there is no adapter to ask here.
pub fn msaa_from_env() -> bevy::render::view::Msaa {
use bevy::render::view::Msaa;
match std::env::var("WOW_MSAA").ok().as_deref() {
Some("off") | Some("0") | Some("1") => Msaa::Off,
Some("2") => Msaa::Sample2,
Some("8") => {
warn!(
"WOW_MSAA=8: Rgba16Float is only guaranteed [1, 4] samples and this adapter \
reports [1, 2, 4] — falling back to 4x rather than panicking the render thread"
);
Msaa::Sample4
}
_ => Msaa::Sample4,
}
}
/// The `nearclip` CVar's registered default, 0.1 (the string at `0x84fb48`, registered at
/// `0x68867a`, record `[0xc7f348]`); the camera ctor's 1/9 (`0x50a6c0`) is overwritten from the
/// record in the first frame, before it renders. The small near keeps the waterline-crossing band
@@ -334,10 +316,54 @@ pub fn stamp_near_clip(
}
}
}
/// The vertical field of view in radians, shared by the projection and every consumer of the near
/// rectangle: 45°, Bevy's `PerspectiveProjection` default; the reference's follows the aspect,
/// 44.1° at 16:9.
pub const CAM_FOVY: f32 = std::f32::consts::FRAC_PI_4;
/// The camera's field-of-view parameter, `[cam+0x40]` = π/2 (the constructor, `0x50a707`).
pub const CAM_FOV: f32 = std::f32::consts::FRAC_PI_2;
/// The world camera's vertical field of view in radians at `aspect` (width over height): the
/// world projection `0x50ab70` hands `[cam+0x40]` and the frame's aspect to `0x5c3cc0`, whose
/// vertical half-angle is `fov / (2·√(aspect² + 1))` (`0x5c3d0b`-`0x5c3d4c`). 54.0° at 4:3, 44.1°
/// at 16:9.
pub fn world_fovy(aspect: f32) -> f32 {
CAM_FOV / (aspect * aspect + 1.0).sqrt()
}
/// The held fraction of `[cam+0x40]`: 1 normally; the spyglass (aura 76, `0x50d320`) writes
/// `n × π/180` there, so its zoom scales the field of view before `0x5c3cc0` sees it.
#[derive(Resource, Clone, Copy)]
pub struct FovZoom(pub f32);
impl Default for FovZoom {
fn default() -> Self {
Self(1.0)
}
}
/// Derives the world camera's vertical field of view from its viewport's aspect every frame, so a
/// window resize re-derives it as `0x50ab79`-`0x50ab93` re-reads the frame's aspect.
pub fn stamp_world_fov(
zoom: Res<FovZoom>,
mut cam: Query<(&Camera, &mut Projection), With<WorldCamera>>,
) {
for (camera, mut proj) in &mut cam {
let Some(size) = camera
.physical_viewport_size()
.filter(|s| s.x > 0 && s.y > 0)
else {
continue;
};
let want = world_fovy(size.x as f32 / size.y as f32) * zoom.0;
// `&mut` marks the component changed even when the value is equal, so compare first.
let Projection::Perspective(p) = proj.as_ref() else {
continue;
};
if (p.fov - want).abs() <= f32::EPSILON {
continue;
}
if let Projection::Perspective(p) = &mut *proj {
p.fov = want;
}
}
}
/// The world camera's pose made current within `Update`, for every viewer authority there: Bevy
/// propagates `GlobalTransform` in `PostUpdate`, a whole jump late after a teleport snap.
@@ -358,7 +384,7 @@ pub(crate) fn publish_camera_pose(mut cam: RootCameraPose) {
}
}
/// The view lane's plugin: [`stamp_near_clip`] and [`publish_camera_pose`].
/// The view lane's plugin: [`stamp_near_clip`], [`stamp_world_fov`] and [`publish_camera_pose`].
pub(crate) struct ViewPlugin;
impl Plugin for ViewPlugin {
@@ -371,6 +397,8 @@ pub(crate) fn plugin(app: &mut App) {
// In `Update`, so this frame's `nearclip` write reaches this frame's projection, as `0x511bc0`
// runs inside the world-frame driver; initialized here too for a harness with only this plugin.
app.init_resource::<ViewDistance>()
.init_resource::<FovZoom>()
.add_systems(Update, stamp_world_fov)
// After the frame's script calls, whose `nearclip` callback writes it.
.add_systems(
Update,
@@ -390,6 +418,60 @@ mod tests {
use super::*;
use bevy::ecs::system::RunSystemOnce;
/// `0x5c3cc0`'s relation: 90° over `√(a² + 1)`.
#[test]
fn the_vertical_fov_follows_the_aspect() {
for (a, deg) in [
(4.0 / 3.0, 54.0),
(16.0 / 10.0, 47.7),
(16.0 / 9.0, 44.1),
(64.0 / 27.0, 35.0),
] {
let got = world_fovy(a).to_degrees();
assert!((got - deg).abs() < 0.06, "aspect {a}: {got}");
}
}
/// A resize re-derives the field of view, and the spyglass scales it.
#[test]
fn the_stamp_rederives_on_a_resize_and_scales_by_the_zoom() {
let mut app = App::new();
app.init_resource::<FovZoom>();
app.add_systems(Update, stamp_world_fov);
let cam = app
.world_mut()
.spawn((
WorldCamera,
Camera {
viewport: Some(bevy::camera::Viewport {
physical_size: UVec2::new(1600, 1200),
..default()
}),
..default()
},
Projection::from(PerspectiveProjection::default()),
))
.id();
let fov = |app: &App| match app.world().get::<Projection>(cam).unwrap() {
Projection::Perspective(p) => p.fov,
_ => unreachable!(),
};
app.update();
assert!((fov(&app) - world_fovy(4.0 / 3.0)).abs() < 1e-6);
app.world_mut()
.get_mut::<Camera>(cam)
.unwrap()
.viewport
.as_mut()
.unwrap()
.physical_size = UVec2::new(1920, 1080);
app.update();
assert!((fov(&app) - world_fovy(16.0 / 9.0)).abs() < 1e-6);
app.insert_resource(FovZoom(1.0 / 6.0));
app.update();
assert!((fov(&app) - world_fovy(16.0 / 9.0) / 6.0).abs() < 1e-6);
}
/// `0x511bc0`'s near stamp reaches the projection, and an unchanged one stays unmarked.
#[test]
fn the_near_plane_follows_the_cvar_and_settles() {
@@ -404,7 +404,7 @@ fn push_precip(
world_per_px: match proj {
Projection::Perspective(p) => (p.fov * 0.5).tan(),
// The reference has no orthographic mode; this keeps the sprites sane.
_ => (crate::view::CAM_FOVY * 0.5).tan(),
_ => (crate::view::world_fovy(16.0 / 9.0) * 0.5).tan(),
} / SNOW_PX_REF_HEIGHT,
});
let spec = |texture: &Handle<Image>, blend: EffectBlend, fog: EffectFog| EffectDrawSpec {
@@ -349,7 +349,7 @@ fn wmo_pin_probe() {
.unwrap_or_else(|_| panic!("{var} wants a number; got {s:?}"))
})
};
let fovy = scalar("WOW_WMO_PIN_FOVY", crate::view::CAM_FOVY);
let fovy = scalar("WOW_WMO_PIN_FOVY", crate::view::world_fovy(16.0 / 9.0));
let aspect = scalar("WOW_WMO_PIN_ASPECT", 16.0 / 9.0);
let clip = Mat4::perspective_rh(fovy, aspect, 0.1, 1000.0)
* Mat4::look_at_rh(eye_bevy, look_bevy, Vec3::Y);
@@ -523,7 +523,7 @@ fn shadowfang_courtyard_leaves_the_far_room_on_the_scene_fog() {
let look_bevy = placed
.world_from_local
.transform_point3(wow_to_bevy(to_local(look_world)));
let clip = Mat4::perspective_rh(crate::view::CAM_FOVY, 16.0 / 9.0, 0.1, 1000.0)
let clip = Mat4::perspective_rh(crate::view::world_fovy(16.0 / 9.0), 16.0 / 9.0, 0.1, 1000.0)
* Mat4::look_at_rh(eye_bevy, look_bevy, Vec3::Y);
let pvs = crate::wmo_portal::compute_pvs_traced(
&subject.model,
@@ -581,8 +581,9 @@ fn shadowfang_courtyard_leaves_the_far_room_on_the_scene_fog() {
let inside_bevy = placed
.world_from_local
.transform_point3(wow_to_bevy(inside));
let inside_clip = Mat4::perspective_rh(crate::view::CAM_FOVY, 16.0 / 9.0, 0.1, 1000.0)
* Mat4::look_at_rh(inside_bevy, eye_bevy, Vec3::Y);
let inside_clip =
Mat4::perspective_rh(crate::view::world_fovy(16.0 / 9.0), 16.0 / 9.0, 0.1, 1000.0)
* Mat4::look_at_rh(inside_bevy, eye_bevy, Vec3::Y);
let inside_pvs = crate::wmo_portal::compute_pvs_traced(
&subject.model,
inside,
@@ -619,7 +620,7 @@ fn shadowfang_sees_true_interiors_that_are_off_the_fog_chain() {
.world_from_local
.transform_point3(wow_to_bevy(inside));
let look_bevy = placed.world_from_local.transform_point3(wow_to_bevy(look));
let clip = Mat4::perspective_rh(crate::view::CAM_FOVY, 16.0 / 9.0, 0.1, 1000.0)
let clip = Mat4::perspective_rh(crate::view::world_fovy(16.0 / 9.0), 16.0 / 9.0, 0.1, 1000.0)
* Mat4::look_at_rh(inside_bevy, look_bevy, Vec3::Y);
let pvs = crate::wmo_portal::compute_pvs_traced(
&subject.model,
@@ -774,8 +775,9 @@ fn darnassus_pass_two_walks_on_from_the_window() {
}
let eye_bevy = placed.world_from_local.transform_point3(wow_to_bevy(eye));
let look_bevy = placed.world_from_local.transform_point3(wow_to_bevy(look));
let clip = Mat4::perspective_rh(crate::view::CAM_FOVY, 16.0 / 9.0, 0.1, 1000.0)
* Mat4::look_at_rh(eye_bevy, look_bevy, Vec3::Y);
let clip =
Mat4::perspective_rh(crate::view::world_fovy(16.0 / 9.0), 16.0 / 9.0, 0.1, 1000.0)
* Mat4::look_at_rh(eye_bevy, look_bevy, Vec3::Y);
let mut tap = WalkOn::default();
let pvs = crate::wmo_portal::compute_pvs_traced(
&subject.model,
+2 -4
View File
@@ -47,9 +47,6 @@ const VIEW_OPEN_Z: f32 = 100.0;
/// Near plane in yards: the `nearclip` CVar's default, since the viewer has no CVar table.
const NEAR: f32 = crate::view::NEARCLIP_DEFAULT;
/// Vertical FOV in radians (70°), wider than the client camera's [`crate::view::CAM_FOVY`].
const FOVY: f32 = 1.221_730_5;
/// Builds and runs the world viewer; `build` is the launcher shim's compile-time git stamp.
pub fn run(build: BuildId) -> AppExit {
// The install resolves from the launcher's folder when its stamp names one, as the client's
@@ -273,7 +270,8 @@ fn spawn_view_camera(mut commands: Commands, msaa: Res<crate::view::MsaaSetting>
Projection::from(PerspectiveProjection {
far,
near: NEAR,
fov: FOVY,
// The client camera's relation, kept current by `view::stamp_world_fov`.
fov: crate::view::world_fovy(16.0 / 9.0),
..default()
}),
Hdr,