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:
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user