mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc9dcba5e3 |
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in new issue
Block a user