initial commit

This commit is contained in:
2025-12-13 22:03:49 +01:00
commit 4249326355
8 changed files with 3243 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

2604
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

14
Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "rust-sponges"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.100"
bytemuck = "1.24.0"
cgmath = "0.18.0"
crevice = {version = "0.18.0", features = ["cgmath"]}
env_logger = "0.11.8"
pollster = "0.4.0"
wgpu = "27.0.1"
winit = "0.30.12"

187
shaders/ray_marching.wgsl Normal file
View File

@ -0,0 +1,187 @@
struct RayMarchingPushConstants
{
inverse_proj: mat4x4<f32>,
view_matrix: mat4x4<f32>,
camera_pos: vec3<f32>,
scale: f32,
translation: vec3<f32>,
rotation: vec3<f32>,
}
var<push_constant> constants: RayMarchingPushConstants;
struct Vertex
{
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>
}
@vertex
fn vertex(@builtin(vertex_index) i: u32) -> Vertex
{
let vertices = array<vec4<f32>, 4>(
vec4<f32>(-1., -1., 0., 1.),
vec4<f32>(1., -1., 0., 1.),
vec4<f32>(-1., 1., 0., 1.),
vec4<f32>(1., 1., 0., 1.),
);
let uvs = array<vec2<f32>, 4>(
vec2<f32>(0., 0.),
vec2<f32>(1., 0.),
vec2<f32>(0., 1.),
vec2<f32>(1., 1.),
);
let indices = array<u32, 6>(
0, 1, 2,
2, 1, 3
);
var v: Vertex;
v.pos = vertices[indices[i]];
v.uv = uvs[indices[i]];
return v;
}
@fragment
fn fragment(in: Vertex) -> @location(0) vec4<f32>
{
// Produce input ray
let ndc_ray = vec4<f32>(in.uv.xy * 2. - vec2<f32>(1.), -1.0, 1.0);
var unproj =constants.inverse_proj * ndc_ray;
unproj.w = 0.;
// Reproject into frustum
var ray = (constants.view_matrix * unproj).xyz;
ray /= length(ray);
//return vec4<f32>(ray, 1.);
// Ray march
var t = 0.;
var pos = constants.camera_pos;
loop
{
// Sample sdf
let sdf = sdf(pos);
if sdf < 0.00001
{
let grad = sdf_gradient(pos);
return vec4<f32>((dot(grad, normalize(vec3<f32>(1.))) + 1.) * 0.5);
}
if sdf > 1000
{
break;
}
t += sdf;
pos += ray * sdf;
}
// Ray escaped, get skybox
return skybox(ray);
}
fn sdf(pos: vec3<f32>) -> f32
{
var x = pos;
var t = 1.;
for(var i = 0u; i < 3; i++)
{
x *= 1.5;
t *= 1.5;
x = abs(x);
x += vec3<f32>(0.01, 0.02, 0.08);
x *= rot();
}
return sdf_box(x, vec3<f32>(1.)) / t;
}
fn rot() -> mat3x3<f32>
{
let rx = 0.6;
let ry = 8.1;
return mat3x3<f32>(
cos(rx), sin(rx), 0.,
-sin(rx), cos(rx), 0.,
0., 0., 1.
)*
mat3x3<f32>(
1., 0., 0.,
0., cos(ry), sin(ry),
0, -sin(ry), cos(ry)
);
}
fn sdf_box(pos: vec3<f32>, b: vec3<f32>) -> f32
{
let q = abs(pos) - b;
return length(max(q, vec3<f32>(0.))) + min(max(q.x, max(q.y, q.z)), 0.);
}
fn sdf_gradient(p: vec3<f32>) -> vec3<f32>
{
let eps = 0.000001;
let h = vec2<f32>(eps, 0);
return normalize(
vec3<f32>(
sdf(p + h.xyy) - sdf(p-h.xyy),
sdf(p + h.yxy) - sdf(p-h.yxy),
sdf(p + h.yyx) - sdf(p-h.yyx)
)
);
}
fn skybox(dir: vec3<f32>) -> vec4<f32>
{
let sun_dir = normalize(vec3<f32>(1., 1., 1.));
let gnd_under = vec4<f32>(0.423, 0.450, 0.448, 1.0);
let gnd_top = vec4<f32>(0.323, 0.350, 0.348, 1.0);
let gnd = interpolate(gnd_top, gnd_under, map(dir.y, -0.7, 0, 0, 1));
let b = vec4<f32>(0.545, 0.874, 0.940, 1.0);
let top: vec4<f32> = vec4<f32>(0.0891, 0.464, 0.990, 1.0);
let sky: vec4<f32> = interpolate(b, top, dir.y);
let height = map(dir.y, -0.01, 0.01, 0.0, 1.0);
var res = interpolate(gnd, sky, height);
var dt = map( dot(dir, normalize(sun_dir)), 0.999, 1, 0, 1);
if(dt < 0) {dt = 0;}
res += dt * vec4(0.990, 0.973, 0.782, 1.0);
return res;
}
fn interpolate(a: vec4<f32>, b: vec4<f32>, x: f32) -> vec4<f32>
{
var t = x;
if(t > 1.) {t = 1.;};
if(t < 0.) {t = 0.;};
let at = 1. - t;
return vec4<f32>
(
a.x * at + b.x * t,
a.y * at + b.y * t,
a.z * at + b.z * t,
a.w * at + b.w * t
);
}
fn map(x: f32, xmin: f32, xmax: f32, ymin: f32, ymax: f32) -> f32
{
return ((x - xmin) / (xmax - xmin)) * (ymax - ymin) + ymin;
}

