fix: correct 3d rendering
This commit is contained in:
+164
-114
@@ -4,6 +4,8 @@ use eframe::CreationContext;
|
||||
use eframe::egui_wgpu;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use eframe::egui_wgpu::wgpu;
|
||||
use glam::Vec4;
|
||||
use glam::Vec4Swizzles;
|
||||
|
||||
use crate::camera::Camera;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -205,6 +207,12 @@ struct AnimState {
|
||||
zoom: bool,
|
||||
/// e-folds per second; positive zooms in, negative zooms out.
|
||||
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 {
|
||||
@@ -229,6 +237,8 @@ impl Default for AnimState {
|
||||
lambda_angle: 0.0,
|
||||
zoom: false,
|
||||
zoom_speed: 0.5,
|
||||
camera_progress: 0.,
|
||||
camera_state: 0.,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -350,6 +360,8 @@ pub struct FractalApp {
|
||||
|
||||
/// The camera used to render 3D fractals
|
||||
camera: Camera,
|
||||
/// Screen dimension.
|
||||
screen_dim: [f32; 2],
|
||||
}
|
||||
|
||||
/// Significant decimal digits to show for a center at the given precision (bits).
|
||||
@@ -473,6 +485,7 @@ impl FractalApp {
|
||||
zoom_edit,
|
||||
zoom_edited: false,
|
||||
camera: Camera::new(),
|
||||
screen_dim: [0., 0.],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,6 +510,15 @@ impl FractalApp {
|
||||
}
|
||||
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 {
|
||||
let p: Vec<&str> = jc.split(',').collect();
|
||||
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],
|
||||
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,
|
||||
rendering_mode: self.rendering_mode,
|
||||
camera_direction: self.camera.direction().to_array(),
|
||||
camera_inv_proj: self.camera.orthographic().inverse().to_cols_array(),
|
||||
rendering_mode: if self.anim.camera_state > 0.0 {
|
||||
2
|
||||
} 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; _],
|
||||
_pad2: [0; _],
|
||||
_pad3: [0; _],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1479,13 +1511,33 @@ impl FractalApp {
|
||||
let julia_on = self.anim.julia && self.mode == FractalMode::Julia;
|
||||
let phoenix_on = self.anim.phoenix && self.kind == FractalKind::Phoenix;
|
||||
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.
|
||||
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 {
|
||||
self.color_offset =
|
||||
(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.
|
||||
const ROT_SENS: f32 = 0.002; // radians per dragged pixel
|
||||
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 {
|
||||
let t = mt.translation_delta;
|
||||
if t.x != 0.0 || t.y != 0.0 {
|
||||
@@ -2040,7 +2092,25 @@ impl FractalApp {
|
||||
interacted = true;
|
||||
}
|
||||
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;
|
||||
}
|
||||
ui.ctx().request_repaint();
|
||||
@@ -2048,7 +2118,6 @@ impl FractalApp {
|
||||
let d = response.drag_delta();
|
||||
if d.x != 0.0 || d.y != 0.0 {
|
||||
self.camera.rotate(d.x * ROT_SENS, -d.y * ROT_SENS);
|
||||
interacted = true;
|
||||
}
|
||||
}
|
||||
} else if let Some(mt) = multi_touch {
|
||||
@@ -2083,10 +2152,24 @@ impl FractalApp {
|
||||
&& rect.contains(pos)
|
||||
{
|
||||
let factor = (-scroll_y as f64 * 0.0015).exp();
|
||||
if self.rendering_mode == 3 {
|
||||
self.camera.zoom(factor as f32);
|
||||
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 {
|
||||
let off = pos - rect.center();
|
||||
self.view
|
||||
.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() {
|
||||
let dt = ui.input(|i| i.stable_dt as f64).clamp(0.0, 0.1);
|
||||
|
||||
if self.rendering_mode == 3 {
|
||||
let (fwd, back, strafe_l, strafe_r, up, down, look_l, look_r, look_u, look_d) = ui
|
||||
.input(|i| {
|
||||
(
|
||||
i.key_down(egui::Key::Z),
|
||||
i.key_down(egui::Key::S),
|
||||
i.key_down(egui::Key::Q),
|
||||
i.key_down(egui::Key::D),
|
||||
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),
|
||||
)
|
||||
});
|
||||
let not_modifier_ctrl = ui.input(|i| !i.modifiers.ctrl) || self.rendering_mode != 2;
|
||||
if self.rendering_mode == 2 {
|
||||
let (look_l, look_r, look_u, look_d) = ui.input(|i| {
|
||||
(
|
||||
i.key_down(egui::Key::ArrowLeft) && i.modifiers.ctrl,
|
||||
i.key_down(egui::Key::ArrowRight) && i.modifiers.ctrl,
|
||||
i.key_down(egui::Key::ArrowUp) && i.modifiers.ctrl,
|
||||
i.key_down(egui::Key::ArrowDown) && i.modifiers.ctrl,
|
||||
)
|
||||
});
|
||||
|
||||
// Units/sec move speed and radians/sec look speed.
|
||||
const MOVE_SPEED: f32 = 0.1;
|
||||
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;
|
||||
}
|
||||
const LOOK_SPEED: f32 = 0.5;
|
||||
|
||||
let mut dyaw = 0.0f32;
|
||||
let mut dpitch = 0.0f32;
|
||||
@@ -2165,70 +2216,64 @@ impl FractalApp {
|
||||
}
|
||||
if dyaw != 0.0 || dpitch != 0.0 {
|
||||
self.camera.rotate(dyaw, dpitch);
|
||||
interacted = true;
|
||||
}
|
||||
|
||||
if fwd
|
||||
|| back
|
||||
|| strafe_l
|
||||
|| strafe_r
|
||||
|| up
|
||||
|| down
|
||||
|| look_l
|
||||
|| look_r
|
||||
|| look_u
|
||||
|| look_d
|
||||
{
|
||||
if look_l || look_r || look_u || look_d {
|
||||
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
|
||||
// frame rate. See `pan_pixels`'s screen-space (+x right, +y down)
|
||||
// convention: Right/Down pan the *camera* right/down, which is
|
||||
// the opposite delta sign from a drag that would show the same
|
||||
// content (a drag grabs the canvas; these keys move the camera).
|
||||
const PAN_SPEED_PX: f64 = 700.0;
|
||||
let mut dx = 0.0;
|
||||
let mut dy = 0.0;
|
||||
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;
|
||||
}
|
||||
let (left, right, up, down, zoom_in, zoom_out) = ui.input(|i| {
|
||||
(
|
||||
i.key_down(egui::Key::ArrowLeft) && not_modifier_ctrl,
|
||||
i.key_down(egui::Key::ArrowRight) && not_modifier_ctrl,
|
||||
i.key_down(egui::Key::ArrowUp) && not_modifier_ctrl,
|
||||
i.key_down(egui::Key::ArrowDown) && not_modifier_ctrl,
|
||||
i.key_down(egui::Key::Z),
|
||||
i.key_down(egui::Key::S),
|
||||
)
|
||||
});
|
||||
|
||||
// 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();
|
||||
}
|
||||
// Pixels/sec pan speed — matches a brisk mouse drag regardless of
|
||||
// frame rate. See `pan_pixels`'s screen-space (+x right, +y down)
|
||||
// convention: Right/Down pan the *camera* right/down, which is
|
||||
// the opposite delta sign from a drag that would show the same
|
||||
// content (a drag grabs the canvas; these keys move the camera).
|
||||
const PAN_SPEED_PX: f64 = 700.0;
|
||||
let mut dx = 0.0;
|
||||
let mut dy = 0.0;
|
||||
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 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)) {
|
||||
@@ -2323,11 +2368,16 @@ impl FractalApp {
|
||||
// down while interacting (the linear blit upsamples it to the widget).
|
||||
let ppp = ui.ctx().pixels_per_point();
|
||||
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.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);
|
||||
let mut uniforms = self.make_uniforms(aspect);
|
||||
if interacting {
|
||||
|
||||
+22
-37
@@ -9,9 +9,7 @@ pub struct Camera {
|
||||
pub yaw: f32,
|
||||
pub pitch: f32,
|
||||
|
||||
/// Demi-hauteur du volume visible (remplace fov_y_radians)
|
||||
ortho_height: f32,
|
||||
aspect_ratio: f32,
|
||||
pub aspect_ratio: f32,
|
||||
z_near: f32,
|
||||
z_far: f32,
|
||||
}
|
||||
@@ -21,8 +19,7 @@ impl Camera {
|
||||
Self {
|
||||
position: Vec3::new(0., 0., -1.),
|
||||
yaw: 0. * PI / 180.,
|
||||
pitch: 0. * PI / 180.,
|
||||
ortho_height: 1.0,
|
||||
pitch: -30. * PI / 180.,
|
||||
aspect_ratio: 1.,
|
||||
z_near: 0.1,
|
||||
z_far: 100.,
|
||||
@@ -33,19 +30,6 @@ impl Camera {
|
||||
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
|
||||
/// short of straight up/down to avoid the view flipping past the pole.
|
||||
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);
|
||||
}
|
||||
|
||||
/// Scale the visible ortho volume by `factor` (<1 zooms in, >1 zooms
|
||||
/// out), clamped to a sane range.
|
||||
pub fn zoom(&mut self, factor: f32) {
|
||||
self.ortho_height = (self.ortho_height * factor).clamp(0.001, 1000.0);
|
||||
}
|
||||
pub fn orthographic(&self, t: f32) -> glam::Mat4 {
|
||||
let yaw = self.yaw * t;
|
||||
let pitch = self.pitch * t;
|
||||
|
||||
pub fn orthographic(&self) -> glam::Mat4 {
|
||||
let view = glam::Mat4::from_translation(self.position)
|
||||
* glam::Mat4::from_rotation_y(-self.yaw)
|
||||
* glam::Mat4::from_rotation_x(-self.pitch);
|
||||
|
||||
let half_height = self.ortho_height;
|
||||
let half_width = half_height * self.aspect_ratio;
|
||||
let zoom = 0.5 * (1. + t);
|
||||
// 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.))
|
||||
* glam::Mat4::from_rotation_z(-yaw)
|
||||
* glam::Mat4::from_rotation_x(-pitch)
|
||||
* glam::Mat4::from_translation(self.position);
|
||||
|
||||
glam::camera::lh::proj::directx::orthographic(
|
||||
-half_width,
|
||||
half_width,
|
||||
-half_height,
|
||||
half_height,
|
||||
-self.aspect_ratio / 4. / zoom,
|
||||
self.aspect_ratio / 4. / zoom,
|
||||
-0.25 / zoom,
|
||||
0.25 / zoom,
|
||||
self.z_near,
|
||||
self.z_far,
|
||||
) * view.inverse()
|
||||
}
|
||||
|
||||
pub fn direction(&self) -> glam::Vec3 {
|
||||
let forward = glam::Mat3::from_rotation_y(-self.yaw)
|
||||
* glam::Mat3::from_rotation_x(-self.pitch)
|
||||
pub fn direction(&self, t: f32) -> glam::Vec3 {
|
||||
let yaw = self.yaw * t;
|
||||
let pitch = self.pitch * t;
|
||||
|
||||
let forward = glam::Mat3::from_rotation_z(-yaw)
|
||||
* glam::Mat3::from_rotation_x(-pitch)
|
||||
* glam::Vec3::Z;
|
||||
|
||||
forward.normalize()
|
||||
|
||||
+12
@@ -21,6 +21,10 @@ pub struct Cli {
|
||||
#[arg(long)]
|
||||
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].
|
||||
#[arg(long)]
|
||||
pub power: Option<u32>,
|
||||
@@ -144,6 +148,14 @@ pub enum KindArg {
|
||||
ComplexMultibrot,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, ValueEnum)]
|
||||
pub enum RenderingKindArg {
|
||||
Classic,
|
||||
Shadow,
|
||||
#[value(alias = "3d")]
|
||||
Dimension3,
|
||||
}
|
||||
|
||||
impl From<KindArg> for FractalKind {
|
||||
fn from(k: KindArg) -> Self {
|
||||
match k {
|
||||
|
||||
@@ -101,6 +101,9 @@ pub struct Uniforms {
|
||||
/// Inverse of the camera's view-projection matrix (column-major), for
|
||||
/// reconstructing a world-space ray origin per pixel in the raymarcher.
|
||||
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
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
use std::f32::consts::PI;
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use egui::{Button, Color32, Ui};
|
||||
use egui::{Color32, Ui};
|
||||
|
||||
/// Maximum number of simultaneous lights.
|
||||
pub const MAX_LIGHT_COUNT: usize = 16;
|
||||
|
||||
+1
-1
@@ -124,7 +124,7 @@ fn main() {
|
||||
Ok(_) => loading.remove(),
|
||||
Err(e) => {
|
||||
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:?}");
|
||||
}
|
||||
|
||||
+67
-20
@@ -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);
|
||||
}
|
||||
|
||||
fn shadow_fragment(pos: vec3<f32>) -> vec4<f32> {
|
||||
fn shadow_fragment(pos: vec2<f32>) -> vec4<f32> {
|
||||
|
||||
let x = i32(pos.x);
|
||||
let y = i32(pos.y);
|
||||
let size = textureDimensions(data_tex);
|
||||
if textureLoad(data_tex, vec2<i32>(x, y), 0).b != 0. {
|
||||
return vec4<f32>(0.1, 0.1, 0.1, 1.0);
|
||||
} 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 h1 = textureLoad(data_tex, vec2<i32>(x + 1, y), 0).g;
|
||||
let h2 = textureLoad(data_tex, vec2<i32>(x, y + 1), 0).g;
|
||||
var h1: f32;
|
||||
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);
|
||||
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 {
|
||||
return ray_marching(pos);
|
||||
} else if u.shadow == 1u {
|
||||
return shadow_fragment(pos.xyz);
|
||||
return shadow_fragment(pos.xy);
|
||||
} else {
|
||||
let d = textureLoad(data_tex, vec2<i32>(i32(pos.x), i32(pos.y)), 0);
|
||||
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 {
|
||||
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 de = px.g;
|
||||
let z = pos.z;
|
||||
let de = (px.g / size.y) * 0.5;
|
||||
// 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. {
|
||||
return 0.;
|
||||
d = z;
|
||||
} 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> {
|
||||
let size = vec2<f32>(textureDimensions(data_tex));
|
||||
let in_texture = vec2<f32>(
|
||||
pos.x / 1980. * size.x,
|
||||
pos.y / 1080. * size.y,
|
||||
(pos.x / size.x) * 2. - 1.,
|
||||
(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_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 dist = 0.0;
|
||||
|
||||
while i < 1000u {
|
||||
dist = sdf(p);
|
||||
if dist < 0.0001 {
|
||||
let dist_threshold = 0.000001;
|
||||
while i < 100u {
|
||||
if length(p - ray_origin) > 3. {
|
||||
break;
|
||||
}
|
||||
p += dist * ray_dir;
|
||||
i += 1u;
|
||||
dist = sdf(p);
|
||||
if dist < dist_threshold {
|
||||
break;
|
||||
}
|
||||
p += dist * ray_dir;
|
||||
i += 1u;
|
||||
}
|
||||
|
||||
if dist < 0.0001 {
|
||||
return shadow_fragment(p);
|
||||
if dist < dist_threshold {
|
||||
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.);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ struct Uniforms {
|
||||
// inverse of the camera's view-projection matrix, for reconstructing a
|
||||
// world-space ray origin per pixel in the raymarcher
|
||||
camera_inv_proj: mat4x4<f32>,
|
||||
// Screen dimensions
|
||||
screen_dim: vec2<f32>
|
||||
};
|
||||
|
||||
// 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 {
|
||||
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 e = vec2<f32>(0.0, 0.0);
|
||||
// Orbit derivative for distance estimation. For the set plane it is d/dc
|
||||
|
||||
Reference in New Issue
Block a user