From 583a535806f380bc8378cc81c4de2ce80b703aaa Mon Sep 17 00:00:00 2001 From: supersurviveur Date: Fri, 25 Sep 2026 09:43:20 +0200 Subject: [PATCH] feat: allow bigger render distance in 3d view --- src/app.rs | 31 ++++++++++++++++++++++++++++--- src/camera.rs | 8 ++++++-- src/fractal/renderer.rs | 5 +++-- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/app.rs b/src/app.rs index 49912d0..c81d824 100644 --- a/src/app.rs +++ b/src/app.rs @@ -503,6 +503,10 @@ pub struct FractalApp { // Use shadow coloring // Use 3D raymarching rendering 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 lights: Vec, @@ -716,6 +720,7 @@ impl FractalApp { worker: crate::worker::RefWorker::spawn(), pending: false, export_scale: 2.0, + render_scale_3d: 2.0, last_size_px: egui::vec2(1280.0, 720.0), last_interact_time: -1.0e9, export_requested: false, @@ -1360,7 +1365,10 @@ impl FractalApp { /// 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) { 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.); 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), camera_inv_proj: self .camera - .orthographic(self.anim.camera_state) + .orthographic(self.anim.camera_state, self.render_scale_3d) .inverse() .to_cols_array(), 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, 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.separator(); @@ -2832,7 +2856,8 @@ impl FractalApp { ]; 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()]; diff --git a/src/camera.rs b/src/camera.rs index 4328f0b..a6ea302 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -53,11 +53,15 @@ impl Camera { 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 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 // `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.)) diff --git a/src/fractal/renderer.rs b/src/fractal/renderer.rs index ee042a3..0ac355a 100644 --- a/src/fractal/renderer.rs +++ b/src/fractal/renderer.rs @@ -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, /// 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 /// what native gets, so the web budget is ~4K (≈300 MB); native, ~8K. #[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 // (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. // Also cap the total pixel count (`MAX_CACHE_PIXELS`), same way. let max_dim = device.limits().max_texture_dimension_2d;