0
src/app.rs Normal file
View File

115
src/lib.rs Normal file
View File

@ -0,0 +1,115 @@
pub mod state;
use std::{os::linux::raw::stat, sync::Arc};
use winit::{
application::ApplicationHandler,
dpi::{LogicalPosition, PhysicalPosition},
event::{KeyEvent, RawKeyEvent, WindowEvent},
event_loop::{self, EventLoop},
keyboard::{Key, KeyCode, PhysicalKey},
platform::modifier_supplement::KeyEventExtModifierSupplement,
window::Window,
};
use crate::state::State;
pub fn run() -> anyhow::Result<()> {
env_logger::init();
let event_loop = EventLoop::with_user_event().build()?;
let mut app = RustSponges::default();
event_loop.run_app(&mut app)?;
Ok(())
}
// App struct
pub struct RustSponges {
state: Option<State>,
}
impl Default for RustSponges {
fn default() -> Self {
Self { state: None }
}
}
impl ApplicationHandler for RustSponges {
fn resumed(&mut self, event_loop: &event_loop::ActiveEventLoop) {
// Create window
let window = Arc::new(
event_loop
.create_window(
Window::default_attributes()
.with_title("Rust Sponges")
.with_resizable(true),
)
.unwrap(),
);
let state = pollster::block_on(State::new(window.clone()));
self.state = Some(state);
window.request_redraw();
}
fn window_event(
&mut self,
event_loop: &event_loop::ActiveEventLoop,
window_id: winit::window::WindowId,
event: winit::event::WindowEvent,
) {
let state = self.state.as_mut().unwrap();
match event {
WindowEvent::CloseRequested => {
event_loop.exit();
}
WindowEvent::RedrawRequested => {
state.render();
state.get_window().request_redraw();
}
WindowEvent::Resized(size) => {
state.resize(size);
}
WindowEvent::KeyboardInput {
device_id: _,
event: key_event,
is_synthetic: _,
} => {
state.key_event(key_event);
}
WindowEvent::MouseInput {
device_id: _,
state: mouse_state,
button,
} => {
state.mouse_event(mouse_state, button);
}
_ => {}
}
}
fn device_event(
&mut self,
_event_loop: &event_loop::ActiveEventLoop,
_device_id: winit::event::DeviceId,
event: winit::event::DeviceEvent,
) {
let state = self.state.as_mut().unwrap();
#[allow(clippy::single_match)]
match event {
winit::event::DeviceEvent::MouseMotion { delta } => {
state.cursor_moved(delta.0 as f32, delta.1 as f32);
}
_ => {}
}
}
}

