diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index 7d6dd5a..0000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,3 +0,0 @@ -[build] -target = "wasm32-unknown-unknown" -rustflags ="--cfg=web_sys_unstable_apis" diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..186dfe4 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +github: [simbleau] +custom: ["buymeacoffee.com/simbleau"] diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..a7aad4d --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,14 @@ +{ + "extends": [ + "config:base" + ], + "packageRules": [ + { + "matchUpdateTypes": [ + "minor", + "patch" + ], + "automerge": true + } + ] +} \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..6530762 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,47 @@ +name: build + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + TOOLCHAIN: nightly + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + # Cargo cache + - name: Cargo cache + id: cache-cargo + uses: actions/cache@v3 + with: + path: ~/.cargo/bin + key: cargo + + # Install cargo deps + - name: Pull Cargo dependencies + if: steps.cache-cargo.outputs.cache-hit != 'true' + run: | + rustup update $TOOLCHAIN + rustup default $TOOLCHAIN + cargo install trunk + + # Pull website + - name: Build cache + id: cache-build + uses: actions/cache@v3 + with: + path: ./dist + key: dist-${{ hashFiles('./src') }}-${{ hashFiles('./assets') }} + + # Install cargo deps + - name: Trunk build + if: steps.cache-build.outputs.cache-hit != 'true' + run: | + trunk build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..698812f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,39 @@ +name: release + +on: + push: + branches: [main] + +env: + TOOLCHAIN: nightly + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + # Pull website + - name: Build cache + id: cache-build + uses: actions/cache@v3 + with: + path: ./dist + key: dist-${{ hashFiles('./src') }}-${{ hashFiles('./assets') }} + + # Install cargo deps + - name: Trunk build + if: steps.cache-build.outputs.cache-hit != 'true' + run: | + rustup update $TOOLCHAIN + rustup default $TOOLCHAIN + cargo install trunk + trunk build --release + + # Deploy, GitHub Pages + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist diff --git a/Cargo.toml b/Cargo.toml index 1a71a66..5e631b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,6 @@ nalgebra = "0.31.1" [dependencies.rapier2d] version = "0.14.0" -opt-level = 3 features = [ "wasm-bindgen" ] [dependencies.web-sys] diff --git a/README.md b/README.md index 2424d17..59a229e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,46 @@ -# GPU-Accelerated 2D N-Body Simulation, in WASM -An N-body WebAssembly simulation using WebGPU. +
-# Run -- `trunk serve` +

GPU N-body WASM Simulation +
+ ++ + ++ + +

-# Progress -In progress. Currently a research project of [@seabass247](https://github.com/seabass247) and [@simbleau](https://github.com/simbleau). +

Click here to demo the simulation.

