fix: correct 3d rendering
This commit is contained in:
+121
-71
@@ -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 {
|
|
||||||
self.camera.zoom(factor as f32);
|
|
||||||
} else {
|
|
||||||
let off = pos - rect.center();
|
let off = pos - rect.center();
|
||||||
|
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 {
|
||||||
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,29 +2216,19 @@ 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| {
|
let (left, right, up, down, zoom_in, zoom_out) = ui.input(|i| {
|
||||||
(
|
(
|
||||||
i.key_down(egui::Key::ArrowLeft),
|
i.key_down(egui::Key::ArrowLeft) && not_modifier_ctrl,
|
||||||
i.key_down(egui::Key::ArrowRight),
|
i.key_down(egui::Key::ArrowRight) && not_modifier_ctrl,
|
||||||
i.key_down(egui::Key::ArrowUp),
|
i.key_down(egui::Key::ArrowUp) && not_modifier_ctrl,
|
||||||
i.key_down(egui::Key::ArrowDown),
|
i.key_down(egui::Key::ArrowDown) && not_modifier_ctrl,
|
||||||
i.key_down(egui::Key::Z),
|
i.key_down(egui::Key::Z),
|
||||||
i.key_down(egui::Key::S),
|
i.key_down(egui::Key::S),
|
||||||
)
|
)
|
||||||
@@ -2213,6 +2254,11 @@ impl FractalApp {
|
|||||||
if up {
|
if up {
|
||||||
dy += PAN_SPEED_PX * dt;
|
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 {
|
if dx != 0.0 || dy != 0.0 {
|
||||||
self.view.pan_pixels(dx, dy, height_px);
|
self.view.pan_pixels(dx, dy, height_px);
|
||||||
interacted = true;
|
interacted = true;
|
||||||
@@ -2229,7 +2275,6 @@ impl FractalApp {
|
|||||||
if left || right || up || down || zoom_in || zoom_out {
|
if left || right || up || down || zoom_in || zoom_out {
|
||||||
ui.ctx().request_repaint();
|
ui.ctx().request_repaint();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ui.input(|i| i.key_pressed(egui::Key::R)) {
|
if ui.input(|i| i.key_pressed(egui::Key::R)) {
|
||||||
self.view = Self::default_view_for(self.mode, self.kind);
|
self.view = Self::default_view_for(self.mode, self.kind);
|
||||||
@@ -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
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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:?}");
|
||||||
}
|
}
|
||||||
|
|||||||
+64
-17
@@ -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;
|
||||||
|
while i < 100u {
|
||||||
|
if length(p - ray_origin) > 3. {
|
||||||
|
break;
|
||||||
|
}
|
||||||
dist = sdf(p);
|
dist = sdf(p);
|
||||||
if dist < 0.0001 {
|
if dist < dist_threshold {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
p += dist * ray_dir;
|
p += dist * ray_dir;
|
||||||
i += 1u;
|
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.);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user