3
src/main.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() -> anyhow::Result<()> {
rust_sponges::run()
}

319
src/state.rs Normal file
View File

@ -0,0 +1,319 @@
use std::{collections::HashSet, sync::Arc};
use cgmath::{Deg, Matrix4, SquareMatrix, Vector3};
use crevice::std430::AsStd430;
use wgpu::{
ColorWrites, Features, FeaturesWGPU, FeaturesWebGPU, FragmentState, PipelineLayoutDescriptor,
PushConstantRange, RenderPipeline, RenderPipelineDescriptor, ShaderStages, VertexState,
include_wgsl,
};
use winit::{
dpi::PhysicalPosition,
event::{ElementState, KeyEvent, MouseButton, RawKeyEvent},
keyboard::{KeyCode, PhysicalKey},
window::Window,
};
pub struct State {
window: Arc<Window>,
device: wgpu::Device,
queue: wgpu::Queue,
size: winit::dpi::PhysicalSize<u32>,
surface: wgpu::Surface<'static>,
surface_format: wgpu::TextureFormat,
// World
camera: Camera,
// Raymarching
ray_marching_renderer: RenderPipeline,
// Controls
pressed_set: HashSet<KeyCode>,
mouse_inside: bool,
}
pub struct Camera {
position: cgmath::Vector3<f32>,
pitch: f32,
yaw: f32,
fovy: f32,
aspect: f32,
znear: f32,
zfar: f32,
}
impl Camera {
fn get_proj(&self) -> Matrix4<f32> {
cgmath::perspective(cgmath::Deg(self.fovy), self.aspect, self.znear, self.zfar)
}
fn get_view(&self) -> Matrix4<f32> {
cgmath::Matrix4::from_angle_y(cgmath::Deg(-self.yaw))
* cgmath::Matrix4::from_angle_x(cgmath::Deg(-self.pitch))
}
}
impl State {
pub async fn new(window: Arc<Window>) -> State {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions::default())
.await
.unwrap();
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
required_features: Features {
features_wgpu: FeaturesWGPU::PUSH_CONSTANTS,
features_webgpu: FeaturesWebGPU::empty(),
},
required_limits: wgpu::Limits {
max_push_constant_size: RayMarchingPushConstants::std430_size_static() as u32,
..Default::default()
},
..Default::default()
})
.await
.unwrap();
let size = window.inner_size();
let surface = instance.create_surface(window.clone()).unwrap();
let cap = surface.get_capabilities(&adapter);
let surface_format = cap.formats[0];
let ray_marching_renderer_layout =
device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("ray_marching_compute_pipeline_layout"),
bind_group_layouts: &[],
push_constant_ranges: &[PushConstantRange {
stages: ShaderStages::FRAGMENT,
range: 0..RayMarchingPushConstants::std430_size_static() as u32,
}],
});
let ray_marching_renderer_shader_module =
device.create_shader_module(include_wgsl!("../shaders/ray_marching.wgsl"));
let ray_marching_renderer = device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some("ray_marching_renderer"),
layout: Some(&ray_marching_renderer_layout),
vertex: VertexState {
module: &ray_marching_renderer_shader_module,
entry_point: Some("vertex"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
buffers: &[],
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
fragment: Some(FragmentState {
module: &ray_marching_renderer_shader_module,
entry_point: Some("fragment"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: ColorWrites::all(),
})],
}),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
let state = State {
window,
device,
queue,
size,
surface,
surface_format,
camera: Camera {
position: Vector3::new(0., 0., 0.),
pitch: 0.,
yaw: 0.,
fovy: 90.,
aspect: size.width as f32 / size.height as f32,
znear: 0.001,
zfar: 1000.,
},
ray_marching_renderer,
pressed_set: HashSet::new(),
mouse_inside: false,
};
// Configure surface for the first time
state.configure_surface();
state
}
pub fn get_window(&self) -> &Window {
&self.window
}
pub fn configure_surface(&self) {
let surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: self.surface_format,
// Request compatibility with the sRGB-format texture view were going to create later.
view_formats: vec![self.surface_format.add_srgb_suffix()],
alpha_mode: wgpu::CompositeAlphaMode::Auto,
width: self.size.width,
height: self.size.height,
desired_maximum_frame_latency: 2,
present_mode: wgpu::PresentMode::AutoVsync,
};
self.surface.configure(&self.device, &surface_config);
}
pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
self.size = new_size;
self.camera.aspect = new_size.width as f32 / new_size.height as f32;
// reconfigure the surface
self.configure_surface();
}
pub fn render(&mut self) {
self.handle_movement();
// Create texture view
let surface_texture = self
.surface
.get_current_texture()
.expect("failed to acquire next swapchain texture");
let texture_view = surface_texture
.texture
.create_view(&wgpu::TextureViewDescriptor {
// Without add_srgb_suffix() the image we will be working with
// might not be "gamma correct".
format: Some(self.surface_format.add_srgb_suffix()),
..Default::default()
});
// Renders a GREEN screen
let mut encoder = self.device.create_command_encoder(&Default::default());
// Create the renderpass which will clear the screen.
let mut renderpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &texture_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::GREEN),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
// If you wanted to call any drawing commands, they would go here.
renderpass.set_pipeline(&self.ray_marching_renderer);
renderpass.set_push_constants(
ShaderStages::FRAGMENT,
0,
RayMarchingPushConstants {
inverse_projection_matrix: self.camera.get_proj().invert().unwrap(),
camera_pos: self.camera.position,
view_matrix: self.camera.get_view(),
}
.as_std430()
.as_bytes(),
);
renderpass.draw(0..6, 0..1);
// End the renderpass.
drop(renderpass);
// Submit the command in the queue to execute
self.queue.submit([encoder.finish()]);
self.window.pre_present_notify();
surface_texture.present();
}
pub fn cursor_moved(&mut self, x: f32, y: f32) {
const SENSIBILITY: f32 = 0.02;
let position = cgmath::Vector2::new(x, y);
let offset = position * SENSIBILITY;
if self.mouse_inside {
self.camera.yaw += offset.x;
self.camera.pitch += offset.y;
}
}
pub fn key_event(&mut self, event: KeyEvent) {
match (event.state, event.physical_key) {
(ElementState::Pressed, PhysicalKey::Code(KeyCode::Escape)) => {
self.window.set_cursor_visible(true);
self.window
.set_cursor_grab(winit::window::CursorGrabMode::None)
.unwrap();
self.mouse_inside = false;
}
(ElementState::Pressed, PhysicalKey::Code(c)) => {
self.pressed_set.insert(c);
}
(ElementState::Released, PhysicalKey::Code(c)) => {
self.pressed_set.remove(&c);
}
_ => {}
}
}
pub fn mouse_event(&mut self, state: ElementState, button: MouseButton) {
if state == ElementState::Pressed && button == MouseButton::Left {
self.mouse_inside = true;
self.window.set_cursor_visible(false);
self.window
.set_cursor_grab(winit::window::CursorGrabMode::Confined)
.or_else(|_| {
self.window
.set_cursor_grab(winit::window::CursorGrabMode::Locked)
})
.unwrap();
}
}
fn handle_movement(&mut self) {
const SPEED: f32 = 0.1;
// Forward-backward
let mut movement = cgmath::Vector3::new(0., 0., 0.);
if self.pressed_set.contains(&KeyCode::KeyW) {
movement.z += SPEED;
}
if self.pressed_set.contains(&KeyCode::KeyS) {
movement.z -= SPEED;
}
// Left rigth
if self.pressed_set.contains(&KeyCode::KeyA) {
movement.x += SPEED;
}
if self.pressed_set.contains(&KeyCode::KeyD) {
movement.x -= SPEED;
}
let rot_movement = cgmath::Matrix3::from_angle_y(cgmath::Deg(-self.camera.yaw))
* cgmath::Matrix3::from_angle_x(cgmath::Deg(-self.camera.pitch))
* movement;
self.camera.position -= rot_movement;
}
}
#[derive(AsStd430)]
pub struct RayMarchingPushConstants {
inverse_projection_matrix: Matrix4<f32>,
view_matrix: Matrix4<f32>,
camera_pos: Vector3<f32>,
}