feat: add 3d support
This commit is contained in:
@@ -8,6 +8,7 @@ bytemuck = { version = "1.25.2", features = ["derive"] }
|
|||||||
dashu-float = "0.6.0"
|
dashu-float = "0.6.0"
|
||||||
eframe = { version = "0.36.2", default-features = false, features = ["wgpu", "default_fonts", "x11", "wayland", "accesskit"] }
|
eframe = { version = "0.36.2", default-features = false, features = ["wgpu", "default_fonts", "x11", "wayland", "accesskit"] }
|
||||||
egui = "0.36.2"
|
egui = "0.36.2"
|
||||||
|
glam = "0.33.8"
|
||||||
log = "0.4.34"
|
log = "0.4.34"
|
||||||
png = "0.18.1"
|
png = "0.18.1"
|
||||||
|
|
||||||
|
|||||||
+180
-53
@@ -5,6 +5,7 @@ use eframe::egui_wgpu;
|
|||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use eframe::egui_wgpu::wgpu;
|
use eframe::egui_wgpu::wgpu;
|
||||||
|
|
||||||
|
use crate::camera::Camera;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use crate::cli::Cli;
|
use crate::cli::Cli;
|
||||||
use crate::fractal::{
|
use crate::fractal::{
|
||||||
@@ -261,6 +262,8 @@ pub struct FractalApp {
|
|||||||
de_coloring: bool,
|
de_coloring: bool,
|
||||||
// Use shadow coloring
|
// Use shadow coloring
|
||||||
shadow: bool,
|
shadow: bool,
|
||||||
|
// Use 3D raymarching rendering
|
||||||
|
dimension3: bool,
|
||||||
|
|
||||||
/// List of enabled lights in the world
|
/// List of enabled lights in the world
|
||||||
lights: Vec<Light>,
|
lights: Vec<Light>,
|
||||||
@@ -343,6 +346,9 @@ pub struct FractalApp {
|
|||||||
/// through the display format and drift the zoom.
|
/// through the display format and drift the zoom.
|
||||||
zoom_edit: String,
|
zoom_edit: String,
|
||||||
zoom_edited: bool,
|
zoom_edited: bool,
|
||||||
|
|
||||||
|
/// The camera used to render 3D fractals
|
||||||
|
camera: Camera,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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).
|
||||||
@@ -430,6 +436,7 @@ impl FractalApp {
|
|||||||
antialias: false,
|
antialias: false,
|
||||||
de_coloring: false,
|
de_coloring: false,
|
||||||
shadow: false,
|
shadow: false,
|
||||||
|
dimension3: false,
|
||||||
lights: vec![Light::default()],
|
lights: vec![Light::default()],
|
||||||
buddha_r_cap: 50,
|
buddha_r_cap: 50,
|
||||||
buddha_g_cap: 500,
|
buddha_g_cap: 500,
|
||||||
@@ -465,6 +472,7 @@ impl FractalApp {
|
|||||||
center_im_edit,
|
center_im_edit,
|
||||||
zoom_edit,
|
zoom_edit,
|
||||||
zoom_edited: false,
|
zoom_edited: false,
|
||||||
|
camera: Camera::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -929,9 +937,14 @@ impl FractalApp {
|
|||||||
phoenix_p: [self.phoenix_p.0 as f32, self.phoenix_p.1 as f32],
|
phoenix_p: [self.phoenix_p.0 as f32, self.phoenix_p.1 as f32],
|
||||||
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.shadow) as u32,
|
de_coloring: (self.de_coloring | self.shadow | self.dimension3) as u32,
|
||||||
shadow: self.shadow as u32,
|
shadow: self.shadow as u32,
|
||||||
|
dimension3: self.dimension3 as u32,
|
||||||
|
camera_direction: self.camera.direction().to_array(),
|
||||||
_pad: [0; _],
|
_pad: [0; _],
|
||||||
|
_pad2: [0; _],
|
||||||
|
_pad3: [0; _],
|
||||||
|
camera_inv_proj: self.camera.orthographic().inverse().to_cols_array(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1639,7 +1652,8 @@ impl FractalApp {
|
|||||||
.logarithmic(true),
|
.logarithmic(true),
|
||||||
);
|
);
|
||||||
ui.add(egui::Slider::new(&mut self.color_offset, 0.0..=1.0).text("color offset"));
|
ui.add(egui::Slider::new(&mut self.color_offset, 0.0..=1.0).text("color offset"));
|
||||||
ui.checkbox(&mut self.shadow, "Shadow");
|
ui.checkbox(&mut self.shadow, "Shadow rendering");
|
||||||
|
ui.checkbox(&mut self.dimension3, "3D rendering");
|
||||||
if !self.shadow {
|
if !self.shadow {
|
||||||
egui::ComboBox::from_label("palette")
|
egui::ComboBox::from_label("palette")
|
||||||
.selected_text(PALETTE_NAMES[self.palette as usize])
|
.selected_text(PALETTE_NAMES[self.palette as usize])
|
||||||
@@ -1964,8 +1978,30 @@ impl FractalApp {
|
|||||||
|
|
||||||
// Touch: pinch to zoom (toward the gesture center) and two-finger pan.
|
// Touch: pinch to zoom (toward the gesture center) and two-finger pan.
|
||||||
// Takes precedence over single-finger drag while two fingers are down.
|
// Takes precedence over single-finger drag while two fingers are down.
|
||||||
|
// In 3D mode the same gestures orbit/dolly the raymarch camera
|
||||||
|
// 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());
|
let multi_touch = ui.input(|i| i.multi_touch());
|
||||||
if let Some(mt) = multi_touch {
|
if self.dimension3 {
|
||||||
|
if let Some(mt) = multi_touch {
|
||||||
|
let t = mt.translation_delta;
|
||||||
|
if t.x != 0.0 || t.y != 0.0 {
|
||||||
|
self.camera.rotate(t.x * ROT_SENS, -t.y * ROT_SENS);
|
||||||
|
interacted = true;
|
||||||
|
}
|
||||||
|
if mt.zoom_delta != 1.0 {
|
||||||
|
self.camera.zoom(1.0 / mt.zoom_delta);
|
||||||
|
interacted = true;
|
||||||
|
}
|
||||||
|
ui.ctx().request_repaint();
|
||||||
|
} else if response.dragged() {
|
||||||
|
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 {
|
||||||
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 {
|
||||||
self.view.pan_pixels(t.x as f64, t.y as f64, height_px);
|
self.view.pan_pixels(t.x as f64, t.y as f64, height_px);
|
||||||
@@ -1989,75 +2025,165 @@ impl FractalApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mouse wheel / trackpad: zoom toward the cursor.
|
// Mouse wheel / trackpad: zoom toward the cursor (2D), or dolly the
|
||||||
|
// camera's ortho volume (3D).
|
||||||
let (scroll_y, hover) = ui.input(|i| (i.smooth_scroll_delta.y, i.pointer.hover_pos()));
|
let (scroll_y, hover) = ui.input(|i| (i.smooth_scroll_delta.y, i.pointer.hover_pos()));
|
||||||
if scroll_y != 0.0
|
if scroll_y != 0.0
|
||||||
&& let Some(pos) = hover
|
&& let Some(pos) = hover
|
||||||
&& rect.contains(pos)
|
&& rect.contains(pos)
|
||||||
{
|
{
|
||||||
let off = pos - rect.center();
|
|
||||||
let factor = (-scroll_y as f64 * 0.0015).exp();
|
let factor = (-scroll_y as f64 * 0.0015).exp();
|
||||||
self.view
|
if self.dimension3 {
|
||||||
.zoom_at_pixel(off.x as f64, off.y as f64, height_px, factor);
|
self.camera.zoom(factor as f32);
|
||||||
|
} else {
|
||||||
|
let off = pos - rect.center();
|
||||||
|
self.view
|
||||||
|
.zoom_at_pixel(off.x as f64, off.y as f64, height_px, factor);
|
||||||
|
}
|
||||||
interacted = true;
|
interacted = true;
|
||||||
ui.ctx().request_repaint();
|
ui.ctx().request_repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyboard: arrows pan, z/s zoom in/out, +/- adjust iterations, R
|
// Keyboard: arrows pan, z/s zoom in/out, +/- adjust iterations, R
|
||||||
// resets the view, H/I toggle the Help/Info windows. Skipped while a
|
// resets the view, H/I toggle the Help/Info windows. In 3D mode,
|
||||||
// text field (e.g. the center/zoom edit boxes) has focus.
|
// ZQSD move the camera (forward/left/back/right), space/ctrl move it
|
||||||
|
// up/down, and the arrow keys look around instead of panning.
|
||||||
|
// Skipped while a text field (e.g. the center/zoom edit boxes) has
|
||||||
|
// focus.
|
||||||
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);
|
||||||
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
|
if self.dimension3 {
|
||||||
// frame rate. See `pan_pixels`'s screen-space (+x right, +y down)
|
let (fwd, back, strafe_l, strafe_r, up, down, look_l, look_r, look_u, look_d) = ui
|
||||||
// convention: Right/Down pan the *camera* right/down, which is
|
.input(|i| {
|
||||||
// the opposite delta sign from a drag that would show the same
|
(
|
||||||
// content (a drag grabs the canvas; these keys move the camera).
|
i.key_down(egui::Key::Z),
|
||||||
const PAN_SPEED_PX: f64 = 700.0;
|
i.key_down(egui::Key::S),
|
||||||
let mut dx = 0.0;
|
i.key_down(egui::Key::Q),
|
||||||
let mut dy = 0.0;
|
i.key_down(egui::Key::D),
|
||||||
if left {
|
i.key_down(egui::Key::Space),
|
||||||
dx += PAN_SPEED_PX * dt;
|
i.modifiers.ctrl || i.modifiers.command,
|
||||||
}
|
i.key_down(egui::Key::ArrowLeft),
|
||||||
if right {
|
i.key_down(egui::Key::ArrowRight),
|
||||||
dx -= PAN_SPEED_PX * dt;
|
i.key_down(egui::Key::ArrowUp),
|
||||||
}
|
i.key_down(egui::Key::ArrowDown),
|
||||||
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.
|
// Units/sec move speed and radians/sec look speed.
|
||||||
const ZOOM_SPEED: f64 = 1.0;
|
const MOVE_SPEED: f32 = 0.1;
|
||||||
if zoom_in != zoom_out {
|
const LOOK_SPEED: f32 = 0.05;
|
||||||
let rate = if zoom_in { ZOOM_SPEED } else { -ZOOM_SPEED };
|
let mut mv_fwd = 0.0f32;
|
||||||
let factor = (-rate * dt).exp();
|
let mut mv_right = 0.0f32;
|
||||||
self.view.zoom_at_pixel(0.0, 0.0, height_px, factor);
|
let mut mv_up = 0.0f32;
|
||||||
interacted = true;
|
if fwd {
|
||||||
}
|
mv_fwd += MOVE_SPEED * dt as f32;
|
||||||
if left || right || up || down || zoom_in || zoom_out {
|
}
|
||||||
ui.ctx().request_repaint();
|
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 dpitch = 0.0f32;
|
||||||
|
if look_r {
|
||||||
|
dyaw += LOOK_SPEED * dt as f32;
|
||||||
|
}
|
||||||
|
if look_l {
|
||||||
|
dyaw -= LOOK_SPEED * dt as f32;
|
||||||
|
}
|
||||||
|
if look_u {
|
||||||
|
dpitch += LOOK_SPEED * dt as f32;
|
||||||
|
}
|
||||||
|
if look_d {
|
||||||
|
dpitch -= LOOK_SPEED * dt as f32;
|
||||||
|
}
|
||||||
|
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
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)) {
|
||||||
self.view = Self::default_view_for(self.mode, self.kind);
|
self.view = Self::default_view_for(self.mode, self.kind);
|
||||||
|
self.camera = Camera::new();
|
||||||
interacted = true;
|
interacted = true;
|
||||||
}
|
}
|
||||||
if ui.input(|i| i.key_pressed(egui::Key::H)) {
|
if ui.input(|i| i.key_pressed(egui::Key::H)) {
|
||||||
@@ -2152,6 +2278,7 @@ impl FractalApp {
|
|||||||
(((rect.height() * ppp).round() as u32) / downscale).max(1),
|
(((rect.height() * ppp).round() as u32) / downscale).max(1),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
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 {
|
||||||
uniforms.aa_level = 1; // supersampling is wasted on the low-res pass
|
uniforms.aa_level = 1; // supersampling is wasted on the low-res pass
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
use std::f32::consts::PI;
|
||||||
|
|
||||||
|
use glam::Vec3;
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct Camera {
|
||||||
|
pub position: glam::Vec3,
|
||||||
|
|
||||||
|
pub yaw: f32,
|
||||||
|
pub pitch: f32,
|
||||||
|
|
||||||
|
/// Demi-hauteur du volume visible (remplace fov_y_radians)
|
||||||
|
ortho_height: f32,
|
||||||
|
aspect_ratio: f32,
|
||||||
|
z_near: f32,
|
||||||
|
z_far: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Camera {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
position: Vec3::new(0., 0., -1.),
|
||||||
|
yaw: 0. * PI / 180.,
|
||||||
|
pitch: 0. * PI / 180.,
|
||||||
|
ortho_height: 1.0,
|
||||||
|
aspect_ratio: 1.,
|
||||||
|
z_near: 0.1,
|
||||||
|
z_far: 100.,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_aspect_ratio(&mut self, aspect_ratio: f32) {
|
||||||
|
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) {
|
||||||
|
const PITCH_LIMIT: f32 = PI / 2.0 - 0.01;
|
||||||
|
self.yaw += dyaw;
|
||||||
|
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) -> 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;
|
||||||
|
|
||||||
|
glam::camera::lh::proj::directx::orthographic(
|
||||||
|
-half_width,
|
||||||
|
half_width,
|
||||||
|
-half_height,
|
||||||
|
half_height,
|
||||||
|
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)
|
||||||
|
* glam::Vec3::Z;
|
||||||
|
|
||||||
|
forward.normalize()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -95,6 +95,15 @@ pub struct Uniforms {
|
|||||||
pub de_coloring: u32,
|
pub de_coloring: u32,
|
||||||
// 0 = classic colors, 1 = shadows
|
// 0 = classic colors, 1 = shadows
|
||||||
pub shadow: u32,
|
pub shadow: u32,
|
||||||
|
// 0 = classic colors, 1 = 3D raymarching rendering
|
||||||
|
pub dimension3: u32,
|
||||||
|
pub _pad2: [u32; 3],
|
||||||
|
// camera direction vector
|
||||||
|
pub camera_direction: [f32; 3],
|
||||||
|
pub _pad3: [u32; 1],
|
||||||
|
/// 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],
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Offscreen textures for the two-pass render, recreated whenever the widget's
|
/// Offscreen textures for the two-pass render, recreated whenever the widget's
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
// and calls the wasm `main`, which boots eframe onto the page's <canvas>.
|
// and calls the wasm `main`, which boots eframe onto the page's <canvas>.
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
|
mod camera;
|
||||||
mod fractal;
|
mod fractal;
|
||||||
mod lights;
|
mod lights;
|
||||||
mod view;
|
mod view;
|
||||||
|
|||||||
+75
-24
@@ -19,30 +19,81 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@fragment
|
fn shadow_fragment(pos: vec3<f32>) -> vec4<f32> {
|
||||||
fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
|
||||||
if u.shadow != 0u {
|
|
||||||
let x = i32(pos.x);
|
|
||||||
let y = i32(pos.y);
|
|
||||||
if textureLoad(data_tex, vec2<i32>(x, y), 0).b != 0. {
|
|
||||||
return vec4<f32>(0.1, 0.1, 0.1, 1.0);
|
|
||||||
} else {
|
|
||||||
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;
|
|
||||||
let normal = normal_from_heights(h0, h1, h2);
|
|
||||||
return vec4<f32>(shadow_color(normal), 1.0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let d = textureLoad(data_tex, vec2<i32>(i32(pos.x), i32(pos.y)), 0);
|
|
||||||
let ci = d.r;
|
|
||||||
let de = d.g;
|
|
||||||
let interior_frac = d.b;
|
|
||||||
|
|
||||||
var col = classic_color(ci, de);
|
let x = i32(pos.x);
|
||||||
// Anti-alias the set boundary: fade toward black by the fraction of the
|
let y = i32(pos.y);
|
||||||
// pixel's sub-samples that landed in the interior.
|
if textureLoad(data_tex, vec2<i32>(x, y), 0).b != 0. {
|
||||||
col = col * (1.0 - interior_frac);
|
return vec4<f32>(0.1, 0.1, 0.1, 1.0);
|
||||||
return vec4<f32>(col, 1.0);
|
} else {
|
||||||
|
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;
|
||||||
|
let normal = normal_from_heights(h0, h1, h2);
|
||||||
|
return vec4<f32>(shadow_color(normal), 1.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@fragment
|
||||||
|
fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
||||||
|
if u.dimension3 == 0u {
|
||||||
|
if u.shadow != 0u {
|
||||||
|
return shadow_fragment(pos.xyz);
|
||||||
|
} else {
|
||||||
|
let d = textureLoad(data_tex, vec2<i32>(i32(pos.x), i32(pos.y)), 0);
|
||||||
|
let ci = d.r;
|
||||||
|
let de = d.g;
|
||||||
|
let interior_frac = d.b;
|
||||||
|
|
||||||
|
var col = classic_color(ci, de);
|
||||||
|
// Anti-alias the set boundary: fade toward black by the fraction of the
|
||||||
|
// pixel's sub-samples that landed in the interior.
|
||||||
|
col = col * (1.0 - interior_frac);
|
||||||
|
return vec4<f32>(col, 1.0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ray_marching(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sdf(pos: vec3<f32>) -> f32 {
|
||||||
|
let texture_pos = vec2<i32>(i32(pos.x), i32(pos.y));
|
||||||
|
let px = textureLoad(data_tex, texture_pos, 0);
|
||||||
|
let de = px.g;
|
||||||
|
let z = pos.z;
|
||||||
|
if px.b != 0. {
|
||||||
|
return 0.;
|
||||||
|
} else {
|
||||||
|
return sqrt(z * z + (de * de));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
var world_pos = u.camera_inv_proj * vec4<f32>(in_texture, pos.z - 10, 1.0);
|
||||||
|
|
||||||
|
let ray_origin = world_pos.xyz;
|
||||||
|
let ray_dir = u.camera_direction;
|
||||||
|
|
||||||
|
var p = ray_origin;
|
||||||
|
var i = 0u;
|
||||||
|
var dist = 0.0;
|
||||||
|
|
||||||
|
while i < 1000u {
|
||||||
|
dist = sdf(p);
|
||||||
|
if dist < 0.0001 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
p += dist * ray_dir;
|
||||||
|
i += 1u;
|
||||||
|
}
|
||||||
|
|
||||||
|
if dist < 0.0001 {
|
||||||
|
return shadow_fragment(p);
|
||||||
|
}
|
||||||
|
return vec4<f32>(1., 0., 0., 1.);
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ struct Uniforms {
|
|||||||
de_coloring: u32,
|
de_coloring: u32,
|
||||||
// 0 = classic colors, 1 = shadows
|
// 0 = classic colors, 1 = shadows
|
||||||
shadow: u32,
|
shadow: u32,
|
||||||
|
// Use 3D raymarching rendering
|
||||||
|
dimension3: u32,
|
||||||
|
// camera direction vector
|
||||||
|
camera_direction: vec3<f32>,
|
||||||
|
// 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>,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Smooth cyclic palettes (Inigo Quilez cosine palettes), selected by id.
|
// Smooth cyclic palettes (Inigo Quilez cosine palettes), selected by id.
|
||||||
@@ -146,10 +153,10 @@ fn shadow_color(normal: vec3<f32>) -> vec3<f32> {
|
|||||||
for (var i = 0u; i < 16; i++) {
|
for (var i = 0u; i < 16; i++) {
|
||||||
let light_color = unpack4x8unorm(lights[i].color);
|
let light_color = unpack4x8unorm(lights[i].color);
|
||||||
if any(light_color != vec4<f32>(0)) {
|
if any(light_color != vec4<f32>(0)) {
|
||||||
light_count += 1;
|
light_count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
color += compute_light(normal, vec3<f32>(
|
color += compute_light(normal, vec3<f32>(
|
||||||
cos(lights[i].azimuth) * cos(lights[i].altitude),
|
cos(lights[i].azimuth) * cos(lights[i].altitude),
|
||||||
sin(lights[i].azimuth) * cos(lights[i].altitude),
|
sin(lights[i].azimuth) * cos(lights[i].altitude),
|
||||||
sin(lights[i].altitude))) * light_color.xyz * light_color.a;
|
sin(lights[i].altitude))) * light_color.xyz * light_color.a;
|
||||||
|
|||||||
Reference in New Issue
Block a user