Compare commits

...
Author SHA1 Message Date
Ludovic MarieandClaude Opus 5.5 dc9dcba5e3 Keep the cloud mesh and rebuild it only when vanilla does
CloudRenderer keeps the cells it built and rebuilds them only when the
camera changes cell, side of the cloud layer (above, inside, below), cloud
type or Cloud Distance; each frame it only moves them by the offset into
the cell. Horizon rebuilt every cloud quad on the main thread every frame
and uploaded them with the entities, about 1.8 ms at Cloud Distance 128.

The mesh now lives with clouds.png (clouds::Clouds), in its own vertex
buffer uploaded only when rebuilt. Vertices hold their cell corner, the
far-side flags and the shade index; entity.wgsl adds the offset, then
the cell size, so positions round exactly as before, and takes the shaded
cloud colour from Globals.

Fixes #480

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-16 13:10:19 +02:00
7 changed files with 515 additions and 154 deletions

No files matched your search

+390
View File
@@ -1,4 +1,210 @@
//! Cloud visibility and flat/fancy cell geometry (`CloudRenderer`).
use crate::mesher::Vertex;
use std::sync::atomic::{AtomicU64, Ordering};
/// Face shades (UP, WEST/EAST, NORTH/SOUTH, DOWN). A cloud vertex's `pos` is
/// its cell's low corner; `light` holds the shade index (byte 0, the colour is
/// `Globals.cloud_colors[i]`) and whether the vertex is on the far side of the
/// cell in x, y and z (bytes 1..4). The shader adds the cell's 12x4x12 size
/// after the offset, so positions round exactly as when built in place.
pub const SHADES: [f32; 4] = [1.0, 0.9, 0.8, 0.7];
/// `CloudRenderer.RelativeCameraPos`: the camera against the cloud layer.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RelativePos {
Above,
Inside,
Below,
}
impl RelativePos {
/// `bottom`: the cloud bottom relative to the camera (`relativeBottomY`).
pub fn new(bottom: f32) -> Self {
if bottom + 4.0 < 0.0 {
Self::Above
} else if bottom > 0.0 {
Self::Below
} else {
Self::Inside
}
}
}
/// Everything the cell mesh depends on: the camera's cell, its position
/// against the layer, the cloud type and the radius.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct MeshKey {
cell: (i64, i64),
pos: RelativePos,
fancy: bool,
radius: i32,
}
/// Mesh generations are unique across reloads, so a renderer never keeps a
/// stale upload.
static GENERATION: AtomicU64 = AtomicU64::new(0);
/// clouds.png as filled cells and the cell mesh `CloudRenderer` keeps.
pub struct Clouds {
width: i64,
height: i64,
cells: Vec<bool>,
key: Option<MeshKey>,
/// Quads relative to the camera's cell corner (x, z) and the cloud bottom
/// (y), nearest first, encoded as `SHADES` says; moved into place by
/// `Prepared::offset`.
pub mesh: Vec<Vertex>,
/// Changes whenever `mesh` is rebuilt.
pub generation: u64,
}
/// One frame's clouds: the cached mesh's generation and where it is drawn.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Prepared {
pub generation: u64,
/// `CloudInfo`'s offset: (-xInCell, relativeBottomY, -zInCell).
pub offset: [f32; 3],
}
impl Clouds {
/// `CloudRenderer.prepare` (resources): cells with alpha 10 or more are filled.
pub fn load(root: &std::path::Path) -> Self {
let (width, height, cells) =
crate::hud::decode(&root.join("assets/minecraft/textures/environment/clouds.png"))
.map(|(w, h, px)| {
(
w,
h,
px.as_chunks::<4>().0.iter().map(|p| p[3] >= 10).collect(),
)
})
.unwrap_or((1, 1, vec![false]));
Self::new(width, height, cells)
}
pub fn new(width: u32, height: u32, cells: Vec<bool>) -> Self {
Self {
width: i64::from(width),
height: i64::from(height),
cells,
key: None,
mesh: Vec::new(),
generation: 0,
}
}
fn filled(&self, cx: i64, cz: i64) -> bool {
self.cells[(cz.rem_euclid(self.height) * self.width + cx.rem_euclid(self.width)) as usize]
}
/// `CloudRenderer.prepare`: the cell under the camera and the offset into
/// it; the mesh is rebuilt only when the cell, the camera's position
/// against the layer, the cloud type or the radius changes.
pub fn prepare(
&mut self,
camera: glam::DVec3,
game_time: i64,
partial_tick: f64,
fancy: bool,
radius: i32,
uv: [f32; 2],
) -> Prepared {
let cloud_height = 192.33;
let offset = (game_time.rem_euclid(self.width * 400)) as f64 + partial_tick;
let mut cloud_x = camera.x + offset * 0.030_000_001;
let mut cloud_z = camera.z + 3.96;
let (tw, th) = (self.width as f64 * 12.0, self.height as f64 * 12.0);
cloud_x -= (cloud_x / tw).floor() * tw;
cloud_z -= (cloud_z / th).floor() * th;
let cell_x = (cloud_x / 12.0).floor() as i64;
let cell_z = (cloud_z / 12.0).floor() as i64;
let x_in = (cloud_x - cell_x as f64 * 12.0) as f32;
let z_in = (cloud_z - cell_z as f64 * 12.0) as f32;
let bottom = (cloud_height - camera.y) as f32;
let key = MeshKey {
cell: (cell_x, cell_z),
pos: RelativePos::new(bottom),
fancy,
radius,
};
if self.key != Some(key) {
self.key = Some(key);
self.build(key, bottom, uv);
}
Prepared {
generation: self.generation,
offset: [-x_in, bottom, -z_in],
}
}
fn build(&mut self, key: MeshKey, bottom: f32, uv: [f32; 2]) {
let mut mesh = std::mem::take(&mut self.mesh);
mesh.clear();
let (cell_x, cell_z) = key.cell;
for (dx, dz) in cells(key.radius) {
let (cx, cz) = (cell_x + i64::from(dx), cell_z + i64::from(dz));
if !self.filled(cx, cz) {
continue;
}
let x0 = dx as f32 * 12.0;
let z0 = dz as f32 * 12.0;
let (x1, z1, y0, y1) = (x0 + 12.0, z0 + 12.0, 0.0, 4.0);
let mut quad = |p: [[f32; 3]; 4], shade: u32| {
for v in p {
let far = |axis: usize, near: f32| u32::from(v[axis] != near) << (8 + 8 * axis);
// Overlay 4: clouds (unlit, faded by the cloud fog end).
mesh.push(Vertex {
normal: [0; 4],
pos: [x0, y0, z0],
uv,
layer: 4,
color: u32::MAX,
light: shade | far(0, x0) | far(1, y0) | far(2, z0),
});
}
};
if !key.fancy {
for p in flat_cell(x0, z0, y0) {
quad(p, 0);
}
continue;
}
let sides = [
([[x1, y0, z0], [x1, y0, z1], [x0, y0, z1], [x0, y0, z0]], 3),
([[x0, y1, z0], [x0, y1, z1], [x1, y1, z1], [x1, y1, z0]], 0),
([[x0, y0, z0], [x0, y1, z0], [x1, y1, z0], [x1, y0, z0]], 2),
([[x1, y0, z1], [x1, y1, z1], [x0, y1, z1], [x0, y0, z1]], 2),
([[x0, y0, z1], [x0, y1, z1], [x0, y1, z0], [x0, y0, z0]], 1),
([[x1, y0, z0], [x1, y1, z0], [x1, y1, z1], [x1, y0, z1]], 1),
];
let mask = face_mask(
dx,
dz,
bottom,
[
!self.filled(cx, cz - 1),
!self.filled(cx, cz + 1),
!self.filled(cx - 1, cz),
!self.filled(cx + 1, cz),
],
);
for (direction, inside) in faces(mask) {
let (p, shade) = sides[usize::from(direction)];
quad(if inside { [p[3], p[2], p[1], p[0]] } else { p }, shade);
}
}
self.mesh = mesh;
self.generation = GENERATION.fetch_add(1, Ordering::Relaxed) + 1;
}
}
/// The cloud colour times each of `SHADES`, packed RGBA8 like vertex colours.
pub fn shaded_colors(color: [f32; 4]) -> [u32; 4] {
SHADES.map(|shade| {
let ch = |v: f32| ((v * shade).clamp(0.0, 1.0) * 255.0) as u32;
ch(color[3].clamp(0.0, 1.0)) << 24 | ch(color[2]) << 16 | ch(color[1]) << 8 | ch(color[0])
})
}
/// `LevelRenderer`: skip OFF clouds and colors with no packed alpha.
pub fn visible(mode: &str, alpha: f32) -> bool {
mode != "false" && (alpha.clamp(0.0, 1.0) * 255.0) as u8 > 0
@@ -137,4 +343,188 @@ mod tests {
fn nearby_cells_keep_inward_faces_for_camera_inside_clouds() {
assert_eq!(face_mask(0, 0, 0.0, [false; 4]), 3 | (63 << 6));
}
fn sky() -> Clouds {
// A 16x16 sky with a few clouds of various shapes.
let cells = (0..256).map(|i| (i * 7 + i / 16 * 3) % 5 < 2).collect();
Clouds::new(16, 16, cells)
}
/// The per-frame builder this cache replaced, kept as the oracle:
/// camera-relative positions and face shades.
fn built_in_place(
clouds: &Clouds,
camera: glam::DVec3,
time: f64,
fancy: bool,
radius: i32,
) -> Vec<([f32; 3], f32)> {
let (w, h) = (clouds.width, clouds.height);
let offset = (time.floor() as i64).rem_euclid(w * 400) as f64 + time.fract();
let mut cloud_x = camera.x + offset * 0.030_000_001;
let mut cloud_z = camera.z + 3.96;
let (tw, th) = (w as f64 * 12.0, h as f64 * 12.0);
cloud_x -= (cloud_x / tw).floor() * tw;
cloud_z -= (cloud_z / th).floor() * th;
let cell_x = (cloud_x / 12.0).floor() as i64;
let cell_z = (cloud_z / 12.0).floor() as i64;
let x_in = (cloud_x - cell_x as f64 * 12.0) as f32;
let z_in = (cloud_z - cell_z as f64 * 12.0) as f32;
let bottom = (192.33 - camera.y) as f32;
let mut out = Vec::new();
for (dx, dz) in cells(radius) {
let (cx, cz) = (cell_x + i64::from(dx), cell_z + i64::from(dz));
if !clouds.filled(cx, cz) {
continue;
}
let x0 = dx as f32 * 12.0 - x_in;
let z0 = dz as f32 * 12.0 - z_in;
let (x1, z1, y0, y1) = (x0 + 12.0, z0 + 12.0, bottom, bottom + 4.0);
let mut quad = |p: [[f32; 3]; 4], shade: f32| out.extend(p.map(|v| (v, shade)));
if !fancy {
for p in flat_cell(x0, z0, bottom) {
quad(p, 1.0);
}
continue;
}
let sides = [
(
[[x1, y0, z0], [x1, y0, z1], [x0, y0, z1], [x0, y0, z0]],
0.7,
),
(
[[x0, y1, z0], [x0, y1, z1], [x1, y1, z1], [x1, y1, z0]],
1.0,
),
(
[[x0, y0, z0], [x0, y1, z0], [x1, y1, z0], [x1, y0, z0]],
0.8,
),
(
[[x1, y0, z1], [x1, y1, z1], [x0, y1, z1], [x0, y0, z1]],
0.8,
),
(
[[x0, y0, z1], [x0, y1, z1], [x0, y1, z0], [x0, y0, z0]],
0.9,
),
(
[[x1, y0, z0], [x1, y1, z0], [x1, y1, z1], [x1, y0, z1]],
0.9,
),
];
let empty = [
!clouds.filled(cx, cz - 1),
!clouds.filled(cx, cz + 1),
!clouds.filled(cx - 1, cz),
!clouds.filled(cx + 1, cz),
];
for (direction, inside) in faces(face_mask(dx, dz, bottom, empty)) {
let (p, shade) = sides[usize::from(direction)];
quad(if inside { [p[3], p[2], p[1], p[0]] } else { p }, shade);
}
}
out
}
#[test]
fn cached_mesh_moved_into_place_matches_building_in_place() {
let mut clouds = sky();
for (x, y, z) in [
(0.3, 150.0, 0.7),
(0.3, 194.2, 0.7),
(-1234.56, 240.0, 987.65),
(23.99, 192.33, 35.99),
(11.9, 196.33, -0.1),
] {
for time in [0.0_f64, 51_234.5, 6_399.999, 1e7 + 0.25] {
for fancy in [true, false] {
let camera = glam::DVec3::new(x, y, z);
let prepared = clouds.prepare(
camera,
time.floor() as i64,
time.fract(),
fancy,
6,
[0.5; 2],
);
// entity.wgsl: (corner + offset) + the cell size on the far sides.
let [ox, oy, oz] = prepared.offset;
let moved: Vec<_> = clouds
.mesh
.iter()
.map(|v| {
let far = |axis: usize, size: f32| {
((v.light >> (8 + 8 * axis)) & 255) as f32 * size
};
let p = [
v.pos[0] + ox + far(0, 12.0),
v.pos[1] + oy + far(1, 4.0),
v.pos[2] + oz + far(2, 12.0),
];
(p, SHADES[(v.light & 255) as usize])
})
.collect();
let expected = built_in_place(&clouds, camera, time, fancy, 6);
assert!(!expected.is_empty());
let bits = |m: &[([f32; 3], f32)]| -> Vec<_> {
m.iter()
.map(|(p, s)| (p.map(f32::to_bits), s.to_bits()))
.collect()
};
assert_eq!(
bits(&moved),
bits(&expected),
"camera {camera} time {time} fancy {fancy}"
);
}
}
}
}
#[test]
fn mesh_is_rebuilt_only_when_vanilla_rebuilds_it() {
let mut clouds = sky();
let prepare = |c: &mut Clouds, x: f64, y: f64, time: i64, fancy: bool, radius: i32| {
c.prepare(
glam::DVec3::new(x, y, 0.5),
time,
0.0,
fancy,
radius,
[0.5; 2],
)
.generation
};
let first = prepare(&mut clouds, 0.5, 100.0, 0, true, 6);
// Moving and drifting inside the cell: translated, not rebuilt.
assert_eq!(prepare(&mut clouds, 4.0, 120.0, 20, true, 6), first);
// Another cell, either by moving or by drifting with time.
let moved = prepare(&mut clouds, 20.0, 120.0, 20, true, 6);
assert_ne!(moved, first);
let drifted = prepare(&mut clouds, 20.0, 120.0, 420, true, 6);
assert_ne!(drifted, moved);
// Into the layer, above it, a cloud type and a Cloud Distance change.
let mut last = drifted;
for (y, fancy, radius) in [
(194.0, true, 6),
(200.0, true, 6),
(200.0, false, 6),
(200.0, false, 8),
] {
let generation = prepare(&mut clouds, 20.0, y, 420, fancy, radius);
assert_ne!(generation, last);
last = generation;
}
assert_eq!(prepare(&mut clouds, 20.0, 300.0, 420, false, 8), last);
}
#[test]
fn shaded_colors_pack_like_vertex_colors() {
assert_eq!(
shaded_colors([1.0; 4]),
[u32::MAX, 0xE5E5_E5E5, 0xCCCC_CCCC, 0xB2B2_B2B2]
);
assert_eq!(shaded_colors([0.5, 0.25, 2.0, 0.8])[0], 0xCCFF_3F7F);
}
}
+11 -2
View File
@@ -19,6 +19,9 @@ struct Globals {
crumbling_sprite: vec4<f32>,
// Selection outline parameters (read by terrain.wgsl).
outline_params: vec4<f32>,
// CloudRenderer CloudInfo: xyz the cloud mesh offset; the cloud colour per face shade.
cloud_offset: vec4<f32>,
cloud_colors: vec4<u32>,
}
@group(0) @binding(0) var<uniform> globals: Globals;
@group(0) @binding(1) var atlas: texture_2d<f32>;
@@ -58,9 +61,15 @@ fn overlay_texel(overlay: u32) -> vec4<f32> {
if overlay >= 16u && overlay < 32u { return vec4(1.0, 1.0, 1.0, WHITE_OVERLAY_ALPHA[overlay - 16u]); }
return vec4(1.0);
}
@vertex fn vs_entity(@location(0) position: vec3<f32>, @location(1) uv: vec2<f32>,
@location(2) overlay: u32, @location(3) color: vec4<f32>, @location(4) light: vec4<u32>, @location(5) normal: vec4<f32>) -> Out {
@vertex fn vs_entity(@location(0) vertex_position: vec3<f32>, @location(1) uv: vec2<f32>,
@location(2) overlay: u32, @location(3) vertex_color: vec4<f32>, @location(4) light: vec4<u32>, @location(5) normal: vec4<f32>) -> Out {
var out: Out;
// Clouds: the cached cell corners moved into place, then the cell size on
// the far sides (light.yzw), coloured per face shade (light.x).
let cloud = overlay == 4u;
let far = vec3<f32>(light.yzw) * vec3(12.0, 4.0, 12.0);
let position = select(vertex_position, vertex_position + globals.cloud_offset.xyz + far, cloud);
let color = select(vertex_color, unpack4x8unorm(globals.cloud_colors[min(light.x, 3u)]), cloud);
out.clip = globals.view_proj * vec4(position, 1.0);
out.proj = vec4(out.clip.xy * 0.5 + out.clip.w * 0.5, out.clip.zw);
out.uv = uv;
+26 -118
View File
@@ -661,8 +661,8 @@ pub struct Resources {
end_environment: Environment,
behaviours: Behaviours,
names: Names,
/// clouds.png: (width, height, filled cells).
clouds: (u32, u32, Vec<bool>),
/// clouds.png and the cloud mesh built from it.
clouds: horizon_client::clouds::Clouds,
/// misc/forcefield.png: (width, height, RGBA) for the world border wall.
forcefield: (u32, u32, Vec<u8>),
context: Arc<WorkerContext>,
@@ -6052,111 +6052,25 @@ impl Game<'_> {
}
out
}
/// CloudRenderer: flat or 12x4x12 extruded cells drifting east.
fn cloud_geometry(
&self,
sky: &horizon_client::environment::Sky,
/// CloudRenderer.prepare: flat or 12x4x12 extruded cells drifting east,
/// rebuilt only when the camera changes cell or side of the cloud layer.
fn prepare_clouds(
&mut self,
camera: glam::DVec3,
alpha: f64,
out: &mut Vec<Vertex>,
) {
let (w, h, cells) = &self.res.clouds;
let (w, h) = (i64::from(*w), i64::from(*h));
let Some(white) = self.res.atlas.sprites.get("horizon/white").copied() else {
return;
};
) -> Option<horizon_client::clouds::Prepared> {
let white = self.res.atlas.sprites.get("horizon/white").copied()?;
let uv = [(white.u0 + white.u1) / 2.0, (white.v0 + white.v1) / 2.0];
let cloud_height = 192.33;
let offset = (self.world.game_time.rem_euclid(w * 400)) as f64 + alpha;
let mut cloud_x = camera.x + offset * 0.030_000_001;
let mut cloud_z = camera.z + 3.96;
let (tw, th) = (w as f64 * 12.0, h as f64 * 12.0);
cloud_x -= (cloud_x / tw).floor() * tw;
cloud_z -= (cloud_z / th).floor() * th;
let cell_x = (cloud_x / 12.0).floor() as i64;
let cell_z = (cloud_z / 12.0).floor() as i64;
let x_in = (cloud_x - cell_x as f64 * 12.0) as f32;
let z_in = (cloud_z - cell_z as f64 * 12.0) as f32;
let bottom = (cloud_height - camera.y) as f32;
let filled = |cx: i64, cz: i64| cells[(cz.rem_euclid(h) * w + cx.rem_euclid(w)) as usize];
// The Cloud Distance option, not the render distance.
let radius = horizon_client::clouds::radius_cells(self.settings.cloud_range);
let c = sky.cloud_color;
let color = |shade: f32| {
let ch = |v: f32| ((v * shade).clamp(0.0, 1.0) * 255.0) as u32;
ch(c[3].clamp(0.0, 1.0)) << 24 | ch(c[2]) << 16 | ch(c[1]) << 8 | ch(c[0])
};
let light = 240 | 240 << 8;
for (dx, dz) in horizon_client::clouds::cells(radius) {
let (dx, dz) = (i64::from(dx), i64::from(dz));
let (cx, cz) = (cell_x + dx, cell_z + dz);
if !filled(cx, cz) {
continue;
}
let x0 = dx as f32 * 12.0 - x_in;
let z0 = dz as f32 * 12.0 - z_in;
let (x1, z1, y0, y1) = (x0 + 12.0, z0 + 12.0, bottom, bottom + 4.0);
let mut quad = |p: [[f32; 3]; 4], shade: f32| {
for v in p {
// Overlay 4: clouds (unlit, faded by the cloud fog end).
out.push(Vertex {
normal: [0; 4],
pos: v,
uv,
layer: 4,
color: color(shade),
light,
});
}
};
if self.settings.clouds == "fast" {
for p in horizon_client::clouds::flat_cell(x0, z0, bottom) {
quad(p, 1.0);
}
continue;
}
let faces = [
(
[[x1, y0, z0], [x1, y0, z1], [x0, y0, z1], [x0, y0, z0]],
0.7,
),
(
[[x0, y1, z0], [x0, y1, z1], [x1, y1, z1], [x1, y1, z0]],
1.0,
),
(
[[x0, y0, z0], [x0, y1, z0], [x1, y1, z0], [x1, y0, z0]],
0.8,
),
(
[[x1, y0, z1], [x1, y1, z1], [x0, y1, z1], [x0, y0, z1]],
0.8,
),
(
[[x0, y0, z1], [x0, y1, z1], [x0, y1, z0], [x0, y0, z0]],
0.9,
),
(
[[x1, y0, z0], [x1, y1, z0], [x1, y1, z1], [x1, y0, z1]],
0.9,
),
];
let mask = horizon_client::clouds::face_mask(
dx as i32,
dz as i32,
bottom,
[
!filled(cx, cz - 1),
!filled(cx, cz + 1),
!filled(cx - 1, cz),
!filled(cx + 1, cz),
],
);
for (direction, inside) in horizon_client::clouds::faces(mask) {
let (p, shade) = faces[usize::from(direction)];
quad(if inside { [p[3], p[2], p[1], p[0]] } else { p }, shade);
}
}
Some(self.res.clouds.prepare(
camera,
self.world.game_time,
alpha,
self.settings.clouds != "fast",
radius,
uv,
))
}
/// Load (or create) a world folder under saves/ and start streaming it.
fn open_world(
@@ -8575,11 +8489,12 @@ impl Game<'_> {
if !title {
self.end_geometry(camera, &mut entities);
}
let mut clouds = None;
if (!title || live) && self.dim == Dim::Overworld {
if horizon_client::clouds::visible(&self.settings.clouds, sky.cloud_color[3]) {
horizon_client::spans::time("clouds", || {
self.cloud_geometry(&sky, camera, alpha, &mut entities)
});
clouds =
horizon_client::spans::time("clouds", || self.prepare_clouds(camera, alpha))
.map(|p| (p, horizon_client::clouds::shaded_colors(sky.cloud_color)));
}
if self.settings.horizon.weather {
horizon_client::spans::time("weather", || {
@@ -8641,6 +8556,11 @@ impl Game<'_> {
entities: &entities,
additive: &additive,
see_through: &see_through,
clouds: clouds.map(|(prepared, colors)| horizon_client::gfx::CloudDraw {
mesh: &self.res.clouds.mesh,
prepared,
colors,
}),
celestial: if self.settings.horizon.sky || title {
&celestial
} else {
@@ -9797,18 +9717,6 @@ impl ApplicationHandler for Game<'_> {
self.finish_perf();
}
}
/// clouds.png as filled cells (`CloudRenderer.prepare`: alpha 10 or more).
fn load_clouds(root: &std::path::Path) -> (u32, u32, Vec<bool>) {
horizon_client::hud::decode(&root.join("assets/minecraft/textures/environment/clouds.png"))
.map(|(w, h, px)| {
(
w,
h,
px.as_chunks::<4>().0.iter().map(|p| p[3] >= 10).collect(),
)
})
.unwrap_or((1, 1, vec![false]))
}
/// misc/forcefield.png for the world border wall (square, power of two).
fn load_forcefield(root: &std::path::Path) -> (u32, u32, Vec<u8>) {
horizon_client::hud::decode(&root.join("assets/minecraft/textures/misc/forcefield.png"))
@@ -9955,7 +9863,7 @@ pub fn run() -> Result<()> {
end_environment: Environment::load_dimension(&root, "the_end", false)?,
behaviours: Behaviours::load(registry_owner.registry, &root)?,
names: Names::load(&root),
clouds: load_clouds(&root),
clouds: horizon_client::clouds::Clouds::load(&root),
forcefield: load_forcefield(&root),
mob_types: MobTypes::load(&root)?
.types
+1 -1
View File
@@ -144,7 +144,7 @@ impl Game<'_> {
self.res.atlas = atlas;
self.res.panorama = panorama;
self.res.item_renderer = std::sync::Arc::new(item_renderer);
self.res.clouds = super::load_clouds(root);
self.res.clouds = horizon_client::clouds::Clouds::load(root);
self.res.forcefield = super::load_forcefield(root);
self.res.names = super::Names::load(root);
horizon_client::ui::load_metadata(root);
+78 -25
View File
@@ -86,6 +86,10 @@ struct Globals {
crumbling_sprite: [f32; 4],
/// Framebuffer size, selection stroke width, perspective layering scale.
outline_params: [f32; 4],
/// `CloudInfo`: xyz the cloud mesh offset (-xInCell, relativeBottomY, -zInCell), w unused.
cloud_offset: [f32; 4],
/// The cloud colour per face shade (`clouds::SHADES`), packed RGBA8.
cloud_colors: [u32; 4],
}
pub type SectionKey = (i32, i32, i32);
struct SectionGpu {
@@ -148,6 +152,8 @@ pub struct Frame<'a> {
pub additive: &'a [Vertex],
/// Name tags over everything, without depth (`TEXT_SEE_THROUGH`).
pub see_through: &'a [Vertex],
/// The clouds, when drawn.
pub clouds: Option<CloudDraw<'a>>,
/// Camera-relative sun, moon and stars (drawn behind everything, additive).
pub celestial: &'a [Vertex],
/// First-person hand and held item, drawn after clearing depth.
@@ -200,6 +206,15 @@ pub struct ShaderInputs {
pub day_fraction: f32,
pub partial_tick: f32,
}
/// One frame of the clouds (`CloudRenderer.prepare`).
#[derive(Clone, Copy, Debug)]
pub struct CloudDraw<'a> {
/// The cached cell mesh; uploaded only when its generation changes.
pub mesh: &'a [Vertex],
pub prepared: crate::clouds::Prepared,
/// `clouds::shaded_colors` of the cloud colour.
pub colors: [u32; 4],
}
/// One frame of the world border wall (`WorldBorderRenderer.render`).
#[derive(Clone, Debug)]
pub struct BorderDraw {
@@ -257,6 +272,11 @@ pub struct Renderer {
entity_pipeline: wgpu::RenderPipeline,
crumbling_pipeline: wgpu::RenderPipeline,
cloud_pipeline: wgpu::RenderPipeline,
/// The cloud mesh, its capacity in vertices, its generation and quads.
cloud_buffer: Option<wgpu::Buffer>,
cloud_capacity: u64,
cloud_generation: Option<u64>,
cloud_quads: u32,
additive_pipeline: wgpu::RenderPipeline,
see_through_pipeline: wgpu::RenderPipeline,
celestial_pipeline: wgpu::RenderPipeline,
@@ -1471,6 +1491,10 @@ impl Renderer {
entity_pipeline,
crumbling_pipeline,
cloud_pipeline,
cloud_buffer: None,
cloud_capacity: 0,
cloud_generation: None,
cloud_quads: 0,
additive_pipeline,
see_through_pipeline,
celestial_pipeline,
@@ -1535,6 +1559,31 @@ impl Renderer {
present_modes,
})
}
/// `CloudRenderer`: upload the cloud mesh only when it was rebuilt.
fn upload_clouds(&mut self, clouds: CloudDraw) {
if self.cloud_generation == Some(clouds.prepared.generation) {
return;
}
self.cloud_generation = Some(clouds.prepared.generation);
self.cloud_quads = (clouds.mesh.len() / 4) as u32;
let vertices = clouds.mesh.len() as u64;
if vertices == 0 {
return;
}
if vertices > self.cloud_capacity {
self.cloud_capacity = vertices.next_power_of_two();
self.cloud_buffer = Some(self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Cloud vertices"),
size: self.cloud_capacity * std::mem::size_of::<Vertex>() as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
}));
}
if let Some(buffer) = &self.cloud_buffer {
self.queue
.write_buffer(buffer, 0, bytemuck::cast_slice(clouds.mesh));
}
}
fn quad_indices(device: &wgpu::Device, quads: u32) -> wgpu::Buffer {
let indices: Vec<u32> = (0..quads)
.flat_map(|q| {
@@ -2069,6 +2118,11 @@ impl Renderer {
0.0,
],
outline_params: outline_params(self.config.width, self.config.height),
cloud_offset: frame.clouds.map_or([0.0; 4], |c| {
let [x, y, z] = c.prepared.offset;
[x, y, z, 0.0]
}),
cloud_colors: frame.clouds.map_or([0; 4], |c| c.colors),
};
self.queue
.write_buffer(&self.globals, 0, bytemuck::bytes_of(&globals));
@@ -2242,19 +2296,21 @@ impl Renderer {
self.queue
.write_buffer(&self.outline, 0, bytemuck::cast_slice(&lines));
}
let [
entity_quads,
crumbling_quads,
translucent_crumbling_quads,
cloud_quads,
] = self.entity_batch.update(
frame.entities,
frame.additive,
frame.hand,
frame.see_through,
);
let world_quads =
entity_quads + crumbling_quads + translucent_crumbling_quads + cloud_quads;
let [entity_quads, crumbling_quads, translucent_crumbling_quads] =
self.entity_batch.update(
frame.entities,
frame.additive,
frame.hand,
frame.see_through,
);
let world_quads = entity_quads + crumbling_quads + translucent_crumbling_quads;
let cloud_quads = match frame.clouds {
Some(clouds) => {
self.upload_clouds(clouds);
self.cloud_quads
}
None => 0,
};
let all_entities = &self.entity_batch.combined;
let hand_quads = (frame.hand.len() / 4) as u32;
let additive_quads = (frame.additive.len() / 4) as u32;
@@ -2281,8 +2337,9 @@ impl Renderer {
bytemuck::cast_slice(&frame.celestial[..celestial_vertices]),
);
}
if world_quads + additive_quads + hand_quads > self.index_quads {
self.index_quads = (world_quads + additive_quads + hand_quads).next_power_of_two();
let quads = (world_quads + additive_quads + hand_quads).max(cloud_quads);
if quads > self.index_quads {
self.index_quads = quads.next_power_of_two();
self.indices = Self::quad_indices(&self.device, self.index_quads);
}
if let Some(b) = &frame.border {
@@ -2459,15 +2516,11 @@ impl Renderer {
0..1,
);
}
if cloud_quads > 0 {
if let (true, Some(buffer)) = (cloud_quads > 0, &self.cloud_buffer) {
pass.set_pipeline(&self.cloud_pipeline);
pass.set_bind_group(0, &self.entity_group, &[]);
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
pass.draw_indexed(
0..cloud_quads * 6,
((entity_quads + crumbling_quads + translucent_crumbling_quads) * 4) as i32,
0..1,
);
pass.set_vertex_buffer(0, buffer.slice(..));
pass.draw_indexed(0..cloud_quads * 6, 0, 0..1);
}
// LevelRenderer.executeSeeThrough, after the rest of the world.
if see_through_quads > 0 {
@@ -2900,14 +2953,14 @@ mod tests {
normal: [0; 4],
}; 4]
};
let entities: Vec<_> = [(0, 0), (4, 1), (11, 2), (7, 3), (4, 4), (12, 7)]
let entities: Vec<_> = [(0, 0), (2, 1), (11, 2), (7, 3), (6, 4), (12, 7)]
.into_iter()
.flat_map(|(layer, id)| quad(layer, id))
.collect();
let mut batch = super::EntityBatch::default();
let counts = batch.update(&entities, &quad(3, 5), &quad(0, 6), &quad(0, 8));
let vertices = &batch.combined;
assert_eq!(counts, [2, 1, 1, 2]);
assert_eq!(counts, [4, 1, 1]);
assert_eq!(
vertices
.as_chunks::<4>()
@@ -2915,7 +2968,7 @@ mod tests {
.iter()
.map(|q| q[0].light)
.collect::<Vec<_>>(),
[0, 3, 2, 7, 1, 4, 5, 6, 8]
[0, 1, 3, 4, 2, 7, 5, 6, 8]
);
}
#[test]
@@ -6,7 +6,7 @@ pub(super) struct EntityBatch {
// Quad indices avoid copying vertices twice while grouping materials.
// Keep initialized high-water storage; local counts delimit active entries.
// Each bucket can hold all quads, removing per-quad Vec growth checks.
buckets: [Vec<usize>; 4],
buckets: [Vec<usize>; 3],
pub(super) combined: Vec<Vertex>,
}
@@ -17,19 +17,18 @@ impl EntityBatch {
additive: &[Vertex],
hand: &[Vertex],
see_through: &[Vertex],
) -> [u32; 4] {
) -> [u32; 3] {
let quads = entities.as_chunks::<4>().0;
for bucket in &mut self.buckets {
if bucket.len() < quads.len() {
bucket.resize(quads.len(), 0);
}
}
let mut counts = [0usize; 4];
let mut counts = [0usize; 3];
for (index, quad) in quads.iter().enumerate() {
let material = match quad[0].layer {
11 => 1,
12 => 2,
4 => 3,
_ => 0,
};
self.buckets[material][counts[material]] = index;
@@ -78,15 +77,14 @@ mod tests {
entities: &[Vertex],
additive: &[Vertex],
hand: &[Vertex],
) -> (Vec<Vertex>, [u32; 4]) {
) -> (Vec<Vertex>, [u32; 3]) {
let mut combined = Vec::with_capacity(entities.len() + additive.len() + hand.len());
let mut counts = [0; 4];
for (i, layer) in [0, 11, 12, 4].into_iter().enumerate() {
let mut counts = [0; 3];
for (i, layer) in [0, 11, 12].into_iter().enumerate() {
for quad in entities.as_chunks::<4>().0 {
let material = match quad[0].layer {
11 => 11,
12 => 12,
4 => 4,
_ => 0,
};
if material == layer {
+3
View File
@@ -21,6 +21,9 @@ struct Globals {
// Atlas rect of the current destroy stage (read by entity.wgsl).
crumbling_sprite: vec4<f32>,
outline_params: vec4<f32>,
// CloudRenderer CloudInfo: xyz the cloud mesh offset; the cloud colour per face shade.
cloud_offset: vec4<f32>,
cloud_colors: vec4<u32>,
}
@group(0) @binding(0) var<uniform> globals: Globals;
@group(0) @binding(1) var sprites: texture_2d_array<f32>;