fix: correct 3d rendering

This commit is contained in:
2026-09-23 18:34:00 +02:00
parent 694f63ab9d
commit 63cee5d8b2
9 changed files with 290 additions and 173 deletions
+164 -114
View File
@@ -4,6 +4,8 @@ use eframe::CreationContext;
use eframe::egui_wgpu; use eframe::egui_wgpu;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
use eframe::egui_wgpu::wgpu; use eframe::egui_wgpu::wgpu;
use glam::Vec4;
use glam::Vec4Swizzles;
use crate::camera::Camera; use crate::camera::Camera;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@@ -205,6 +207,12 @@ struct AnimState {
zoom: bool, zoom: bool,
/// e-folds per second; positive zooms in, negative zooms out. /// e-folds per second; positive zooms in, negative zooms out.
zoom_speed: f32, zoom_speed: f32,
/// Linear 2D <-> 3D transition progress in [0, 1], advanced at a constant
/// rate; `camera_state` is its smoothstep-eased value.
camera_progress: f32,
/// Camera state in [0, 1]: 0 = top-down 2D view, 1 = full 3D camera.
camera_state: f32,
} }
impl Default for AnimState { impl Default for AnimState {
@@ -229,6 +237,8 @@ impl Default for AnimState {
lambda_angle: 0.0, lambda_angle: 0.0,
zoom: false, zoom: false,
zoom_speed: 0.5, zoom_speed: 0.5,
camera_progress: 0.,
camera_state: 0.,
} }
} }
} }
@@ -350,6 +360,8 @@ pub struct FractalApp {
/// The camera used to render 3D fractals /// The camera used to render 3D fractals
camera: Camera, camera: Camera,
/// Screen dimension.
screen_dim: [f32; 2],
} }
/// Significant decimal digits to show for a center at the given precision (bits). /// Significant decimal digits to show for a center at the given precision (bits).
@@ -473,6 +485,7 @@ impl FractalApp {
zoom_edit, zoom_edit,
zoom_edited: false, zoom_edited: false,
camera: Camera::new(), camera: Camera::new(),
screen_dim: [0., 0.],
} }
} }
@@ -497,6 +510,15 @@ impl FractalApp {
} }
self.view = Self::default_view_for(self.mode, self.kind); self.view = Self::default_view_for(self.mode, self.kind);
} }
if let Some(k) = cli.rendering_kind {
use crate::cli::RenderingKindArg;
match k {
RenderingKindArg::Classic => self.rendering_mode = 0,
RenderingKindArg::Shadow => self.rendering_mode = 1,
RenderingKindArg::Dimension3 => self.rendering_mode = 2,
}
}
if let Some(jc) = cli.julia { if let Some(jc) = cli.julia {
let p: Vec<&str> = jc.split(',').collect(); let p: Vec<&str> = jc.split(',').collect();
if let (Some(Ok(re)), Some(Ok(im))) = ( if let (Some(Ok(re)), Some(Ok(im))) = (
@@ -980,11 +1002,21 @@ impl FractalApp {
lambda_l: [self.lambda_l.0 as f32, self.lambda_l.1 as f32], lambda_l: [self.lambda_l.0 as f32, self.lambda_l.1 as f32],
complex_power: [self.complex_power.0 as f32, self.complex_power.1 as f32], complex_power: [self.complex_power.0 as f32, self.complex_power.1 as f32],
de_coloring: (self.de_coloring | (self.rendering_mode > 0)) as u32, de_coloring: (self.de_coloring | (self.rendering_mode > 0)) as u32,
rendering_mode: self.rendering_mode, rendering_mode: if self.anim.camera_state > 0.0 {
camera_direction: self.camera.direction().to_array(), 2
camera_inv_proj: self.camera.orthographic().inverse().to_cols_array(), } else {
self.rendering_mode
},
camera_direction: self.camera.direction(self.anim.camera_state).to_array(),
camera_inv_proj: self
.camera
.orthographic(self.anim.camera_state)
.inverse()
.to_cols_array(),
screen_dim: self.screen_dim,
_pad: [0; _], _pad: [0; _],
_pad2: [0; _], _pad2: [0; _],
_pad3: [0; _],
} }
} }
@@ -1479,13 +1511,33 @@ impl FractalApp {
let julia_on = self.anim.julia && self.mode == FractalMode::Julia; let julia_on = self.anim.julia && self.mode == FractalMode::Julia;
let phoenix_on = self.anim.phoenix && self.kind == FractalKind::Phoenix; let phoenix_on = self.anim.phoenix && self.kind == FractalKind::Phoenix;
let lambda_on = self.anim.lambda && self.kind == FractalKind::Lambda; let lambda_on = self.anim.lambda && self.kind == FractalKind::Lambda;
if !(self.anim.color || self.anim.zoom || julia_on || phoenix_on || lambda_on) {
return;
}
// Clamp dt so a stall (tab hidden, first frame) can't jump the animation. // Clamp dt so a stall (tab hidden, first frame) can't jump the animation.
let dt = ui.input(|i| i.stable_dt as f64).clamp(0.0, 0.1); let dt = ui.input(|i| i.stable_dt as f64).clamp(0.0, 0.1);
// Animate the 2D <-> 3D camera transition over a fixed duration with
// smoothstep easing: it lands on exactly 0 or 1 (no asymptotic tail,
// no snap), so the shader's mode switch (`camera_state > 0.0` in
// `make_uniforms`) happens only once the camera is exactly top-down.
const CAMERA_DURATION: f32 = 0.6; // seconds
let target = if self.rendering_mode == 2 { 1.0 } else { 0.0 };
let p = self.anim.camera_progress;
if p != target {
let step = dt as f32 / CAMERA_DURATION;
self.anim.camera_progress = if target > p {
(p + step).min(target)
} else {
(p - step).max(target)
};
ui.ctx().request_repaint();
}
let p = self.anim.camera_progress;
self.anim.camera_state = p * p * (3.0 - 2.0 * p);
if !(self.anim.color || self.anim.zoom || julia_on || phoenix_on || lambda_on) {
return;
}
if self.anim.color { if self.anim.color {
self.color_offset = self.color_offset =
(self.color_offset + self.anim.color_speed * dt as f32).rem_euclid(1.0); (self.color_offset + self.anim.color_speed * dt as f32).rem_euclid(1.0);
@@ -2032,7 +2084,7 @@ impl FractalApp {
// instead of panning/zooming the 2D fractal view. // instead of panning/zooming the 2D fractal view.
const ROT_SENS: f32 = 0.002; // radians per dragged pixel const ROT_SENS: f32 = 0.002; // radians per dragged pixel
let multi_touch = ui.input(|i| i.multi_touch()); let multi_touch = ui.input(|i| i.multi_touch());
if self.rendering_mode == 3 { if self.rendering_mode == 2 {
if let Some(mt) = multi_touch { if let Some(mt) = multi_touch {
let t = mt.translation_delta; let t = mt.translation_delta;
if t.x != 0.0 || t.y != 0.0 { if t.x != 0.0 || t.y != 0.0 {
@@ -2040,7 +2092,25 @@ impl FractalApp {
interacted = true; interacted = true;
} }
if mt.zoom_delta != 1.0 { if mt.zoom_delta != 1.0 {
self.camera.zoom(1.0 / mt.zoom_delta); let ndc = (mt.center_pos.to_vec2() / rect.size()) * 2.;
let camera_ndc_pos = self.camera.orthographic(self.anim.camera_state).inverse()
* Vec4::new(ndc.x, ndc.y, 0., 1.);
let view_direction = self.camera.direction(self.anim.camera_state);
let z_move = camera_ndc_pos.z / view_direction.z;
let ndc_pos = camera_ndc_pos.xyz() + view_direction * -z_move;
let pos = egui::Vec2::new(ndc_pos.x / self.camera.aspect_ratio, ndc_pos.y)
* rect.size()
- rect.center().to_vec2();
self.view.zoom_at_pixel(
pos.x as f64,
pos.y as f64,
height_px,
1. / (mt.zoom_delta as f64),
);
interacted = true; interacted = true;
} }
ui.ctx().request_repaint(); ui.ctx().request_repaint();
@@ -2048,7 +2118,6 @@ impl FractalApp {
let d = response.drag_delta(); let d = response.drag_delta();
if d.x != 0.0 || d.y != 0.0 { if d.x != 0.0 || d.y != 0.0 {
self.camera.rotate(d.x * ROT_SENS, -d.y * ROT_SENS); self.camera.rotate(d.x * ROT_SENS, -d.y * ROT_SENS);
interacted = true;
} }
} }
} else if let Some(mt) = multi_touch { } else if let Some(mt) = multi_touch {
@@ -2083,10 +2152,24 @@ impl FractalApp {
&& rect.contains(pos) && rect.contains(pos)
{ {
let factor = (-scroll_y as f64 * 0.0015).exp(); let factor = (-scroll_y as f64 * 0.0015).exp();
if self.rendering_mode == 3 { let off = pos - rect.center();
self.camera.zoom(factor as f32); if self.rendering_mode == 2 {
let ndc = (off / rect.size()) * 2.;
let camera_ndc_pos = self.camera.orthographic(self.anim.camera_state).inverse()
* Vec4::new(ndc.x, ndc.y, 0., 1.);
let view_direction = self.camera.direction(self.anim.camera_state);
let z_move = camera_ndc_pos.z / view_direction.z;
let ndc_pos = camera_ndc_pos.xyz() + view_direction * -z_move;
let pos = egui::Vec2::new(ndc_pos.x / self.camera.aspect_ratio, ndc_pos.y)
* rect.size()
- rect.center().to_vec2();
self.view
.zoom_at_pixel(pos.x as f64, pos.y as f64, height_px, factor);
} else { } else {
let off = pos - rect.center();
self.view self.view
.zoom_at_pixel(off.x as f64, off.y as f64, height_px, factor); .zoom_at_pixel(off.x as f64, off.y as f64, height_px, factor);
} }
@@ -2103,51 +2186,19 @@ impl FractalApp {
if !ui.ctx().egui_wants_keyboard_input() { if !ui.ctx().egui_wants_keyboard_input() {
let dt = ui.input(|i| i.stable_dt as f64).clamp(0.0, 0.1); let dt = ui.input(|i| i.stable_dt as f64).clamp(0.0, 0.1);
if self.rendering_mode == 3 { let not_modifier_ctrl = ui.input(|i| !i.modifiers.ctrl) || self.rendering_mode != 2;
let (fwd, back, strafe_l, strafe_r, up, down, look_l, look_r, look_u, look_d) = ui if self.rendering_mode == 2 {
.input(|i| { let (look_l, look_r, look_u, look_d) = ui.input(|i| {
( (
i.key_down(egui::Key::Z), i.key_down(egui::Key::ArrowLeft) && i.modifiers.ctrl,
i.key_down(egui::Key::S), i.key_down(egui::Key::ArrowRight) && i.modifiers.ctrl,
i.key_down(egui::Key::Q), i.key_down(egui::Key::ArrowUp) && i.modifiers.ctrl,
i.key_down(egui::Key::D), i.key_down(egui::Key::ArrowDown) && i.modifiers.ctrl,
i.key_down(egui::Key::Space), )
i.modifiers.ctrl || i.modifiers.command, });
i.key_down(egui::Key::ArrowLeft),
i.key_down(egui::Key::ArrowRight),
i.key_down(egui::Key::ArrowUp),
i.key_down(egui::Key::ArrowDown),
)
});
// Units/sec move speed and radians/sec look speed. // Units/sec move speed and radians/sec look speed.
const MOVE_SPEED: f32 = 0.1; const LOOK_SPEED: f32 = 0.5;
const LOOK_SPEED: f32 = 0.05;
let mut mv_fwd = 0.0f32;
let mut mv_right = 0.0f32;
let mut mv_up = 0.0f32;
if fwd {
mv_fwd += MOVE_SPEED * dt as f32;
}
if back {
mv_fwd -= MOVE_SPEED * dt as f32;
}
if strafe_r {
mv_right += MOVE_SPEED * dt as f32;
}
if strafe_l {
mv_right -= MOVE_SPEED * dt as f32;
}
if up {
mv_up += MOVE_SPEED * dt as f32;
}
if down {
mv_up -= MOVE_SPEED * dt as f32;
}
if mv_fwd != 0.0 || mv_right != 0.0 || mv_up != 0.0 {
self.camera.translate(mv_fwd, mv_right, mv_up);
interacted = true;
}
let mut dyaw = 0.0f32; let mut dyaw = 0.0f32;
let mut dpitch = 0.0f32; let mut dpitch = 0.0f32;
@@ -2165,70 +2216,64 @@ impl FractalApp {
} }
if dyaw != 0.0 || dpitch != 0.0 { if dyaw != 0.0 || dpitch != 0.0 {
self.camera.rotate(dyaw, dpitch); self.camera.rotate(dyaw, dpitch);
interacted = true;
} }
if fwd if look_l || look_r || look_u || look_d {
|| back
|| strafe_l
|| strafe_r
|| up
|| down
|| look_l
|| look_r
|| look_u
|| look_d
{
ui.ctx().request_repaint(); ui.ctx().request_repaint();
} }
} else { }
let (left, right, up, down, zoom_in, zoom_out) = ui.input(|i| {
(
i.key_down(egui::Key::ArrowLeft),
i.key_down(egui::Key::ArrowRight),
i.key_down(egui::Key::ArrowUp),
i.key_down(egui::Key::ArrowDown),
i.key_down(egui::Key::Z),
i.key_down(egui::Key::S),
)
});
// Pixels/sec pan speed — matches a brisk mouse drag regardless of let (left, right, up, down, zoom_in, zoom_out) = ui.input(|i| {
// frame rate. See `pan_pixels`'s screen-space (+x right, +y down) (
// convention: Right/Down pan the *camera* right/down, which is i.key_down(egui::Key::ArrowLeft) && not_modifier_ctrl,
// the opposite delta sign from a drag that would show the same i.key_down(egui::Key::ArrowRight) && not_modifier_ctrl,
// content (a drag grabs the canvas; these keys move the camera). i.key_down(egui::Key::ArrowUp) && not_modifier_ctrl,
const PAN_SPEED_PX: f64 = 700.0; i.key_down(egui::Key::ArrowDown) && not_modifier_ctrl,
let mut dx = 0.0; i.key_down(egui::Key::Z),
let mut dy = 0.0; i.key_down(egui::Key::S),
if left { )
dx += PAN_SPEED_PX * dt; });
}
if right {
dx -= PAN_SPEED_PX * dt;
}
if down {
dy -= PAN_SPEED_PX * dt;
}
if up {
dy += PAN_SPEED_PX * dt;
}
if dx != 0.0 || dy != 0.0 {
self.view.pan_pixels(dx, dy, height_px);
interacted = true;
}
// e-folds/sec, same scale as the auto-zoom animation. // Pixels/sec pan speed — matches a brisk mouse drag regardless of
const ZOOM_SPEED: f64 = 1.0; // frame rate. See `pan_pixels`'s screen-space (+x right, +y down)
if zoom_in != zoom_out { // convention: Right/Down pan the *camera* right/down, which is
let rate = if zoom_in { ZOOM_SPEED } else { -ZOOM_SPEED }; // the opposite delta sign from a drag that would show the same
let factor = (-rate * dt).exp(); // content (a drag grabs the canvas; these keys move the camera).
self.view.zoom_at_pixel(0.0, 0.0, height_px, factor); const PAN_SPEED_PX: f64 = 700.0;
interacted = true; let mut dx = 0.0;
} let mut dy = 0.0;
if left || right || up || down || zoom_in || zoom_out { if left {
ui.ctx().request_repaint(); dx += PAN_SPEED_PX * dt;
} }
if right {
dx -= PAN_SPEED_PX * dt;
}
if down {
dy -= PAN_SPEED_PX * dt;
}
if up {
dy += PAN_SPEED_PX * dt;
}
if self.rendering_mode == 2 {
let cos = self.camera.yaw.cos() as f64;
let sin = self.camera.yaw.sin() as f64;
(dx, dy) = (dx * cos + sin * dy, -dx * sin + cos * dy);
}
if dx != 0.0 || dy != 0.0 {
self.view.pan_pixels(dx, dy, height_px);
interacted = true;
}
// e-folds/sec, same scale as the auto-zoom animation.
const ZOOM_SPEED: f64 = 1.0;
if zoom_in != zoom_out {
let rate = if zoom_in { ZOOM_SPEED } else { -ZOOM_SPEED };
let factor = (-rate * dt).exp();
self.view.zoom_at_pixel(0.0, 0.0, height_px, factor);
interacted = true;
}
if left || right || up || down || zoom_in || zoom_out {
ui.ctx().request_repaint();
} }
if ui.input(|i| i.key_pressed(egui::Key::R)) { if ui.input(|i| i.key_pressed(egui::Key::R)) {
@@ -2323,11 +2368,16 @@ impl FractalApp {
// down while interacting (the linear blit upsamples it to the widget). // down while interacting (the linear blit upsamples it to the widget).
let ppp = ui.ctx().pixels_per_point(); let ppp = ui.ctx().pixels_per_point();
let downscale = if interacting { INTERACT_DOWNSCALE } else { 1 }; let downscale = if interacting { INTERACT_DOWNSCALE } else { 1 };
let size_px = [ let mut size_px = [
(((rect.width() * ppp).round() as u32) / downscale).max(1), (((rect.width() * ppp).round() as u32) / downscale).max(1),
(((rect.height() * ppp).round() as u32) / downscale).max(1), (((rect.height() * ppp).round() as u32) / downscale).max(1),
]; ];
if self.rendering_mode == 2 {
size_px = [size_px[0] * 2, size_px[1] * 2];
}
self.screen_dim = [rect.width(), rect.height()];
self.camera.set_aspect_ratio(aspect as f32); self.camera.set_aspect_ratio(aspect as f32);
let mut uniforms = self.make_uniforms(aspect); let mut uniforms = self.make_uniforms(aspect);
if interacting { if interacting {
+22 -37
View File
@@ -9,9 +9,7 @@ pub struct Camera {
pub yaw: f32, pub yaw: f32,
pub pitch: f32, pub pitch: f32,
/// Demi-hauteur du volume visible (remplace fov_y_radians) pub aspect_ratio: f32,
ortho_height: f32,
aspect_ratio: f32,
z_near: f32, z_near: f32,
z_far: f32, z_far: f32,
} }
@@ -21,8 +19,7 @@ impl Camera {
Self { Self {
position: Vec3::new(0., 0., -1.), position: Vec3::new(0., 0., -1.),
yaw: 0. * PI / 180., yaw: 0. * PI / 180.,
pitch: 0. * PI / 180., pitch: -30. * PI / 180.,
ortho_height: 1.0,
aspect_ratio: 1., aspect_ratio: 1.,
z_near: 0.1, z_near: 0.1,
z_far: 100., z_far: 100.,
@@ -33,19 +30,6 @@ impl Camera {
self.aspect_ratio = aspect_ratio; self.aspect_ratio = aspect_ratio;
} }
/// Camera-local right vector: perpendicular to yaw, ignoring pitch (so
/// strafing stays level regardless of where the camera is looking).
pub fn right(&self) -> glam::Vec3 {
glam::Mat3::from_rotation_y(-self.yaw) * glam::Vec3::X
}
/// Move the camera in its own local space: `forward`/`right` follow the
/// (pitch-aware) view direction and its horizontal right vector, `up`
/// moves along the fixed world Y axis.
pub fn translate(&mut self, forward: f32, right: f32, up: f32) {
self.position += self.direction() * forward + self.right() * right + Vec3::Y * up;
}
/// Adjust yaw/pitch by the given deltas (radians). Pitch is clamped just /// Adjust yaw/pitch by the given deltas (radians). Pitch is clamped just
/// short of straight up/down to avoid the view flipping past the pole. /// short of straight up/down to avoid the view flipping past the pole.
pub fn rotate(&mut self, dyaw: f32, dpitch: f32) { pub fn rotate(&mut self, dyaw: f32, dpitch: f32) {
@@ -54,33 +38,34 @@ impl Camera {
self.pitch = (self.pitch + dpitch).clamp(-PITCH_LIMIT, PITCH_LIMIT); self.pitch = (self.pitch + dpitch).clamp(-PITCH_LIMIT, PITCH_LIMIT);
} }
/// Scale the visible ortho volume by `factor` (<1 zooms in, >1 zooms pub fn orthographic(&self, t: f32) -> glam::Mat4 {
/// out), clamped to a sane range. let yaw = self.yaw * t;
pub fn zoom(&mut self, factor: f32) { let pitch = self.pitch * t;
self.ortho_height = (self.ortho_height * factor).clamp(0.001, 1000.0);
}
pub fn orthographic(&self) -> glam::Mat4 { let zoom = 0.5 * (1. + t);
let view = glam::Mat4::from_translation(self.position) // Orbit pivot: the center of the fractal texture, which the raymarcher's
* glam::Mat4::from_rotation_y(-self.yaw) // `sdf` lays out over world x ∈ [0, aspect], y ∈ [0, 1] on the z = 0 plane.
* glam::Mat4::from_rotation_x(-self.pitch); let view = glam::Mat4::from_translation(Vec3::new(0.5 * self.aspect_ratio, 0.5, 0.))
* glam::Mat4::from_rotation_z(-yaw)
let half_height = self.ortho_height; * glam::Mat4::from_rotation_x(-pitch)
let half_width = half_height * self.aspect_ratio; * glam::Mat4::from_translation(self.position);
glam::camera::lh::proj::directx::orthographic( glam::camera::lh::proj::directx::orthographic(
-half_width, -self.aspect_ratio / 4. / zoom,
half_width, self.aspect_ratio / 4. / zoom,
-half_height, -0.25 / zoom,
half_height, 0.25 / zoom,
self.z_near, self.z_near,
self.z_far, self.z_far,
) * view.inverse() ) * view.inverse()
} }
pub fn direction(&self) -> glam::Vec3 { pub fn direction(&self, t: f32) -> glam::Vec3 {
let forward = glam::Mat3::from_rotation_y(-self.yaw) let yaw = self.yaw * t;
* glam::Mat3::from_rotation_x(-self.pitch) let pitch = self.pitch * t;
let forward = glam::Mat3::from_rotation_z(-yaw)
* glam::Mat3::from_rotation_x(-pitch)
* glam::Vec3::Z; * glam::Vec3::Z;
forward.normalize() forward.normalize()
+12
View File
@@ -21,6 +21,10 @@ pub struct Cli {
#[arg(long)] #[arg(long)]
pub buddhabrot: bool, pub buddhabrot: bool,
/// Rendering mode to use.
#[arg(long)]
pub rendering_kind: Option<RenderingKindArg>,
/// Exponent for the Multibrot kind (z -> z^power + c), clamped to [2, 8]. /// Exponent for the Multibrot kind (z -> z^power + c), clamped to [2, 8].
#[arg(long)] #[arg(long)]
pub power: Option<u32>, pub power: Option<u32>,
@@ -144,6 +148,14 @@ pub enum KindArg {
ComplexMultibrot, ComplexMultibrot,
} }
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum RenderingKindArg {
Classic,
Shadow,
#[value(alias = "3d")]
Dimension3,
}
impl From<KindArg> for FractalKind { impl From<KindArg> for FractalKind {
fn from(k: KindArg) -> Self { fn from(k: KindArg) -> Self {
match k { match k {
+3
View File
@@ -101,6 +101,9 @@ pub struct Uniforms {
/// Inverse of the camera's view-projection matrix (column-major), for /// Inverse of the camera's view-projection matrix (column-major), for
/// reconstructing a world-space ray origin per pixel in the raymarcher. /// reconstructing a world-space ray origin per pixel in the raymarcher.
pub camera_inv_proj: [f32; 16], pub camera_inv_proj: [f32; 16],
/// Screen dimension
pub screen_dim: [f32; 2],
pub _pad3: [u32; 2],
} }
/// Offscreen textures for the two-pass render, recreated whenever the widget's /// Offscreen textures for the two-pass render, recreated whenever the widget's
+1 -1
View File
@@ -1,7 +1,7 @@
use std::f32::consts::PI; use std::f32::consts::PI;
use bytemuck::{Pod, Zeroable}; use bytemuck::{Pod, Zeroable};
use egui::{Button, Color32, Ui}; use egui::{Color32, Ui};
/// Maximum number of simultaneous lights. /// Maximum number of simultaneous lights.
pub const MAX_LIGHT_COUNT: usize = 16; pub const MAX_LIGHT_COUNT: usize = 16;
+1 -1
View File
@@ -124,7 +124,7 @@ fn main() {
Ok(_) => loading.remove(), Ok(_) => loading.remove(),
Err(e) => { Err(e) => {
loading.set_inner_html( loading.set_inner_html(
"<p>The app has crashed. See the developer console for details.</p>", &format!("<p>The app has crashed.</br>{e:?}</p>"),
); );
log::error!("failed to start eframe: {e:?}"); log::error!("failed to start eframe: {e:?}");
} }
+67 -20
View File
@@ -19,16 +19,31 @@ fn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {
return vec4<f32>(fullscreen_triangle_pos(idx), 0.0, 1.0); return vec4<f32>(fullscreen_triangle_pos(idx), 0.0, 1.0);
} }
fn shadow_fragment(pos: vec3<f32>) -> vec4<f32> { fn shadow_fragment(pos: vec2<f32>) -> vec4<f32> {
let x = i32(pos.x); let x = i32(pos.x);
let y = i32(pos.y); let y = i32(pos.y);
let size = textureDimensions(data_tex);
if textureLoad(data_tex, vec2<i32>(x, y), 0).b != 0. { if textureLoad(data_tex, vec2<i32>(x, y), 0).b != 0. {
return vec4<f32>(0.1, 0.1, 0.1, 1.0); return vec4<f32>(0.1, 0.1, 0.1, 1.0);
} else { } else {
// Forward differences, except on the last column/row where x+1 / y+1
// is off the texture: fall back to a backward difference, mirrored
// (h0 + (h0 - h[-1])) so the slope keeps the sign normal_from_heights
// expects — plugging h[-1] in directly would flip the normal there.
let h0 = textureLoad(data_tex, vec2<i32>(x, y), 0).g; let h0 = textureLoad(data_tex, vec2<i32>(x, y), 0).g;
let h1 = textureLoad(data_tex, vec2<i32>(x + 1, y), 0).g; var h1: f32;
let h2 = textureLoad(data_tex, vec2<i32>(x, y + 1), 0).g; if x + 1 < i32(size.x) {
h1 = textureLoad(data_tex, vec2<i32>(x + 1, y), 0).g;
} else {
h1 = 2.0 * h0 - textureLoad(data_tex, vec2<i32>(x - 1, y), 0).g;
}
var h2: f32;
if y + 1 < i32(size.y) {
h2 = textureLoad(data_tex, vec2<i32>(x, y + 1), 0).g;
} else {
h2 = 2.0 * h0 - textureLoad(data_tex, vec2<i32>(x, y - 1), 0).g;
}
let normal = normal_from_heights(h0, h1, h2); let normal = normal_from_heights(h0, h1, h2);
return vec4<f32>(shadow_color(normal), 1.0); return vec4<f32>(shadow_color(normal), 1.0);
} }
@@ -38,7 +53,7 @@ fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
if u.shadow == 2u { if u.shadow == 2u {
return ray_marching(pos); return ray_marching(pos);
} else if u.shadow == 1u { } else if u.shadow == 1u {
return shadow_fragment(pos.xyz); return shadow_fragment(pos.xy);
} else { } else {
let d = textureLoad(data_tex, vec2<i32>(i32(pos.x), i32(pos.y)), 0); let d = textureLoad(data_tex, vec2<i32>(i32(pos.x), i32(pos.y)), 0);
let ci = d.r; let ci = d.r;
@@ -54,44 +69,76 @@ fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
} }
fn sdf(pos: vec3<f32>) -> f32 { fn sdf(pos: vec3<f32>) -> f32 {
let texture_pos = vec2<i32>(i32(pos.x), i32(pos.y)); let aspect_ratio = u.screen_dim.x / u.screen_dim.y;
let size = vec2<f32>(textureDimensions(data_tex));
var texture_pos_f32 = vec2<f32>(pos.x * size.x / aspect_ratio, pos.y * size.y);
var texture_pos = vec2<i32>(i32(texture_pos_f32.x), i32(texture_pos_f32.y));
texture_pos.x = clamp(texture_pos.x, 0, i32(size.x) - 1);
texture_pos.y = clamp(texture_pos.y, 0, i32(size.y) - 1);
let to_texture = max(-min(texture_pos_f32, vec2(0.)), max(texture_pos_f32 - size, vec2(0.)));
let dist_to_texture = length(to_texture) / size.y;
let px = textureLoad(data_tex, texture_pos, 0); let px = textureLoad(data_tex, texture_pos, 0);
let de = px.g; let de = (px.g / size.y) * 0.5;
let z = pos.z; // Height is measured toward -z, the side the camera sits on (it looks
// along +z), so the terrain is solid on +z: interior plateau at z = 0,
// exterior sloping away from the camera as `de` grows.
let signed_z = -pos.z;
let z = max(signed_z, 0.);
var d: f32;
if px.b != 0. { if px.b != 0. {
return 0.; d = z;
} else { } else {
return sqrt(z * z + (de * de)); d = min(sqrt(z * z + de * de), signed_z + 1. - exp(-de * 5.));
} }
// Outside the texture footprint, `d` is the distance from the clamped
// point q on the footprint's edge. The terrain lies over the (convex)
// footprint, so |p - x|² ≥ |q - x|² + |p - q|² for every terrain point x:
// combine in quadrature (not by adding, which overshoots). p can't be in
// the solid out here, so a negative `d` counts as 0.
if dist_to_texture > 0. {
let d_pos = max(d, 0.);
return sqrt(d_pos * d_pos + dist_to_texture * dist_to_texture);
}
return d;
} }
fn ray_marching(pos: vec4<f32>) -> vec4<f32> { fn ray_marching(pos: vec4<f32>) -> vec4<f32> {
let size = vec2<f32>(textureDimensions(data_tex)); let size = vec2<f32>(textureDimensions(data_tex));
let in_texture = vec2<f32>( let in_texture = vec2<f32>(
pos.x / 1980. * size.x, (pos.x / size.x) * 2. - 1.,
pos.y / 1080. * size.y, (pos.y / size.y) * 2. - 1.,
); );
var world_pos = u.camera_inv_proj * vec4<f32>(in_texture, pos.z - 10, 1.0); var world_pos = u.camera_inv_proj * vec4<f32>(in_texture, 0., 1.0);
let ray_origin = world_pos.xyz; let ray_origin = world_pos.xyz;
let ray_dir = u.camera_direction; let ray_dir = u.camera_direction;
var p = ray_origin; let z_intersect = ray_origin.z / ray_dir.z;
var p = ray_origin - ray_dir * z_intersect;
var i = 0u; var i = 0u;
var dist = 0.0; var dist = 0.0;
while i < 1000u { let dist_threshold = 0.000001;
dist = sdf(p); while i < 100u {
if dist < 0.0001 { if length(p - ray_origin) > 3. {
break; break;
} }
p += dist * ray_dir; dist = sdf(p);
i += 1u; if dist < dist_threshold {
break;
}
p += dist * ray_dir;
i += 1u;
} }
if dist < 0.0001 { if dist < dist_threshold {
return shadow_fragment(p); let aspect_ratio = u.screen_dim.x / u.screen_dim.y;
let size = vec2<f32>(textureDimensions(data_tex));
let texture_pos_f32 = vec2<f32>(p.x * size.x / aspect_ratio, p.y * size.y);
return shadow_fragment(texture_pos_f32);
} }
return vec4<f32>(1., 0., 0., 1.); return vec4<f32>(1., 0., 0., 1.);
} }
+2
View File
@@ -38,6 +38,8 @@ struct Uniforms {
// inverse of the camera's view-projection matrix, for reconstructing a // inverse of the camera's view-projection matrix, for reconstructing a
// world-space ray origin per pixel in the raymarcher // world-space ray origin per pixel in the raymarcher
camera_inv_proj: mat4x4<f32>, camera_inv_proj: mat4x4<f32>,
// Screen dimensions
screen_dim: vec2<f32>
}; };
// Smooth cyclic palettes (Inigo Quilez cosine palettes), selected by id. // Smooth cyclic palettes (Inigo Quilez cosine palettes), selected by id.
+18
View File
@@ -211,6 +211,24 @@ struct Sample {
fn iterate_sample(offset: vec2<f32>, px: f32) -> Sample { fn iterate_sample(offset: vec2<f32>, px: f32) -> Sample {
let z0 = ref_orbit[0]; // reference start (0 for Mandelbrot, center for Julia) let z0 = ref_orbit[0]; // reference start (0 for Mandelbrot, center for Julia)
// Main cardioid / period-2 bulb bypass: those points never escape, so skip
// iterating them (they'd otherwise all burn the full max_iter). `offset` is
// relative to the reference center; the absolute c is recovered from the
// orbit itself, since X_1 = X_0^2 + C_ref = C_ref. That's only f32-accurate,
// so skip the test once a pixel is smaller than that error (deep zoom),
// where it could misclassify pixels right at the boundary.
if u.kind == KIND_MANDELBROT && u.is_julia == 0u && u.ref_len > 1u && px > 1e-6 {
let c = ref_orbit[1] + offset;
let xq = c.x - 0.25;
let q = xq * xq + c.y * c.y;
let in_cardioid = q * (q + xq) <= 0.25 * c.y * c.y;
let xb = c.x + 1.0;
let in_bulb = xb * xb + c.y * c.y <= 0.0625;
if in_cardioid || in_bulb {
return Sample(0.0, 1.0, false); // interior of the set
}
}
var step_add = offset; var step_add = offset;
var e = vec2<f32>(0.0, 0.0); var e = vec2<f32>(0.0, 0.0);
// Orbit derivative for distance estimation. For the set plane it is d/dc // Orbit derivative for distance estimation. For the set plane it is d/dc