feat: allow bigger render distance in 3d view

This commit is contained in:
2026-09-25 09:46:36 +02:00
parent ba385be18d
commit 583a535806
3 changed files with 37 additions and 7 deletions
+28 -3
View File
@@ -503,6 +503,10 @@ pub struct FractalApp {
// Use shadow coloring // Use shadow coloring
// Use 3D raymarching rendering // Use 3D raymarching rendering
rendering_mode: u32, rendering_mode: u32,
/// Per-axis scale of the 3D view's height-field texture relative to the
/// widget: higher shows sharper, more distant terrain but costs GPU time
/// and memory.
render_scale_3d: f32,
/// List of enabled lights in the world /// List of enabled lights in the world
lights: Vec<Light>, lights: Vec<Light>,
@@ -716,6 +720,7 @@ impl FractalApp {
worker: crate::worker::RefWorker::spawn(), worker: crate::worker::RefWorker::spawn(),
pending: false, pending: false,
export_scale: 2.0, export_scale: 2.0,
render_scale_3d: 2.0,
last_size_px: egui::vec2(1280.0, 720.0), last_size_px: egui::vec2(1280.0, 720.0),
last_interact_time: -1.0e9, last_interact_time: -1.0e9,
export_requested: false, export_requested: false,
@@ -1360,7 +1365,10 @@ impl FractalApp {
/// then zoom the 2D view about the matching fractal-texture pixel. /// then zoom the 2D view about the matching fractal-texture pixel.
fn zoom_3d_at(&mut self, off: egui::Vec2, rect: egui::Rect, height_px: f64, factor: f64) { fn zoom_3d_at(&mut self, off: egui::Vec2, rect: egui::Rect, height_px: f64, factor: f64) {
let ndc = (off / rect.size()) * 2.; let ndc = (off / rect.size()) * 2.;
let camera_ndc_pos = self.camera.orthographic(self.anim.camera_state).inverse() let camera_ndc_pos = self
.camera
.orthographic(self.anim.camera_state, self.render_scale_3d)
.inverse()
* Vec4::new(ndc.x, ndc.y, 0., 1.); * Vec4::new(ndc.x, ndc.y, 0., 1.);
let view_direction = self.camera.direction(self.anim.camera_state); let view_direction = self.camera.direction(self.anim.camera_state);
@@ -1402,7 +1410,7 @@ impl FractalApp {
morph_w: self.ref_morph.map_or(0.0, |(_, w)| w), morph_w: self.ref_morph.map_or(0.0, |(_, w)| w),
camera_inv_proj: self camera_inv_proj: self
.camera .camera
.orthographic(self.anim.camera_state) .orthographic(self.anim.camera_state, self.render_scale_3d)
.inverse() .inverse()
.to_cols_array(), .to_cols_array(),
screen_dim: self.screen_dim, screen_dim: self.screen_dim,
@@ -2188,6 +2196,22 @@ impl FractalApp {
ui.radio_value(&mut self.rendering_mode, 1, "Shadow"); ui.radio_value(&mut self.rendering_mode, 1, "Shadow");
ui.radio_value(&mut self.rendering_mode, 2, "3D"); ui.radio_value(&mut self.rendering_mode, 2, "3D");
}); });
if self.rendering_mode == 2 {
let old_scale = self.render_scale_3d;
ui.add(egui::Slider::new(&mut self.render_scale_3d, 1.0..=4.0).text("3D render scale"))
.on_hover_text(
"Resolution multiplier of the 3D height field. Higher shows more \
distant detail but costs GPU time and memory.",
);
// The 3D camera zooms in by the render scale (`Camera::orthographic`):
// zoom the view out by the same ratio so the fractal keeps its
// on-screen size and the extra texels become surrounding terrain.
if self.render_scale_3d != old_scale {
let factor = (self.render_scale_3d / old_scale) as f64;
self.view
.zoom_at_pixel(0.0, 0.0, self.last_size_px.y.max(1.0) as f64, factor);
}
}
ui.add_space(4.); ui.add_space(4.);
ui.separator(); ui.separator();
@@ -2832,7 +2856,8 @@ impl FractalApp {
]; ];
if self.effective_rendering_mode() == 2 { if self.effective_rendering_mode() == 2 {
size_px = [size_px[0] * 2, size_px[1] * 2]; let s = self.render_scale_3d;
size_px = size_px.map(|v| ((v as f32 * s).round() as u32).max(1));
} }
self.screen_dim = [rect.width(), rect.height()]; self.screen_dim = [rect.width(), rect.height()];
+6 -2
View File
@@ -53,11 +53,15 @@ impl Camera {
self.pitch = pitch.clamp(-PITCH_LIMIT, PITCH_LIMIT); self.pitch = pitch.clamp(-PITCH_LIMIT, PITCH_LIMIT);
} }
pub fn orthographic(&self, t: f32) -> glam::Mat4 { /// `render_scale` is the 3D texture's per-axis scale (see
/// `FractalApp::render_scale_3d`): the full 3D camera (`t = 1`) zooms in
/// by the same factor, so one texel still covers one screen pixel and the
/// extra texels become terrain beyond the screen edges.
pub fn orthographic(&self, t: f32, render_scale: f32) -> glam::Mat4 {
let yaw = self.yaw * t; let yaw = self.yaw * t;
let pitch = self.pitch * t; let pitch = self.pitch * t;
let zoom = 0.5 * (1. + t); let zoom = 0.5 * (1. + t * (render_scale - 1.));
// Orbit pivot: the center of the fractal texture, which the raymarcher's // Orbit pivot: the center of the fractal texture, which the raymarcher's
// `sdf` lays out over world x ∈ [0, aspect], y ∈ [0, 1] on the z = 0 plane. // `sdf` lays out over world x ∈ [0, aspect], y ∈ [0, 1] on the z = 0 plane.
let view = glam::Mat4::from_translation(Vec3::new(0.5 * self.aspect_ratio, 0.5, 0.)) let view = glam::Mat4::from_translation(Vec3::new(0.5 * self.aspect_ratio, 0.5, 0.))
+3 -2
View File
@@ -29,7 +29,8 @@ const DATA_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba32Float;
/// Cap on the interactive cache's pixel count (the texture is scaled down, /// Cap on the interactive cache's pixel count (the texture is scaled down,
/// aspect kept, above it). Each pixel costs 36 bytes across the data, AA and /// aspect kept, above it). Each pixel costs 36 bytes across the data, AA and
/// colour textures, and the 3D view renders at 2× per axis, so a HiDPI screen /// colour textures, and the 3D view renders at a configurable multiple per axis
/// (default 2×), so a HiDPI screen
/// in 3D would otherwise want 0.5 GB+. Browsers cap WebGPU memory well below /// in 3D would otherwise want 0.5 GB+. Browsers cap WebGPU memory well below
/// what native gets, so the web budget is ~4K (≈300 MB); native, ~8K. /// what native gets, so the web budget is ~4K (≈300 MB); native, ~8K.
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
@@ -1411,7 +1412,7 @@ impl egui_wgpu::CallbackTrait for FractalCallback {
// Clamp to the device's texture-size limit, keeping the aspect ratio // Clamp to the device's texture-size limit, keeping the aspect ratio
// (the iterate pass maps pixels through NDC, so the view is unchanged; // (the iterate pass maps pixels through NDC, so the view is unchanged;
// the blit just upsamples). The 2× 3D supersample on a large/HiDPI // the blit just upsamples). The 3D supersample (default 2×) on a large/HiDPI
// screen can otherwise exceed it. // screen can otherwise exceed it.
// Also cap the total pixel count (`MAX_CACHE_PIXELS`), same way. // Also cap the total pixel count (`MAX_CACHE_PIXELS`), same way.
let max_dim = device.limits().max_texture_dimension_2d; let max_dim = device.limits().max_texture_dimension_2d;