Phase 1: sim/lighting + sim/visibility + mesh utilities
New modules:
src/sim/lighting.rs - DAY_PERIOD, SUN_OFFSET constants; sun_direction,
day_strength, twilight_amount, LightingFrame::at;
is_in_direct_sun(world, p, t) via 3D DDA — the
mechanical sunlight predicate that future mob
burn / plant growth / shade pathfinding will all
consume. Mirrors the shader's sun math exactly
(Phase 2 will wire the WGSL side to consume the
same constants from this module).
src/sim/visibility.rs - compute_visible_chunks moved out of state.rs;
frustum_planes + chunk_in_frustum decomposed.
Moves into mesh.rs:
emit_oriented_box, name_hash + their tests. These are mesh utilities
(Vertex output, color hash for remote-player boxes), not GPU shell.
state.rs:
- drops the moved functions
- imports compute_visible_chunks / emit_oriented_box / name_hash from
their new homes
- 230 lines lighter
Tests: 51 passing (up from 45). New coverage: sun-below-horizon=>night,
open-sky-at-noon-is-lit, occluded-by-overhead-block, ramp shapes match
the WGSL smoothstep, name_hash determinism.
This commit is contained in:
parent
c6f50bcb50
commit
989de4f43d
5 changed files with 447 additions and 204 deletions
153
src/mesh.rs
153
src/mesh.rs
|
|
@ -1,6 +1,6 @@
|
|||
use crate::world::{Block, Chunk, Face, World, CHUNK_HEIGHT, CHUNK_SIZE};
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use glam::IVec3;
|
||||
use glam::{IVec3, Vec3};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Pod, Zeroable, Debug)]
|
||||
|
|
@ -267,6 +267,99 @@ pub fn build_chunk_mesh(world: &World, chunk: &Chunk) -> ChunkMesh {
|
|||
ChunkMesh { vertices, indices }
|
||||
}
|
||||
|
||||
/// Emit 6 faces of a box centered at `center`, rotated around Y by
|
||||
/// `yaw`, extending `half_extents` in each local-space axis. Each face
|
||||
/// winds CCW outward — verified by `oriented_box_winds_correctly`.
|
||||
///
|
||||
/// Used by the renderer to draw remote players (body + head boxes).
|
||||
/// Lives in `mesh.rs` because the output is `Vertex`es with normals and
|
||||
/// AO, same as the chunk meshes.
|
||||
pub fn emit_oriented_box(
|
||||
center: Vec3,
|
||||
half_extents: Vec3,
|
||||
yaw: f32,
|
||||
color: [f32; 3],
|
||||
verts: &mut Vec<Vertex>,
|
||||
indices: &mut Vec<u32>,
|
||||
) {
|
||||
let cos_y = yaw.cos();
|
||||
let sin_y = yaw.sin();
|
||||
let rotate_xz = |x: f32, z: f32| (x * cos_y - z * sin_y, x * sin_y + z * cos_y);
|
||||
let world_pt = |lx: f32, ly: f32, lz: f32| {
|
||||
let (rx, rz) = rotate_xz(lx, lz);
|
||||
[center.x + rx, center.y + ly, center.z + rz]
|
||||
};
|
||||
let world_normal = |nx: f32, ny: f32, nz: f32| {
|
||||
let (rx, rz) = rotate_xz(nx, nz);
|
||||
[rx, ny, rz]
|
||||
};
|
||||
|
||||
let (hx, hy, hz) = (half_extents.x, half_extents.y, half_extents.z);
|
||||
let faces: [([f32; 3], [[f32; 3]; 4]); 6] = [
|
||||
([1.0, 0.0, 0.0], [
|
||||
[ hx, -hy, -hz],
|
||||
[ hx, -hy, hz],
|
||||
[ hx, hy, hz],
|
||||
[ hx, hy, -hz],
|
||||
]),
|
||||
([-1.0, 0.0, 0.0], [
|
||||
[-hx, -hy, hz],
|
||||
[-hx, -hy, -hz],
|
||||
[-hx, hy, -hz],
|
||||
[-hx, hy, hz],
|
||||
]),
|
||||
([0.0, 1.0, 0.0], [
|
||||
[-hx, hy, -hz],
|
||||
[ hx, hy, -hz],
|
||||
[ hx, hy, hz],
|
||||
[-hx, hy, hz],
|
||||
]),
|
||||
([0.0, -1.0, 0.0], [
|
||||
[-hx, -hy, hz],
|
||||
[ hx, -hy, hz],
|
||||
[ hx, -hy, -hz],
|
||||
[-hx, -hy, -hz],
|
||||
]),
|
||||
([0.0, 0.0, 1.0], [
|
||||
[ hx, -hy, hz],
|
||||
[-hx, -hy, hz],
|
||||
[-hx, hy, hz],
|
||||
[ hx, hy, hz],
|
||||
]),
|
||||
([0.0, 0.0, -1.0], [
|
||||
[-hx, -hy, -hz],
|
||||
[ hx, -hy, -hz],
|
||||
[ hx, hy, -hz],
|
||||
[-hx, hy, -hz],
|
||||
]),
|
||||
];
|
||||
for (n_local, corners_local) in faces {
|
||||
let n_world = world_normal(n_local[0], n_local[1], n_local[2]);
|
||||
let base = verts.len() as u32;
|
||||
for c in corners_local {
|
||||
verts.push(Vertex {
|
||||
pos: world_pt(c[0], c[1], c[2]),
|
||||
color,
|
||||
normal: n_world,
|
||||
leaf: 0.0,
|
||||
ao: 1.0,
|
||||
});
|
||||
}
|
||||
indices.extend_from_slice(&[base, base + 2, base + 1, base, base + 3, base + 2]);
|
||||
}
|
||||
}
|
||||
|
||||
/// FNV-1a 32-bit hash of a string. Used to colorize remote-player
|
||||
/// boxes deterministically by display name.
|
||||
pub fn name_hash(s: &str) -> u32 {
|
||||
let mut h: u32 = 2166136261;
|
||||
for b in s.bytes() {
|
||||
h ^= b as u32;
|
||||
h = h.wrapping_mul(16777619);
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -423,4 +516,62 @@ mod tests {
|
|||
assert!(CHUNK_SIZE > 0);
|
||||
assert!(CHUNK_HEIGHT > 0);
|
||||
}
|
||||
|
||||
// ---------- emit_oriented_box ----------
|
||||
|
||||
#[test]
|
||||
fn oriented_box_emits_six_quads() {
|
||||
let mut v = vec![];
|
||||
let mut i = vec![];
|
||||
emit_oriented_box(Vec3::ZERO, Vec3::splat(0.5), 0.0, [1.0; 3], &mut v, &mut i);
|
||||
assert_eq!(v.len(), 24);
|
||||
assert_eq!(i.len(), 36);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oriented_box_winds_correctly_at_any_yaw() {
|
||||
for &yaw in &[0.0_f32, 0.7, 1.5708, 3.14, -1.0, 5.0] {
|
||||
let mut v = vec![];
|
||||
let mut i = vec![];
|
||||
emit_oriented_box(
|
||||
Vec3::new(5.0, 7.0, -3.0),
|
||||
Vec3::new(0.3, 0.6, 0.2),
|
||||
yaw,
|
||||
[1.0; 3],
|
||||
&mut v,
|
||||
&mut i,
|
||||
);
|
||||
for tri in i.chunks_exact(3) {
|
||||
let a = v[tri[0] as usize].pos;
|
||||
let b = v[tri[1] as usize].pos;
|
||||
let c = v[tri[2] as usize].pos;
|
||||
let n = v[tri[0] as usize].normal;
|
||||
let geo = cross_normal(a, b, c);
|
||||
let dot = geo[0] * n[0] + geo[1] * n[1] + geo[2] * n[2];
|
||||
assert!(dot > 0.0, "yaw {} tri winds opposite normal {:?}", yaw, n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oriented_box_normal_rotates_with_yaw() {
|
||||
let mut v = vec![];
|
||||
let mut i = vec![];
|
||||
let yaw = std::f32::consts::FRAC_PI_2;
|
||||
emit_oriented_box(Vec3::ZERO, Vec3::splat(0.5), yaw, [1.0; 3], &mut v, &mut i);
|
||||
let n = v[0].normal;
|
||||
assert!(
|
||||
(n[0]).abs() < 1e-5 && (n[2] - 1.0).abs() < 1e-5,
|
||||
"+X normal at yaw 90° expected (0,0,1), got {:?}",
|
||||
n
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn name_hash_is_deterministic_and_distinct() {
|
||||
let a = name_hash("alice");
|
||||
let b = name_hash("bob");
|
||||
assert_eq!(a, name_hash("alice"));
|
||||
assert_ne!(a, b);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
228
src/sim/lighting.rs
Normal file
228
src/sim/lighting.rs
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
//! Canonical sun/sky/lighting math — the single source of truth shared
|
||||
//! between the renderer (visual shading) and game systems (mob burn,
|
||||
//! plant growth, AI pathfinding around shade).
|
||||
//!
|
||||
//! The WGSL shader gets the same constants via runtime concatenation
|
||||
//! (see `crate::render::shader_source`), so changing `DAY_PERIOD` here
|
||||
//! updates the shader too. This is the architectural guarantee that
|
||||
//! prevents the "mob burns while standing in visible shade" class of
|
||||
//! bug — visual and mechanical sun direction can never drift.
|
||||
use crate::world::World;
|
||||
use glam::{IVec3, Vec3};
|
||||
|
||||
/// One in-game day in real seconds. Multiplied by the user's
|
||||
/// `time_scale` setting before being summed each tick.
|
||||
pub const DAY_PERIOD: f32 = 300.0;
|
||||
/// Phase offset so a fresh world starts at noon (0.25 cycles past dawn).
|
||||
pub const SUN_OFFSET: f32 = 0.25;
|
||||
/// Y above which the sun-occlusion ray is considered to have escaped
|
||||
/// the world (open sky). Must be ≥ `world::CHUNK_HEIGHT`.
|
||||
pub const SUN_RAY_TOP_Y: i32 = 128;
|
||||
/// Hard cap on DDA steps so a degenerate near-horizontal ray can't loop
|
||||
/// forever. 512 voxels is well past any reasonable world width.
|
||||
const SUN_RAY_MAX_STEPS: u32 = 512;
|
||||
|
||||
/// World-space sun direction at time `t`. Matches the WGSL
|
||||
/// `sun_direction(t)` bit for bit — both consume the same `DAY_PERIOD`
|
||||
/// and `SUN_OFFSET` (the WGSL constants are emitted from these via
|
||||
/// `render::shader_source::wgsl_constants_header()`).
|
||||
pub fn sun_direction(t: f32) -> Vec3 {
|
||||
let a = (t / DAY_PERIOD + SUN_OFFSET) * std::f32::consts::TAU;
|
||||
Vec3::new(a.cos(), a.sin(), 0.25).normalize()
|
||||
}
|
||||
|
||||
/// 0..1 going from "sun barely under horizon (blue hour)" to "clearly
|
||||
/// above horizon (full daylight)". Same formula as the shader's
|
||||
/// `day_strength`.
|
||||
pub fn day_strength(sun: Vec3) -> f32 {
|
||||
smoothstep(-0.05, 0.20, sun.y)
|
||||
}
|
||||
|
||||
/// Peaks while the sun is near the horizon — sunrise + sunset.
|
||||
pub fn twilight_amount(sun: Vec3) -> f32 {
|
||||
smoothstep(-0.10, 0.05, sun.y) - smoothstep(0.05, 0.30, sun.y)
|
||||
}
|
||||
|
||||
/// All four lighting scalars derived once for a tick. Cheaper than
|
||||
/// recomputing each piece, and gives downstream systems a single value
|
||||
/// to pass around.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct LightingFrame {
|
||||
pub time: f32,
|
||||
pub sun: Vec3,
|
||||
pub day: f32,
|
||||
pub twilight: f32,
|
||||
}
|
||||
|
||||
impl LightingFrame {
|
||||
pub fn at(t: f32) -> Self {
|
||||
let sun = sun_direction(t);
|
||||
Self {
|
||||
time: t,
|
||||
sun,
|
||||
day: day_strength(sun),
|
||||
twilight: twilight_amount(sun),
|
||||
}
|
||||
}
|
||||
|
||||
/// Mechanically night: nothing burns, plants don't grow, AI is
|
||||
/// free to wander in the open. True iff the sun is below the
|
||||
/// horizon.
|
||||
pub fn is_night(&self) -> bool {
|
||||
self.sun.y <= 0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Is point `p` directly lit by the sun in `world` at time `t`?
|
||||
///
|
||||
/// 3D DDA from `p` toward the sun until the ray either escapes through
|
||||
/// the world top (lit) or hits a solid voxel (occluded). The
|
||||
/// mechanical sunlight test — consumed by mob burn checks, plant
|
||||
/// growth, daylight sensors, shade pathfinding. Visual shadows in the
|
||||
/// shader should approximate the *same* predicate.
|
||||
pub fn is_in_direct_sun(world: &World, p: Vec3, t: f32) -> bool {
|
||||
let sun = sun_direction(t);
|
||||
if sun.y <= 0.0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// DDA setup. Walking from `p` along `sun` through unit voxels.
|
||||
let mut cell = IVec3::new(
|
||||
p.x.floor() as i32,
|
||||
p.y.floor() as i32,
|
||||
p.z.floor() as i32,
|
||||
);
|
||||
let step = IVec3::new(
|
||||
sign_to_step(sun.x),
|
||||
sign_to_step(sun.y),
|
||||
sign_to_step(sun.z),
|
||||
);
|
||||
let t_delta = Vec3::new(
|
||||
axis_t_delta(sun.x),
|
||||
axis_t_delta(sun.y),
|
||||
axis_t_delta(sun.z),
|
||||
);
|
||||
let mut t_max = Vec3::new(
|
||||
first_t_max(p.x, sun.x, cell.x),
|
||||
first_t_max(p.y, sun.y, cell.y),
|
||||
first_t_max(p.z, sun.z, cell.z),
|
||||
);
|
||||
|
||||
for _ in 0..SUN_RAY_MAX_STEPS {
|
||||
if cell.y >= SUN_RAY_TOP_Y {
|
||||
return true;
|
||||
}
|
||||
if t_max.x < t_max.y && t_max.x < t_max.z {
|
||||
cell.x += step.x;
|
||||
t_max.x += t_delta.x;
|
||||
} else if t_max.y < t_max.z {
|
||||
cell.y += step.y;
|
||||
t_max.y += t_delta.y;
|
||||
} else {
|
||||
cell.z += step.z;
|
||||
t_max.z += t_delta.z;
|
||||
}
|
||||
if world.get_block(cell).solid() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Ran out of steps — treat as open sky (the alternative is "hangs
|
||||
// forever on degenerate rays", which is worse).
|
||||
true
|
||||
}
|
||||
|
||||
fn sign_to_step(v: f32) -> i32 {
|
||||
if v > 0.0 {
|
||||
1
|
||||
} else if v < 0.0 {
|
||||
-1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fn axis_t_delta(v: f32) -> f32 {
|
||||
if v.abs() < 1e-6 {
|
||||
f32::INFINITY
|
||||
} else {
|
||||
1.0 / v.abs()
|
||||
}
|
||||
}
|
||||
|
||||
fn first_t_max(origin: f32, dir: f32, cell: i32) -> f32 {
|
||||
if dir.abs() < 1e-6 {
|
||||
f32::INFINITY
|
||||
} else if dir > 0.0 {
|
||||
((cell + 1) as f32 - origin) / dir
|
||||
} else {
|
||||
(origin - cell as f32) / -dir
|
||||
}
|
||||
}
|
||||
|
||||
fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
|
||||
let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
|
||||
t * t * (3.0 - 2.0 * t)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::world::{natural_surface_y, Block, World};
|
||||
|
||||
#[test]
|
||||
fn sun_below_horizon_means_night_and_no_direct_sun() {
|
||||
// Midnight = half a day past noon (t such that sun.y < 0).
|
||||
let t = DAY_PERIOD * 0.5;
|
||||
let lf = LightingFrame::at(t);
|
||||
assert!(lf.is_night(), "midnight should report as night, sun={:?}", lf.sun);
|
||||
let world = World::new();
|
||||
assert!(!is_in_direct_sun(
|
||||
&world,
|
||||
Vec3::new(0.5, 200.0, 0.5),
|
||||
t
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_sky_above_terrain_is_lit_at_noon() {
|
||||
let world = World::new();
|
||||
let lf = LightingFrame::at(0.0); // game starts at noon
|
||||
assert!(!lf.is_night());
|
||||
assert!(lf.sun.y > 0.0, "noon sun must be above horizon");
|
||||
let high_above = Vec3::new(0.5, 200.0, 0.5);
|
||||
assert!(is_in_direct_sun(&world, high_above, 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_under_solid_overhead_is_occluded() {
|
||||
// Place a stone block right above (0.5, surface+1, 0.5) and
|
||||
// check that the player feet position below it is shaded.
|
||||
let mut world = World::new();
|
||||
let surface = natural_surface_y(0, 0);
|
||||
let feet_y = surface + 1;
|
||||
// Cap the column with stones so any upward ray hits one.
|
||||
for y in (feet_y + 2)..(feet_y + 12) {
|
||||
world.set_block(IVec3::new(0, y, 0), Block::Stone);
|
||||
}
|
||||
let p = Vec3::new(0.5, feet_y as f32 + 0.1, 0.5);
|
||||
assert!(!is_in_direct_sun(&world, p, 0.0), "covered point must be occluded");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn day_strength_and_twilight_match_shader_ramps() {
|
||||
// Spot-checks at known sun heights.
|
||||
assert!(day_strength(Vec3::new(0.0, -0.5, 0.0)).abs() < 1e-5);
|
||||
assert!((day_strength(Vec3::new(0.0, 1.0, 0.0)) - 1.0).abs() < 1e-5);
|
||||
assert!(twilight_amount(Vec3::new(0.0, 0.05, 0.0)) > 0.5);
|
||||
assert!(twilight_amount(Vec3::new(0.0, 1.0, 0.0)).abs() < 1e-5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sun_direction_constants_round_trip() {
|
||||
// At t=0 (noon by construction) the angle is SUN_OFFSET turns,
|
||||
// i.e. sun is at cos(τ·0.25) = 0, sin(τ·0.25) = 1 → straight up
|
||||
// (plus the small Z tilt). The Y should be the dominant axis.
|
||||
let s = sun_direction(0.0);
|
||||
assert!(s.y > 0.9, "noon sun y should be ~1, got {}", s.y);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,10 +18,14 @@ pub mod collision;
|
|||
pub mod edit;
|
||||
pub mod event;
|
||||
pub mod input;
|
||||
pub mod lighting;
|
||||
pub mod physics;
|
||||
pub mod spawn;
|
||||
pub mod visibility;
|
||||
|
||||
pub use body::PlayerBody;
|
||||
pub use event::SimEvent;
|
||||
pub use input::{merge_held, Input, TouchBridge};
|
||||
pub use lighting::{is_in_direct_sun, sun_direction, LightingFrame};
|
||||
pub use physics::{step_movement, MoveInput, MoveOutcome};
|
||||
pub use visibility::compute_visible_chunks;
|
||||
|
|
|
|||
60
src/sim/visibility.rs
Normal file
60
src/sim/visibility.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
//! Frustum + radial culling for chunk visibility. Pure function on
|
||||
//! `(World, Camera, render_dist) -> Vec<ChunkCoord>`.
|
||||
use crate::camera::Camera;
|
||||
use crate::world::{World, CHUNK_HEIGHT, CHUNK_SIZE};
|
||||
use glam::{IVec3, Vec3};
|
||||
|
||||
/// Returns the chunk coordinates whose AABB intersects the camera's
|
||||
/// view frustum AND whose center is within `render_dist` on XZ. The
|
||||
/// cheap radial cull runs first — frustum tests are skipped for chunks
|
||||
/// already beyond the configured render distance.
|
||||
pub fn compute_visible_chunks(world: &World, camera: &Camera, render_dist: f32) -> Vec<IVec3> {
|
||||
let planes = frustum_planes(camera);
|
||||
let dist2 = render_dist * render_dist;
|
||||
let cam_xz = (camera.position.x, camera.position.z);
|
||||
|
||||
world
|
||||
.chunks
|
||||
.keys()
|
||||
.filter(|coord| {
|
||||
let cx = (coord.x * CHUNK_SIZE + CHUNK_SIZE / 2) as f32;
|
||||
let cz = (coord.z * CHUNK_SIZE + CHUNK_SIZE / 2) as f32;
|
||||
let dx = cx - cam_xz.0;
|
||||
let dz = cz - cam_xz.1;
|
||||
dx * dx + dz * dz <= dist2
|
||||
})
|
||||
.filter(|coord| chunk_in_frustum(**coord, &planes))
|
||||
.copied()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn frustum_planes(camera: &Camera) -> [[f32; 4]; 6] {
|
||||
let m = camera.view_proj().to_cols_array_2d();
|
||||
let row = |i: usize| [m[0][i], m[1][i], m[2][i], m[3][i]];
|
||||
let r0 = row(0);
|
||||
let r1 = row(1);
|
||||
let r2 = row(2);
|
||||
let r3 = row(3);
|
||||
let add = |a: [f32; 4], b: [f32; 4]| [a[0] + b[0], a[1] + b[1], a[2] + b[2], a[3] + b[3]];
|
||||
let sub = |a: [f32; 4], b: [f32; 4]| [a[0] - b[0], a[1] - b[1], a[2] - b[2], a[3] - b[3]];
|
||||
[add(r3, r0), sub(r3, r0), add(r3, r1), sub(r3, r1), add(r3, r2), sub(r3, r2)]
|
||||
}
|
||||
|
||||
fn chunk_in_frustum(coord: IVec3, planes: &[[f32; 4]; 6]) -> bool {
|
||||
let min = Vec3::new(
|
||||
(coord.x * CHUNK_SIZE) as f32,
|
||||
0.0,
|
||||
(coord.z * CHUNK_SIZE) as f32,
|
||||
);
|
||||
let max = min + Vec3::new(CHUNK_SIZE as f32, CHUNK_HEIGHT as f32, CHUNK_SIZE as f32);
|
||||
for p in planes {
|
||||
// p-vertex (the AABB corner furthest along plane normal).
|
||||
let px = if p[0] > 0.0 { max.x } else { min.x };
|
||||
let py = if p[1] > 0.0 { max.y } else { min.y };
|
||||
let pz = if p[2] > 0.0 { max.z } else { min.z };
|
||||
if p[0] * px + p[1] * py + p[2] * pz + p[3] < 0.0 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
206
src/state.rs
206
src/state.rs
|
|
@ -3,7 +3,7 @@
|
|||
//! (`crate::sim`) and pure net parser (`crate::net`) through the
|
||||
//! world/renderer/network — this file should remain *thin*.
|
||||
use crate::camera::{Camera, InputState, KbHeld};
|
||||
use crate::mesh::{build_chunk_mesh, Vertex};
|
||||
use crate::mesh::{build_chunk_mesh, emit_oriented_box, name_hash, Vertex};
|
||||
use crate::net::{parse_inbox, NetEvent};
|
||||
use crate::proto::{ClientMsg, EditRec};
|
||||
use crate::sim::collision::{aabb_overlap_player, AabbI, EYE_HEIGHT};
|
||||
|
|
@ -11,8 +11,9 @@ use crate::sim::edit::{apply_edit, block_from_u8, chunks_for_edit};
|
|||
use crate::sim::input::TouchBridge;
|
||||
use crate::sim::physics::MoveInput;
|
||||
use crate::sim::spawn::{fall_damage, find_safe_spawn};
|
||||
use crate::sim::visibility::compute_visible_chunks;
|
||||
use crate::sim::{merge_held, step_movement, PlayerBody, SimEvent};
|
||||
use crate::world::{Block, World, CHUNK_HEIGHT, CHUNK_SIZE, WORLD_RADIUS};
|
||||
use crate::world::{Block, World, WORLD_RADIUS};
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use glam::{IVec3, Mat4, Vec3};
|
||||
use std::cell::RefCell;
|
||||
|
|
@ -1742,204 +1743,3 @@ fn html_escape(s: &str) -> String {
|
|||
.replace('>', ">")
|
||||
}
|
||||
|
||||
fn name_hash(s: &str) -> u32 {
|
||||
let mut h: u32 = 2166136261;
|
||||
for b in s.bytes() {
|
||||
h ^= b as u32;
|
||||
h = h.wrapping_mul(16777619);
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
/// Emit 6 faces of a box centered at `center`, rotated around Y by
|
||||
/// `yaw`, extending `half_extents` in each local-space axis. Each face
|
||||
/// still winds CCW outward (verified by `oriented_box_winds_correctly`).
|
||||
pub fn emit_oriented_box(
|
||||
center: Vec3,
|
||||
half_extents: Vec3,
|
||||
yaw: f32,
|
||||
color: [f32; 3],
|
||||
verts: &mut Vec<Vertex>,
|
||||
indices: &mut Vec<u32>,
|
||||
) {
|
||||
let cos_y = yaw.cos();
|
||||
let sin_y = yaw.sin();
|
||||
let rotate_xz = |x: f32, z: f32| (x * cos_y - z * sin_y, x * sin_y + z * cos_y);
|
||||
let world_pt = |lx: f32, ly: f32, lz: f32| {
|
||||
let (rx, rz) = rotate_xz(lx, lz);
|
||||
[center.x + rx, center.y + ly, center.z + rz]
|
||||
};
|
||||
let world_normal = |nx: f32, ny: f32, nz: f32| {
|
||||
let (rx, rz) = rotate_xz(nx, nz);
|
||||
[rx, ny, rz]
|
||||
};
|
||||
|
||||
let (hx, hy, hz) = (half_extents.x, half_extents.y, half_extents.z);
|
||||
let faces: [([f32; 3], [[f32; 3]; 4]); 6] = [
|
||||
([1.0, 0.0, 0.0], [
|
||||
[ hx, -hy, -hz],
|
||||
[ hx, -hy, hz],
|
||||
[ hx, hy, hz],
|
||||
[ hx, hy, -hz],
|
||||
]),
|
||||
([-1.0, 0.0, 0.0], [
|
||||
[-hx, -hy, hz],
|
||||
[-hx, -hy, -hz],
|
||||
[-hx, hy, -hz],
|
||||
[-hx, hy, hz],
|
||||
]),
|
||||
([0.0, 1.0, 0.0], [
|
||||
[-hx, hy, -hz],
|
||||
[ hx, hy, -hz],
|
||||
[ hx, hy, hz],
|
||||
[-hx, hy, hz],
|
||||
]),
|
||||
([0.0, -1.0, 0.0], [
|
||||
[-hx, -hy, hz],
|
||||
[ hx, -hy, hz],
|
||||
[ hx, -hy, -hz],
|
||||
[-hx, -hy, -hz],
|
||||
]),
|
||||
([0.0, 0.0, 1.0], [
|
||||
[ hx, -hy, hz],
|
||||
[-hx, -hy, hz],
|
||||
[-hx, hy, hz],
|
||||
[ hx, hy, hz],
|
||||
]),
|
||||
([0.0, 0.0, -1.0], [
|
||||
[-hx, -hy, -hz],
|
||||
[ hx, -hy, -hz],
|
||||
[ hx, hy, -hz],
|
||||
[-hx, hy, -hz],
|
||||
]),
|
||||
];
|
||||
for (n_local, corners_local) in faces {
|
||||
let n_world = world_normal(n_local[0], n_local[1], n_local[2]);
|
||||
let base = verts.len() as u32;
|
||||
for c in corners_local {
|
||||
verts.push(Vertex {
|
||||
pos: world_pt(c[0], c[1], c[2]),
|
||||
color,
|
||||
normal: n_world,
|
||||
leaf: 0.0,
|
||||
ao: 1.0,
|
||||
});
|
||||
}
|
||||
indices.extend_from_slice(&[base, base + 2, base + 1, base, base + 3, base + 2]);
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_visible_chunks(world: &World, camera: &Camera, render_dist: f32) -> Vec<IVec3> {
|
||||
let vp = camera.view_proj();
|
||||
let m = vp.to_cols_array_2d();
|
||||
let row = |i: usize| [m[0][i], m[1][i], m[2][i], m[3][i]];
|
||||
let r0 = row(0);
|
||||
let r1 = row(1);
|
||||
let r2 = row(2);
|
||||
let r3 = row(3);
|
||||
let add = |a: [f32; 4], b: [f32; 4]| [a[0] + b[0], a[1] + b[1], a[2] + b[2], a[3] + b[3]];
|
||||
let sub = |a: [f32; 4], b: [f32; 4]| [a[0] - b[0], a[1] - b[1], a[2] - b[2], a[3] - b[3]];
|
||||
let planes: [[f32; 4]; 6] = [
|
||||
add(r3, r0),
|
||||
sub(r3, r0),
|
||||
add(r3, r1),
|
||||
sub(r3, r1),
|
||||
add(r3, r2),
|
||||
sub(r3, r2),
|
||||
];
|
||||
let dist2 = render_dist * render_dist;
|
||||
let cam_xz = (camera.position.x, camera.position.z);
|
||||
let mut out = Vec::with_capacity(world.chunks.len());
|
||||
for coord in world.chunks.keys() {
|
||||
let cx = (coord.x * CHUNK_SIZE + CHUNK_SIZE / 2) as f32;
|
||||
let cz = (coord.z * CHUNK_SIZE + CHUNK_SIZE / 2) as f32;
|
||||
let dx = cx - cam_xz.0;
|
||||
let dz = cz - cam_xz.1;
|
||||
if dx * dx + dz * dz > dist2 {
|
||||
continue;
|
||||
}
|
||||
let min = Vec3::new(
|
||||
(coord.x * CHUNK_SIZE) as f32,
|
||||
0.0,
|
||||
(coord.z * CHUNK_SIZE) as f32,
|
||||
);
|
||||
let max = min + Vec3::new(CHUNK_SIZE as f32, CHUNK_HEIGHT as f32, CHUNK_SIZE as f32);
|
||||
let mut inside = true;
|
||||
for p in &planes {
|
||||
let px = if p[0] > 0.0 { max.x } else { min.x };
|
||||
let py = if p[1] > 0.0 { max.y } else { min.y };
|
||||
let pz = if p[2] > 0.0 { max.z } else { min.z };
|
||||
if p[0] * px + p[1] * py + p[2] * pz + p[3] < 0.0 {
|
||||
inside = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if inside {
|
||||
out.push(*coord);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn cross_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] {
|
||||
let u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
||||
let v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
||||
[
|
||||
u[1] * v[2] - u[2] * v[1],
|
||||
u[2] * v[0] - u[0] * v[2],
|
||||
u[0] * v[1] - u[1] * v[0],
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oriented_box_emits_six_quads() {
|
||||
let mut v = vec![];
|
||||
let mut i = vec![];
|
||||
emit_oriented_box(Vec3::ZERO, Vec3::splat(0.5), 0.0, [1.0; 3], &mut v, &mut i);
|
||||
assert_eq!(v.len(), 24);
|
||||
assert_eq!(i.len(), 36);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oriented_box_winds_correctly_at_any_yaw() {
|
||||
for &yaw in &[0.0_f32, 0.7, 1.5708, 3.14, -1.0, 5.0] {
|
||||
let mut v = vec![];
|
||||
let mut i = vec![];
|
||||
emit_oriented_box(
|
||||
Vec3::new(5.0, 7.0, -3.0),
|
||||
Vec3::new(0.3, 0.6, 0.2),
|
||||
yaw,
|
||||
[1.0; 3],
|
||||
&mut v,
|
||||
&mut i,
|
||||
);
|
||||
for tri in i.chunks_exact(3) {
|
||||
let a = v[tri[0] as usize].pos;
|
||||
let b = v[tri[1] as usize].pos;
|
||||
let c = v[tri[2] as usize].pos;
|
||||
let n = v[tri[0] as usize].normal;
|
||||
let geo = cross_normal(a, b, c);
|
||||
let dot = geo[0] * n[0] + geo[1] * n[1] + geo[2] * n[2];
|
||||
assert!(dot > 0.0, "yaw {} tri winds opposite normal {:?}", yaw, n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oriented_box_normal_rotates_with_yaw() {
|
||||
let mut v = vec![];
|
||||
let mut i = vec![];
|
||||
let yaw = std::f32::consts::FRAC_PI_2;
|
||||
emit_oriented_box(Vec3::ZERO, Vec3::splat(0.5), yaw, [1.0; 3], &mut v, &mut i);
|
||||
let n = v[0].normal;
|
||||
assert!(
|
||||
(n[0]).abs() < 1e-5 && (n[2] - 1.0).abs() < 1e-5,
|
||||
"+X normal at yaw 90° expected (0,0,1), got {:?}",
|
||||
n
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue