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.
|
/// Distance to the near clip plane, in blocks.
|
||||||
const NEAR_PLANE: f32 = 0.1;
|
const NEAR_PLANE: f32 = 0.1;
|
||||||
|
|
||||||
/// Distance to the far clip plane, in blocks.
|
/// Lower bound on the distance to the far clip plane, in blocks.
|
||||||
const FAR_PLANE: f32 = 500.0;
|
///
|
||||||
|
/// 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.
|
/// Linear RGB colour of the empty sky.
|
||||||
const SKY_COLOR: [f32; 3] = [0.1, 0.2, 0.4];
|
const SKY_COLOR: [f32; 3] = [0.1, 0.2, 0.4];
|
||||||
|
|
@ -43,6 +45,17 @@ pub struct FrameParams {
|
||||||
pub fog_end_vertical: f32,
|
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.
|
/// 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.
|
/// 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.
|
/// Issues the actual draw calls for the frame, returning what was submitted.
|
||||||
fn issue_draw_calls(&self, cmd: vk::CommandBuffer, frame: FrameParams) -> Submission {
|
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.
|
// 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(
|
let mvp = glam::camera::rh::proj::vulkan::perspective(
|
||||||
|
|
@ -651,8 +664,8 @@ impl Renderer {
|
||||||
submission
|
submission
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Derives this frame's projection parameters from the swapchain extent.
|
/// Derives this frame's projection parameters from the swapchain extent and the frame's fog distances.
|
||||||
fn projection_info(&self) -> ProjectionInfo {
|
fn projection_info(&self, frame: FrameParams) -> ProjectionInfo {
|
||||||
let aspect =
|
let aspect =
|
||||||
f64::from(self.swapchain_extent.width) / f64::from(self.swapchain_extent.height);
|
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(),
|
fov_y_radians: FOV_Y_DEGREES.to_radians(),
|
||||||
aspect: aspect as f32,
|
aspect: aspect as f32,
|
||||||
near: NEAR_PLANE,
|
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