Clouds: draw to the Cloud Distance option, not the render distance (#67)

The cloud mesh radius came from the render distance, the far plane was
capped at max(4 x render distance, 1024) and the cloud fog end was a
constant 2048, so lowering the render distance cut the clouds short.
Vanilla ties all three to options.cloudRange: CloudRenderer.prepare
builds ceil(range * 16 / 12) cells, Camera.depthFar is
max(4 x render distance, range * 16) (also the hand projection, the
frustum and the world border), and AtmosphericFogEnvironment ends
cloud fog at min(range * 16, visual/cloud_fog_end_distance).

The cloud render harness now checks the radius for Cloud Distance
2..128 against the shipped mesh builder, and the environment harness
covers visual/cloud_fog_end_distance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ludovic Marie
2026-10-14 14:38:52 +02:00
parent 6c1000a374
commit 12eca8a5c1
11 changed files with 95 additions and 14 deletions
@@ -20,10 +20,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
write!(output, " {x},{z},{direction},{}", u8::from(inside)).unwrap();
}
};
let radius = if *kind == 2 {
clouds::radius_cells(*x)
} else {
*x
};
if *kind == 0 {
cell(*x, *z, *neighbors);
} else {
for (x, z) in clouds::cells(*x) {
for (x, z) in clouds::cells(radius) {
cell(x, z, 0);
}
}
@@ -64,6 +64,10 @@ fn main() -> horizon_client::Result<()> {
);
put("visual/fog_start_distance", format!("{:?}", sky.fog_start));
put("visual/fog_end_distance", format!("{:?}", sky.fog_end));
put(
"visual/cloud_fog_end_distance",
format!("{:?}", sky.cloud_fog_end),
);
out.push_str(&row);
out.push_str("}\n");
}
+13
View File
@@ -590,6 +590,12 @@ impl Camera {
modify_fov_based_on_death_or_fluid(partial, 70.0, effect_scale, dead, fluid)
}
}
/// `Camera.update`: the far plane covers four times the render distance and
/// the whole cloud range (`depthFar`).
pub fn depth_far(render_distance_chunks: i32, cloud_range_chunks: i32) -> f32 {
(render_distance_chunks as f32 * 16.0 * 4.0).max((cloud_range_chunks * 16) as f32)
}
/// `Projection.getMatrix` for a perspective set up by `setupPerspective`:
/// JOML's `Matrix4f.setPerspective(fov * DEG_TO_RAD, width / height, near,
/// far, zZeroToOne)`, column-major. Vanilla passes its planes reversed
@@ -768,6 +774,13 @@ pub fn bob_matrix(state: &BobState, damage_tilt_strength: f64, bob_view_option:
#[cfg(test)]
mod tests {
#[test]
fn depth_far_covers_the_cloud_range() {
// renderDistance 2 with the default Cloud Distance (128 chunks).
assert_eq!(super::depth_far(2, 128), 2048.0);
assert_eq!(super::depth_far(32, 2), 2048.0);
assert_eq!(super::depth_far(12, 12), 768.0);
}
use super::*;
#[test]
fn view_keeps_yaw_at_vertical_pitch_limits() {
+12
View File
@@ -32,6 +32,12 @@ pub fn faces(mask: u16) -> impl Iterator<Item = (u8, bool)> {
)
}
/// `CloudRenderer.prepare`: the cell radius for the Cloud Distance option
/// (in chunks), independent of the render distance.
pub fn radius_cells(cloud_range_chunks: i32) -> i32 {
((cloud_range_chunks * 16) as f32 / 12.0).ceil() as i32
}
/// CloudRenderer builds cells in expanding Manhattan rings, nearest first.
pub fn cells(radius: i32) -> impl Iterator<Item = (i32, i32)> {
(0..=2 * radius).flat_map(move |ring| {
@@ -71,6 +77,12 @@ mod tests {
assert_eq!(face_mask(2, 2, 0.0, [false; 4]), 3);
}
#[test]
fn cloud_distance_sets_the_radius() {
assert_eq!(radius_cells(2), 3);
assert_eq!(radius_cells(12), 16);
assert_eq!(radius_cells(128), 171);
}
#[test]
fn connected_cells_have_no_exterior_walls() {
assert_eq!(face_mask(2, 2, 10.0, [false; 4]), 1);
}
+6
View File
@@ -139,6 +139,8 @@ pub struct Sky {
/// Environmental fog (`visual/fog_start_distance` / `fog_end_distance`).
pub fog_start: f32,
pub fog_end: f32,
/// `visual/cloud_fog_end_distance`.
pub cloud_fog_end: f32,
}
impl Environment {
pub fn load(root: &Path) -> Result<Self> {
@@ -299,6 +301,10 @@ impl Environment {
.base
.get("minecraft:visual/fog_end_distance")
.map_or(1024.0, |v| v[0]),
cloud_fog_end: self
.base
.get("minecraft:visual/cloud_fog_end_distance")
.map_or(2048.0, |v| v[0]),
}
}
}
+27 -1
View File
@@ -196,6 +196,8 @@ pub struct FogInputs<'a> {
pub water_fog_color: [f32; 3],
pub biome: u16,
pub render_distance_chunks: i32,
/// The Cloud Distance option, in chunks.
pub cloud_range_chunks: i32,
pub camera_y: f64,
pub min_y: i32,
/// Flat worlds use a 1-block void darkness range.
@@ -335,7 +337,7 @@ pub fn setup_fog(tables: &FogTables, state: &FogState, input: &FogInputs) -> Fog
let min_end = sky.fog_end.min(96.0);
fog.environmental_end = min_end.max(sky.fog_end + -256.0 * state.rain_fog_multiplier);
fog.sky_end = rd.min(512.0);
fog.cloud_end = 2048.0f32.min(128.0 * 16.0);
fog.cloud_end = ((input.cloud_range_chunks * 16) as f32).min(sky.cloud_fog_end);
}
}
let span = (rd / 10.0).clamp(4.0, 64.0);
@@ -373,6 +375,7 @@ mod tests {
water_fog_color: [0.0, 0.0, 0.2],
biome: 0,
render_distance_chunks: 12,
cloud_range_chunks: 128,
camera_y: 70.0,
min_y: -64,
flat: false,
@@ -402,6 +405,29 @@ mod tests {
assert_eq!(fog.sky_end, 192.0);
}
#[test]
fn cloud_fog_ends_at_the_cloud_distance() {
let sky = Sky {
fog_end: 1024.0,
cloud_fog_end: 2048.0,
..Sky::default()
};
let tables = FogTables {
water_fog_end: vec![1.0],
};
let mut input = inputs(&sky);
input.render_distance_chunks = 2;
input.cloud_range_chunks = 64;
assert_eq!(
setup_fog(&tables, &FogState::default(), &input).cloud_end,
1024.0
);
input.cloud_range_chunks = 128;
assert_eq!(
setup_fog(&tables, &FogState::default(), &input).cloud_end,
2048.0
);
}
#[test]
fn water_vision_ramps() {
let mut s = FogState::default();
for _ in 0..100 {
+8 -3
View File
@@ -5090,7 +5090,8 @@ impl Game<'_> {
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];
let radius = (self.streaming.radius * 16 + 11) / 12;
// 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;
@@ -7319,10 +7320,12 @@ impl Game<'_> {
self.bob_matrix(alpha as f32)
};
let distance = (self.streaming.radius * 16) as f32;
let depth_far =
horizon_client::camera::depth_far(self.streaming.radius, self.settings.cloud_range);
self.view_frustum = self
.renderer
.as_ref()
.map(|r| r.camera_frustum(fov_degrees, yaw, pitch, bob, distance));
.map(|r| r.camera_frustum(fov_degrees, yaw, pitch, bob, depth_far));
let mut entities = if title {
Vec::new()
} else {
@@ -7394,6 +7397,7 @@ impl Game<'_> {
bob,
sky,
distance,
depth_far,
underwater,
title: title && !live,
skybox: self.dim == Dim::Overworld && self.settings.horizon.sky,
@@ -7480,7 +7484,8 @@ impl Game<'_> {
&mut self.border_state,
);
// Camera.depthFar, as the renderer's far plane.
let depth_far = (distance as f32 * 4.0).max(1024.0);
let depth_far =
horizon_client::camera::depth_far(self.streaming.radius, self.settings.cloud_range);
if !self
.border_renderer
.prepare(&self.border_state, camera, distance, depth_far)
@@ -83,6 +83,7 @@ impl Game<'_> {
],
biome,
render_distance_chunks: self.streaming.radius,
cloud_range_chunks: self.settings.cloud_range,
camera_y: camera.y,
min_y: MIN_Y,
flat: false,
+7 -6
View File
@@ -123,6 +123,8 @@ pub struct Frame<'a> {
pub sky: Sky,
/// Render distance in blocks (fog end).
pub distance: f32,
/// `Camera.depthFar`: the far plane (`crate::camera::depth_far`).
pub depth_far: f32,
pub underwater: bool,
/// Skip sections the camera cannot see through open faces (vanilla's
/// occlusion graph; off in spectator-like views inside blocks).
@@ -1614,7 +1616,7 @@ impl Renderer {
frame.yaw,
frame.pitch,
frame.bob,
frame.distance,
frame.depth_far,
)
}
/// The view frustum `render` will cull with, for culling geometry built
@@ -1625,15 +1627,14 @@ impl Renderer {
yaw: f32,
pitch: f32,
bob: Mat4,
distance: f32,
depth_far: f32,
) -> Frustum {
Frustum {
planes: frustum(self.projection(fov_degrees, yaw, pitch, bob, distance)),
planes: frustum(self.projection(fov_degrees, yaw, pitch, bob, depth_far)),
}
}
fn projection(&self, fov_degrees: f32, yaw: f32, pitch: f32, bob: Mat4, distance: f32) -> Mat4 {
fn projection(&self, fov_degrees: f32, yaw: f32, pitch: f32, bob: Mat4, far: f32) -> Mat4 {
let aspect = self.config.width as f32 / self.config.height.max(1) as f32;
let far = (distance * 4.0).max(1024.0);
Mat4::perspective_rh(fov_degrees.to_radians(), aspect, 0.05, far)
* bob
* crate::camera::view_matrix(yaw, pitch)
@@ -1865,7 +1866,7 @@ impl Renderer {
frame.hand_fov_degrees,
self.config.width as f32,
self.config.height.max(1) as f32,
(frame.distance * 4.0).max(1024.0),
frame.depth_far,
));
let hand_globals = Globals {
view_proj: (hand_projection * frame.bob * view).to_cols_array_2d(),
@@ -29,13 +29,17 @@ public final class VanillaCloudRenderOracle {
int kind = Integer.parseInt(parts[0]), x = Integer.parseInt(parts[1]), z = Integer.parseInt(parts[2]);
int bottom = Integer.parseInt(parts[3]), neighbors = Integer.parseInt(parts[4]);
Object pos = positions[bottom > 0 ? 2 : bottom + 4 < 0 ? 0 : 1];
ByteBuffer buffer = ByteBuffer.allocate(100000);
ByteBuffer buffer = ByteBuffer.allocate(kind == 2 ? 4_000_000 : 100000);
if (kind == 0) {
long flags = ((neighbors & 1) != 0 ? 8 : 0) | ((neighbors & 2) != 0 ? 2 : 0)
| ((neighbors & 4) != 0 ? 1 : 0) | ((neighbors & 8) != 0 ? 4 : 0);
cell.invoke(renderer, pos, buffer, x, z, 0xfffffff0L | flags);
} else {
} else if (kind == 1) {
mesh.invoke(renderer, pos, buffer, 0, 0, true, x);
} else {
// prepare(..., range, ...): radiusCells from the Cloud Distance option.
int radiusBlocks = x * 16;
mesh.invoke(renderer, pos, buffer, 0, 0, true, net.minecraft.util.Mth.ceil(radiusBlocks / 12.0F));
}
output.append(index++);
buffer.flip();
+5 -1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
"""Compare fancy cloud exterior/interior faces and nearest-first mesh traversal with 26.3."""
"""Compare fancy cloud exterior/interior faces, nearest-first mesh traversal and the
cell radius of the Cloud Distance option (independent of the render distance) with 26.3."""
try:
from . import lab, oracle_harness as harness
except ImportError:
@@ -16,6 +17,9 @@ def make_input(build):
for radius in range(11):
for bottom in (-5, -4, 0, 1):
cases.append(f"1 {radius} 0 {bottom} 0\n")
# CloudRenderer.prepare: radius from options.cloudRange (2..128 chunks).
for cloud_range in (2, 3, 4, 7, 8, 12, 32, 64, 127, 128):
cases.append(f"2 {cloud_range} 0 -5 0\n")
inputs = build / "inputs.txt"
inputs.write_text("".join(cases))
return inputs, len(cases)