-![image](https://user-images.githubusercontent.com/48108917/181005443-a6c96151-b7b9-4dee-8ba3-0aaf814ac2c9.png) +
+ +--- + +## 📖 Overview +This repository is a 2D N-body simulation of a dynamical system of bodies, under the influence of physical forces such as gravity. The simulation is written completely in Rust with WebGPU and WGSL shading, exported to WebAssembly. We deploy the demo with GitHub Actions. + +🔸 Simulations like these are common in astrophysics and are used to understand the evolution of large-scale universal structures. + +--- + +# 🚀 Serve Locally +## Dependencies +- [Rust](https://www.rust-lang.org/) +- [trunk](https://trunkrs.dev/) (`cargo install trunk`) +- [wasm32-unkown-unknown](https://yew.rs/docs/getting-started/introduction#install-webassembly-target) (`rustup target add wasm32-unknown-unknown`) +## Serve +- Run: `trunk serve` +- Preview: [`http://localhost:8080/`](http://localhost:8080/) + +![Screenshot](https://user-images.githubusercontent.com/48108917/183275653-a2ee4f9c-a982-482e-8405-bd124d4bbcf5.png) + +--- + +## 📁 Directories + +- [__`assets`__](./assets/): directory contains textures and shaders. +- [__`src`__](./src/): directory contains the rust source code. + +--- + +## 🔏 License +This project is dual-licensed under both [Apache 2.0](LICENSE-APACHE) and [MIT](LICENSE-MIT) licenses. \ No newline at end of file diff --git a/assets/shaders/frag.wgsl b/assets/shaders/frag.wgsl index 52ca692..45ef94d 100644 --- a/assets/shaders/frag.wgsl +++ b/assets/shaders/frag.wgsl @@ -1,6 +1,7 @@ struct Input { @builtin(position) clip_position: vec4, @location(0) uv: vec2, + @location(1) color: vec3, }; struct Output { @@ -16,9 +17,11 @@ var texture_sampler: sampler; @fragment fn fs_main(in: Input) -> Output { var out: Output; - out.color = textureSample(texture, texture_sampler, vec2(in.uv.x, 1.0 - in.uv.y)); + out.color = textureSample(texture, texture_sampler, vec2(1.0 - in.uv.x, 1.0 - in.uv.y)); if ((pow(in.uv.x - 0.5, 2.0) + pow(in.uv.y - 0.5, 2.0)) > pow(0.5, 2.0)) { out.color = vec4(0.0); } + + out.color *= vec4(in.color, 1.0); return out; } diff --git a/assets/shaders/vert.wgsl b/assets/shaders/vert.wgsl index 93834fe..6494b68 100644 --- a/assets/shaders/vert.wgsl +++ b/assets/shaders/vert.wgsl @@ -12,6 +12,7 @@ struct Input { struct Output { @builtin(position) clip_position: vec4, @location(0) uv: vec2, + @location(1) color: vec3, }; struct InstanceInput { @@ -19,8 +20,19 @@ struct InstanceInput { @location(3) model_matrix_1: vec4, @location(4) model_matrix_2: vec4, @location(5) model_matrix_3: vec4, + @location(6) radius: f32, }; +struct WorldUniform { + radius: f32, + boundary_segments: u32, + rave: u32, + padding: u32, +}; + +@group(2) @binding(0) +var world: WorldUniform; + // Vertex shader @vertex fn vs_main( @@ -42,5 +54,32 @@ fn vs_main( // World coords -> Device Coordinates out.clip_position = camera.view_proj * world_vert; + + var brightest: vec3 = vec3(0.97, 0.97, 1.0); + var blue: vec3 = vec3(0.33, 0.4, 1.0); + var yellow: vec3 = vec3(0.97, 0.98, 0.8); + var orange: vec3 = vec3(0.96, 0.6, 0.25); + var red: vec3 = vec3(0.99, 0.25, 0.25); + + var star_color: vec3 = vec3(0.0); + if (instance.radius >= 0.75) { + star_color += mix(orange, red, (instance.radius - 0.75) * 4.0); + } else if (instance.radius >= 0.5) { + star_color += mix(yellow, orange, (instance.radius - 0.5) * 4.0); + } else if (instance.radius >= 0.25) { + star_color += mix(blue, yellow, (instance.radius - 0.25) * 4.0); + } else { + star_color += mix(brightest, blue, instance.radius * 4.0); + } + + var tint: vec3 = vec3(1.0); + var twinkle: vec3 = vec3(1.0); + if (world.rave > 0u) { + tint = vec3(abs(world_vert.xyz) % 2.0); + twinkle = vec3(instance.model_matrix_0.x); + } + + // Calculate color + out.color = star_color * tint * twinkle; return out; } \ No newline at end of file diff --git a/assets/shaders/world.vert.wgsl b/assets/shaders/world.vert.wgsl index 2ed6d3b..11c1dc6 100644 --- a/assets/shaders/world.vert.wgsl +++ b/assets/shaders/world.vert.wgsl @@ -7,7 +7,8 @@ var camera: CameraUniform; struct WorldUniform { radius: f32, boundary_segments: u32, - padding: vec2, + rave: u32, + padding: u32, }; @group(1) @binding(0) diff --git a/assets/textures/cookie.png b/assets/textures/cookie.png deleted file mode 100644 index 5a270b9..0000000 Binary files a/assets/textures/cookie.png and /dev/null differ diff --git a/assets/textures/disco.jpg b/assets/textures/disco.jpg new file mode 100644 index 0000000..91f637d Binary files /dev/null and b/assets/textures/disco.jpg differ diff --git a/assets/textures/moon.jpg b/assets/textures/moon.jpg deleted file mode 100644 index 81e639c..0000000 Binary files a/assets/textures/moon.jpg and /dev/null differ diff --git a/assets/textures/rust.png b/assets/textures/rust.png new file mode 100644 index 0000000..a95ee96 Binary files /dev/null and b/assets/textures/rust.png differ diff --git a/index.html b/index.html index f485824..bbd4872 100644 --- a/index.html +++ b/index.html @@ -4,15 +4,95 @@ N-Body WASM Simulation + -

FPS: 0

- -

Event Log

-
    - ... -
+
+

FPS: --

+
+ Instructions +
+ + WASD: Move Camera +
+ ↕: Scale Camera +
+ ↔: Rotate Camera +
+ Q: Wireframe +
+ E: Rave +
+
+ +
+

Event Log

+
    + ... +
+
+
\ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index bc9287f..18a1e69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,8 +20,7 @@ pub async fn run() { let dom = Dom::new(); let canvas = dom::get_canvas(); - canvas.set_width(600); - canvas.set_height(400); + let (width, height) = (canvas.client_width(), canvas.client_height()); log!("Acquired DOM elements"); // Connect graphics card to window @@ -34,8 +33,9 @@ pub async fn run() { .with_canvas(Some(canvas)) .build(&event_loop) .and_then(|w| { - // Set attributes - w.set_inner_size(LogicalSize::new(600.0, 400.0)); + // Set initial view port -- ** This isn't what we want! ** + // We want the canvas to always fit to the document. + w.set_inner_size(LogicalSize::new(width, height)); Ok(w) }) .expect("Could not build window"); @@ -64,8 +64,8 @@ pub async fn run() { // Load textures context - .add_texture("cookie", include_bytes!("../assets/textures/cookie.png")); - context.add_texture("moon", include_bytes!("../assets/textures/moon.jpg")); + .add_texture("disco", include_bytes!("../assets/textures/disco.jpg")); + context.add_texture("rust", include_bytes!("../assets/textures/rust.png")); log!("Loaded textures"); // Run program diff --git a/src/render/frame_descriptor.rs b/src/render/frame_descriptor.rs index 2d719cc..a148b18 100644 --- a/src/render/frame_descriptor.rs +++ b/src/render/frame_descriptor.rs @@ -14,6 +14,7 @@ use crate::{ pub struct FrameDescriptor { wireframe: bool, + rave: bool, transforms: Vec, camera: Camera, pub clear_color: Color, @@ -22,14 +23,16 @@ pub struct FrameDescriptor { impl FrameDescriptor { pub fn build(sim: &Simulation) -> FrameDescriptor { let mut transforms = Vec::new(); - for body in &sim.bodies { + let ctx = &sim.physics_context; + for body in &ctx.bodies { transforms.push(GpuTransform { model: Mat4::from_scale_rotation_translation( - Vec2::splat(2.0 * body.radius(sim)).extend(1.0), - Quat::from_rotation_z(body.rotation(sim)), - body.position(sim).extend(1.0), + Vec2::splat(2.0 * body.radius(ctx)).extend(1.0), + Quat::from_rotation_z(body.rotation(ctx)), + body.position(ctx).extend(1.0), ) .to_cols_array_2d(), + radius: body.radius(ctx), }) } @@ -49,6 +52,7 @@ impl FrameDescriptor { FrameDescriptor { wireframe: sim.state.wireframe, + rave: sim.state.rave, transforms, camera, clear_color, @@ -86,7 +90,8 @@ impl FrameDescriptor { let instance_data = self .transforms .iter() - .map(|gpu_transform| gpu_transform.model) + .map(GpuTransform::data) + .flatten() .collect::>(); device.create_buffer_init(&wgpu::util::BufferInitDescriptor { @@ -107,6 +112,6 @@ impl FrameDescriptor { &self, device: &Device, ) -> (Buffer, Vec, BindGroup, BindGroupLayout) { - WorldUniform::default().bind(device) + WorldUniform::from(self.rave).bind(device) } } diff --git a/src/render/gpu_types/transform.rs b/src/render/gpu_types/transform.rs index 9b1c818..2529f93 100644 --- a/src/render/gpu_types/transform.rs +++ b/src/render/gpu_types/transform.rs @@ -1,5 +1,5 @@ -use wgpu::VertexBufferLayout; use std::mem; +use wgpu::VertexBufferLayout; use crate::render::gpu_types::GpuPrimitive; @@ -7,6 +7,7 @@ use crate::render::gpu_types::GpuPrimitive; #[derive(Copy, Clone)] pub struct GpuTransform { pub model: [[f32; 4]; 4], + pub radius: f32, } unsafe impl bytemuck::Pod for GpuTransform {} @@ -38,6 +39,11 @@ impl GpuTransform { shader_location: 5, format: wgpu::VertexFormat::Float32x4, }, + wgpu::VertexAttribute { + offset: mem::size_of::<[f32; 16]>() as wgpu::BufferAddress, + shader_location: 6, + format: wgpu::VertexFormat::Float32, + }, ], }; } diff --git a/src/render/gpu_types/world.rs b/src/render/gpu_types/world.rs index d1d897d..1878909 100644 --- a/src/render/gpu_types/world.rs +++ b/src/render/gpu_types/world.rs @@ -10,15 +10,17 @@ use crate::{ pub struct WorldUniform { pub radius: f32, pub boundary_segments: u32, - _padding: [f32; 2], + pub rave_mode: u32, + _padding: u32, } -impl Default for WorldUniform { - fn default() -> Self { +impl From for WorldUniform { + fn from(rave: bool) -> Self { Self { radius: WORLD_RADIUS, boundary_segments: WORLD_EDGE_SEGMENTS, - _padding: [f32::default(), f32::default()], + rave_mode: rave as u32, + _padding: 0_u32, } } } @@ -31,19 +33,20 @@ impl GpuUniform for WorldUniform { &self, device: &Device, ) -> (Buffer, Vec, BindGroup, BindGroupLayout) { - let layout = create_wradius_bind_group_layout(device); - let buffer_contents = get_wradius_buffer_contents(); - let buffer = create_wradius_buffer(device, &buffer_contents); + let layout = create_world_bind_group_layout(device); + let buffer_contents = get_world_buffer_contents(self.rave_mode); + let buffer = create_world_buffer(device, &buffer_contents); let bind_group = create_world_bind_group(&buffer, &layout, device); (buffer, buffer_contents, bind_group, layout) } } -fn get_wradius_buffer_contents() -> Vec { +fn get_world_buffer_contents(rave: u32) -> Vec { let uniform = WorldUniform { radius: WORLD_RADIUS, boundary_segments: WORLD_EDGE_SEGMENTS, - _padding: [0.0, 0.0], + rave_mode: rave, + _padding: 0_u32, }; bytemuck::cast_slice(&[uniform]).to_vec() } @@ -63,7 +66,7 @@ fn create_world_bind_group( }) } -fn create_wradius_buffer(device: &Device, buffer_contents: &[u8]) -> Buffer { +fn create_world_buffer(device: &Device, buffer_contents: &[u8]) -> Buffer { device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("World Radius Buffer"), contents: buffer_contents, @@ -73,7 +76,7 @@ fn create_wradius_buffer(device: &Device, buffer_contents: &[u8]) -> Buffer { }) } -fn create_wradius_bind_group_layout(device: &Device) -> BindGroupLayout { +fn create_world_bind_group_layout(device: &Device) -> BindGroupLayout { device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { entries: &[wgpu::BindGroupLayoutEntry { binding: 0, diff --git a/src/render/wgpu_context.rs b/src/render/wgpu_context.rs index b7462f5..40ef29e 100644 --- a/src/render/wgpu_context.rs +++ b/src/render/wgpu_context.rs @@ -110,8 +110,30 @@ impl WgpuContext { camera_bind_group, camera_bind_group_layout, ) = frame_desc.create_camera_binding(&self.device); + + // Data for world boundaries + let ( + world_buffer, + world_buffer_contents, + world_bind_group, + world_bind_group_layout, + ) = frame_desc.create_world_data_binding(&self.device); + let world_pipeline = { + let pipeline_layout = self.device.create_pipeline_layout( + &wgpu::PipelineLayoutDescriptor { + label: Some("World Pipeline Layout"), + bind_group_layouts: &[ + &camera_bind_group_layout, + &world_bind_group_layout, + ], + push_constant_ranges: &[], + }, + ); + Pipeline::World.get(self, pipeline_layout) + }; + let (_, tex_bind_group, tex_bind_group_layout) = - self.get_texture(sim.state.texture_key); + self.get_texture(&sim.state.texture_key); let instance_buffer = frame_desc.create_instance_buffer(&self.device); // Get rendering pipeline let pipeline = match &sim.state.wireframe { @@ -132,6 +154,7 @@ impl WgpuContext { bind_group_layouts: &[ &camera_bind_group_layout, tex_bind_group_layout, + &world_bind_group_layout, ], push_constant_ranges: &[], }, @@ -140,27 +163,6 @@ impl WgpuContext { } }; - // Data for world boundaries - let ( - wradius_buffer, - world_buffer_contents, - wradius_bind_group, - wradius_bind_group_layout, - ) = frame_desc.create_world_data_binding(&self.device); - let world_pipeline = { - let pipeline_layout = self.device.create_pipeline_layout( - &wgpu::PipelineLayoutDescriptor { - label: Some("World Pipeline Layout"), - bind_group_layouts: &[ - &camera_bind_group_layout, - &wradius_bind_group_layout, - ], - push_constant_ranges: &[], - }, - ); - Pipeline::World.get(self, pipeline_layout) - }; - // Execute render pass { // Make pass @@ -188,6 +190,8 @@ impl WgpuContext { if !sim.state.wireframe { pass.set_bind_group(1, tex_bind_group, &[]); } + pass.set_bind_group(2, &world_bind_group, &[]); + pass.set_vertex_buffer(0, vertex_buffer.slice(..)); pass.set_vertex_buffer(1, instance_buffer.slice(..)); pass.set_index_buffer( @@ -203,7 +207,7 @@ impl WgpuContext { // Draw world boundaries pass.set_pipeline(&world_pipeline); pass.set_bind_group(0, &camera_bind_group, &[]); - pass.set_bind_group(1, &wradius_bind_group, &[]); + pass.set_bind_group(1, &world_bind_group, &[]); pass.draw(0..(WORLD_EDGE_SEGMENTS + 1), 0..1); } @@ -211,7 +215,7 @@ impl WgpuContext { self.queue .write_buffer(&camera_buffer, 0, &camera_buffer_contents); self.queue - .write_buffer(&wradius_buffer, 0, &world_buffer_contents); + .write_buffer(&world_buffer, 0, &world_buffer_contents); // Submit queue self.queue.submit(std::iter::once(encoder.finish())); diff --git a/src/runtime.rs b/src/runtime.rs index 413517e..6557593 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -6,24 +6,24 @@ use winit::window::Window; use crate::dom::Dom; use crate::render::WgpuContext; -use crate::sim::{Simulation, State, WORLD_RADIUS}; +use crate::sim::{Simulation, WORLD_RADIUS}; -pub struct Runtime<'a> { +pub struct Runtime { context: WgpuContext, window: Window, dom: Dom, - sim: Simulation<'a>, + sim: Simulation, } -impl Runtime<'_> { +impl Runtime { pub fn new(context: WgpuContext, window: Window, dom: Dom) -> Self { - let mut state = State::default(); + let mut sim = Simulation::new(100); // Zoom into sim let view_size = Vec2::new( window.inner_size().width as f32, window.inner_size().height as f32, ); - state.zoom = if view_size.y < view_size.x { + sim.state.zoom = if view_size.y < view_size.x { view_size.y / (WORLD_RADIUS * 2.0) } else { view_size.x / (WORLD_RADIUS * 2.0) @@ -33,7 +33,7 @@ impl Runtime<'_> { context, window, dom, - sim: Simulation::new(100, state), + sim, } } diff --git a/src/sim/body.rs b/src/sim/body.rs index 63210a3..aaeb353 100644 --- a/src/sim/body.rs +++ b/src/sim/body.rs @@ -2,7 +2,7 @@ use glam::Vec2; use nalgebra::{Complex, Unit}; use rapier2d::prelude::*; -use crate::sim::simulation::Simulation; +use super::physics::PhysicsContext; #[derive(PartialEq)] pub struct Body { @@ -11,15 +11,15 @@ pub struct Body { } impl Body { - pub fn mass(&self, sim: &Simulation) -> f32 { - sim.rigid_body_set + pub fn mass(&self, ctx: &PhysicsContext) -> f32 { + ctx.rigid_body_set .get(self.rigid_body_handle) .unwrap() .mass() } - pub fn radius(&self, sim: &Simulation) -> f32 { - sim.collider_set + pub fn radius(&self, ctx: &PhysicsContext) -> f32 { + ctx.collider_set .get(self.collider_handle) .map(Collider::shape) .map(::as_ball) @@ -28,16 +28,16 @@ impl Body { .unwrap() } - pub fn rotation(&self, sim: &Simulation) -> f32 { - sim.rigid_body_set + pub fn rotation(&self, ctx: &PhysicsContext) -> f32 { + ctx.rigid_body_set .get(self.rigid_body_handle) .map(RigidBody::rotation) .map(Unit::>::angle) .unwrap() } - pub fn position(&self, sim: &Simulation) -> Vec2 { - sim.rigid_body_set + pub fn position(&self, ctx: &PhysicsContext) -> Vec2 { + ctx.rigid_body_set .get(self.rigid_body_handle) .map(RigidBody::position) .map(|p| Vec2::new(p.translation.x, p.translation.y)) diff --git a/src/sim/input.rs b/src/sim/input.rs index f73e44d..940c1a2 100644 --- a/src/sim/input.rs +++ b/src/sim/input.rs @@ -25,22 +25,6 @@ impl InputController { } false } - // pub fn is_one_of_key_pressed(&self, keys: Vec) -> bool { - // for key in keys { - // if self.is_key_pressed(key) { - // return true; - // } - // } - // false - // } - // pub fn is_one_of_key_released(&self, keys: Vec) -> bool { - // for key in keys { - // if self.is_key_released(key) { - // return true; - // } - // } - // false - // } pub fn is_key_active(&self, key: VirtualKeyCode) -> bool { self.keys.contains(&key) } diff --git a/src/sim/mod.rs b/src/sim/mod.rs index dbe48a7..ec919b2 100644 --- a/src/sim/mod.rs +++ b/src/sim/mod.rs @@ -11,3 +11,5 @@ pub mod input; mod simulation; pub use simulation::*; + +mod physics; diff --git a/src/sim/physics.rs b/src/sim/physics.rs index 8a4c9d9..5330987 100644 --- a/src/sim/physics.rs +++ b/src/sim/physics.rs @@ -1,52 +1,96 @@ use glam::Vec2; +use rapier2d::prelude::*; -use super::Body; +use super::{Body, GRAVITY_AMPLIFIER, UNIVERSAL_GRAVITY}; -pub struct Collision { - pub v1_final: Vec2, - pub v2_final: Vec2, +pub struct PhysicsContext { + pub bodies: Vec, + pub integration_parameters: IntegrationParameters, + pub physics_pipeline: PhysicsPipeline, + pub island_manager: IslandManager, + pub broad_phase: BroadPhase, + pub narrow_phase: NarrowPhase, + pub ccd_solver: CCDSolver, + pub rigid_body_set: RigidBodySet, + pub collider_set: ColliderSet, } -pub fn collides(body: &Body, other: &Body) -> bool { - (other.position - body.position).length() <= (body.radius + other.radius) -} - -pub fn get_collision(b1: &Body, b2: &Body) -> Option { - if collides(b1, b2) { - // The unit vector normal to the collision surface plane - let collision_surface_norm = (b2.position - b1.position).normalize(); - - // Rotate initial velocities to be parallel with the X-axis - let v1_init = b1.velocity.rotate(collision_surface_norm); - let v2_init = b2.velocity.rotate(-collision_surface_norm); - - let v1x = v1_init.x; - let v2x = v2_init.x; - - // Final velocity of body 1. Derived from the 1D equation for - // conservation of momentum (P); P_in = P_out where P = m*v - let v1x_final = ((b1.mass() - b2.mass()) * v1x - + 2.0 * b2.mass() * b2.mass()) - / (b1.mass() + b2.mass()); - - let mut v1_final = Vec2::new(v1x_final, v1_init.y); - - // Rotate body 1's final velocity back to their original plane - v1_final = v1_final.rotate(-collision_surface_norm) * 0.3; +impl PhysicsContext { + pub fn new() -> Self { + Self { + bodies: Vec::new(), + integration_parameters: IntegrationParameters::default(), + physics_pipeline: PhysicsPipeline::new(), + island_manager: IslandManager::new(), + broad_phase: BroadPhase::new(), + narrow_phase: NarrowPhase::new(), + ccd_solver: CCDSolver::new(), + rigid_body_set: RigidBodySet::new(), + collider_set: ColliderSet::new(), + } + } - // Final velocity of body 2. Same calculation as for body 1 - // except b1 is changed to b2 and vice-versa. - let v2x_final = ((b2.mass() - b1.mass()) * v2x - + 2.0 * b1.mass() * b1.mass()) - / (b2.mass() + b1.mass()); + pub fn create_body( + &mut self, + rb: impl Into, + coll: impl Into, + ) { + let rigid_body_handle = self.rigid_body_set.insert(rb); + let collider_handle = self.collider_set.insert_with_parent( + coll, + rigid_body_handle, + &mut self.rigid_body_set, + ); + let body = Body { + rigid_body_handle, + collider_handle, + }; + self.bodies.push(body); + } - let mut v2_final = Vec2::new(v2x_final, v2_init.y); + pub fn step(&mut self) { + // Calculate velocity vectors + let num_bodies = self.bodies.len(); + for i in 0..num_bodies { + // Get displacement + let body = &self.bodies[i]; + let mut force = Vec2::ZERO; + for other in &self.bodies { + if body != other { + let sqr_dist = (other.position(self) - body.position(self)) + .length_squared(); + let force_dir = (other.position(self) + - body.position(self)) + .normalize(); + force += force_dir + * UNIVERSAL_GRAVITY + * GRAVITY_AMPLIFIER + * body.mass(self) + * other.mass(self) + / sqr_dist; + } + } - // Rotate body 2's final velocity back to their original plane - v2_final = v2_final.rotate(collision_surface_norm) * 0.3; + // Apply gravity + let rigid_body = + self.rigid_body_set.get_mut(body.rigid_body_handle).unwrap(); + rigid_body.reset_forces(true); + rigid_body.add_force(vector![force.x, force.y], true); + } - Some(Collision { v1_final, v2_final }) - } else { - None + self.physics_pipeline.step( + &vector![0.0, 0.0], + &self.integration_parameters, + &mut self.island_manager, + &mut self.broad_phase, + &mut self.narrow_phase, + &mut self.rigid_body_set, + &mut self.collider_set, + &mut ImpulseJointSet::new(), + &mut MultibodyJointSet::new(), + &mut self.ccd_solver, + &(), + &(), + ); } } diff --git a/src/sim/simulation.rs b/src/sim/simulation.rs index 240cc48..698ae6d 100644 --- a/src/sim/simulation.rs +++ b/src/sim/simulation.rs @@ -1,45 +1,38 @@ +use std::ops::Mul; + use glam::{Mat3, Quat, Vec2, Vec3, Vec3Swizzles}; use instant::Instant; use rapier2d::prelude::*; -use std::ops::Mul; use winit::event::VirtualKeyCode; -use crate::sim::{Body, State, WORLD_RADIUS}; +use crate::sim::{State, WORLD_RADIUS}; + +use super::physics::PhysicsContext; pub const CAM_ZOOM_SPEED: f32 = 5.0; pub const CAM_ROTATE_SPEED: f32 = 5.0; pub const CAM_PAN_SPEED: f32 = 400.0; pub const DAMPENING: f32 = 0.05; -pub const RESTITUTION: f32 = 0.8; -pub const FRICTION: f32 = 0.8; -pub const PIXEL_DISTANCE: f32 = 100_000_000_000.0; // Meters -pub const UNIVERSAL_GRAVITY: f32 = 0.000000000066743 * PIXEL_DISTANCE; - -pub struct Simulation<'a> { - pub state: State<'a>, - pub bodies: Vec, - integration_parameters: IntegrationParameters, - physics_pipeline: PhysicsPipeline, - island_manager: IslandManager, - broad_phase: BroadPhase, - narrow_phase: NarrowPhase, - ccd_solver: CCDSolver, - pub(crate) rigid_body_set: RigidBodySet, - pub(crate) collider_set: ColliderSet, -} +pub const RESTITUTION: f32 = 0.95; +pub const FRICTION: f32 = 0.1; +pub const GRAVITY_AMPLIFIER: f32 = 10_000_000_000.0; +pub const UNIVERSAL_GRAVITY: f32 = 0.000000000066743; -impl<'a> Simulation<'a> { - pub fn new(num_bodies: u32, state: State<'a>) -> Self { - let mut rigid_body_set = RigidBodySet::new(); - let mut collider_set = ColliderSet::new(); +pub struct Simulation { + pub state: State, + pub physics_context: PhysicsContext, +} +impl Simulation { + pub fn new(num_bodies: usize) -> Self { // Generate a bunch of bodies let radius_max = 1.0; - let angvel_max = 0.0 * (2.0 * std::f64::consts::PI); - let linvel_max = 3.0; + let angvel_max = 0.1 * (2.0 * std::f64::consts::PI); + let linvel_max = 2.0; let rngify = |x| (js_sys::Math::random() * x) as f32; - let mut bodies = vec![]; + + let mut physics_context = PhysicsContext::new(); for _ in 0..num_bodies { // Calculate radius let rotation = rngify(2.0 * std::f64::consts::PI); @@ -59,104 +52,29 @@ impl<'a> Simulation<'a> { // Calculate initial angular velocity let angvel = rngify(angvel_max); - // Create physics components + // Bodies let rigid_body = RigidBodyBuilder::new(RigidBodyType::Dynamic) .translation(vector![position.x, position.y]) .linvel(vector![linvel.x, linvel.y]) .angvel(angvel) - .ccd_enabled(false) + .ccd_enabled(true) .rotation(rotation) .build(); let collider = ColliderBuilder::ball(radius) .restitution(RESTITUTION) .friction(FRICTION) .build(); - let rigid_body_handle = rigid_body_set.insert(rigid_body); - let collider_handle = collider_set.insert_with_parent( - collider, - rigid_body_handle, - &mut rigid_body_set, - ); - - let body = Body { - rigid_body_handle, - collider_handle, - }; - bodies.push(body); + physics_context.create_body(rigid_body, collider); } - /* Create other structures necessary for the simulation. */ - let integration_parameters = IntegrationParameters::default(); - let physics_pipeline = PhysicsPipeline::new(); - let island_manager = IslandManager::new(); - let broad_phase = BroadPhase::new(); - let narrow_phase = NarrowPhase::new(); - let ccd_solver = CCDSolver::new(); - Self { - state, - bodies, - integration_parameters, - physics_pipeline, - island_manager, - broad_phase, - narrow_phase, - ccd_solver, - rigid_body_set, - collider_set, + state: State::default(), + physics_context, } } - pub fn step(&mut self, dt: f32) { - self.integration_parameters.dt = dt; - - // Calculate velocity vectors - let num_bodies = self.bodies.len(); - for i in 0..num_bodies { - // Get displacement - let body = &self.bodies[i]; - let mut force = Vec2::ZERO; - for other in &self.bodies { - if body != other { - let sqr_dist = (other.position(self) - body.position(self)) - .length_squared(); - let force_dir = (other.position(self) - - body.position(self)) - .normalize(); - force += force_dir - * UNIVERSAL_GRAVITY - * body.mass(self) - * other.mass(self) - / sqr_dist; - } - } - - // Apply gravity - let rigid_body = - self.rigid_body_set.get_mut(body.rigid_body_handle).unwrap(); - rigid_body.reset_forces(true); - rigid_body.add_force(vector![force.x, force.y], true); - } - - // Calculate velocity vectors - self.physics_pipeline.step( - &vector![0.0, 0.0], - &self.integration_parameters, - &mut self.island_manager, - &mut self.broad_phase, - &mut self.narrow_phase, - &mut self.rigid_body_set, - &mut self.collider_set, - &mut ImpulseJointSet::new(), - &mut MultibodyJointSet::new(), - &mut self.ccd_solver, - &(), - &(), - ); - } - pub fn update(&mut self) { - // Pausing + // Check for pause key if self .state .input_controller @@ -165,14 +83,17 @@ impl<'a> Simulation<'a> { self.state.paused = !self.state.paused; } - // Get delta time + // Step simulation + if !self.state.paused { + self.physics_context.step(); + } + + // Update last frame, get delta time let now = Instant::now(); let dt = (now - self.state.last_frame.unwrap_or(now)).as_secs_f32(); self.state.last_frame.replace(now); - if !self.state.paused { - self.step(dt); - } + // Control camera self.update_camera(dt); // Reset input controller @@ -237,9 +158,10 @@ impl<'a> Simulation<'a> { } // Texture Change if state.input_controller.is_key_released(VirtualKeyCode::E) { - state.texture_key = match state.texture_key { - "moon" => "cookie", - _ => "moon", + state.rave = !state.rave; + state.texture_key = match &state.texture_key as &str { + "rust" => "disco".to_owned(), + _ => "rust".to_owned(), }; } diff --git a/src/sim/state.rs b/src/sim/state.rs index aa33baf..95748f7 100644 --- a/src/sim/state.rs +++ b/src/sim/state.rs @@ -4,14 +4,15 @@ use winit::event::{ElementState, WindowEvent}; use crate::sim::input::InputController; -pub struct State<'a> { +pub struct State { pub mouse_pos: DVec2, pub view_size: UVec2, pub last_frame: Option, pub wireframe: bool, pub paused: bool, pub bg_color: DVec3, - pub texture_key: &'a str, + pub texture_key: String, + pub rave: bool, pub pan: Vec2, pub pan_velocity: Vec2, pub rotation: f32, @@ -19,7 +20,7 @@ pub struct State<'a> { pub input_controller: InputController, } -impl<'a> Default for State<'a> { +impl Default for State { fn default() -> Self { Self { mouse_pos: DVec2::default(), @@ -28,7 +29,8 @@ impl<'a> Default for State<'a> { wireframe: false, paused: false, bg_color: DVec3::default(), - texture_key: "moon", + texture_key: "rust".to_owned(), + rave: false, pan: Vec2::ZERO, pan_velocity: Vec2::ZERO, rotation: 0.0, @@ -38,7 +40,7 @@ impl<'a> Default for State<'a> { } } -impl<'a> State<'a> { +impl State { pub fn handle_input(&mut self, event: &WindowEvent) { // We have no events to handle currently match event {