fix(renderer): extend the far plane to cover the fog reach
The fixed 500-block far plane clipped geometry before fog saturated, making the clip plane itself the visible boundary at large view distances.
This commit is contained in:
parent
51b6b1e5e8
commit
10b035504c
|
|
@ -18,8 +18,10 @@ const FOV_Y_DEGREES: f32 = 45.0;
|
|||
/// Distance to the near clip plane, in blocks.
|
||||
const NEAR_PLANE: f32 = 0.1;
|
||||
|
||||
/// Distance to the far clip plane, in blocks.
|
||||
const FAR_PLANE: f32 = 500.0;
|
||||
/// Lower bound on the distance to the far clip plane, in blocks.
|
||||
///
|
||||
/// The far plane is extended past this whenever the frame's fog reaches further (see [`far_plane_for`]). The floor applies when the fog is nearer, and keeps the projection well-formed for a caller that supplies no fog distance at all.
|
||||
const MIN_FAR_PLANE: f32 = 500.0;
|
||||
|
||||
/// Linear RGB colour of the empty sky.
|
||||
const SKY_COLOR: [f32; 3] = [0.1, 0.2, 0.4];
|
||||
|
|
@ -43,6 +45,17 @@ pub struct FrameParams {
|
|||
pub fog_end_vertical: f32,
|
||||
}
|
||||
|
||||
/// Returns the far clip distance covering the fog reach implied by the two extents.
|
||||
///
|
||||
/// The far plane must sit beyond every fragment the fog has not yet fully obscured, or the clip plane becomes the visible boundary and replaces the intended fade with a hard edge. Fog opacity saturates as soon as *either* axis passes its own end distance, so a fragment that is still partially visible lies strictly inside the box those two extents bound; the box's diagonal is therefore the furthest such a fragment can be, and covering it is exactly sufficient rather than merely conservative.
|
||||
///
|
||||
/// [`MIN_FAR_PLANE`] applies as a floor, so a caller supplying no fog distance still receives a usable projection.
|
||||
fn far_plane_for(fog_end_horizontal: f32, fog_end_vertical: f32) -> f32 {
|
||||
fog_end_horizontal
|
||||
.hypot(fog_end_vertical)
|
||||
.max(MIN_FAR_PLANE)
|
||||
}
|
||||
|
||||
/// One rasterisation pass over the visible chunk meshes.
|
||||
///
|
||||
/// A pass corresponds one-to-one with a pipeline object, since polygon mode and depth-compare state are baked into a pipeline and cannot be changed by a command. Passes are the GPU-level primitive; [`RenderMode`] composes them into what is actually presented.
|
||||
|
|
@ -632,7 +645,7 @@ impl Renderer {
|
|||
|
||||
/// Issues the actual draw calls for the frame, returning what was submitted.
|
||||
fn issue_draw_calls(&self, cmd: vk::CommandBuffer, frame: FrameParams) -> Submission {
|
||||
let projection = self.projection_info();
|
||||
let projection = self.projection_info(frame);
|
||||
|
||||
// The view matrix is supplied by the caller (the client's camera); the renderer owns only the projection, which depends on the swapchain aspect ratio it manages.
|
||||
let mvp = glam::camera::rh::proj::vulkan::perspective(
|
||||
|
|
@ -651,8 +664,8 @@ impl Renderer {
|
|||
submission
|
||||
}
|
||||
|
||||
/// Derives this frame's projection parameters from the swapchain extent.
|
||||
fn projection_info(&self) -> ProjectionInfo {
|
||||
/// Derives this frame's projection parameters from the swapchain extent and the frame's fog distances.
|
||||
fn projection_info(&self, frame: FrameParams) -> ProjectionInfo {
|
||||
let aspect =
|
||||
f64::from(self.swapchain_extent.width) / f64::from(self.swapchain_extent.height);
|
||||
|
||||
|
|
@ -664,7 +677,7 @@ impl Renderer {
|
|||
fov_y_radians: FOV_Y_DEGREES.to_radians(),
|
||||
aspect: aspect as f32,
|
||||
near: NEAR_PLANE,
|
||||
far: FAR_PLANE,
|
||||
far: far_plane_for(frame.fog_end_horizontal, frame.fog_end_vertical),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1036,3 +1049,7 @@ impl Drop for Renderer {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/renderer.rs"]
|
||||
mod tests;
|
||||
|
|
|
|||
37
crates/renderer/src/tests/renderer.rs
Normal file
37
crates/renderer/src/tests/renderer.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
//! Unit tests for the pure helpers in [`crate::renderer`].
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn far_plane_floors_at_the_minimum_for_near_fog() {
|
||||
// Fog that saturates well inside the minimum leaves the far plane at the floor; shrinking it to match would clip geometry for no gain.
|
||||
assert!((far_plane_for(128.0, 64.0) - MIN_FAR_PLANE).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn far_plane_covers_the_diagonal_of_the_two_extents() {
|
||||
// 768 horizontal and 384 vertical (a radius-24 cylinder) reach 858.6 at the corner, past the 500-block floor.
|
||||
let far = far_plane_for(768.0, 384.0);
|
||||
assert!(far > MIN_FAR_PLANE);
|
||||
assert!(
|
||||
(far - 858.65_f32).abs() < 0.01,
|
||||
"unexpected far plane {far}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn far_plane_reaches_past_each_extent_taken_alone() {
|
||||
// The corner of the box is further than either edge, so covering only the larger extent would still clip partially-visible fragments near the diagonal.
|
||||
let (horizontal, vertical) = (768.0_f32, 384.0_f32);
|
||||
let far = far_plane_for(horizontal, vertical);
|
||||
assert!(far > horizontal);
|
||||
assert!(far > vertical);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn far_plane_is_well_formed_without_fog() {
|
||||
// A caller that supplies no fog distance must still receive a usable projection rather than a degenerate zero-depth one.
|
||||
assert!(far_plane_for(0.0, 0.0) > 0.0);
|
||||
}
|
||||
Loading…
Reference in a new issue