diff --git a/crates/engine/assets/shaders/world.wgsl b/crates/engine/assets/shaders/world.wgsl index 85fb0729..94117fda 100644 --- a/crates/engine/assets/shaders/world.wgsl +++ b/crates/engine/assets/shaders/world.wgsl @@ -353,9 +353,7 @@ fn vs_lod(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> V if a.x + a.z < a.y + a.w { c = (c + 1u) % 4u; } - let lo = lod_pos(w0, w2) - vec3(0.0, 0.125, 0.0); - let size = lod_size(w2, w3); - let local = lo + CORNERS[dir * 4u + c] * size; + let local = lod_pos(w0, w2) + lod_corner(w0, w2, w3, CORNERS[dir * 4u + c]) - vec3(0.0, 0.125, 0.0); let pos = finish(local, origins[ii]); var out: VOut; out.clip = scene.view_proj * pos; @@ -372,7 +370,7 @@ fn vs_lod(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> V out.blend = fr.blend; out.glow = 0xffffffffu; out.slot = 0u; - out.light = vec4(1.0, 0.0, 0.0, 0.0); + out.light = vec4(1.0, lod_light(w0, w2, w3)); let kind = lod_tint(w0); var tint = vec3(1.0); if kind != 0u { diff --git a/crates/engine/src/client/lod.rs b/crates/engine/src/client/lod.rs index 95a0f918..45260b95 100644 --- a/crates/engine/src/client/lod.rs +++ b/crates/engine/src/client/lod.rs @@ -3,7 +3,8 @@ //! kilobytes a tile. A tile is 32x32 cells of 2^level blocks; each cell is the column at its //! middle: its top block, the one under it, its biome, and tree canopies over it. Where saved //! cubes made a column's top other than the terrain's (a town, a crater), the server says so -//! (`server::lod_tops`) and that column is taken instead. +//! (`server::lod_tops`) and that column is taken instead. What stands above the ground is drawn +//! as its own spans, so a building keeps its roof and its walls. //! Tiles are picked by distance, nearer ones split into the four of the level below; one is //! shown once made, its parent or children standing in until then. Where a column of real cubes //! shows its ground the tiles' pixels are dropped (`fs_lod`, `Lod::mask`), so they fill in for @@ -14,18 +15,23 @@ use std::sync::Arc; use crossbeam_channel::{Receiver, Sender, unbounded}; use glam::{DVec3, IVec2}; use rustc_hash::{FxHashMap, FxHashSet}; +use weft_abi::{BlockModel, FACE_DIRS}; -/// Columns whose top isn't the terrain's: height, top block, the one under it. -pub type Columns = FxHashMap; +use crate::background; +use crate::config; +use crate::content::{Content, StateInfo, content, face_paint}; +use crate::gpu_layout::terrain::lod as lod_gpu; +use crate::gpu_layout::{CORNERS, face, lod_box, lod_record}; +use crate::server::lod_tops::{LodColumn, Span, shows}; -use crate::content::{Content, content}; -use crate::gpu_layout::face; +/// Columns that differ from the terrain, as the server said. +pub type Columns = FxHashMap; /// Cells a tile side. pub const CELLS: i32 = 32; /// Columns a tile is made from: its cells and one around them (for the walls at its edges). -pub const SAMPLES: usize = crate::gpu_layout::terrain::lod::SAMPLES as usize; -const SIDE: i32 = crate::gpu_layout::terrain::lod::SIDE as i32; +pub const SAMPLES: usize = lod_gpu::SAMPLES as usize; +const SIDE: i32 = lod_gpu::SIDE as i32; const GRID: usize = (CELLS + 2) as usize; /// Words per tile coming back from the GPU. pub const LOD_WORDS_PER_TILE: usize = SAMPLES * 2; @@ -37,6 +43,10 @@ const SPLIT: f64 = 4.0; const UNDERGROUND: i32 = 32; /// Tiles asked for and not back yet, at most. const IN_FLIGHT: usize = 256; +/// Quads a tile may hold. Past this the index buffer does not cover the draw. +const MAX_QUADS: usize = 48_000; +/// More model boxes than this become one box around all of them. +const MAX_MODEL_BOXES: usize = 6; /// A tile: its level and place in the grid of its level's tiles. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] @@ -96,19 +106,41 @@ pub struct LodMesh { /// What came back for `job`, meshed on a worker. pub fn made(job: LodJob, mut words: Vec) { - crate::background().spawn(move || { + background().spawn(move || { if !job.columns.is_empty() { + let c = content(); + let def = &c.dims[job.dim as usize]; let (s, o) = (1 << job.key.level, job.key.origin()); + // Cells something stands in: the tree cover the terrain puts there is gone. + let built: FxHashSet<(i32, i32)> = job + .columns + .iter() + .filter(|(_, col)| !col.spans.is_empty()) + .map(|(p, _)| ((p.x - o.x).div_euclid(s), (p.y - o.y).div_euclid(s))) + .collect(); for k in 0..SAMPLES { let (i, j) = ((k as i32 % SIDE) - 1, (k as i32 / SIDE) - 1); - let col = o + IVec2::new(i * s + s / 2, j * s + s / 2); - if let Some(&(y, top, under)) = job.columns.get(&col) { + let at = o + IVec2::new(i * s + s / 2, j * s + s / 2); + let (y, top, under) = ((words[k * 2] & 0xFFFF) as u16 as i16 as i32, words[k * 2] >> 16, words[k * 2 + 1] >> 16); + let ground = match job.columns.get(&at) { + // One saved column must not repaint a coarse cell: a patch of dirt the width + // of the cell. Up close a cell is a dug column's width. + Some(col) if s <= 2 && col.top != 0 => Some((col.y, col.top, col.under)), + _ if built.contains(&(i, j)) && top == under && !shows(c, top) => def + .column(job.seed, at.x, at.y) + .filter(|t| t.height < y && def.sea_level().is_none_or(|sea| t.height >= sea)) + .map(|t| (t.height, def.surface_block(job.seed, at.x, at.y))) + .filter(|g| g.1 != 0) + .map(|(y, top)| (y, top, top)), + _ => None, + }; + if let Some((y, top, under)) = ground { words[k * 2] = (y as u32 & 0xFFFF) | (top << 16); words[k * 2 + 1] = (words[k * 2 + 1] & 0xFFFF) | (under << 16); } } } - let (quads, y) = mesh(content(), job.key, &words); + let (quads, y) = mesh(content(), job.key, &words, &job.columns); let _ = job .done .send(LodMesh { dim: job.dim, seed: job.seed, key: job.key, quads, y }); @@ -116,9 +148,11 @@ pub fn made(job: LodJob, mut words: Vec) { } /// A record of `world.wgsl`'s `vs_lod`: the `dir` face of a box at (x, y, z) in the tile, size -/// (sx, sy, sz) blocks, drawn with texture `tex`, tinted for `biome` when `tint`, corners in `ao`. -fn quad(dir: u32, x: i32, y: i32, z: i32, size: (i32, i32, i32), tex: u32, tint: u32, biome: u32, ao: u32) -> [u32; 4] { - let mut r = crate::gpu_layout::lod_record(dir, [x, y, z], [size.0, size.1, size.2], tex, tint, biome); +/// (sx, sy, sz) blocks, drawn with texture `tex`, tinted for `biome` when `tint`, corners in `ao`, +/// lit by block light `light` (`lod_tops::Span::light`). +fn quad(dir: u32, x: i32, y: i32, z: i32, size: (i32, i32, i32), tex: u32, tint: u32, biome: u32, ao: u32, light: u32) -> [u32; 4] { + let rgb = [0, 8, 16].map(|s| (light >> s) as u8); + let mut r = lod_record(dir, [x, y, z], [size.0, size.1, size.2], tex, tint, biome, rgb); r[3] |= (ao & 0xFF) << 24; r } @@ -141,13 +175,16 @@ fn sample_y(words: &[u32], gx: f32, gz: f32) -> i32 { } /// The tile's top faces and the walls down to lower neighbors: the top block for its own -/// height, then the block under it for the rest. -pub fn mesh(c: &Content, key: TileKey, words: &[u32]) -> (Vec<[u32; 4]>, (i32, i32)) { +/// height, then the block under it for the rest, lit as the column the cell is sampled at; then +/// what stands on the ground (`add_structures`). +pub fn mesh(c: &Content, key: TileKey, words: &[u32], columns: &Columns) -> (Vec<[u32; 4]>, (i32, i32)) { let s = 1 << key.level; + let origin = key.origin(); let at = |i: i32, j: i32| { let k = ((j + 1) * SIDE + i + 1) as usize * 2; let (a, b) = (words[k], words[k + 1]); - (a >> 16, b & 0xFFFF, b >> 16) + let col = IVec2::new(origin.x + i * s + s / 2, origin.y + j * s + s / 2); + (a >> 16, b & 0xFFFF, b >> 16, columns.get(&col).map_or(0, |c| c.light)) }; let h = height_grid(words); let looks = super::looks::current(); @@ -155,7 +192,7 @@ pub fn mesh(c: &Content, key: TileKey, words: &[u32]) -> (Vec<[u32; 4]>, (i32, i let face_of = |state: u32, dir: usize| { let info = &c.states[state as usize]; let tex = looks.drawn(state, info).textures[dir] & face::TEX_MASK; - let paint = crate::content::face_paint(info, dir, ctm.has_overlay(tex)); + let paint = face_paint(info, dir, ctm.has_overlay(tex)); let tint = if paint.base_tinted || paint.overlay_tinted { info.tint as u32 } else { 0 }; (tex, tint) }; @@ -164,15 +201,15 @@ pub fn mesh(c: &Content, key: TileKey, words: &[u32]) -> (Vec<[u32; 4]>, (i32, i for j in 0..CELLS { for i in 0..CELLS { let y = column(&h, i, j); - let (top, biome, _) = at(i, j); + let (top, biome, _, light) = at(i, j); let (tex, tint) = face_of(top, 3); - out.push(quad(3, i * s, y, j * s, (s, 1, s), tex, tint, biome, ao_bits(&h, i, j, 3, y))); + out.push(quad(3, i * s, y, j * s, (s, 1, s), tex, tint, biome, ao_bits(&h, i, j, 3, y), light)); (lo, hi) = (lo.min(y), hi.max(y + 1)); } } for j in 0..CELLS { for i in 0..CELLS { - let (top, biome, under) = at(i, j); + let (top, biome, under, light) = at(i, j); let y = column(&h, i, j); for (dir, (di, dj)) in [(0usize, (-1, 0)), (1, (1, 0)), (4, (0, -1)), (5, (0, 1))] { // At the tile's edge the tile beside may be of another level, its ground not @@ -189,16 +226,17 @@ pub fn mesh(c: &Content, key: TileKey, words: &[u32]) -> (Vec<[u32; 4]>, (i32, i let skirt = ny.min(y - 1) - s - 2; if ny < y { let (tex, tint) = face_of(top, dir); - out.push(quad(dir as u32, i * s, y, j * s, (s, 1, s), tex, tint, biome, ao_bits(&h, i, j, dir, y))); + out.push(quad(dir as u32, i * s, y, j * s, (s, 1, s), tex, tint, biome, ao_bits(&h, i, j, dir, y), light)); } let (tex, tint) = face_of(under, dir); let top_y = if ny < y { y - 1 } else { y }; let ao = ao_span(&h, i, j, dir, top_y, (ny + 1).min(top_y)); - out.push(quad(dir as u32, i * s, skirt + 1, j * s, (s, y - 1 - skirt, s), tex, tint, biome, ao)); + out.push(quad(dir as u32, i * s, skirt + 1, j * s, (s, y - 1 - skirt, s), tex, tint, biome, ao, light)); lo = lo.min(skirt + 1); } } } + add_structures(&mut out, &mut lo, &mut hi, c, key, words, columns, &h, &face_of, &ctm); (out, (lo, hi)) } @@ -227,7 +265,7 @@ fn column(h: &[[i32; GRID]; GRID], i: i32, j: i32) -> i32 { } fn ao_bits(h: &[[i32; GRID]; GRID], i: i32, j: i32, dir: usize, by: i32) -> u32 { - let nrm = weft_abi::FACE_DIRS[dir]; + let nrm = FACE_DIRS[dir]; let axis = dir / 2; let (ta, tb) = match axis { 0 => (1, 2), @@ -236,7 +274,7 @@ fn ao_bits(h: &[[i32; GRID]; GRID], i: i32, j: i32, dir: usize, by: i32) -> u32 }; let base = [i + nrm[0], by + nrm[1], j + nrm[2]]; let mut bits = 0u32; - for (k, corner) in crate::gpu_layout::CORNERS[dir].iter().enumerate() { + for (k, corner) in CORNERS[dir].iter().enumerate() { let sa = corner[ta] * 2 - 1; let sb = corner[tb] * 2 - 1; let mut p1 = base; @@ -258,13 +296,216 @@ fn ao_span(h: &[[i32; GRID]; GRID], i: i32, j: i32, dir: usize, top: i32, bot: i let hi = ao_bits(h, i, j, dir, top); let lo = ao_bits(h, i, j, dir, bot); let mut bits = 0u32; - for (k, c) in crate::gpu_layout::CORNERS[dir].iter().enumerate() { + for (k, c) in CORNERS[dir].iter().enumerate() { let src = if c[1] == 1 { hi } else { lo }; bits |= ((src >> (2 * k)) & 3) << (2 * k); } bits } +/// `lo..=hi` with the ranges in `cover` taken out. +fn exposed(lo: i32, hi: i32, cover: &[Span]) -> Vec<(i32, i32)> { + let mut segs = vec![(lo, hi)]; + for span in cover { + let (a, b) = (span.bottom, span.top); + let mut next = Vec::new(); + for (s, e) in segs { + if b < s || a > e { + next.push((s, e)); + } else { + if s < a { + next.push((s, a - 1)); + } + if e > b { + next.push((b + 1, e)); + } + } + } + segs = next; + } + segs +} + +/// A model's boxes in sixteenths, or one box around them when there are many. Tilt is dropped: +/// the distant shape is the box as it was placed. +fn model_boxes(st: &StateInfo) -> Vec<([u32; 3], [u32; 3], [u32; 6])> { + let mut raw: Vec<([u32; 3], [u32; 3], [u32; 6])> = st + .boxes + .iter() + .map(|b| { + let edge = |v: f32| (v * 16.0).round().clamp(0.0, 16.0) as u32; + (b.min.map(edge), b.max.map(edge), b.textures) + }) + .filter(|(mn, mx, _)| mn.iter().zip(mx).all(|(a, c)| a < c)) + .collect(); + if raw.len() > MAX_MODEL_BOXES { + let mut mn = [16u32; 3]; + let mut mx = [0u32; 3]; + for (a, c, _) in &raw { + for i in 0..3 { + mn[i] = mn[i].min(a[i]); + mx[i] = mx[i].max(c[i]); + } + } + let tex = raw[0].2; + raw = vec![(mn, mx, tex)]; + } + raw +} + +/// Saved spans in this tile, one box per run of block, at the column itself. The ground is left +/// where the tile has it, so a roof is not smoothed into the neighbors. +#[allow(clippy::too_many_arguments)] +fn add_structures( + out: &mut Vec<[u32; 4]>, + lo: &mut i32, + hi: &mut i32, + c: &Content, + key: TileKey, + words: &[u32], + columns: &Columns, + h: &[[i32; GRID]; GRID], + face_of: &impl Fn(u32, usize) -> (u32, u32), + ctm: &super::ctm::Ctm, +) { + let s = 1 << key.level; + let origin = key.origin(); + let (x0, z0) = (origin.x, origin.y); + let (x1, z1) = (x0 + CELLS * s, z0 + CELLS * s); + let mut here: Vec = columns + .iter() + .filter(|(p, col)| !col.spans.is_empty() && p.x >= x0 && p.x < x1 && p.y >= z0 && p.y < z1) + .map(|(p, _)| *p) + .collect(); + let (cx, cz) = (x0 + CELLS * s / 2, z0 + CELLS * s / 2); + here.sort_by_key(|p| (p.x - cx).abs() + (p.y - cz).abs()); + let hides = |block: u32, by: &Span| super::mesher::face_hidden(c, ctm, block, by.block); + for p in here { + if out.len() >= MAX_QUADS { + return; + } + let Some(col) = columns.get(&p) else { continue }; + let (i, j) = ((p.x - x0) / s, (p.y - z0) / s); + let k = ((j + 1) * SIDE + i + 1) as usize * 2; + let biome = words[k + 1] & 0xFFFF; + let ground = column(h, i, j); + // How high the ground beside covers this column's sides. + let skirt_hi = |ni: i32, nj: i32| { + let n = column(h, ni, nj); + if ni == i && nj == j || n < ground { + i32::MIN + } else if n > ground { + n + } else { + n - 1 + } + }; + let (lx, lz) = (p.x - x0, p.y - z0); + for span in &col.spans { + let (bottom, span_hi, block, light) = (span.bottom, span.top, span.block, span.light); + if c.states[block as usize].model == BlockModel::Boxes { + for (mn, mx, textures) in model_boxes(&c.states[block as usize]) { + let height = span_hi - bottom + 1; + let extrude = mn[1] == 0 && mx[1] == 16 && height > 1; + let mut y = bottom; + while y <= span_hi { + let top = if extrude { span_hi } else { y }; + for dir in 0..6 { + let at_edge = [mn[0] == 0, mx[0] == 16, mn[1] == 0, mx[1] == 16, mn[2] == 0, mx[2] == 16][dir]; + let hidden = at_edge + && match dir { + 2 => { + y <= ground + 1 + || col + .spans + .iter() + .any(|s| s.bottom <= y - 1 && s.top >= y - 1 && hides(block, s)) + } + 3 => { + (y <= ground && mx[1] == 16) + || col + .spans + .iter() + .any(|s| s.bottom <= top + 1 && s.top >= top + 1 && hides(block, s)) + } + _ => { + let (dx, dz) = [(-1, 0), (1, 0), (0, 0), (0, 0), (0, -1), (0, 1)][dir]; + let np = IVec2::new(p.x + dx, p.y + dz); + let hide = skirt_hi((np.x - x0).div_euclid(s), (np.y - z0).div_euclid(s)); + let beside = columns.get(&np).map_or(&[][..], |n| n.spans.as_slice()); + (hide != i32::MIN && top <= hide) + || beside + .iter() + .any(|s| s.bottom <= y && s.top >= top && hides(block, s)) + } + }; + if hidden { + continue; + } + let (fb, tint) = face_of(block, dir); + let own = textures[dir] & face::TEX_MASK; + let tex = if own == 0 { fb } else { own }; + let r = quad( + dir as u32, + lx, + y, + lz, + (1, if extrude { height } else { 1 }, 1), + tex, + tint, + biome, + ao_bits(h, i, j, dir, top), + light, + ); + out.push(lod_box(r, mn, mx, extrude)); + } + if extrude { + break; + } + y += 1; + } + } + } else { + if !col + .spans + .iter() + .any(|s| s.bottom == span_hi + 1 && hides(block, s)) + { + let (tex, tint) = face_of(block, 3); + out.push(quad(3, lx, span_hi, lz, (1, 1, 1), tex, tint, biome, ao_bits(h, i, j, 3, span_hi), light)); + } + let held = col + .spans + .iter() + .any(|s| s.top + 1 == bottom && hides(block, s)); + if !held && bottom > ground + 1 { + let (tex, tint) = face_of(block, 2); + out.push(quad(2, lx, bottom, lz, (1, 1, 1), tex, tint, biome, ao_bits(h, i, j, 2, bottom), light)); + } + for (dir, dx, dz) in [(0usize, -1, 0), (1, 1, 0), (4, 0, -1), (5, 0, 1)] { + let (tex, tint) = face_of(block, dir); + let np = IVec2::new(p.x + dx, p.y + dz); + let beside = columns.get(&np).map_or(&[][..], |n| n.spans.as_slice()); + let mut cover: Vec = beside.iter().copied().filter(|s| hides(block, s)).collect(); + let n_top = skirt_hi((np.x - x0).div_euclid(s), (np.y - z0).div_euclid(s)); + if n_top != i32::MIN { + cover.push(Span { bottom: n_top.saturating_sub(4096), top: n_top, block: 0, light: 0 }); + } + for (s0, e0) in exposed(bottom, span_hi, &cover) { + let ao = ao_span(h, i, j, dir, e0 + 1, s0); + out.push(quad(dir as u32, lx, s0, lz, (1, e0 - s0 + 1, 1), tex, tint, biome, ao, light)); + } + } + } + *lo = (*lo).min(bottom); + *hi = (*hi).max(span_hi + 1); + if out.len() >= MAX_QUADS { + return; + } + } + } +} + /// The client's distant terrain: which tiles to show, asking for missing ones, taking made ones. pub struct Lod { /// The land's height at the camera's column, kept while it stays in it. @@ -322,19 +563,22 @@ impl Lod { } /// `ToClient::LodColumns`: tiles with those columns are made again. - pub fn set_columns(&mut self, dim: u32, all: bool, cols: &[(i32, i32, i32, u32, u32)]) { + pub fn set_columns(&mut self, dim: u32, all: bool, cols: &[LodColumn]) { if all || dim != self.columns_dim { self.columns = Arc::default(); self.columns_dim = dim; + // One still being made has the columns from before this, so it is made again too. self.stale.extend(self.ready.iter().copied()); + self.stale.extend(self.pending.iter().copied()); } let map = Arc::make_mut(&mut self.columns); - for &(x, z, y, top, under) in cols { + for col in cols { + let (x, z) = (col.x, col.z); let p = IVec2::new(x, z); - if top == u32::MAX { + if col.top == u32::MAX { map.remove(&p); } else { - map.insert(p, (y, top, under)); + map.insert(p, col.clone()); } if !all { // Every level's tile with it, and the ones beside that sample it for their walls. @@ -347,7 +591,7 @@ impl Lod { x: (x + dx * (size / CELLS)).div_euclid(size), z: (z + dz * (size / CELLS)).div_euclid(size), }; - if self.ready.contains(&key) { + if self.ready.contains(&key) || self.pending.contains(&key) { self.stale.insert(key); } } @@ -372,7 +616,7 @@ impl Lod { c.game .config_names .get("core:lod_distance") - .map_or(0, |&id| crate::config::get(id) as i32) + .map_or(0, |&id| config::get(id) as i32) * 16 } @@ -402,7 +646,7 @@ impl Lod { } if (dim, seed) != (self.dim, self.seed) { (self.dim, self.seed) = (dim, seed); - self.able = super::terrain_gpu::describe(c, dim, seed).is_some(); + self.able = super::terrain_gpu::describe_lod(c, dim, seed).is_some(); } // Tiles stand in for cubes not shown yet; the inner half of the view is shown long // before the edge, where flying outruns the cubes. @@ -590,8 +834,12 @@ impl Lod { if self.pending.len() >= IN_FLIGHT { break; } + // Leave the mark: the one in flight still has the old columns. + if self.pending.contains(&key) { + continue; + } let again = self.stale.remove(&key); - if (self.ready.contains(&key) && !again) || self.pending.contains(&key) { + if self.ready.contains(&key) && !again { continue; } self.pending.insert(key); @@ -599,6 +847,7 @@ impl Lod { .jobs .send(LodJob { dim: self.dim, seed: self.seed, key, done: self.done.0.clone(), columns: columns.clone() }); } - self.stale.retain(|k| self.ready.contains(k)); + self.stale + .retain(|k| self.ready.contains(k) || self.pending.contains(k)); } } diff --git a/crates/engine/src/client/mesher.rs b/crates/engine/src/client/mesher.rs index baa4ee05..5a00029d 100644 --- a/crates/engine/src/client/mesher.rs +++ b/crates/engine/src/client/mesher.rs @@ -156,12 +156,7 @@ fn draw_block( for dir in 0..6 { let [dx, dy, dz] = NORMALS[dir]; let ni = pidx(x + dx, y + dy, z + dz); - let ns = n.states[ni]; - let ninfo = &c.states[ns as usize]; - if ctm.hides(ninfo.flags, ns) { - continue; - } - if ninfo.block == info.block && info.layer != RenderLayer::Opaque && info.flags & bf::CULL_SAME != 0 { + if face_hidden(c, ctm, s, n.states[ni]) { continue; } let (ao, light) = corner_data(c, n, ctm, opaque, x, y, z, dir); @@ -236,6 +231,13 @@ pub const DIR_RANK: [u8; 6] = [3, 0, 4, 1, 5, 2]; /// A face of a model's box as it is drawn, in the world and as an item alike: its corners in the /// block, turned and tilted, and the part of its picture (16 across). `None`: it has no such face. +/// Whether a cube's face toward `neighbor` doesn't show: it is opaque (a pack's rule doesn't open +/// it), or the same block, which culls its own (glass). +pub fn face_hidden(c: &Content, ctm: &super::ctm::Ctm, state: u32, neighbor: u32) -> bool { + let (info, n) = (&c.states[state as usize], &c.states[neighbor as usize]); + ctm.hides(n.flags, neighbor) || n.block == info.block && info.layer != RenderLayer::Opaque && info.flags & bf::CULL_SAME != 0 +} + pub fn box_face(bx: &crate::content::ModelBox, dir: usize) -> Option<([[f32; 3]; 4], [f32; 4])> { if bx.uv[dir][0] <= weft_abi::UV_NO_FACE[0] { return None; diff --git a/crates/engine/src/gpu_layout.rs b/crates/engine/src/gpu_layout.rs index 6cce4cbc..d22962f2 100644 --- a/crates/engine/src/gpu_layout.rs +++ b/crates/engine/src/gpu_layout.rs @@ -65,8 +65,21 @@ shared! { Z_SHIFT = 11, DIR_SHIFT = 22, TINT_SHIFT = 25, + /// A part of a block (`lod_box`): word 3 holds its x and z edges (5 bits each, 1/16), + /// word 2 its y edges, or with `EXTRUDE` it goes up the whole height. + EXTRUDE_SHIFT = 30, + PARTIAL_SHIFT = 31, HALF_SHIFT = 16, HALF_MASK = 0xFFFF, + /// A face's width and depth; its height has 10 bits. + SIZE_BITS = 6, + HEIGHT_MASK = 1023, + EDGE_MASK = 31, + /// Block light, 3 bits a channel, where neither a face's size nor a box's edges go: red and + /// green at the top of word 2, blue's low bits under word 3's AO, its high bit in word 0. + LIGHT_RG_SHIFT = 26, + LIGHT_B_SHIFT = 22, + LIGHT_B_HIGH_SHIFT = 29, } /// Tint kinds of faces and quads (0 none); mods' kinds follow the engine's. tint { @@ -310,18 +323,34 @@ pub fn quad_uv(uv: [f32; 4], tint: u32) -> (u32, [u32; 4]) { } /// A distant terrain record (`lod_face`): the `dir` face of a box at `pos` in the tile, `size` -/// blocks. +/// blocks, lit by block light `light` (0..15 a channel). #[inline] -pub fn lod_record(dir: u32, pos: [i32; 3], size: [i32; 3], tex: u32, tint: u32, biome: u32) -> [u32; 4] { - let h = lod_face::HALF_SHIFT; +pub fn lod_record(dir: u32, pos: [i32; 3], size: [i32; 3], tex: u32, tint: u32, biome: u32, light: [u8; 3]) -> [u32; 4] { + use lod_face as l; + let (h, wide) = (l::HALF_SHIFT, (1 << l::SIZE_BITS) - 1); + let [r, g, b] = light.map(|c| c.min(15) as u32 / 2); [ - pos[0] as u32 | (pos[2] as u32) << lod_face::Z_SHIFT | dir << lod_face::DIR_SHIFT | tint << lod_face::TINT_SHIFT, + pos[0] as u32 | (pos[2] as u32) << l::Z_SHIFT | dir << l::DIR_SHIFT | tint << l::TINT_SHIFT | (b >> 2) << l::LIGHT_B_HIGH_SHIFT, tex | biome << face::BIOME_SHIFT, - (pos[1] as u32 & lod_face::HALF_MASK) | (size[1] as u32) << h, - size[0] as u32 | (size[2] as u32) << h, + (pos[1] as u32 & l::HALF_MASK) | (size[1].max(0) as u32).min(l::HEIGHT_MASK) << h | (r | g << 3) << l::LIGHT_RG_SHIFT, + (size[0] as u32).min(wide) | (size[2] as u32).min(wide) << h | (b & 3) << l::LIGHT_B_SHIFT, ] } +/// `r` (a one-block record) as the part of its block from `min` to `max` in 1/16; `extrude`: up +/// the whole height it was given. +#[inline] +pub fn lod_box(mut r: [u32; 4], min: [u32; 3], max: [u32; 3], extrude: bool) -> [u32; 4] { + use lod_face as l; + r[0] |= 1 << l::PARTIAL_SHIFT | (extrude as u32) << l::EXTRUDE_SHIFT; + if !extrude { + r[2] = r[2] & !(l::HEIGHT_MASK << l::HALF_SHIFT) | (min[1] | max[1] << 5) << l::HALF_SHIFT; + } + let keep = r[3] & (3 << l::LIGHT_B_SHIFT | 0xFF << 24); + r[3] = keep | min[0] | max[0] << 5 | min[2] << 10 | max[2] << 15; + r +} + /// The WGSL side of the records above: `CORNERS` and their decoders. fn records() -> String { let corners: Vec = CORNERS @@ -370,8 +399,24 @@ fn records() -> String { "fn lod_pos(w0: u32, w2: u32) -> vec3 {\n", " return vec3(f32(w0 & LOD_FACE_POS_MASK), f32(bitcast(w2 << LOD_FACE_HALF_SHIFT) >> LOD_FACE_HALF_SHIFT), f32((w0 >> LOD_FACE_Z_SHIFT) & LOD_FACE_POS_MASK));\n", "}\n", - "fn lod_size(w2: u32, w3: u32) -> vec3 {\n", - " return vec3(f32(w3 & LOD_FACE_HALF_MASK), f32(w2 >> LOD_FACE_HALF_SHIFT), f32((w3 >> LOD_FACE_HALF_SHIFT) & 255u));\n", + "fn lod_corner(w0: u32, w2: u32, w3: u32, corner: vec3) -> vec3 {\n", + " let height = f32((w2 >> LOD_FACE_HALF_SHIFT) & LOD_FACE_HEIGHT_MASK);\n", + " if (w0 >> LOD_FACE_PARTIAL_SHIFT) == 0u {\n", + " let s = (1u << LOD_FACE_SIZE_BITS) - 1u;\n", + " return corner * vec3(f32(w3 & s), height, f32((w3 >> LOD_FACE_HALF_SHIFT) & s));\n", + " }\n", + " let e = vec4(vec4(w3, w3 >> 5u, w3 >> 10u, w3 >> 15u) & vec4(LOD_FACE_EDGE_MASK)) / 16.0;\n", + " var y = vec2(vec2(w2 >> LOD_FACE_HALF_SHIFT, w2 >> (LOD_FACE_HALF_SHIFT + 5u)) & vec2(LOD_FACE_EDGE_MASK)) / 16.0;\n", + " if ((w0 >> LOD_FACE_EXTRUDE_SHIFT) & 1u) != 0u {\n", + " y = vec2(0.0, height);\n", + " }\n", + " let mn = vec3(e.x, y.x, e.z);\n", + " return mn + corner * (vec3(e.y, y.y, e.w) - mn);\n", + "}\n", + "fn lod_light(w0: u32, w2: u32, w3: u32) -> vec3 {\n", + " let rg = w2 >> LOD_FACE_LIGHT_RG_SHIFT;\n", + " let b = ((w3 >> LOD_FACE_LIGHT_B_SHIFT) & 3u) | ((w0 >> LOD_FACE_LIGHT_B_HIGH_SHIFT) & 1u) << 2u;\n", + " return vec3(f32(rg & 7u), f32((rg >> 3u) & 7u), f32(b)) * 2.0 / 15.0;\n", "}\n", "fn lod_ao(w3: u32) -> vec4 {\n", " let a = w3 >> 24u;\n", @@ -1044,15 +1089,20 @@ mod tests { assert_eq!(corner_light([200, 3, 15, 16]), 15 | 3 << 4 | 15 << 8 | 15 << 12); let q = quad_pos([-2.0, 0.5, 61.9375]); assert_eq!([q & quad::POS_MASK, (q >> quad::POS_BITS) & quad::POS_MASK, q >> (2 * quad::POS_BITS)], [0, 40, 1023]); - let l = lod_record(4, [2047, -300, 5], [16, 7, 32], 9, 3, 2); + let l = lod_record(4, [2047, -300, 5], [16, 7, 32], 9, 3, 2, [15, 4, 13]); assert_eq!(l[0] & lod_face::POS_MASK, 2047); assert_eq!((l[0] >> lod_face::Z_SHIFT) & lod_face::POS_MASK, 5); assert_eq!((l[0] >> lod_face::DIR_SHIFT) & face::DIR_MASK, 4); assert_eq!(((l[2] << 16) as i32) >> 16, -300); - assert_eq!([l[3] & 0xFFFF, l[2] >> 16, l[3] >> 16], [16, 7, 32]); + let wide = (1 << lod_face::SIZE_BITS) - 1; + assert_eq!([l[3] & wide, (l[2] >> 16) & lod_face::HEIGHT_MASK, (l[3] >> 16) & wide], [16, 7, 32]); + // A box keeps the light and AO, and its position. + let b = lod_box([l[0], l[1], l[2], l[3] | 0xAB << 24], [1, 2, 3], [15, 14, 13], false); + assert_eq!([b[0] & !(0b11 << lod_face::EXTRUDE_SHIFT), b[2] >> lod_face::LIGHT_RG_SHIFT, b[3] >> 24], [l[0], 7 | 2 << 3, 0xAB]); + assert_eq!((b[3] >> lod_face::LIGHT_B_SHIFT) & 3 | (b[0] >> lod_face::LIGHT_B_HIGH_SHIFT & 1) << 2, 6); let src = format!( "{}{}const F0: u32 = {}u;\nconst Q: u32 = {}u;\nconst L0: u32 = {}u;\nconst L2: u32 = {}u;\nconst L3: u32 = {}u;\n\ - @compute @workgroup_size(1) fn main() {{ _ = face_pos(F0); _ = face_dir(F0); _ = face_ao(F0); _ = quad_pos(Q); _ = lod_pos(L0, L2); _ = lod_size(L2, L3); _ = corner_light(1u, 2u, 3u); _ = quad_uv(Q); _ = quad_uv_fine(Q, Q, Q, Q, Q); _ = lod_dir(L0); _ = lod_tint(L0); }}\n", + @compute @workgroup_size(1) fn main() {{ _ = face_pos(F0); _ = face_dir(F0); _ = face_ao(F0); _ = quad_pos(Q); _ = lod_pos(L0, L2); _ = lod_corner(L0, L2, L3, vec3(1.0)); _ = lod_light(L0, L2, L3); _ = corner_light(1u, 2u, 3u); _ = quad_uv(Q); _ = quad_uv_fine(Q, Q, Q, Q, Q); _ = lod_dir(L0); _ = lod_tint(L0); }}\n", wgsl(), records(), f[0], diff --git a/crates/engine/src/net/codec.rs b/crates/engine/src/net/codec.rs index d782556a..177dd8a7 100644 --- a/crates/engine/src/net/codec.rs +++ b/crates/engine/src/net/codec.rs @@ -10,6 +10,7 @@ use glam::{DVec3, IVec3}; use weft_abi::{ItemStack, ParticleBurst}; use super::{ToClient, ToServer}; +use crate::server::lod_tops::{LodColumn, Span}; use crate::world::{Cube, GameMode, Pose}; #[derive(Default)] @@ -165,6 +166,27 @@ macro_rules! tuples { } tuples!((0 A, 1 B), (0 A, 1 B, 2 C), (0 A, 1 B, 2 C, 3 D), (0 A, 1 B, 2 C, 3 D, 4 E), (0 A, 1 B, 2 C, 3 D, 4 E, 5 F), (0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G)); +impl Wire for Span { + fn put(&self, w: &mut W) { + (self.bottom, self.top, self.block, self.light).put(w); + } + fn get(r: &mut R) -> Option { + let (bottom, top, block, light) = Wire::get(r)?; + Some(Span { bottom, top, block, light }) + } +} + +impl Wire for LodColumn { + fn put(&self, w: &mut W) { + (self.x, self.z, self.y, self.top, self.under, self.light).put(w); + self.spans.put(w); + } + fn get(r: &mut R) -> Option { + let (x, z, y, top, under, light) = Wire::get(r)?; + Some(LodColumn { x, z, y, top, under, light, spans: Wire::get(r)? }) + } +} + impl Wire for IVec3 { fn put(&self, w: &mut W) { self.to_array().put(w); @@ -296,7 +318,7 @@ impl Wire for ParticleBurst { macro_rules! items { ($($t:ty),*) => {$(impl Item for $t {})*}; } -items!(u32, u64, i32, f32, f64, bool, String, IVec3, DVec3, ItemStack, Arc, Msg, ParticleBurst); +items!(u32, u64, i32, f32, f64, bool, String, IVec3, DVec3, ItemStack, Arc, Msg, ParticleBurst, Span, LodColumn); pub fn encode_client(m: &ToClient) -> Vec { let mut w = W::default(); diff --git a/crates/engine/src/net/mod.rs b/crates/engine/src/net/mod.rs index 9d8df362..1b2e983a 100644 --- a/crates/engine/src/net/mod.rs +++ b/crates/engine/src/net/mod.rs @@ -9,6 +9,7 @@ use glam::{DVec3, IVec3}; use weft_abi::ItemStack; +use crate::server::lod_tops::LodColumn; use crate::world::Cube; use codec::Wire; @@ -294,9 +295,9 @@ messages! { /// Area edits of the dimension (`AreaFill::spec`): `all` replaces what the client has, /// otherwise they come after it. Clients make them on what they generate. Edits { dim: u32, specs: Vec, all: bool }, - /// Columns of dimension `dim` whose top isn't the terrain's, for distant terrain: (x, z, - /// height, top, under), `top` `u32::MAX` for one that is again; `all`: every one there is. - LodColumns { dim: u32, all: bool, cols: Vec<(i32, i32, i32, u32, u32)> }, + /// Columns of dimension `dim` that differ from the terrain, for distant terrain (`top` + /// `u32::MAX` for one that doesn't any more); `all`: every one there is. + LodColumns { dim: u32, all: bool, cols: Vec }, /// Of the world, or of body `frame`. /// `extras`: what else is in cells, after `changes` (which take it away). Blocks { dim: u32, frame: u64, changes: Vec<(IVec3, u32)>, extras: Vec<(IVec3, Vec)> }, diff --git a/crates/engine/src/saves.rs b/crates/engine/src/saves.rs index bdd1d86d..efc9dbf8 100644 --- a/crates/engine/src/saves.rs +++ b/crates/engine/src/saves.rs @@ -517,7 +517,7 @@ impl Storage { Some((cube?, data[4 + len..].to_vec())) } - pub fn save(&mut self, c: &Content, dim: u32, pos: IVec3, cube: &Cube, entities: &[u8]) { + pub fn save(&mut self, c: &Content, dim: u32, pos: IVec3, cube: &Cube, around: &crate::server::lod_tops::Around, entities: &[u8]) { if !c.dims[dim as usize].saved { return; } @@ -555,7 +555,7 @@ impl Storage { } self.saved[dim as usize].insert(pos); let seed = self.meta.seed; - self.lod[dim as usize].saved(c, dim, seed, pos, &cube); + self.lod[dim as usize].saved(c, dim, seed, pos, &cube, around); } pub fn flush(&mut self) { diff --git a/crates/engine/src/server/dim.rs b/crates/engine/src/server/dim.rs index de7cc4ba..c3bc370c 100644 --- a/crates/engine/src/server/dim.rs +++ b/crates/engine/src/server/dim.rs @@ -903,6 +903,18 @@ impl Dim { /// Drops cubes `keep` rejects; returns the ones that must be saved (modified or holding /// entities) together with the entities that lived in them, which leave the world. + /// The cubes beside the one at `pos` for its distant light (`lod_tops::Around`): loaded, or + /// as `gone` has them. + pub fn around(&self, pos: IVec3, gone: impl Fn(IVec3) -> Option>) -> crate::server::lod_tops::Around { + [IVec3::NEG_X, IVec3::X, IVec3::NEG_Z, IVec3::Z].map(|d| { + self.space + .cubes + .get(&(pos + d)) + .cloned() + .or_else(|| gone(pos + d)) + }) + } + pub fn retain_cubes(&mut self, c: &Content, keep: impl Fn(IVec3) -> bool) -> Vec<(IVec3, Arc, Vec)> { let mut out = Vec::new(); let gone: FxHashSet = self diff --git a/crates/engine/src/server/load.rs b/crates/engine/src/server/load.rs index c65ae25d..194ca224 100644 --- a/crates/engine/src/server/load.rs +++ b/crates/engine/src/server/load.rs @@ -318,12 +318,16 @@ impl Server { .any(|&(v, lo, hi)| v.column(pos) && pos.y >= lo && pos.y <= hi) || tickets.iter().any(|t| t.contains(pos)) }); - for (pos, cube, inside) in removed { + let around: Vec<_> = removed + .iter() + .map(|x| r.around(x.0, |p| removed.iter().find(|o| o.0 == p).map(|o| o.1.clone()))) + .collect(); + for ((pos, cube, inside), around) in removed.into_iter().zip(around) { if let Some(s) = &storage { // Written in the background; loads queue behind it there. let (s, mut bytes, dim) = (s.clone(), serialize(c, r, &inside), r.id); persist::append_ticks(&mut bytes, &r.ticks_in(pos)); - crate::background().spawn(move || s.lock().save(c, dim, pos, &cube, &bytes)); + crate::background().spawn(move || s.lock().save(c, dim, pos, &cube, &around, &bytes)); } for id in inside { r.entities.remove(id); diff --git a/crates/engine/src/server/lod_tops.rs b/crates/engine/src/server/lod_tops.rs index e7e99b66..ac04f1aa 100644 --- a/crates/engine/src/server/lod_tops.rs +++ b/crates/engine/src/server/lod_tops.rs @@ -1,83 +1,167 @@ //! What distant terrain needs of the cubes that are saved: the rest generates the same again, so //! the client's tiles take it from the terrain formula, and only columns whose saved cubes make -//! their top differ from it (a town, a crater, a tower) are kept here and sent. Per saved cube, -//! each of its columns' highest solid block; a column's top then comes from those and the formula -//! for the cubes of it that aren't saved. +//! them differ from it (a town, a crater, a tower) are kept here and sent. Per saved cube, the +//! runs of each column's blocks with the block light by them; a column's ground then comes from +//! those and the formula for the cubes of it that aren't saved, and what stands above the ground +//! is sent as spans. use std::collections::BTreeMap; use std::path::Path; +use std::sync::Arc; use glam::{IVec2, IVec3}; use rustc_hash::FxHashMap; +use weft_abi::BlockModel; +use weft_abi::block_flags as bf; + use crate::content::Content; use crate::world::Cube; +use crate::world::cube::index; -/// A column whose top is not the terrain's: its height, top block and the one under it. -/// `top` `u32::MAX`: as the terrain says again. +/// One block's vertical run, inclusive, and the block light by it (`abi::light` without the sky, +/// a byte a channel). #[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Span { + pub bottom: i32, + pub top: i32, + pub block: u32, + pub light: u32, +} + +/// A column that differs from the terrain: its ground's height, top block, the one under it and +/// the light on it, and what stands above the ground. `top` `u32::MAX`: as the terrain says +/// again; 0: the terrain's ground, only spans or light of its own. +#[derive(Clone, Debug, PartialEq, Eq)] pub struct LodColumn { pub x: i32, pub z: i32, pub y: i32, pub top: u32, pub under: u32, + pub light: u32, + pub spans: Vec, +} + +/// The cubes beside one, -x, +x, -z, +z, as far as they are loaded: the light at its sides. +pub type Around = [Option>; 4]; + +/// A saved cube's columns: the runs that make each, low to high, local to the cube (0..15). +#[derive(Clone, PartialEq, Eq)] +struct CubeTops { + /// `at[i]..at[i + 1]` of `runs` is column `i`. + at: Box<[u16; 257]>, + runs: Vec, } -/// One column of a saved cube: its highest block that shows from afar (local y, `NONE` for -/// none), that block and the one under it. -#[derive(Clone, Copy, Default, PartialEq, Eq)] -struct Top { - y: u8, - top: u32, - under: u32, +impl CubeTops { + fn column(&self, i: usize) -> &[Span] { + &self.runs[self.at[i] as usize..self.at[i + 1] as usize] + } } -const NONE: u8 = u8::MAX; +/// Runs kept per column of one cube, the highest. A wall is one run; the rest is trim and a roof. +const MAX_RUNS: usize = 6; +/// Spans sent for one column, the highest of them. +const MAX_SPANS: usize = 8; +const MAGIC: &[u8] = b"LOD4"; -/// A dimension's saved cubes' tops, by cube column then cube height. +/// A dimension's saved cubes' runs, by cube column then cube height. #[derive(Default)] pub struct DimTops { - cubes: FxHashMap>>, - /// Columns whose top differs from the terrain's, as last worked out. + cubes: FxHashMap>, + /// Columns that differ from the terrain, as last worked out. pub columns: FxHashMap, /// Columns that changed since `take_changed`. changed: Vec, dirty: bool, } -/// Whether a block shows as the top from afar: solid and opaque (not leaves, glass or a plant), -/// or a liquid. -fn shows(c: &Content, s: u32) -> bool { - use weft_abi::block_flags as bf; - let st = &c.states[s as usize]; +/// Solid and opaque, or a liquid: the ground from afar. A window or a stair is not. +pub fn shows(c: &Content, s: u32) -> bool { + let Some(st) = c.states.get(s as usize) else { return false }; s != 0 && (st.liquid.is_some() || (st.flags & bf::SOLID != 0 && st.flags & bf::OPAQUE != 0)) } -fn tops_of(c: &Content, cube: &Cube) -> Box<[Top; 256]> { - let mut out = Box::new([Top { y: NONE, top: 0, under: 0 }; 256]); +/// A cube or a model box the distance can draw. A cross, a wire, an empty model, or a block a +/// placement replaces is not. +fn drawn(c: &Content, s: u32) -> bool { + let Some(st) = c.states.get(s as usize) else { return false }; + if st.liquid.is_some() { + return true; + } + s != 0 + && st.flags & bf::REPLACEABLE == 0 + && match st.model { + BlockModel::Cube => true, + BlockModel::Boxes => !st.boxes.is_empty(), + _ => false, + } +} + +/// Each channel the brighter of the two. +fn brighter(a: u32, b: u32) -> u32 { + (0..24) + .step_by(8) + .fold(0, |l, s| l | ((a >> s) & 0xFF).max((b >> s) & 0xFF) << s) +} + +/// The block light by the run from `lo` to `hi` of column (`x`, `z`): of its cells and the ones +/// beside them, in the cubes around too. +fn light_by(cube: &Cube, around: &Around, x: usize, z: usize, lo: usize, hi: usize) -> u32 { + let at = |x: i32, y: usize, z: i32| { + let (cube, x, z) = match (x, z) { + (-1, _) => (around[0].as_deref(), 15, z), + (16, _) => (around[1].as_deref(), 0, z), + (_, -1) => (around[2].as_deref(), x, 15), + (_, 16) => (around[3].as_deref(), x, 0), + _ => (Some(cube), x, z), + }; + cube.map_or(0, |c| c.light(index(x as usize, y, z as usize)) >> 8) + }; + let mut l = 0; + for y in lo.saturating_sub(1)..=(hi + 1).min(15) { + for (dx, dz) in [(0, 0), (-1, 0), (1, 0), (0, -1), (0, 1)] { + l = brighter(l, at(x as i32 + dx, y, z as i32 + dz)); + } + } + l +} + +fn tops_of(c: &Content, cube: &Cube, around: &Around) -> CubeTops { + let mut at = Box::new([0u16; 257]); + let mut runs = Vec::new(); for z in 0..16 { for x in 0..16 { - for y in (0..16).rev() { - let s = cube.get(crate::world::cube::index(x, y, z)); - if shows(c, s) { - let under = if y > 0 { cube.get(crate::world::cube::index(x, y - 1, z)) } else { s }; - out[z * 16 + x] = Top { y: y as u8, top: s, under: if under == 0 { s } else { under } }; - break; + at[z * 16 + x] = runs.len() as u16; + let mut column: Vec = Vec::new(); + let mut y = 0; + while y < 16 { + let s = cube.get(index(x, y, z)); + if !drawn(c, s) { + y += 1; + continue; + } + let lo = y; + while y < 16 && cube.get(index(x, y, z)) == s { + y += 1; } + column.push(Span { bottom: lo as i32, top: y as i32 - 1, block: s, light: light_by(cube, around, x, z, lo, y - 1) }); } + runs.extend_from_slice(&column[column.len().saturating_sub(MAX_RUNS)..]); } } - out + at[256] = runs.len() as u16; + CubeTops { at, runs } } impl DimTops { /// A cube was saved as `cube`. - pub fn saved(&mut self, c: &Content, dim: u32, seed: u64, pos: IVec3, cube: &Cube) { - let tops = tops_of(c, cube); + pub fn saved(&mut self, c: &Content, dim: u32, seed: u64, pos: IVec3, cube: &Cube, around: &Around) { + let tops = tops_of(c, cube, around); let col = IVec2::new(pos.x, pos.z); let list = self.cubes.entry(col).or_default(); - if list.get(&pos.y).is_some_and(|t| **t == *tops) { + if list.get(&pos.y).is_some_and(|t| *t == tops) { return; } list.insert(pos.y, tops); @@ -106,39 +190,76 @@ impl DimTops { .copied() .unwrap_or(ground) .max(ground); - // From the highest saved cube down: what is in it; where one isn't saved above the - // ground, air; at or under it, the terrain, solid up to its ground. + // What rises above the ground, from the ground up; the rest stays the ground. + let mut spans: Vec = Vec::new(); + for (&cy, tops) in list.range(lowest..=highest) { + for r in tops.column(i) { + let (bottom, top) = (cy * 16 + r.bottom, cy * 16 + r.top); + match spans.last_mut() { + Some(prev) if prev.top + 1 == bottom && prev.block == r.block => { + (prev.top, prev.light) = (top, brighter(prev.light, r.light)); + } + _ => spans.push(Span { bottom, top, ..*r }), + } + } + } + spans.retain(|s| s.top > fy); + for s in &mut spans { + s.bottom = s.bottom.max(fy); + } + spans.drain(..spans.len().saturating_sub(MAX_SPANS)); + // The ground under them: from the highest saved cube down, what shows in it; where + // one isn't saved above the terrain's ground, air; at or under it, the terrain. + let limit = spans.first().map_or(i32::MAX, |s| s.bottom - 1); let mut here = None; for cy in (lowest.min(ground) - 1..=highest).rev() { match list.get(&cy) { - Some(tops) if tops[i].y != NONE => { - let tp = tops[i]; - here = Some((cy * 16 + tp.y as i32, tp.top, tp.under)); - break; + Some(tops) => { + let runs = tops.column(i); + let found = (0..runs.len()) + .rev() + .find(|&k| shows(c, runs[k].block) && cy * 16 + runs[k].bottom <= limit); + if let Some(k) = found { + let r = runs[k]; + let y = (cy * 16 + r.top).min(limit); + let below = k + .checked_sub(1) + .map(|b| runs[b]) + .filter(|b| b.top + 1 == r.bottom); + let under = if y > cy * 16 + r.bottom { r.block } else { below.map_or(r.block, |b| b.block) }; + here = Some((y, r.block, under, r.light)); + break; + } } - Some(_) => {} None if cy == ground => break, None if cy < ground => { let stone = def.terrain.as_ref().map_or(0, |t| t.stone); - here = Some((cy * 16 + 15, stone, stone)); + here = Some((cy * 16 + 15, stone, stone, 0)); break; } None => {} } } + let terrain = |&(y, top, _, _): &(i32, u32, u32, u32)| { + y == fy && (top == ftop || ftop == u32::MAX && c.states[top as usize].liquid.is_some()) + }; + let light = here.map_or(0, |h| h.3); + let now = match here { + // The terrain's own ground, as it was: kept only for what stands on it or shines. + Some(h) if terrain(&h) && spans.is_empty() && light == 0 => None, + Some(h) if terrain(&h) => Some(LodColumn { x, z, y: fy, top: 0, under: 0, light, spans }), + Some((y, top, under, light)) => Some(LodColumn { x, z, y, top, under, light, spans }), + // Its ground cube isn't saved: the terrain's ground stands. + None if spans.is_empty() => None, + None => Some(LodColumn { x, z, y: fy, top: 0, under: 0, light: 0, spans }), + }; let p = IVec2::new(x, z); - // Nothing found: its ground cube isn't saved, the terrain's own top stands. - let differs = - here.filter(|&(y, top, _)| !(y == fy && (top == ftop || ftop == u32::MAX && c.states[top as usize].liquid.is_some()))); - let before = self.columns.get(&p).copied(); - match differs { - Some((y, top, under)) => { - let now = LodColumn { x, z, y, top, under }; - if before != Some(now) { - self.columns.insert(p, now); - self.changed.push(p); - } + match now { + Some(now) if self.columns.get(&p) != Some(&now) => { + self.columns.insert(p, now); + self.changed.push(p); } + Some(_) => {} None => { if self.columns.remove(&p).is_some() { self.changed.push(p); @@ -155,10 +276,15 @@ impl DimTops { ps.dedup(); ps.into_iter() .map(|p| { - self.columns - .get(&p) - .copied() - .unwrap_or(LodColumn { x: p.x, z: p.y, y: 0, top: u32::MAX, under: 0 }) + self.columns.get(&p).cloned().unwrap_or(LodColumn { + x: p.x, + z: p.y, + y: 0, + top: u32::MAX, + under: 0, + light: 0, + spans: Vec::new(), + }) }) .collect() } @@ -169,17 +295,19 @@ impl DimTops { return; } self.dirty = false; - let mut b: Vec = Vec::new(); + let mut b: Vec = MAGIC.to_vec(); for (col, list) in &self.cubes { for (y, tops) in list { for v in [col.x, *y, col.y] { b.extend_from_slice(&v.to_le_bytes()); } - for t in tops.iter() { - b.push(t.y); - if t.y != NONE { - b.extend_from_slice(&world_id(t.top).to_le_bytes()); - b.extend_from_slice(&world_id(t.under).to_le_bytes()); + for i in 0..256 { + let rs = tops.column(i); + b.push(rs.len() as u8); + for r in rs { + b.extend_from_slice(&[r.bottom as u8, r.top as u8]); + b.extend_from_slice(&world_id(r.block).to_le_bytes()); + b.extend_from_slice(&r.light.to_le_bytes()); } } } @@ -192,7 +320,8 @@ impl DimTops { } } - /// What `write` wrote, its columns worked out. + /// What `write` wrote, its columns worked out. A file of another layout is left out: its + /// columns come back as their cubes are saved again. pub fn read(path: &Path, c: &Content, dim: u32, seed: u64, runtime_id: &mut impl FnMut(u32) -> u32) -> DimTops { let mut d = DimTops::default(); let Some(b) = std::fs::read(path) @@ -201,25 +330,36 @@ impl DimTops { else { return d; }; - let mut r = &b[..]; - let i32_ = |r: &mut &[u8]| -> Option { + let Some(mut r) = b.strip_prefix(MAGIC) else { return d }; + let u32_ = |r: &mut &[u8]| -> Option { let (v, rest) = r.split_first_chunk::<4>()?; *r = rest; - Some(i32::from_le_bytes(*v)) + Some(u32::from_le_bytes(*v)) + }; + let u8_ = |r: &mut &[u8]| -> Option { + let (v, rest) = r.split_first()?; + *r = rest; + Some(*v) }; 'cubes: while !r.is_empty() { - let (Some(x), Some(y), Some(z)) = (i32_(&mut r), i32_(&mut r), i32_(&mut r)) else { break }; - let mut tops = Box::new([Top::default(); 256]); - for t in tops.iter_mut() { - let Some((&ty, rest)) = r.split_first() else { break 'cubes }; - r = rest; - t.y = ty; - if ty != NONE { - let (Some(a), Some(u)) = (i32_(&mut r), i32_(&mut r)) else { break 'cubes }; - (t.top, t.under) = (runtime_id(a as u32), runtime_id(u as u32)); + let (Some(x), Some(y), Some(z)) = (u32_(&mut r), u32_(&mut r), u32_(&mut r)) else { break }; + let mut at = Box::new([0u16; 257]); + let mut runs = Vec::new(); + for i in 0..256 { + at[i] = runs.len() as u16; + let Some(n) = u8_(&mut r) else { break 'cubes }; + for _ in 0..n { + let (Some(lo), Some(hi), Some(block), Some(light)) = (u8_(&mut r), u8_(&mut r), u32_(&mut r), u32_(&mut r)) else { + break 'cubes; + }; + runs.push(Span { bottom: lo as i32, top: hi as i32, block: runtime_id(block), light }); } } - d.cubes.entry(IVec2::new(x, z)).or_default().insert(y, tops); + at[256] = runs.len() as u16; + d.cubes + .entry(IVec2::new(x as i32, z as i32)) + .or_default() + .insert(y as i32, CubeTops { at, runs }); } let cols: Vec = d.cubes.keys().copied().collect(); for col in cols { diff --git a/crates/engine/src/server/mod.rs b/crates/engine/src/server/mod.rs index 2fe700bd..e1b0070e 100644 --- a/crates/engine/src/server/mod.rs +++ b/crates/engine/src/server/mod.rs @@ -860,7 +860,11 @@ impl Server { /// Every column of `dim` distant terrain shows other than the terrain says, to player `pi`. pub(super) fn send_lod_columns(&mut self, pi: usize, dim: u32) { if let Some(s) = &self.storage { - let cols = lod_columns(s.lock().lod[dim as usize].columns.values()); + let cols = s.lock().lod[dim as usize] + .columns + .values() + .cloned() + .collect(); self.players[pi] .conn .send(ToClient::LodColumns { dim, all: true, cols }); @@ -879,7 +883,6 @@ impl Server { .filter(|(_, c)| !c.is_empty()) .collect(); for (dim, cols) in changed { - let cols = lod_columns(cols.iter()); for p in self.players.iter_mut().filter(|p| p.joined && p.dim == dim) { p.conn .send(ToClient::LodColumns { dim, all: false, cols: cols.clone() }); @@ -921,7 +924,7 @@ impl Server { .iter() .any(|e| e.saved_with_cube() && cube_of(e.pos.floor().as_ivec3()) == pos); persist::append_ticks(&mut bytes, &ticks); - s.save(c, r.id, pos, cube, &bytes); + s.save(c, r.id, pos, cube, &r.around(pos, |_| None), &bytes); if holds || !ticks.is_empty() { r.on_disk.insert(pos); } else { @@ -1056,7 +1059,11 @@ impl Server { p.teleported = Some(pos); p.conn.send(ToClient::Teleport { dim: to, pos, frame }); if let Some(s) = storage { - let cols = lod_columns(s.lock().lod[to as usize].columns.values()); + let cols = s.lock().lod[to as usize] + .columns + .values() + .cloned() + .collect(); players[pi] .conn .send(ToClient::LodColumns { dim: to, all: true, cols }); @@ -1442,7 +1449,3 @@ fn death_message(c: &Content, cause: u32, killer: Option, name: String) -> fn void_note(c: &Content) -> Msg { Msg::new(if c.spawn_dim == c.void_dim { "void.no_dimension" } else { "void.here" }, Vec::new()) } - -fn lod_columns<'a>(cols: impl Iterator) -> Vec<(i32, i32, i32, u32, u32)> { - cols.map(|c| (c.x, c.z, c.y, c.top, c.under)).collect() -}