├── rustfmt.toml ├── src ├── lib.rs ├── scene.rs └── time.rs ├── shaders ├── pathtracer │ ├── spectrum.slang │ ├── bxdfs │ │ ├── diffuse.slang │ │ └── conductor.slang │ ├── light.slang │ ├── lights │ │ ├── dome-light.slang │ │ ├── rect-light.slang │ │ └── sphere-light.slang │ ├── kernels │ │ └── env-map-prepare.slang │ ├── sample-generator.slang │ ├── fresnel.slang │ ├── importance-map.slang │ ├── onb.slang │ ├── bxdf.slang │ ├── microfacet.slang │ └── sampling.slang ├── util.slang ├── mipgen.slang ├── compositor.slang ├── editor │ ├── egui.slang │ └── gizmo.slang ├── geometry │ └── bone-deform.slang ├── math.slang └── view-transform.slang ├── .gitignore ├── crates ├── ecs │ ├── Cargo.toml │ └── src │ │ ├── name.rs │ │ ├── lib.rs │ │ ├── commands.rs │ │ └── query.rs ├── asset │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── math │ ├── Cargo.toml │ └── src │ │ ├── primitives │ │ ├── mod.rs │ │ ├── shapes.rs │ │ ├── measure.rs │ │ └── sample.rs │ │ ├── complex.rs │ │ ├── dual.rs │ │ ├── unit.rs │ │ ├── lib.rs │ │ ├── isometry.rs │ │ ├── transform.rs │ │ ├── num.rs │ │ └── quaternion.rs ├── graphics │ ├── src │ │ ├── lib.rs │ │ ├── mipgen.rs │ │ ├── camera.rs │ │ ├── env_map.rs │ │ ├── acceleration_structure.rs │ │ └── pathtracer.rs │ └── Cargo.toml ├── geometry │ ├── src │ │ ├── lib.rs │ │ ├── primitives │ │ │ ├── grid.rs │ │ │ ├── torus.rs │ │ │ ├── sphere.rs │ │ │ ├── cylinder.rs │ │ │ └── platonic.rs │ │ ├── mesh.rs │ │ └── bone_deform.rs │ └── Cargo.toml ├── usd │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── os │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── gpu │ ├── Cargo.toml │ └── src │ ├── shader_compiler.rs │ └── d3d12 │ └── pix.rs ├── editor ├── windows │ ├── mod.rs │ ├── content.rs │ ├── console.rs │ ├── outliner.rs │ ├── inspector.rs │ └── viewport.rs ├── tabs │ ├── tab.rs │ └── tree.rs ├── camera.rs ├── editor.rs ├── main.rs └── gizmo.rs ├── README.md ├── Cargo.toml ├── LICENSE-MIT └── LICENSE-APACHE /rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod scene; 2 | pub mod time; 3 | -------------------------------------------------------------------------------- /shaders/pathtracer/spectrum.slang: -------------------------------------------------------------------------------- 1 | typealias SampledSpectrum = float3; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | debug/ 2 | target/ 3 | Cargo.lock 4 | 5 | .cargo/ 6 | .vscode/ 7 | 8 | resources/ 9 | -------------------------------------------------------------------------------- /crates/ecs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ecs" 3 | version = "0.1.0" 4 | edition = "2024" 5 | -------------------------------------------------------------------------------- /crates/asset/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "asset" 3 | version = "0.1.0" 4 | edition = "2024" 5 | -------------------------------------------------------------------------------- /crates/math/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "math" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | rand = "0.9.0" 8 | -------------------------------------------------------------------------------- /crates/math/src/primitives/mod.rs: -------------------------------------------------------------------------------- 1 | mod measure; 2 | mod sample; 3 | mod shapes; 4 | 5 | pub use measure::*; 6 | pub use sample::*; 7 | pub use shapes::*; 8 | -------------------------------------------------------------------------------- /crates/graphics/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod acceleration_structure; 2 | pub mod camera; 3 | pub mod env_map; 4 | pub mod mipgen; 5 | pub mod pathtracer; 6 | pub mod scene; 7 | -------------------------------------------------------------------------------- /crates/ecs/src/name.rs: -------------------------------------------------------------------------------- 1 | pub struct Name { 2 | pub name: String, 3 | } 4 | 5 | impl Name { 6 | pub fn new(name: impl ToString) -> Self { 7 | Self { 8 | name: name.to_string(), 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /crates/geometry/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod bone_deform; 2 | pub mod mesh; 3 | 4 | pub mod primitives { 5 | pub mod cylinder; 6 | pub mod grid; 7 | pub mod platonic; 8 | pub mod sphere; 9 | pub mod torus; 10 | } 11 | -------------------------------------------------------------------------------- /crates/geometry/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "geometry" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | asset = { path = "../asset" } 8 | gpu = { path = "../gpu" } 9 | math = { path = "../math" } 10 | -------------------------------------------------------------------------------- /editor/windows/mod.rs: -------------------------------------------------------------------------------- 1 | mod console; 2 | mod content; 3 | mod inspector; 4 | mod outliner; 5 | mod viewport; 6 | 7 | pub use console::ConsoleTab; 8 | pub use content::ContentTab; 9 | pub use inspector::InspectorTab; 10 | pub use outliner::OutlinerTab; 11 | pub use viewport::ViewportTab; 12 | 13 | pub use console::Log; 14 | -------------------------------------------------------------------------------- /crates/graphics/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graphics" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | asset = { path = "../asset" } 8 | ecs = { path = "../ecs" } 9 | geometry = { path = "../geometry" } 10 | gpu = { path = "../gpu" } 11 | math = { path = "../math" } 12 | 13 | exr = "1.73.0" 14 | rand = "0.9.0" 15 | -------------------------------------------------------------------------------- /crates/math/src/complex.rs: -------------------------------------------------------------------------------- 1 | use super::Unit; 2 | 3 | #[repr(C)] 4 | pub struct Complex { 5 | pub r: T, 6 | pub i: T, 7 | } 8 | 9 | impl Complex { 10 | pub const fn new(r: T, i: T) -> Self { 11 | Self { r, i } 12 | } 13 | } 14 | 15 | /// A unit complex number. May be used to represent a 2D rotation. 16 | pub type UnitComplex = Unit>; 17 | -------------------------------------------------------------------------------- /crates/usd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "usd" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | asset = { path = "../asset" } 8 | ecs = { path = "../ecs" } 9 | geometry = { path = "../geometry" } 10 | graphics = { path = "../graphics" } 11 | math = { path = "../math" } 12 | 13 | openusd-rs = { git = "https://github.com/FloatyMonkey/openusd-rs.git", tag = "v0.1.0" } 14 | -------------------------------------------------------------------------------- /crates/os/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "os" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | windows = { version = "0.59.0", features = [ 8 | "Win32_Foundation", 9 | "Win32_Graphics_Dwm", 10 | "Win32_Graphics_Gdi", 11 | "Win32_Security", 12 | "Win32_System_Com", 13 | "Win32_System_LibraryLoader", 14 | "Win32_UI_Controls", 15 | "Win32_UI_HiDpi", 16 | "Win32_UI_Input_KeyboardAndMouse", 17 | "Win32_UI_WindowsAndMessaging", 18 | ] } 19 | -------------------------------------------------------------------------------- /crates/math/src/primitives/shapes.rs: -------------------------------------------------------------------------------- 1 | use crate::Vec3; 2 | 3 | pub struct Sphere { 4 | pub radius: f32, 5 | } 6 | 7 | pub struct Cylinder { 8 | pub radius: f32, 9 | pub half_height: f32, 10 | } 11 | 12 | pub struct Capsule { 13 | /// Radius of the capsule. 14 | pub radius: f32, 15 | /// Height of the the cylinder part, exluding the hemispheres. 16 | pub half_length: f32, 17 | } 18 | 19 | pub struct Cuboid { 20 | pub half_size: Vec3, 21 | } 22 | 23 | pub struct TriMesh<'a> { 24 | pub vertices: &'a [Vec3], 25 | pub indices: &'a [usize], 26 | } 27 | -------------------------------------------------------------------------------- /src/scene.rs: -------------------------------------------------------------------------------- 1 | use asset::AssetServer; 2 | use ecs::{Name, World}; 3 | use graphics::camera::Camera; 4 | use math::{PI, UnitQuaternion, Vec3, transform::Transform3}; 5 | 6 | pub fn setup_scene(world: &mut World, assets: &mut AssetServer) { 7 | usd::populate_world_from_usd("../assets/usd/ybot-scene.usdc", world, assets); 8 | 9 | world.spawn(( 10 | Name::new("Camera"), 11 | Camera::default(), 12 | Transform3 { 13 | translation: Vec3::new(0.0, -5.0, 0.9), 14 | rotation: UnitQuaternion::from_axis_angle(Vec3::X, PI / 2.0), 15 | scale: Vec3::ONE, 16 | }, 17 | )); 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Engine 2 | 3 | Early stage of a new game engine, focusing on fully path-traced graphics. 4 | 5 | ![Editor](https://floatymonkey.com/engine/editor.png) 6 | 7 | ## Notable Features 8 | 9 | - Fully path-traced graphics. 10 | - Graphics API abstraction with DirectX 12 backend and Vulkan in progress. 11 | - Loads OpenUSD scenes using [openusd-rs](https://github.com/floatymonkey/openusd-rs), our work in progress pure Rust implementation of OpenUSD. 12 | 13 | ## Credits 14 | 15 | Maintained by Lauro Oyen ([@laurooyen](https://github.com/laurooyen)). 16 | 17 | Licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE). 18 | -------------------------------------------------------------------------------- /src/time.rs: -------------------------------------------------------------------------------- 1 | pub struct Time { 2 | start: std::time::Instant, 3 | delta: std::time::Duration, 4 | } 5 | 6 | impl Default for Time { 7 | fn default() -> Self { 8 | Self::new() 9 | } 10 | } 11 | 12 | impl Time { 13 | pub fn new() -> Self { 14 | let now = std::time::Instant::now(); 15 | 16 | Time { 17 | start: now, 18 | delta: now.duration_since(now), 19 | } 20 | } 21 | 22 | pub fn update(&mut self) { 23 | let now = std::time::Instant::now(); 24 | self.delta = now.duration_since(self.start); 25 | self.start = now; 26 | } 27 | 28 | pub fn delta_seconds(&self) -> f32 { 29 | self.delta.as_secs_f32() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /crates/ecs/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod commands; 2 | mod name; 3 | mod query; 4 | mod world; 5 | 6 | pub use commands::*; 7 | pub use name::*; 8 | pub use query::*; 9 | pub use world::*; 10 | 11 | /// Recursive macro treating arguments as a progression. 12 | /// 13 | /// Expansion of recursive!(macro, A, B, C) is equivalent to the expansion of sequence: 14 | /// - macro!(A) 15 | /// - macro!(A, B) 16 | /// - macro!(A, B, C) 17 | #[macro_export] 18 | macro_rules! recursive { 19 | ($macro: ident, $args: ident) => { 20 | $macro!{$args} 21 | }; 22 | ($macro: ident, $first: ident, $($rest: ident),*) => { 23 | $macro!{$first, $($rest),*} 24 | recursive!{$macro, $($rest),*} 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "engine" 3 | version = "0.1.0" 4 | edition = "2024" 5 | publish = false 6 | license = "MIT OR Apache-2.0" 7 | 8 | [workspace] 9 | members = ["crates/*"] 10 | 11 | [dependencies] 12 | asset = { path = "crates/asset" } 13 | ecs = { path = "crates/ecs" } 14 | geometry = { path = "crates/geometry" } 15 | gpu = { path = "crates/gpu" } 16 | graphics = { path = "crates/graphics" } 17 | math = { path = "crates/math" } 18 | os = { path = "crates/os" } 19 | usd = { path = "crates/usd" } 20 | 21 | egui = "0.26.2" 22 | egui_extras = { version = "0.26.2", features = ["svg"] } 23 | egui-gizmo = "0.16.1" 24 | log = "0.4.26" 25 | 26 | [[bin]] 27 | name = "editor" 28 | path = "editor/main.rs" 29 | -------------------------------------------------------------------------------- /shaders/util.slang: -------------------------------------------------------------------------------- 1 | func pack_float4(f: float4) -> uint { 2 | uint u = 0; 3 | 4 | u |= (uint)(f.r * 255.0) << 24; 5 | u |= (uint)(f.g * 255.0) << 16; 6 | u |= (uint)(f.b * 255.0) << 8; 7 | u |= (uint)(f.a * 255.0) << 0; 8 | 9 | return u; 10 | } 11 | 12 | func unpack_float4(u: uint) -> float4 { 13 | float4 f; 14 | 15 | f.r = (float)((u >> 24) & 0xff) / 255.0; 16 | f.g = (float)((u >> 16) & 0xff) / 255.0; 17 | f.b = (float)((u >> 8) & 0xff) / 255.0; 18 | f.a = (float)((u >> 0) & 0xff) / 255.0; 19 | 20 | return f; 21 | } 22 | 23 | func pack_float4_signed(f: float4) -> uint { 24 | return pack_float4(f * 0.5 + 0.5); 25 | } 26 | 27 | func unpack_float4_signed(u: uint) -> float4 { 28 | return unpack_float4(u) * 2.0 - 1.0; 29 | } 30 | -------------------------------------------------------------------------------- /crates/gpu/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gpu" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [features] 7 | default = ["d3d12", "vulkan"] 8 | d3d12 = [ 9 | "gpu-allocator/d3d12", 10 | ] 11 | vulkan = [ 12 | "dep:ash", 13 | "gpu-allocator/vulkan" 14 | ] 15 | 16 | [dependencies] 17 | ash = { version = "0.38.0", optional = true } 18 | bitflags = "2.8.0" 19 | gpu-allocator = { version = "0.27.0", default-features = false } 20 | log = "0.4.26" 21 | shader-slang = "0.1.0" 22 | windows = { version = "0.59.0", features = [ 23 | "Win32_Foundation", 24 | "Win32_Graphics_Direct3D", 25 | "Win32_Graphics_Direct3D12", 26 | "Win32_Graphics_Dxgi_Common", 27 | "Win32_Security", 28 | "Win32_System_LibraryLoader", 29 | "Win32_System_Threading", 30 | ] } 31 | -------------------------------------------------------------------------------- /shaders/mipgen.slang: -------------------------------------------------------------------------------- 1 | import math; 2 | 3 | cbuffer ConstantBuffer : register(b0) { 4 | uint output_id; 5 | uint2 output_res; 6 | uint input_id; 7 | uint input_mip; 8 | } 9 | 10 | Texture2D textures_2d_float[] : register(t0, space1); 11 | RWTexture2D rw_textures_2d_float[] : register(u0, space2); 12 | 13 | SamplerState linear_sampler : register(s0, space0); 14 | 15 | [shader("compute")] 16 | [numthreads(16, 16, 1)] 17 | void main(uint3 id : SV_DispatchThreadID) { 18 | let pixel = id.xy; 19 | if (any(pixel >= output_res)) return; 20 | 21 | let input = textures_2d_float[input_id]; 22 | let output = rw_textures_2d_float[output_id]; 23 | 24 | let uv = ((float2)pixel + 0.5) / (float2)output_res; 25 | let color = input.SampleLevel(linear_sampler, uv, input_mip); 26 | output[pixel] = color; 27 | } 28 | -------------------------------------------------------------------------------- /shaders/pathtracer/bxdfs/diffuse.slang: -------------------------------------------------------------------------------- 1 | import bxdf; 2 | import math; 3 | import onb; 4 | import spectrum; 5 | import sample_generator; 6 | import pathtracer.sampling; 7 | 8 | struct DiffuseBxdf: Bxdf { 9 | SampledSpectrum albedo; 10 | 11 | func eval(wi: float3, wo: float3) -> BxdfEval { 12 | if (!onb::same_hemisphere(wi, wo)) { 13 | return {}; 14 | } 15 | 16 | let pdf = cosine_hemisphere_pdf(onb::abs_cos_theta(wo)); 17 | 18 | return { albedo / PI, pdf }; 19 | } 20 | 21 | func sample(wi: float3, inout sg: S) -> BxdfSample { 22 | var wo = sample_cosine_hemisphere(sample_next_2d(sg)); 23 | 24 | if (wi.z < 0.0) { 25 | wo.z *= -1.0; 26 | } 27 | 28 | let pdf = cosine_hemisphere_pdf(onb::abs_cos_theta(wo)); 29 | 30 | return { wo, pdf, albedo / PI, BxdfLobe::DiffuseReflection }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/ecs/src/commands.rs: -------------------------------------------------------------------------------- 1 | use super::{Entity, World}; 2 | 3 | #[derive(Default)] 4 | pub struct Commands { 5 | commands: Vec>, // TODO: Optimize 6 | } 7 | 8 | impl Commands { 9 | pub fn new() -> Self { 10 | Default::default() 11 | } 12 | 13 | pub fn push(&mut self, command: C) { 14 | self.commands.push(Box::new(command)); 15 | } 16 | 17 | pub fn despawn(&mut self, entity: Entity) { 18 | self.push(Despawn { entity }) 19 | } 20 | 21 | pub fn execute(&mut self, world: &mut World) { 22 | for mut command in self.commands.drain(..) { 23 | command.execute(world); 24 | } 25 | } 26 | } 27 | 28 | pub trait Command { 29 | fn execute(&mut self, world: &mut World); 30 | } 31 | 32 | pub struct Despawn { 33 | pub entity: Entity, 34 | } 35 | 36 | impl Command for Despawn { 37 | fn execute(&mut self, world: &mut World) { 38 | world.entity_mut(self.entity).despawn(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /shaders/compositor.slang: -------------------------------------------------------------------------------- 1 | import view_transform; 2 | 3 | cbuffer ConstantBuffer : register(b0) { 4 | uint input_id; 5 | uint output_id; 6 | uint2 output_res; 7 | uint overlay_id; 8 | } 9 | 10 | Texture2D textures_2d_float4[] : register(t0, space1); 11 | RWTexture2D rw_textures_2d_float4[] : register(u0, space2); 12 | 13 | [shader("compute")] 14 | [numthreads(16, 16, 1)] 15 | void main(uint3 id : SV_DispatchThreadID) { 16 | let pixel = id.xy; 17 | if (any(pixel >= output_res)) return; 18 | 19 | let input = textures_2d_float4[input_id]; 20 | let overlay = textures_2d_float4[overlay_id]; 21 | let output = rw_textures_2d_float4[output_id]; 22 | 23 | var color = input[pixel].xyz; 24 | color = agx(color); 25 | //color = agx_golden(color); 26 | //color = agx_punchy(color); 27 | color = agx_eotf(color); 28 | 29 | let alpha = overlay[pixel].w; 30 | output[pixel] = float4(overlay[pixel].xyz * alpha + color * (1.0 - alpha), 1.0); 31 | } 32 | -------------------------------------------------------------------------------- /editor/windows/content.rs: -------------------------------------------------------------------------------- 1 | use crate::editor::MyContext; 2 | use crate::icons; 3 | use crate::tabs; 4 | 5 | pub struct ContentTab { 6 | name: String, 7 | } 8 | 9 | impl ContentTab { 10 | pub fn new() -> Self { 11 | ContentTab { 12 | name: format!("{} Content", icons::ASSET_MANAGER), 13 | } 14 | } 15 | } 16 | 17 | impl tabs::Tab for ContentTab { 18 | fn title(&self) -> &str { 19 | &self.name 20 | } 21 | 22 | fn ui(&mut self, ui: &mut egui::Ui, _ctx: &mut MyContext) { 23 | ui.horizontal(|ui| { 24 | for i in 0..100 { 25 | egui::Frame::window(ui.style()) 26 | .shadow(egui::epaint::Shadow { 27 | extrusion: 8.0, 28 | color: egui::Color32::from_black_alpha(25), 29 | }) 30 | .inner_margin(egui::Margin::symmetric(10.0, 30.0)) 31 | .outer_margin(egui::Margin::same(5.0)) 32 | .fill(egui::Color32::from_gray(48)) 33 | .stroke(egui::Stroke::new(1.0, egui::Color32::from_gray(255))) 34 | .show(ui, |ui| { 35 | ui.label(format!("Asset {}", i)); 36 | }); 37 | } 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /shaders/pathtracer/light.slang: -------------------------------------------------------------------------------- 1 | module light; 2 | 3 | __include lights.dome_light; 4 | __include lights.rect_light; 5 | __include lights.sphere_light; 6 | 7 | import sample_generator; 8 | import spectrum; 9 | 10 | public struct LightLiSample { 11 | /// Incident radiance at the shading point (unshadowed). 12 | public SampledSpectrum li; 13 | /// Normalized direction from the shading point to the sampled point on the light in world space. 14 | public float3 wi; 15 | /// Distance from the shading point to the sampled point on the light. 16 | public float distance; 17 | /// Probability density with respect to solid angle to sample direction `wi`. 18 | public float pdf; 19 | } 20 | 21 | public interface Light { 22 | /// Samples an incident direction at point `p` along which illumination from the light may be arriving. 23 | func sample_li(p: float3, inout sg: S) -> LightLiSample; 24 | 25 | /// Probability density with respect to solid angle to sample direction `wi` from point `p`. 26 | func pdf_li(p: float3, wi: float3) -> float; 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /crates/math/src/dual.rs: -------------------------------------------------------------------------------- 1 | use super::Number; 2 | use std::ops::{Add, Div, Mul, Sub}; 3 | 4 | #[repr(C)] 5 | pub struct Dual { 6 | pub r: T, 7 | pub d: T, 8 | } 9 | 10 | impl Dual { 11 | pub const fn new(r: T, d: T) -> Self { 12 | Self { r, d } 13 | } 14 | } 15 | 16 | impl Add for Dual { 17 | type Output = Dual; 18 | 19 | fn add(self, rhs: Dual) -> Self::Output { 20 | Self { 21 | r: self.r + rhs.r, 22 | d: self.d + rhs.d, 23 | } 24 | } 25 | } 26 | 27 | impl Sub for Dual { 28 | type Output = Dual; 29 | 30 | fn sub(self, rhs: Dual) -> Self::Output { 31 | Self { 32 | r: self.r - rhs.r, 33 | d: self.d - rhs.d, 34 | } 35 | } 36 | } 37 | 38 | impl Mul for Dual { 39 | type Output = Dual; 40 | 41 | fn mul(self, rhs: Dual) -> Self::Output { 42 | Self { 43 | r: self.r * rhs.r, 44 | d: self.r * rhs.d + self.d * rhs.r, 45 | } 46 | } 47 | } 48 | 49 | impl Div for Dual { 50 | type Output = Dual; 51 | 52 | fn div(self, rhs: Dual) -> Self::Output { 53 | Self { 54 | r: self.r / rhs.r, 55 | d: (self.d * rhs.r - self.r * rhs.d) / (rhs.r * rhs.r), 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /crates/geometry/src/primitives/grid.rs: -------------------------------------------------------------------------------- 1 | use super::super::mesh::{Mesh, MeshBuilder}; 2 | 3 | pub fn grid(size_x: f32, size_y: f32, vertices_x: usize, vertices_y: usize) -> Mesh { 4 | assert!(vertices_x >= 2); 5 | assert!(vertices_y >= 2); 6 | 7 | let mut mesh = MeshBuilder::new(); 8 | 9 | let faces_x = vertices_x - 1; 10 | let faces_y = vertices_y - 1; 11 | 12 | let vertex_count = vertices_x * vertices_y; 13 | let edge_count = faces_x * vertices_y + faces_y * vertices_x; 14 | let face_count = faces_x * faces_y; 15 | 16 | mesh.reserve(vertex_count, edge_count, face_count); 17 | 18 | let delta_x = size_x / faces_x as f32; 19 | let delta_y = size_y / faces_y as f32; 20 | let shift_x = size_x / 2.0; 21 | let shift_y = size_y / 2.0; 22 | 23 | for y in 0..vertices_y { 24 | for x in 0..vertices_x { 25 | mesh.add_vertex([ 26 | x as f32 * delta_x - shift_x, 27 | y as f32 * delta_y - shift_y, 28 | 0.0, 29 | ]); 30 | } 31 | } 32 | 33 | for y in 0..faces_y { 34 | for x in 0..faces_x { 35 | let i0 = x + y * vertices_x; 36 | let i1 = i0 + 1; 37 | let i2 = i1 + vertices_x; 38 | let i3 = i0 + vertices_x; 39 | mesh.add_quad(i0, i1, i2, i3); 40 | } 41 | } 42 | 43 | mesh.build() 44 | } 45 | -------------------------------------------------------------------------------- /shaders/pathtracer/lights/dome-light.slang: -------------------------------------------------------------------------------- 1 | implementing light; 2 | 3 | import math; 4 | import pathtracer.importance_map; 5 | import sample_generator; 6 | 7 | public struct DomeLightData { 8 | public uint env_map_id; 9 | public uint importance_map_id; 10 | public uint base_mip; 11 | } 12 | 13 | public struct DomeLight: Light { 14 | public Texture2D env_map; 15 | public SamplerState linear_sampler; 16 | public HierarchicalImportanceMap importance_map; 17 | 18 | public func le(dir: float3, lod: float = 0.0) -> float3 { 19 | let uv = ndir_to_equirect_unorm(dir); 20 | return env_map.SampleLevel(linear_sampler, uv, lod).rgb; 21 | } 22 | 23 | public func sample_li(p: float3, inout sg: S) -> LightLiSample { 24 | var pdf: float; 25 | let uv = importance_map.sample(sample_next_2d(sg), pdf); 26 | 27 | let wi = oct_to_ndir_equal_area_unorm(uv); 28 | 29 | var result: LightLiSample; 30 | result.li = le(wi); 31 | result.wi = wi; 32 | result.distance = float::maxValue; 33 | result.pdf = pdf / (4.0 * PI); 34 | 35 | return result; 36 | } 37 | 38 | public func pdf_li(p: float3, wi: float3) -> float { 39 | let uv = ndir_to_oct_equal_area_unorm(wi); 40 | return importance_map.pdf(uv) / (4.0 * PI); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /editor/tabs/tab.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | 3 | pub trait Tab: Send + Sync + TabDowncast { 4 | fn title(&self) -> &str; 5 | fn ui(&mut self, ui: &mut egui::Ui, ctx: &mut Context); 6 | } 7 | 8 | pub trait TabDowncast { 9 | fn as_any(&self) -> &dyn Any; 10 | fn as_any_mut(&mut self) -> &mut dyn Any; 11 | } 12 | 13 | impl TabDowncast for T { 14 | fn as_any(&self) -> &dyn Any { 15 | self 16 | } 17 | 18 | fn as_any_mut(&mut self) -> &mut dyn Any { 19 | self 20 | } 21 | } 22 | 23 | impl dyn Tab { 24 | /// Returns true if the trait object wraps an object of type `T`. 25 | #[inline] 26 | pub fn is + 'static>(&self) -> bool { 27 | TabDowncast::as_any(self).is::() 28 | } 29 | 30 | /// Returns a reference to the object within the trait object if it is of type `T`, or `None` if it isn't. 31 | #[inline] 32 | pub fn downcast_ref + 'static>(&self) -> Option<&T> { 33 | TabDowncast::as_any(self).downcast_ref::() 34 | } 35 | 36 | /// Returns a mutable reference to the object within the trait object if it is of type `T`, or `None` if it isn't. 37 | #[inline] 38 | pub fn downcast_mut + 'static>(&mut self) -> Option<&mut T> { 39 | TabDowncast::as_any_mut(self).downcast_mut::() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /shaders/pathtracer/kernels/env-map-prepare.slang: -------------------------------------------------------------------------------- 1 | import math; 2 | 3 | cbuffer ConstantBuffer : register(b0) { 4 | uint env_map_id; 5 | uint importance_map_id; 6 | 7 | uint2 output_res; 8 | uint2 output_res_in_samples; 9 | uint2 num_samples; 10 | float inv_samples; 11 | } 12 | 13 | Texture2D textures_2d_float4[] : register(t0, space1); 14 | RWTexture2D rw_textures_2d_float[] : register(u0, space2); 15 | 16 | SamplerState linear_sampler : register(s0, space0); 17 | 18 | [shader("compute")] 19 | [numthreads(16, 16, 1)] 20 | void main(uint3 id : SV_DispatchThreadID) { 21 | let pixel = id.xy; 22 | if (any(pixel >= output_res)) return; 23 | 24 | let env_map = textures_2d_float4[env_map_id]; 25 | let importance_map = rw_textures_2d_float[importance_map_id]; 26 | 27 | var L = 0.0; 28 | for (uint y = 0; y < num_samples.y; y++) { 29 | for (uint x = 0; x < num_samples.x; x++) { 30 | let sample_pos = pixel * num_samples + uint2(x, y); 31 | let p = ((float2)sample_pos + 0.5) / output_res_in_samples; 32 | 33 | let dir = oct_to_ndir_equal_area_unorm(p); 34 | let uv = ndir_to_equirect_unorm(dir); 35 | 36 | let radiance = env_map.SampleLevel(linear_sampler, uv, 0).rgb; 37 | L += luminance(radiance); 38 | } 39 | } 40 | 41 | importance_map[pixel] = L * inv_samples; 42 | } 43 | -------------------------------------------------------------------------------- /shaders/pathtracer/sample-generator.slang: -------------------------------------------------------------------------------- 1 | interface SampleGenerator { 2 | [mutating] 3 | func next() -> uint; 4 | } 5 | 6 | /// Returns a random number in [0,1). 7 | func sample_next_1d(inout sg: S) -> float { 8 | // Divide upper 24 bits by 2^24 to get a number u in [0,1). 9 | // In floating-point precision this also ensures that 1.0 - u != 0.0. 10 | uint bits = sg.next(); 11 | return (bits >> 8) * 0x1p-24; 12 | } 13 | 14 | /// Returns two random numbers in [0,1). 15 | func sample_next_2d(inout sg: S) -> float2 { 16 | return float2(sample_next_1d(sg), sample_next_1d(sg)); 17 | } 18 | 19 | struct PCG32si: SampleGenerator { 20 | uint state; 21 | 22 | __init(uint seed) { 23 | state = seed; 24 | pcg_oneseq_32_step_r(); 25 | state += seed; 26 | pcg_oneseq_32_step_r(); 27 | } 28 | 29 | [mutating] 30 | func pcg_oneseq_32_step_r() { 31 | state = (state * 747796405u) + 2891336453u; 32 | } 33 | 34 | static func pcg_output_rxs_m_xs_32_32(state: uint) -> uint { 35 | let word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u; 36 | return (word >> 22u) ^ word; 37 | } 38 | 39 | [mutating] 40 | func next() -> uint { 41 | let old_state = state; 42 | pcg_oneseq_32_step_r(); 43 | return pcg_output_rxs_m_xs_32_32(old_state); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/asset/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | #[derive(Default)] 4 | pub struct AssetServer { 5 | id: u64, 6 | assets: HashMap>, 7 | } 8 | 9 | impl AssetServer { 10 | pub fn new() -> Self { 11 | Default::default() 12 | } 13 | 14 | pub fn insert(&mut self, asset: T) -> AssetId { 15 | let handle = AssetId { 16 | id: self.id, 17 | phantom: std::marker::PhantomData, 18 | }; 19 | self.assets.insert(handle.id, Box::new(asset)); 20 | self.id += 1; 21 | handle 22 | } 23 | 24 | pub fn get(&self, handle: &AssetId) -> Option<&T> { 25 | self.assets 26 | .get(&handle.id) 27 | .and_then(|asset| asset.downcast_ref::()) 28 | } 29 | 30 | pub fn get_mut(&mut self, handle: &AssetId) -> Option<&mut T> { 31 | self.assets 32 | .get_mut(&handle.id) 33 | .and_then(|asset| asset.downcast_mut::()) 34 | } 35 | } 36 | 37 | pub trait Asset: std::any::Any {} 38 | 39 | pub struct AssetId { 40 | id: u64, 41 | phantom: std::marker::PhantomData, 42 | } 43 | 44 | impl AssetId { 45 | pub fn id(&self) -> UntypedAssetId { 46 | self.id 47 | } 48 | } 49 | 50 | impl Clone for AssetId { 51 | fn clone(&self) -> Self { 52 | *self 53 | } 54 | } 55 | 56 | impl Copy for AssetId {} 57 | 58 | pub type UntypedAssetId = u64; 59 | -------------------------------------------------------------------------------- /crates/math/src/unit.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, Div, DivAssign, Mul, MulAssign, Neg}; 2 | 3 | /// A wrapper that ensures the underlying value has a unit norm. 4 | #[derive(Clone, Copy, PartialEq)] 5 | pub struct Unit { 6 | unit: T, 7 | } 8 | 9 | impl Unit { 10 | /// Wraps the given value, assuming it is already normalized. 11 | pub const fn new_unchecked(unit: T) -> Self { 12 | Self { unit } 13 | } 14 | } 15 | 16 | impl AsRef for Unit { 17 | fn as_ref(&self) -> &T { 18 | &self.unit 19 | } 20 | } 21 | 22 | impl Deref for Unit { 23 | type Target = T; 24 | 25 | fn deref(&self) -> &Self::Target { 26 | &self.unit 27 | } 28 | } 29 | 30 | impl> Neg for Unit { 31 | type Output = Unit; 32 | 33 | fn neg(self) -> Self::Output { 34 | Unit::new_unchecked(-self.unit) 35 | } 36 | } 37 | 38 | impl, U> Mul for Unit { 39 | type Output = T::Output; 40 | 41 | fn mul(self, rhs: U) -> Self::Output { 42 | self.unit * rhs 43 | } 44 | } 45 | 46 | impl, U> MulAssign for Unit { 47 | fn mul_assign(&mut self, rhs: U) { 48 | self.unit *= rhs; 49 | } 50 | } 51 | 52 | impl, U> Div for Unit { 53 | type Output = T::Output; 54 | 55 | fn div(self, rhs: U) -> Self::Output { 56 | self.unit / rhs 57 | } 58 | } 59 | 60 | impl, U> DivAssign for Unit { 61 | fn div_assign(&mut self, rhs: U) { 62 | self.unit /= rhs; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /shaders/pathtracer/lights/rect-light.slang: -------------------------------------------------------------------------------- 1 | implementing light; 2 | 3 | import sample_generator; 4 | 5 | public struct RectLight: Light { 6 | public float3 emission; 7 | public float3 position; 8 | public float3 area_scaled_normal; 9 | public float3 x; 10 | public float3 y; 11 | 12 | public func sample_li(p: float3, inout sg: S) -> LightLiSample { 13 | let u = sample_next_2d(sg); 14 | 15 | let on_light = position + x * (u.x - 0.5) + y * (u.y - 0.5); 16 | 17 | let to_light = on_light - p; 18 | 19 | let dist_squared = dot(to_light, to_light); 20 | 21 | let dist = sqrt(dist_squared); 22 | 23 | let dir = to_light / dist; 24 | 25 | let area_scaled_cos_theta = dot(-dir, area_scaled_normal); 26 | 27 | if (area_scaled_cos_theta <= 0) { 28 | return {}; 29 | } 30 | 31 | let pdf = dist_squared / area_scaled_cos_theta; 32 | 33 | var ls: LightLiSample; 34 | ls.li = emission; 35 | ls.wi = dir; 36 | ls.distance = dist; 37 | ls.pdf = pdf; 38 | 39 | return ls; 40 | } 41 | 42 | public func pdf_li(p: float3, wi: float3) -> float { 43 | let to_light = position - p; 44 | 45 | let dist_squared = dot(to_light, to_light); 46 | 47 | let dist = sqrt(dist_squared); 48 | 49 | let dir = to_light / dist; 50 | 51 | let area_scaled_cos_theta = dot(-dir, area_scaled_normal); 52 | 53 | if (area_scaled_cos_theta <= 0) { 54 | return 0; 55 | } 56 | 57 | let pdf = dist_squared / area_scaled_cos_theta; 58 | 59 | return pdf; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/geometry/src/primitives/torus.rs: -------------------------------------------------------------------------------- 1 | use super::super::mesh::{Mesh, MeshBuilder}; 2 | 3 | use std::f32::consts::PI; 4 | 5 | pub fn torus(tubular_segments: usize, radial_segments: usize, radius: f32, thickness: f32) -> Mesh { 6 | assert!(radial_segments >= 3); 7 | assert!(tubular_segments >= 3); 8 | 9 | let mut mesh = MeshBuilder::new(); 10 | 11 | let vertex_count = radial_segments * tubular_segments; 12 | let edge_count = vertex_count * 2; 13 | let face_count = vertex_count; 14 | 15 | mesh.reserve(vertex_count, edge_count, face_count); 16 | 17 | let delta_theta = 2.0 * PI / radial_segments as f32; 18 | let delta_phi = 2.0 * PI / tubular_segments as f32; 19 | 20 | for rs in 0..radial_segments { 21 | let theta = rs as f32 * delta_theta; 22 | let sin_theta = theta.sin(); 23 | let cos_theta = theta.cos(); 24 | 25 | for ts in 0..tubular_segments { 26 | let phi = ts as f32 * delta_phi; 27 | let x = (radius + thickness * cos_theta) * phi.cos(); 28 | let y = (radius + thickness * cos_theta) * phi.sin(); 29 | let z = thickness * sin_theta; 30 | 31 | mesh.add_vertex([x, y, z]); 32 | } 33 | } 34 | 35 | for rs in 0..radial_segments { 36 | let rs_next = (rs + 1) % radial_segments; 37 | 38 | for ts in 0..tubular_segments { 39 | let ts_next = (ts + 1) % tubular_segments; 40 | 41 | let i0 = rs * tubular_segments + ts; 42 | let i1 = rs * tubular_segments + ts_next; 43 | let i2 = rs_next * tubular_segments + ts_next; 44 | let i3 = rs_next * tubular_segments + ts; 45 | mesh.add_quad(i0, i1, i2, i3); 46 | } 47 | } 48 | 49 | mesh.build() 50 | } 51 | -------------------------------------------------------------------------------- /shaders/editor/egui.slang: -------------------------------------------------------------------------------- 1 | cbuffer ConstantBuffer : register(b0) { 2 | float2 screen_size; 3 | uint vb_index; 4 | uint texture_index; 5 | } 6 | 7 | ByteAddressBuffer buffers[] : register(t0, space1); 8 | Texture2D textures_2d_float4[] : register(t0, space2); 9 | 10 | SamplerState linear_sampler : register(s0); 11 | 12 | func unpack_float4(u: uint) -> float4 { 13 | float4 f; 14 | 15 | f.a = (float)((u >> 24) & 0xff) / 255.0; 16 | f.b = (float)((u >> 16) & 0xff) / 255.0; 17 | f.g = (float)((u >> 8 ) & 0xff) / 255.0; 18 | f.r = (float)((u >> 0 ) & 0xff) / 255.0; 19 | 20 | return f; 21 | } 22 | 23 | struct Vertex { 24 | float2 position; 25 | float2 uv; 26 | uint color; 27 | } 28 | 29 | struct VsInput { 30 | uint vertex_id : SV_VertexID; 31 | } 32 | 33 | struct PsInput { 34 | float4 position : SV_POSITION; 35 | float4 color : COLOR0; 36 | float2 uv : TEXCOORD0; 37 | } 38 | 39 | [shader("vertex")] 40 | PsInput main_vs(VsInput input) { 41 | let vertex_buffer = buffers[vb_index]; 42 | let vertex = vertex_buffer.Load(input.vertex_id * sizeof(Vertex)); 43 | 44 | var output: PsInput; 45 | output.position = float4( 46 | 2.0 * vertex.position.x / screen_size.x - 1.0, 47 | 1.0 - 2.0 * vertex.position.y / screen_size.y, 48 | 0.0, 49 | 1.0 50 | ); 51 | output.color = unpack_float4(vertex.color); 52 | output.uv = vertex.uv; 53 | 54 | return output; 55 | } 56 | 57 | [shader("pixel")] 58 | float4 main_ps(PsInput input) : SV_Target { 59 | let texture = textures_2d_float4[texture_index]; 60 | 61 | return input.color * texture.Sample(linear_sampler, input.uv); 62 | } 63 | -------------------------------------------------------------------------------- /shaders/pathtracer/fresnel.slang: -------------------------------------------------------------------------------- 1 | import math; 2 | 3 | /// [Gulbrandsen 2014, *Artist Friendly Metallic Fresnel*](http://jcgt.org/published/0003/04/03) 4 | func artistic_ior(reflectivity: float3, edge_color: float3, out ior: float3, out extinction: float3) { 5 | let r = clamp(reflectivity, 0.0, 0.99); 6 | let r_sqrt = sqrt(r); 7 | let n_min = (1.0 - r) / (1.0 + r); 8 | let n_max = (1.0 + r_sqrt) / (1.0 - r_sqrt); 9 | ior = lerp(n_max, n_min, edge_color); 10 | 11 | let np1 = ior + 1.0; 12 | let nm1 = ior - 1.0; 13 | let k2 = (np1 * np1 * r - nm1 * nm1) / (1.0 - r); 14 | extinction = safe_sqrt(k2); 15 | } 16 | 17 | func fr_conductor(cos_theta_i: float, eta: float, k: float) -> float { 18 | float cos_theta_i_sq = cos_theta_i * cos_theta_i; 19 | float sin_theta_i_sq = max(1.0 - cos_theta_i_sq, 0.0); 20 | float sin_theta_i_qu = sin_theta_i_sq * sin_theta_i_sq; 21 | 22 | float inner_term = eta * eta - k * k - sin_theta_i_sq; 23 | float a_sq_plus_b_sq = sqrt(max(inner_term * inner_term + 4.0 * eta * eta * k * k, 0.0)); 24 | float a = sqrt(max((a_sq_plus_b_sq + inner_term) * 0.5, 0.0)); 25 | 26 | float rs = ((a_sq_plus_b_sq + cos_theta_i_sq) - (2.0 * a * cos_theta_i))/ 27 | ((a_sq_plus_b_sq + cos_theta_i_sq) + (2.0 * a * cos_theta_i)); 28 | float rp = ((cos_theta_i_sq * a_sq_plus_b_sq + sin_theta_i_qu) - (2.0 * a * cos_theta_i * sin_theta_i_sq))/ 29 | ((cos_theta_i_sq * a_sq_plus_b_sq + sin_theta_i_qu) + (2.0 * a * cos_theta_i * sin_theta_i_sq)); 30 | 31 | return 0.5 * (rs + rs * rp); 32 | } 33 | 34 | func fr_conductor(cos_theta_i: float, eta: float3, k: float3) -> float3 { 35 | return float3( 36 | fr_conductor(cos_theta_i, eta.x, k.x), 37 | fr_conductor(cos_theta_i, eta.y, k.y), 38 | fr_conductor(cos_theta_i, eta.z, k.z) 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /crates/math/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | pub mod isometry; 4 | pub mod matrix; 5 | pub mod num; 6 | pub mod primitives; 7 | pub mod transform; 8 | 9 | mod complex; 10 | mod dual; 11 | mod quaternion; 12 | mod unit; 13 | 14 | use num::{Number, NumberOps, cast}; 15 | 16 | pub use complex::{Complex, UnitComplex}; 17 | pub use dual::Dual; 18 | pub use matrix::{Matrix, Matrix2, Matrix3, Matrix4, Vector, Vector2, Vector3, Vector4}; 19 | pub use quaternion::{Quaternion, UnitQuaternion}; 20 | pub use unit::Unit; 21 | 22 | pub type Vec2 = Vector2; 23 | pub type Vec3 = Vector3; 24 | 25 | pub type Mat3 = Matrix3; 26 | pub type Mat4 = Matrix4; 27 | pub type Mat3x4 = Matrix; 28 | 29 | pub const E: f32 = std::f32::consts::E; 30 | pub const PI: f32 = std::f32::consts::PI; 31 | 32 | /// Clamps x to be in the range [min, max]. 33 | pub fn clamp>(x: T, min: T, max: T) -> T { 34 | T::max(min, T::min(max, x)) 35 | } 36 | 37 | /// Wraps x to be in the range [min, max]. 38 | pub fn wrap(mut x: T, min: T, max: T) -> T { 39 | let range = max - min; 40 | 41 | while x < min { 42 | x += range; 43 | } 44 | while x > max { 45 | x -= range; 46 | } 47 | 48 | x 49 | } 50 | 51 | /// Unwinds an angle in radians to the range [-pi, pi]. 52 | pub fn unwind_radians(radians: T) -> T { 53 | wrap(radians, cast(-PI), cast(PI)) 54 | } 55 | 56 | /// Unwinds an angle in degrees to the range [-180, 180]. 57 | pub fn unwind_degrees(degrees: T) -> T { 58 | wrap(degrees, cast(-180.0), cast(180.0)) 59 | } 60 | 61 | /// Remaps a value from one range to another. 62 | /// The minimum of either range may be larger or smaller than the maximum. 63 | pub fn map_range(x: T, min: T, max: T, new_min: T, new_max: T) -> T { 64 | (x - min) * (new_max - new_min) / (max - min) + new_min 65 | } 66 | -------------------------------------------------------------------------------- /shaders/geometry/bone-deform.slang: -------------------------------------------------------------------------------- 1 | cbuffer ConstantBuffer : register(b0) { 2 | uint num_vertices; 3 | 4 | uint in_vertices; 5 | uint out_vertices; 6 | 7 | uint lookup_stream; 8 | uint weight_stream; 9 | uint bone_matrices; 10 | } 11 | 12 | ByteAddressBuffer buffers[] : register(t0, space1); 13 | RWByteAddressBuffer rw_buffers[] : register(u0, space2); 14 | 15 | struct Vertex { 16 | float3 position; 17 | float3 normal; 18 | } 19 | 20 | func get_bone_transform(vertex_id: uint, lookup: ByteAddressBuffer, weights: ByteAddressBuffer, bones: ByteAddressBuffer) -> float3x4 { 21 | let offsets = lookup.Load(vertex_id * sizeof(uint)); 22 | let weights_offset = offsets[0]; 23 | let weights_count = offsets[1] - offsets[0]; 24 | 25 | var bone_transform = (float3x4)0.0; 26 | 27 | for (uint i = 0; i < weights_count; i++) { 28 | let index_weight = weights.Load((weights_offset + i) * sizeof(uint)); 29 | let index = index_weight & 0xffff; 30 | let weight = float(index_weight >> 16) / 65535.0; 31 | 32 | bone_transform += weight * bones.Load(index * sizeof(float3x4)); 33 | } 34 | 35 | return bone_transform; 36 | } 37 | 38 | [shader("compute")] 39 | [numthreads(32, 1, 1)] 40 | func main(uint id : SV_DispatchThreadID) { 41 | if (id >= num_vertices) return; 42 | 43 | let in_vertices = buffers[in_vertices]; 44 | let out_vertices = rw_buffers[out_vertices]; 45 | 46 | let lookup = buffers[lookup_stream]; 47 | let weights = buffers[weight_stream]; 48 | let bones = buffers[bone_matrices]; 49 | 50 | var vertex = in_vertices.Load(id * sizeof(Vertex)); 51 | 52 | let transform = get_bone_transform(id, lookup, weights, bones); 53 | 54 | vertex.position = mul(transform, float4(vertex.position, 1.0)); 55 | vertex.normal = normalize(mul((float3x3)transform, vertex.normal)); 56 | 57 | out_vertices.Store(id * sizeof(Vertex), vertex); 58 | } 59 | -------------------------------------------------------------------------------- /editor/camera.rs: -------------------------------------------------------------------------------- 1 | use math::{UnitQuaternion, Vec3, transform::Transform3}; 2 | 3 | pub struct EditorCamera; 4 | 5 | impl EditorCamera { 6 | pub fn update(transform: &mut Transform3, ui: &mut egui::Ui, response: &egui::Response) { 7 | const PAN_SPEED: f32 = 0.005; 8 | const LOOK_SPEED: f32 = 0.003; 9 | const ZOOM_SPEED: f32 = 0.004; 10 | const MOVE_SPEED: f32 = 3.0; 11 | 12 | let response = response.interact(egui::Sense::click_and_drag()); 13 | 14 | let delta = response.drag_delta(); 15 | 16 | if response.dragged_by(egui::PointerButton::Middle) { 17 | let right = transform.rotation * Vec3::X * -delta.x; 18 | let up = transform.rotation * Vec3::Y * delta.y; 19 | transform.translation += (right + up) * PAN_SPEED; 20 | } 21 | 22 | if response.dragged_by(egui::PointerButton::Secondary) { 23 | let yaw = UnitQuaternion::from_axis_angle(Vec3::Z, -delta.x * LOOK_SPEED); 24 | let pitch = UnitQuaternion::from_axis_angle(Vec3::X, -delta.y * LOOK_SPEED); 25 | 26 | transform.rotation = yaw * transform.rotation * pitch; 27 | } 28 | 29 | if response.hovered() { 30 | let dt = ui.input(|i| i.predicted_dt); 31 | let scroll = ui.input(|i| i.smooth_scroll_delta.y); 32 | 33 | transform.translation -= transform.rotation * Vec3::Z * scroll * ZOOM_SPEED; 34 | 35 | let mut movement = Vec3::ZERO; 36 | 37 | if ui.input(|i| i.key_down(egui::Key::Z)) { 38 | movement -= *Vec3::Z; 39 | } // TODO: Prevent dereferencing these 40 | if ui.input(|i| i.key_down(egui::Key::S)) { 41 | movement += *Vec3::Z; 42 | } 43 | if ui.input(|i| i.key_down(egui::Key::Q)) { 44 | movement -= *Vec3::X; 45 | } 46 | if ui.input(|i| i.key_down(egui::Key::D)) { 47 | movement += *Vec3::X; 48 | } 49 | 50 | if movement != Vec3::ZERO { 51 | transform.translation += 52 | transform.rotation * *movement.normalize() * dt * MOVE_SPEED; 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /crates/gpu/src/shader_compiler.rs: -------------------------------------------------------------------------------- 1 | use shader_slang::{self as slang, Downcast}; 2 | 3 | pub struct ShaderCompiler { 4 | global_session: slang::GlobalSession, 5 | backend: super::Backend, 6 | } 7 | 8 | impl ShaderCompiler { 9 | pub fn new(backend: super::Backend) -> Self { 10 | Self { 11 | global_session: slang::GlobalSession::new().unwrap(), 12 | backend, 13 | } 14 | } 15 | 16 | pub fn compile(&self, file: &str, entry_point_name: &str) -> Vec { 17 | let search_path = std::ffi::CString::new("shaders").unwrap(); 18 | 19 | let session_options = slang::CompilerOptions::default() 20 | .optimization(slang::OptimizationLevel::High) 21 | .matrix_layout_row(true); 22 | 23 | let target_desc = slang::TargetDesc::default() 24 | .format(match self.backend { 25 | super::Backend::D3D12 => slang::CompileTarget::Dxil, 26 | super::Backend::Vulkan => slang::CompileTarget::Spirv, 27 | }) 28 | .profile(self.global_session.find_profile(match self.backend { 29 | super::Backend::D3D12 => "sm_6_5", 30 | super::Backend::Vulkan => "glsl_450", 31 | })); 32 | 33 | let targets = [target_desc]; 34 | let search_paths = [search_path.as_ptr()]; 35 | 36 | let session_desc = slang::SessionDesc::default() 37 | .targets(&targets) 38 | .search_paths(&search_paths) 39 | .options(&session_options); 40 | 41 | let session = self.global_session.create_session(&session_desc).unwrap(); 42 | 43 | let module = session.load_module(file).unwrap(); 44 | let entry_point = module.find_entry_point_by_name(entry_point_name).unwrap(); 45 | 46 | let program = session 47 | .create_composite_component_type(&[ 48 | module.downcast().clone(), 49 | entry_point.downcast().clone(), 50 | ]) 51 | .unwrap(); 52 | 53 | let linked_program = program.link().unwrap(); 54 | 55 | linked_program 56 | .entry_point_code(0, 0) 57 | .unwrap() 58 | .as_slice() 59 | .to_vec() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /shaders/pathtracer/importance-map.slang: -------------------------------------------------------------------------------- 1 | struct HierarchicalImportanceMap { 2 | /// Hierarchical importance map, entire mip chain. 3 | Texture2D importance_map; 4 | /// Mip level for 1x1 resolution of `importance_map`. 5 | uint base_mip; 6 | 7 | func sample(u: float2, out pdf: float) -> float2 { 8 | var p = u; 9 | var pos: uint2 = 0; 10 | 11 | // Loop over mips of 2x2...NxN resolution. 12 | for (int mip = base_mip - 1; mip >= 0; mip--) { 13 | pos *= 2; 14 | 15 | let w: float[] = { 16 | importance_map.Load(int3(pos, mip)), 17 | importance_map.Load(int3(pos + uint2(1, 0), mip)), 18 | importance_map.Load(int3(pos + uint2(0, 1), mip)), 19 | importance_map.Load(int3(pos + uint2(1, 1), mip)) 20 | }; 21 | 22 | let q: float[] = { 23 | w[0] + w[2], 24 | w[1] + w[3] 25 | }; 26 | 27 | var off: uint2; 28 | 29 | let d = q[0] / (q[0] + q[1]); // TODO: Prevent division by zero? 30 | 31 | if (p.x < d) { 32 | off.x = 0; 33 | p.x = p.x / d; 34 | } else { 35 | off.x = 1; 36 | p.x = (p.x - d) / (1.0 - d); 37 | } 38 | 39 | let e = off.x == 0 ? (w[0] / q[0]) : (w[1] / q[1]); 40 | 41 | if (p.y < e) { 42 | off.y = 0; 43 | p.y = p.y / e; 44 | } else { 45 | off.y = 1; 46 | p.y = (p.y - e) / (1.0 - e); 47 | } 48 | 49 | pos += off; 50 | } 51 | 52 | let uv = ((float2)pos + p) / (1u << base_mip); 53 | 54 | /// TODO: 1x1 mip holds integral over the entire importance map. 55 | /// Rescale the entire map so that the integral is 1.0, 56 | /// this allows to remove the texture `Load()` here and in `pdf()`. 57 | let avg_w = importance_map.Load(int3(0, 0, base_mip)); 58 | pdf = importance_map[pos] / avg_w; 59 | 60 | return uv; 61 | } 62 | 63 | func pdf(uv: float2) -> float { 64 | let pos = uint2(uv * (1u << base_mip)); 65 | let avg_w = importance_map.Load(int3(0, 0, base_mip)); 66 | let pdf = importance_map[pos] / avg_w; 67 | return pdf; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /shaders/pathtracer/onb.slang: -------------------------------------------------------------------------------- 1 | /// [Duff et al. 2017, *Building an Orthonormal Basis, Revisited*](https://jcgt.org/published/0006/01/01/paper.pdf) 2 | func orthonormal_basis(n: float3) -> Onb { 3 | let sign = n.z < 0.0 ? -1.0 : 1.0; 4 | let p = -1.0 / (sign + n.z); 5 | let q = n.x * n.y * p; 6 | 7 | let t = float3(1.0 + sign * n.x * n.x * p, sign * q, -sign * n.x); 8 | let b = float3(q, sign + n.y * n.y * p, -n.y); 9 | 10 | return { t, b, n }; 11 | } 12 | 13 | struct Onb { 14 | float3 x, y, z; 15 | 16 | static func from_z(n: float3) -> Onb { 17 | return orthonormal_basis(n); 18 | } 19 | 20 | func local_to_world(v: float3) -> float3 { 21 | return v.x * x + v.y * y + v.z * z; 22 | } 23 | 24 | func world_to_local(v: float3) -> float3 { 25 | return float3(dot(v, x), dot(v, y), dot(v, z)); 26 | } 27 | } 28 | 29 | namespace onb { 30 | func cos_theta(w: float3) -> float { 31 | return w.z; 32 | } 33 | 34 | func cos2_theta(w: float3) -> float { 35 | return w.z * w.z; 36 | } 37 | 38 | func sin_theta(w: float3) -> float { 39 | return sqrt(sin2_theta(w)); 40 | } 41 | 42 | func sin2_theta(w: float3) -> float { 43 | return max(0.0, 1.0 - cos2_theta(w)); 44 | } 45 | 46 | func tan_theta(w: float3) -> float { 47 | return sin_theta(w) / cos_theta(w); 48 | } 49 | 50 | func tan2_theta(w: float3) -> float { 51 | return sin2_theta(w) / cos2_theta(w); 52 | } 53 | 54 | func cos_phi(w: float3) -> float { 55 | float st = sin_theta(w); 56 | return (st == 0) ? 1 : clamp(w.x / st, -1, 1); 57 | } 58 | 59 | func sin_phi(w: float3) -> float { 60 | float st = sin_theta(w); 61 | return (st == 0) ? 0 : clamp(w.y / st, -1, 1); 62 | } 63 | 64 | func cos2_phi(w: float3) -> float { 65 | return cos_phi(w) * cos_phi(w); 66 | } 67 | 68 | func sin2_phi(w: float3) -> float { 69 | return sin_phi(w) * sin_phi(w); 70 | } 71 | 72 | func abs_cos_theta(w: float3) -> float { 73 | return abs(cos_theta(w)); 74 | } 75 | 76 | func same_hemisphere(w: float3, wp: float3) -> bool { 77 | return w.z * wp.z > 0.0; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /crates/geometry/src/primitives/sphere.rs: -------------------------------------------------------------------------------- 1 | use super::super::mesh::{Mesh, MeshBuilder}; 2 | 3 | use std::f32::consts::PI; 4 | 5 | /// Creates a UV-Sphere Mesh. 6 | /// # Arguments 7 | /// * `meridians` - Number of 'vertical' lines. 8 | /// * `parallels` - Number of 'horizontal' lines. 9 | pub fn sphere(radius: f32, meridians: usize, parallels: usize) -> Mesh { 10 | assert!(meridians >= 3); 11 | assert!(parallels >= 2); 12 | 13 | let mut mesh = MeshBuilder::new(); 14 | 15 | let vertex_count = meridians * (parallels - 1) + 2; 16 | let edge_count = meridians * (parallels * 2 - 1); 17 | let face_count = (meridians * (parallels - 2)) + (meridians * 2); 18 | 19 | mesh.reserve(vertex_count, edge_count, face_count); 20 | 21 | let delta_theta = PI / parallels as f32; 22 | let delta_phi = 2.0 * PI / meridians as f32; 23 | 24 | let v_top = mesh.add_vertex([0.0, 0.0, radius]); 25 | 26 | for p in 0..parallels - 1 { 27 | let theta = (p + 1) as f32 * delta_theta; 28 | let sin_theta = theta.sin(); 29 | let cos_theta = theta.cos(); 30 | 31 | for m in 0..meridians { 32 | let phi = m as f32 * delta_phi; 33 | let x = radius * sin_theta * phi.cos(); 34 | let y = radius * sin_theta * phi.sin(); 35 | let z = radius * cos_theta; 36 | 37 | mesh.add_vertex([x, y, z]); 38 | } 39 | } 40 | 41 | let v_bottom = mesh.add_vertex([0.0, 0.0, -radius]); 42 | 43 | for m in 0..meridians { 44 | let i0 = m + 1; 45 | let i1 = (m + 1) % meridians + 1; 46 | mesh.add_triangle(v_top, i0, i1); 47 | } 48 | 49 | for p in 0..parallels - 2 { 50 | let idx0 = p * meridians + 1; 51 | let idx1 = (p + 1) * meridians + 1; 52 | 53 | for m in 0..meridians { 54 | let i0 = idx0 + m; 55 | let i1 = idx1 + m; 56 | let i2 = idx1 + (m + 1) % meridians; 57 | let i3 = idx0 + (m + 1) % meridians; 58 | mesh.add_quad(i0, i1, i2, i3); 59 | } 60 | } 61 | 62 | for m in 0..meridians { 63 | let i0 = m + meridians * (parallels - 2) + 1; 64 | let i1 = (m + 1) % meridians + meridians * (parallels - 2) + 1; 65 | mesh.add_triangle(v_bottom, i1, i0); 66 | } 67 | 68 | mesh.build() 69 | } 70 | -------------------------------------------------------------------------------- /editor/windows/console.rs: -------------------------------------------------------------------------------- 1 | use crate::editor::MyContext; 2 | use crate::icons; 3 | use crate::tabs; 4 | 5 | type GlobalLog = Vec; 6 | static LOG: std::sync::Mutex = std::sync::Mutex::new(Vec::new()); 7 | 8 | pub struct Log {} 9 | 10 | impl log::Log for Log { 11 | fn enabled(&self, metadata: &log::Metadata) -> bool { 12 | metadata.level() <= log::Level::Info 13 | } 14 | 15 | fn log(&self, record: &log::Record) { 16 | if self.enabled(record.metadata()) { 17 | let msg = format!("{}: {}", record.level(), record.args()); 18 | let mut log = LOG.lock().unwrap(); 19 | log.push(msg); 20 | } 21 | } 22 | 23 | fn flush(&self) {} 24 | } 25 | 26 | pub struct ConsoleTab { 27 | name: String, 28 | auto_scroll: bool, 29 | } 30 | 31 | impl ConsoleTab { 32 | pub fn new() -> Self { 33 | ConsoleTab { 34 | name: format!("{} Console", icons::CONSOLE), 35 | auto_scroll: true, 36 | } 37 | } 38 | } 39 | 40 | impl tabs::Tab for ConsoleTab { 41 | fn title(&self) -> &str { 42 | &self.name 43 | } 44 | 45 | fn ui(&mut self, ui: &mut egui::Ui, _ctx: &mut MyContext) { 46 | let log = &mut LOG.lock().unwrap(); 47 | 48 | let dropped_entries = log.len().saturating_sub(10000); 49 | drop(log.drain(..dropped_entries)); 50 | 51 | ui.push_id("console_tab", |ui| { 52 | egui::Frame::none().inner_margin(4.0).show(ui, |ui| { 53 | ui.horizontal(|ui| { 54 | if ui.button("Clear").clicked() { 55 | log.clear(); 56 | } 57 | ui.checkbox(&mut self.auto_scroll, "Auto-scroll"); 58 | }); 59 | 60 | ui.add_space(4.0); 61 | 62 | let mut table = egui_extras::TableBuilder::new(ui) 63 | .striped(true) 64 | .column(egui_extras::Column::remainder().clip(true)); 65 | 66 | if self.auto_scroll { 67 | table = table.scroll_to_row(log.len().saturating_sub(1), None); 68 | } 69 | 70 | table.body(|body| { 71 | body.rows(18.0, log.len(), |mut row| { 72 | let row_idx = row.index(); 73 | row.col(|ui| { 74 | ui.label(log[row_idx].as_str()); 75 | }); 76 | }); 77 | }); 78 | }); 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /shaders/pathtracer/bxdfs/conductor.slang: -------------------------------------------------------------------------------- 1 | import bxdf; 2 | import math; 3 | import onb; 4 | import sample_generator; 5 | import spectrum; 6 | import pathtracer.sampling; 7 | import pathtracer.microfacet; 8 | import pathtracer.fresnel; 9 | 10 | struct ConductorBxdf: Bxdf { 11 | TrowbridgeReitzDistribution distribution; 12 | SampledSpectrum eta; 13 | SampledSpectrum k; 14 | 15 | func eval(wi: float3, wo: float3) -> BxdfEval { 16 | if (!onb::same_hemisphere(wi, wo)) { 17 | return {}; 18 | } 19 | 20 | if (distribution.is_singular()) { 21 | return {}; 22 | } 23 | 24 | let cos_theta_i = onb::abs_cos_theta(wi); 25 | let cos_theta_o = onb::abs_cos_theta(wo); 26 | 27 | if (cos_theta_i == 0.0 || cos_theta_o == 0.0) { 28 | return {}; 29 | } 30 | 31 | let wm = normalize(wi + wo); 32 | 33 | let fr = fr_conductor(abs(dot(wi, wm)), eta, k); 34 | 35 | let f = distribution.d(wm) * fr * g2(distribution, wi, wo) / (4.0 * cos_theta_i * cos_theta_o); 36 | let pdf = distribution.pdf(wi, wm) / (4.0 * abs(dot(wi, wm))); 37 | 38 | return { f, pdf }; 39 | } 40 | 41 | func sample(wi: float3, inout sg: S) -> BxdfSample { 42 | if (distribution.is_singular()) { 43 | let wo = float3(-wi.x, -wi.y, wi.z); 44 | let f = fr_conductor(onb::abs_cos_theta(wo), eta, k) / onb::abs_cos_theta(wo); 45 | 46 | return { wo, 1.0, f, BxdfLobe::SingularReflection }; 47 | } 48 | 49 | if (onb::cos_theta(wi) == 0.0) { 50 | return {}; 51 | } 52 | 53 | let wm = distribution.sample_wm(wi, sg); 54 | let wo = reflect(-wi, wm); 55 | 56 | if (!onb::same_hemisphere(wi, wo)) { 57 | return {}; 58 | } 59 | 60 | let pdf = distribution.pdf(wi, wm) / (4.0 * abs(dot(wi, wm))); 61 | 62 | let cos_theta_i = onb::abs_cos_theta(wi); 63 | let cos_theta_o = onb::abs_cos_theta(wo); 64 | 65 | if (cos_theta_i == 0.0 || cos_theta_o == 0.0) { 66 | return {}; 67 | } 68 | 69 | let fr = fr_conductor(abs(dot(wi, wm)), eta, k); 70 | 71 | let f = distribution.d(wm) * fr * g2(distribution, wi, wo) / (4.0 * cos_theta_i * cos_theta_o); 72 | 73 | return { wo, pdf, f, BxdfLobe::GlossyReflection }; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /crates/gpu/src/d3d12/pix.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CString; 2 | use windows::{Win32::Graphics::Direct3D12::*, Win32::System::LibraryLoader::*, core::*}; 3 | 4 | type ProcAddress = unsafe extern "system" fn() -> isize; 5 | 6 | type BeginEventOnCommandList = unsafe extern "system" fn(*const std::ffi::c_void, u64, PSTR) -> i32; 7 | type EndEventOnCommandList = unsafe extern "system" fn(*const std::ffi::c_void) -> i32; 8 | type SetMarkerOnCommandList = unsafe extern "system" fn(*const std::ffi::c_void, u64, PSTR) -> i32; 9 | 10 | #[derive(Clone, Copy)] 11 | pub struct WinPixEventRuntime { 12 | begin_event: BeginEventOnCommandList, 13 | end_event: EndEventOnCommandList, 14 | set_marker: SetMarkerOnCommandList, 15 | } 16 | 17 | impl WinPixEventRuntime { 18 | pub fn load() -> Option { 19 | unsafe { 20 | let module = LoadLibraryA(s!("WinPixEventRuntime.dll")).ok()?; 21 | 22 | Some(Self { 23 | begin_event: std::mem::transmute::( 24 | GetProcAddress(module, s!("PIXBeginEventOnCommandList"))?, 25 | ), 26 | end_event: std::mem::transmute::( 27 | GetProcAddress(module, s!("PIXEndEventOnCommandList"))?, 28 | ), 29 | set_marker: std::mem::transmute::( 30 | GetProcAddress(module, s!("PIXSetMarkerOnCommandList"))?, 31 | ), 32 | }) 33 | } 34 | } 35 | 36 | pub fn begin_event_on_command_list( 37 | &self, 38 | command_list: &ID3D12GraphicsCommandList7, 39 | color: u64, 40 | name: &str, 41 | ) { 42 | let name = CString::new(name).unwrap(); 43 | unsafe { (self.begin_event)(command_list.as_raw(), color, PSTR(name.as_ptr() as _)) }; 44 | } 45 | 46 | pub fn end_event_on_command_list(&self, command_list: &ID3D12GraphicsCommandList7) { 47 | unsafe { (self.end_event)(command_list.as_raw()) }; 48 | } 49 | 50 | pub fn set_marker_on_command_list( 51 | &self, 52 | command_list: &ID3D12GraphicsCommandList7, 53 | color: u64, 54 | name: &str, 55 | ) { 56 | let name = CString::new(name).unwrap(); 57 | unsafe { (self.set_marker)(command_list.as_raw(), color, PSTR(name.as_ptr() as _)) }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /shaders/pathtracer/lights/sphere-light.slang: -------------------------------------------------------------------------------- 1 | implementing light; 2 | 3 | import math; 4 | import onb; 5 | import pathtracer.sampling; 6 | import sample_generator; 7 | 8 | public struct SphereLight: Light { 9 | public float3 emission; 10 | public float3 position; 11 | public float radius; 12 | 13 | public func sample_li(p: float3, inout sg: S) -> LightLiSample { 14 | let u = sample_next_2d(sg); 15 | 16 | let r_sq = sqr(radius); 17 | let d_sq = distance_sq(p, position); 18 | 19 | // TODO: No light emitted if point is inside sphere. 20 | if (d_sq <= r_sq) { 21 | return {}; 22 | } 23 | 24 | let sin2_theta_max = r_sq / d_sq; 25 | let sin_theta_max = safe_sqrt(sin2_theta_max); 26 | let cos_theta_max = safe_sqrt(1.0 - sin2_theta_max); 27 | var one_minus_cos_theta_max = 1.0 - cos_theta_max; 28 | 29 | var cos_theta = (cos_theta_max - 1.0) * u[0] + 1.0; 30 | var sin2_theta = 1.0 - sqr(cos_theta); 31 | if (sin2_theta_max < sqr(sin(radians(1.5)))) { 32 | // Use Taylor series expansion for small angles. 33 | sin2_theta = sin2_theta_max * u[0]; 34 | cos_theta = sqrt(1.0 - sin2_theta); 35 | one_minus_cos_theta_max = sin2_theta_max / 2.0; 36 | } 37 | 38 | let cos_alpha = sin2_theta / sin_theta_max + cos_theta * safe_sqrt(1.0 - sin2_theta / sin2_theta_max); 39 | let sin_alpha = safe_sqrt(1.0 - sqr(cos_alpha)); 40 | 41 | let phi = 2.0 * PI * u[1]; 42 | let w = spherical_direction(sin_alpha, cos_alpha, phi); 43 | let frame = Onb::from_z(normalize(position - p)); 44 | let n = frame.local_to_world(-w); 45 | let p_on_sphere = position + radius * n; 46 | 47 | var ls: LightLiSample; 48 | ls.li = emission; 49 | ls.wi = normalize(p_on_sphere - p); 50 | ls.distance = distance(p_on_sphere, p); 51 | ls.pdf = 1.0 / (2.0 * PI * one_minus_cos_theta_max); 52 | 53 | return ls; 54 | } 55 | 56 | public func pdf_li(p: float3, wi: float3) -> float { 57 | return 0.0; // TODO: implement 58 | } 59 | } 60 | 61 | func spherical_direction(sin_theta: float, cos_theta: float, phi: float) -> float3 { 62 | return float3(clamp(sin_theta, -1, 1) * cos(phi), clamp(sin_theta, -1, 1) * sin(phi), clamp(cos_theta, -1, 1)); 63 | } 64 | -------------------------------------------------------------------------------- /crates/geometry/src/primitives/cylinder.rs: -------------------------------------------------------------------------------- 1 | use super::super::mesh::{Mesh, MeshBuilder}; 2 | 3 | use std::f32::consts::PI; 4 | 5 | /// Creates a cylinder Mesh. 6 | /// # Arguments 7 | /// * `resolution` - Number of vertices on the top and bottom circles. 8 | /// * `segments` - Number of segments along the height of the cylinder. 9 | pub fn cylinder(radius: f32, height: f32, resolution: usize, segments: usize, caps: bool) -> Mesh { 10 | assert!(resolution >= 3); 11 | assert!(segments >= 1); 12 | 13 | let mut mesh = MeshBuilder::new(); 14 | 15 | let vertex_count = resolution * (segments + 1) + if caps { 2 } else { 0 }; 16 | let edge_count = 17 | resolution * (segments + 1) + resolution * segments + if caps { 2 * resolution } else { 0 }; 18 | let face_count = resolution * segments + if caps { 2 * resolution } else { 0 }; 19 | 20 | mesh.reserve(vertex_count, edge_count, face_count); 21 | 22 | let delta_phi = 2.0 * PI / resolution as f32; 23 | let delta_z = height / segments as f32; 24 | let shift_z = height / 2.0; 25 | 26 | for s in 0..=segments { 27 | let z = shift_z - s as f32 * delta_z; 28 | 29 | for r in 0..resolution { 30 | let phi = r as f32 * delta_phi; 31 | let x = radius * phi.cos(); 32 | let y = radius * phi.sin(); 33 | 34 | mesh.add_vertex([x, y, z]); 35 | } 36 | } 37 | 38 | for s in 0..segments { 39 | let idx0 = s * resolution; 40 | let idx1 = (s + 1) * resolution; 41 | 42 | for r in 0..resolution { 43 | let i0 = idx0 + r; 44 | let i1 = idx1 + r; 45 | let i2 = idx1 + (r + 1) % resolution; 46 | let i3 = idx0 + (r + 1) % resolution; 47 | mesh.add_quad(i0, i1, i2, i3); 48 | } 49 | } 50 | 51 | if caps { 52 | // TODO: Ensure that the normals are flat for the caps. 53 | let v_top = mesh.add_vertex([0.0, 0.0, shift_z]); 54 | let v_bottom = mesh.add_vertex([0.0, 0.0, -shift_z]); 55 | 56 | for r in 0..resolution { 57 | let i0 = r; 58 | let i1 = (r + 1) % resolution; 59 | mesh.add_triangle(v_top, i0, i1); 60 | } 61 | 62 | for r in 0..resolution { 63 | let i0 = r + resolution * segments; 64 | let i1 = (r + 1) % resolution + resolution * segments; 65 | mesh.add_triangle(v_bottom, i1, i0); 66 | } 67 | } 68 | 69 | mesh.build() 70 | } 71 | -------------------------------------------------------------------------------- /shaders/pathtracer/bxdf.slang: -------------------------------------------------------------------------------- 1 | import onb; 2 | import sample_generator; 3 | import spectrum; 4 | 5 | public enum BxdfLobe { 6 | Reflection = 1 << 0, 7 | Transmission = 1 << 1, 8 | 9 | Diffuse = 1 << 2, 10 | Glossy = 1 << 3, 11 | Singular = 1 << 4, 12 | 13 | DiffuseReflection = Diffuse | Reflection, 14 | DiffuseTransmission = Diffuse | Transmission, 15 | GlossyReflection = Glossy | Reflection, 16 | GlossyTransmission = Glossy | Transmission, 17 | SingularReflection = Singular | Reflection, 18 | SingularTransmission = Singular | Transmission, 19 | } 20 | 21 | public struct BxdfEval { 22 | public SampledSpectrum f; 23 | public float pdf; 24 | } 25 | 26 | public struct BxdfSample { 27 | /// Incident direction. 28 | public float3 wo; 29 | /// Probability density with respect to solid angle to sample direction `wo`. 30 | public float pdf; 31 | /// BxDF value. 32 | public SampledSpectrum f; // TODO: Optimize by making this the samples `weight` ie. f * cos_theta / pdf. 33 | /// Sampled lobe. 34 | public BxdfLobe lobe; 35 | } 36 | 37 | /// Bidirectional Distribution Function. 38 | /// 39 | /// # Conventions: 40 | /// - Operations are done in a local coordinate frame with normal N=(0,0,1), tangent T=(1,0,0) and bitangent B=(0,1,0). 41 | /// - The incident and outgoing direction point away from the shading location. 42 | /// - The outgoing direction `wo` is sampled. 43 | public interface Bxdf { 44 | /// Evaluates the BxDF for incident direction `wi` and outgoing direction `wo`. 45 | func eval(wi: float3, wo: float3) -> BxdfEval; 46 | 47 | /// Samples an outgoing direction for incident direction `wi`. 48 | func sample(wi: float3, inout sg: S) -> BxdfSample; 49 | } 50 | 51 | public struct Bsdf { 52 | public B bxdf; 53 | public Onb basis; 54 | 55 | public func eval(wi: float3, wo: float3) -> BxdfEval { 56 | let wi_local = basis.world_to_local(wi); 57 | let wo_local = basis.world_to_local(wo); 58 | return bxdf.eval(wi_local, wo_local); 59 | } 60 | 61 | public func sample(wi: float3, inout sg: S) -> BxdfSample { 62 | let wi_local = basis.world_to_local(wi); 63 | var sample = bxdf.sample(wi_local, sg); 64 | sample.wo = basis.local_to_world(sample.wo); 65 | return sample; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /shaders/math.slang: -------------------------------------------------------------------------------- 1 | static const float PI = 3.14159265358979323846; 2 | 3 | func sqr(x: T) -> T { 4 | return x * x; 5 | } 6 | 7 | func safe_sqrt(x: float) -> float { 8 | return sqrt(max(0.0, x)); 9 | } 10 | 11 | func safe_sqrt(x: float3) -> float3 { 12 | return sqrt(max(0.0, x)); 13 | } 14 | 15 | func length_sq(x: float3) -> float { 16 | return dot(x, x); 17 | } 18 | 19 | func distance_sq(a: float3, b: float3) -> float { 20 | return dot(a - b, a - b); 21 | } 22 | 23 | func srgb_from_linear(rgb: float3) -> float3 { 24 | return select(rgb <= 0.0031308, rgb * 12.92, 1.055 * pow(rgb, 1.0 / 2.4) - 0.055); 25 | } 26 | 27 | func linear_from_srgb(rgb: float3) -> float3 { 28 | return select(rgb <= 0.040449936, rgb / 12.92, pow((rgb + 0.055) / 1.055, 2.4)); 29 | } 30 | 31 | /// Returns a relative luminance of an input linear RGB color in the ITU-R BT.709 color space. 32 | func luminance(rgb: float3) -> float { 33 | return dot(rgb, float3(0.2126, 0.7152, 0.0722)); 34 | } 35 | 36 | /// Converts normalized direction `n` to position in [0, 1] in the equirectangular map (unsigned normalized). 37 | func ndir_to_equirect_unorm(n: float3) -> float2 { 38 | let phi = atan2(n.y, n.x); 39 | let theta = acos(n.z); 40 | return float2(0.5 - phi / (2.0 * PI), theta / PI); 41 | } 42 | 43 | /// Converts normalized direction `n` to position in [0, 1] in the octahedral map (equal-area, unsigned normalized). 44 | func ndir_to_oct_equal_area_unorm(n: float3) -> float2 { 45 | let r = sqrt(1.0 - abs(n.z)); 46 | let phi = atan2(abs(n.y), abs(n.x)); 47 | 48 | float2 p; 49 | p.y = r * phi * (2.0 / PI); 50 | p.x = r - p.y; 51 | 52 | if (n.z < 0.0) { 53 | p = 1.0 - p.yx; 54 | } 55 | p *= sign(n.xy); 56 | 57 | return p * 0.5 + 0.5; 58 | } 59 | 60 | /// Converts position `p` in [0, 1] in the octahedral map to normalized direction (equal area, unsigned normalized). 61 | func oct_to_ndir_equal_area_unorm(float2 p) -> float3 { 62 | p = p * 2.0 - 1.0; 63 | 64 | let d = 1.0 - (abs(p.x) + abs(p.y)); 65 | let r = 1.0 - abs(d); 66 | 67 | let phi = (r > 0.0) ? ((abs(p.y) - abs(p.x)) / r + 1.0) * (PI / 4.0) : 0.0; 68 | 69 | let f = r * sqrt(2.0 - r * r); 70 | let x = f * sign(p.x) * cos(phi); 71 | let y = f * sign(p.y) * sin(phi); 72 | let z = sign(d) * (1.0 - r * r); 73 | 74 | return float3(x, y, z); 75 | } 76 | -------------------------------------------------------------------------------- /shaders/view-transform.slang: -------------------------------------------------------------------------------- 1 | import math; 2 | 3 | /// [Wrensch 2023, *Minimal AgX Implementation*](https://iolite-engine.com/blog_posts/minimal_agx_implementation) 4 | func agx_default_contrast_approx(x: float3) -> float3 { 5 | // Mean error^2: 3.6705141e-06 6 | //return (((((15.5 * x - 40.14) * x + 31.96) * x - 6.868) * x + 0.4298) * x + 0.1191) * x - 0.00232; 7 | 8 | // Mean error^2: 1.85907662e-06 9 | return ((((((-17.86 * x + 78.01) * x - 126.7) * x + 92.06) * x - 28.72) * x + 4.361) * x - 0.1718) * x + 0.002857; 10 | } 11 | 12 | func agx(val: float3) -> float3 { 13 | var res: float3 = val; 14 | 15 | /// Input transform (inset) 16 | const float3x3 agx_mat = { 17 | 0.842479062253094, 0.0784335999999992, 0.0792237451477643, 18 | 0.0423282422610123, 0.878468636469772, 0.0791661274605434, 19 | 0.0423756549057051, 0.0784336, 0.879142973793104 20 | }; 21 | 22 | res = mul(agx_mat, res); 23 | 24 | /// Log2 space encoding 25 | const float min_ev = -12.47393; 26 | const float max_ev = 4.026069; 27 | 28 | res = clamp(log2(res), min_ev, max_ev); 29 | res = (res - min_ev) / (max_ev - min_ev); 30 | 31 | // Sigmoid function approximation 32 | res = agx_default_contrast_approx(res); 33 | 34 | return res; 35 | } 36 | 37 | func agx_eotf(val: float3) -> float3 { 38 | var res: float3 = val; 39 | 40 | // Inverse input transform (outset) 41 | const float3x3 agx_mat_inv = { 42 | 1.19687900512017, -0.0980208811401368, -0.0990297440797205, 43 | -0.0528968517574562, 1.15190312990417, -0.0989611768448433, 44 | -0.0529716355144438, -0.0980434501171241, 1.15107367264116 45 | }; 46 | 47 | res = mul(agx_mat_inv, res); 48 | 49 | return res; 50 | } 51 | 52 | /// American Society of Cinematographers Color Decision List (ASC CDL) transform. 53 | func cdl_transform(i: float3, slope: float3, offset: float3, power: float3, saturation: float) -> float3 { 54 | let luma = luminance(i); 55 | let v = pow(i * slope + offset, power); 56 | return luma + saturation * (v - luma); 57 | } 58 | 59 | /// A golden tinted, slightly washed look. 60 | func agx_golden(c: float3) -> float3 { 61 | return cdl_transform(c, float3(1.0, 0.9, 0.5), 0.0, 0.8, 1.3); 62 | } 63 | 64 | /// A punchy and more chroma laden look. 65 | func agx_punchy(c: float3) -> float3 { 66 | return cdl_transform(c, 1.0, 0.0, 1.35, 1.4); 67 | } 68 | -------------------------------------------------------------------------------- /crates/graphics/src/mipgen.rs: -------------------------------------------------------------------------------- 1 | use gpu::{self, CmdListImpl, DeviceImpl, TextureImpl}; 2 | 3 | #[repr(C)] 4 | struct PushConstants { 5 | output_id: u32, 6 | output_res: [u32; 2], 7 | input_id: u32, 8 | input_mip: u32, 9 | } 10 | 11 | pub struct MipGen { 12 | mipgen_pipeline: gpu::ComputePipeline, 13 | } 14 | 15 | impl MipGen { 16 | pub fn setup(device: &mut gpu::Device, shader_compiler: &gpu::ShaderCompiler) -> Self { 17 | let shader = shader_compiler.compile("shaders/mipgen.slang", "main"); 18 | 19 | let descriptor_layout = gpu::DescriptorLayout { 20 | push_constants: Some(gpu::PushConstantBinding { 21 | size: size_of::() as u32, 22 | }), 23 | bindings: Some(vec![ 24 | gpu::DescriptorBinding::bindless_srv(1), 25 | gpu::DescriptorBinding::bindless_uav(2), 26 | ]), 27 | static_samplers: Some(vec![gpu::SamplerBinding { 28 | shader_register: 0, 29 | register_space: 0, 30 | sampler_desc: gpu::SamplerDesc { 31 | filter_min: gpu::FilterMode::Linear, 32 | filter_mag: gpu::FilterMode::Linear, 33 | filter_mip: gpu::FilterMode::Linear, 34 | ..Default::default() 35 | }, 36 | }]), 37 | }; 38 | 39 | let compute_pipeline = device 40 | .create_compute_pipeline(&gpu::ComputePipelineDesc { 41 | cs: &shader, 42 | descriptor_layout: &descriptor_layout, 43 | }) 44 | .unwrap(); 45 | 46 | Self { 47 | mipgen_pipeline: compute_pipeline, 48 | } 49 | } 50 | 51 | pub fn generate_mips( 52 | &self, 53 | cmd: &gpu::CmdList, 54 | texture: &gpu::Texture, 55 | base_resolution: u32, 56 | uavs: &[gpu::TextureView], // Largest to smallest mip resolution, excluding the base mip. 57 | ) { 58 | let mip_levels = uavs.len() + 1; // TODO: Get from texture. 59 | 60 | cmd.set_compute_pipeline(&self.mipgen_pipeline); 61 | 62 | for i in 1..mip_levels { 63 | let output_res = gpu::at_mip_level(base_resolution, i as u32); 64 | 65 | let push_constants = PushConstants { 66 | output_id: uavs[i - 1].index, 67 | output_res: [output_res; 2], 68 | input_id: texture.srv_index().unwrap(), 69 | input_mip: (i - 1) as u32, 70 | }; 71 | 72 | cmd.compute_push_constants(0, gpu::as_u8_slice(&push_constants)); 73 | cmd.dispatch([output_res.div_ceil(16), output_res.div_ceil(16), 1]); 74 | 75 | cmd.barriers(&gpu::Barriers::global()); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /crates/geometry/src/mesh.rs: -------------------------------------------------------------------------------- 1 | use asset::Asset; 2 | use math::Vec3; 3 | 4 | #[derive(Default)] 5 | pub struct Mesh { 6 | pub vertices: Vec, 7 | pub indices: Vec, 8 | pub vertex_groups: VertexGroups, 9 | } 10 | 11 | pub struct Vertex { 12 | pub p: Vec3, 13 | pub n: Vec3, 14 | } 15 | 16 | #[derive(Default)] 17 | pub struct AttributeGroup { 18 | /// Attribute names. 19 | pub names: Vec, 20 | /// Offset to index into `values` for each primitive + 1. 21 | pub lookup: Vec, 22 | /// Tuples of (attribute_id, value) for all attribute values of all primitives. 23 | pub values: Vec<(usize, T)>, 24 | } 25 | 26 | pub type VertexGroups = AttributeGroup; 27 | 28 | impl Mesh { 29 | pub fn new() -> Self { 30 | Default::default() 31 | } 32 | } 33 | 34 | #[derive(Default)] 35 | pub struct MeshBuilder { 36 | pub mesh: Mesh, 37 | } 38 | 39 | impl MeshBuilder { 40 | pub fn new() -> Self { 41 | Default::default() 42 | } 43 | 44 | pub fn reserve(&mut self, vertex_count: usize, _edge_count: usize, _face_count: usize) { 45 | self.mesh.vertices.reserve(vertex_count); 46 | } 47 | 48 | pub fn add_vertex(&mut self, coords: [f32; 3]) -> usize { 49 | let index = self.mesh.vertices.len(); 50 | self.mesh.vertices.push(Vertex { 51 | p: Vec3::new(coords[0], coords[1], coords[2]), 52 | n: Vec3::ZERO, 53 | }); 54 | index 55 | } 56 | 57 | pub fn add_triangle(&mut self, v0: usize, v1: usize, v2: usize) { 58 | self.mesh.indices.extend_from_slice(&[v0, v1, v2]); 59 | } 60 | 61 | pub fn add_quad(&mut self, v0: usize, v1: usize, v2: usize, v3: usize) { 62 | self.mesh 63 | .indices 64 | .extend_from_slice(&[v0, v1, v2, v0, v2, v3]); 65 | } 66 | 67 | pub fn build(self) -> Mesh { 68 | let mut mesh = self.mesh; 69 | calculate_vert_normals(&mut mesh); 70 | mesh 71 | } 72 | } 73 | 74 | pub fn calculate_vert_normals(mesh: &mut Mesh) { 75 | for vertex in &mut mesh.vertices { 76 | vertex.n = Vec3::ZERO; 77 | } 78 | 79 | for face in mesh.indices.chunks_exact(3) { 80 | let v0 = &mesh.vertices[face[0]]; 81 | let v1 = &mesh.vertices[face[1]]; 82 | let v2 = &mesh.vertices[face[2]]; 83 | 84 | let e0 = v1.p - v0.p; 85 | let e1 = v2.p - v0.p; 86 | 87 | let normal = e0.cross(e1); 88 | 89 | for &i in face { 90 | mesh.vertices[i].n += normal; 91 | } 92 | } 93 | 94 | for vertex in &mut mesh.vertices { 95 | vertex.n = *vertex.n.normalize(); 96 | } 97 | } 98 | 99 | impl Asset for Mesh {} 100 | -------------------------------------------------------------------------------- /shaders/editor/gizmo.slang: -------------------------------------------------------------------------------- 1 | import util; 2 | 3 | cbuffer ConstantBuffer : register(b0) { 4 | float4x4 view_projection; 5 | float2 screen_size; 6 | uint vb_index; 7 | uint depth_texture_id; 8 | } 9 | 10 | ByteAddressBuffer buffers[] : register(t0, space1); 11 | Texture2D textures_2d_float[] : register(t0, space2); 12 | 13 | SamplerState linear_sampler : register(s0, space0); 14 | 15 | static const float ANTIALIASING = 1.0; 16 | 17 | static const float2[4] POSITIONS = { 18 | float2(-1.0, -1.0), 19 | float2( 1.0, -1.0), 20 | float2(-1.0, 1.0), 21 | float2( 1.0, 1.0), 22 | }; 23 | 24 | struct Vertex { 25 | float3 position; 26 | float size; 27 | uint color; 28 | } 29 | 30 | struct VsInput { 31 | uint vertex_id : SV_VertexID; 32 | uint instance_id : SV_InstanceID; 33 | } 34 | 35 | struct PsInput { 36 | float4 position : SV_POSITION; 37 | float4 color : COLOR0; 38 | float2 uv : TEXCOORD0; 39 | float size : SIZE; 40 | float edge_distance : EDGE_DISTANCE; 41 | } 42 | 43 | [shader("vertex")] 44 | PsInput main_vs(VsInput input) { 45 | let vertex_buffer = buffers[vb_index]; 46 | 47 | let position = POSITIONS[input.vertex_id]; 48 | 49 | int v0_id = input.instance_id * 2; 50 | int v1_id = v0_id + 1; 51 | 52 | let v0 = vertex_buffer.Load(v0_id * sizeof(Vertex)); 53 | let v1 = vertex_buffer.Load(v1_id * sizeof(Vertex)); 54 | let v = (input.vertex_id % 2 == 0) ? v0 : v1; 55 | 56 | var output: PsInput; 57 | 58 | output.color = unpack_float4(v.color); 59 | output.size = max(v.size, ANTIALIASING); 60 | output.edge_distance = output.size * position.y; 61 | 62 | let p0 = mul(view_projection, float4(v0.position, 1.0)); 63 | let p1 = mul(view_projection, float4(v1.position, 1.0)); 64 | var dir = (p0.xy / p0.w) - (p1.xy / p1.w); 65 | dir = normalize(float2(dir.x, dir.y * screen_size.y / screen_size.x)); 66 | let tng = float2(-dir.y, dir.x) * output.size / screen_size; 67 | 68 | output.position = (input.vertex_id % 2 == 0) ? p0 : p1; 69 | output.position.xy += tng * position.y * output.position.w; 70 | 71 | return output; 72 | } 73 | 74 | [shader("pixel")] 75 | float4 main_ps(PsInput input) : SV_Target { 76 | let scene_depth_texture = textures_2d_float[depth_texture_id]; 77 | 78 | float2 uv = input.position.xy / screen_size; 79 | float scene_depth = scene_depth_texture.Sample(linear_sampler, uv).r; 80 | 81 | float gizmo_depth = input.position.w; 82 | 83 | let alpha_multiplier = gizmo_depth > scene_depth ? 0.25 : 1.0; 84 | 85 | let color = input.color; 86 | 87 | var d = abs(input.edge_distance) / input.size; 88 | d = smoothstep(1.0, 1.0 - (ANTIALIASING / input.size), d); 89 | 90 | return float4(color.rgb, color.a * d * alpha_multiplier); 91 | } 92 | -------------------------------------------------------------------------------- /crates/math/src/isometry.rs: -------------------------------------------------------------------------------- 1 | use super::matrix::{Vector2, Vector3}; 2 | use super::num::Number; 3 | use super::{UnitComplex, UnitQuaternion}; 4 | use std::ops::Mul; 5 | 6 | pub struct Isometry { 7 | pub translation: T, 8 | pub rotation: R, 9 | } 10 | 11 | /// A 2-dimensional direct isometry using a [`UnitComplex`] number for its rotational part. 12 | pub type Isometry2 = Isometry, UnitComplex>; 13 | 14 | /// A 3-dimensional direct isometry using a [`UnitQuaternion`] for its rotational part. 15 | pub type Isometry3 = Isometry, UnitQuaternion>; 16 | 17 | impl Isometry3 { 18 | pub const IDENTITY: Self = Self { 19 | translation: Vector3::ZERO, 20 | rotation: UnitQuaternion::IDENTITY, 21 | }; 22 | 23 | pub const fn from_translation(translation: Vector3) -> Self { 24 | Self { 25 | translation, 26 | ..Self::IDENTITY 27 | } 28 | } 29 | 30 | pub const fn from_rotation(rotation: UnitQuaternion) -> Self { 31 | Self { 32 | rotation, 33 | ..Self::IDENTITY 34 | } 35 | } 36 | 37 | pub const fn with_translation(self, translation: Vector3) -> Self { 38 | Self { 39 | translation, 40 | ..self 41 | } 42 | } 43 | 44 | pub const fn with_rotation(self, rotation: UnitQuaternion) -> Self { 45 | Self { rotation, ..self } 46 | } 47 | } 48 | 49 | impl Isometry3 { 50 | pub fn inv(&self) -> Self { 51 | let rotation = self.rotation.inv(); 52 | let translation = rotation * -self.translation; 53 | 54 | Self { 55 | translation, 56 | rotation, 57 | } 58 | } 59 | 60 | /// Translates and rotates a point by this isometry. 61 | pub fn transform_point(&self, point: Vector3) -> Vector3 { 62 | self.rotation * point + self.translation 63 | } 64 | 65 | /// Translates and rotates a point by the inverse of this isometry. 66 | /// Shorthand for `inv()` followed by `transform_point()`. 67 | pub fn inv_transform_point(&self, point: Vector3) -> Vector3 { 68 | self.rotation.inv() * (point - self.translation) 69 | } 70 | 71 | /// Rotates a vector by this isometry. 72 | pub fn transform_vector(&self, vector: Vector3) -> Vector3 { 73 | self.rotation * vector 74 | } 75 | 76 | /// Rotates a vector by the inverse of this isometry. 77 | /// Shorthand for `inv()` followed by `transform_vector()`. 78 | pub fn inv_transform_vector(&self, vector: Vector3) -> Vector3 { 79 | self.rotation.inv() * vector 80 | } 81 | } 82 | 83 | impl Mul for Isometry3 { 84 | type Output = Self; 85 | 86 | fn mul(self, rhs: Self) -> Self::Output { 87 | let translation = self.rotation * rhs.translation + self.translation; 88 | let rotation = self.rotation * rhs.rotation; 89 | 90 | Self { 91 | translation, 92 | rotation, 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /editor/windows/outliner.rs: -------------------------------------------------------------------------------- 1 | use crate::editor::MyContext; 2 | use crate::icons; 3 | use crate::tabs; 4 | use ecs::{self, Entity, Name, World}; 5 | 6 | pub struct OutlinerTab { 7 | name: String, 8 | } 9 | 10 | impl OutlinerTab { 11 | pub fn new() -> Self { 12 | OutlinerTab { 13 | name: format!("{} Outliner", icons::OUTLINER), 14 | } 15 | } 16 | } 17 | 18 | fn icon_for_entity(world: &World, entity: Entity) -> char { 19 | let e = world.entity(entity); 20 | 21 | if e.contains::() { 22 | icons::OUTLINER_DATA_CAMERA 23 | } else if e.contains::() { 24 | icons::LIGHT_HEMI 25 | } else if e.contains::() { 26 | icons::LIGHT_AREA 27 | } else if e.contains::() { 28 | icons::LIGHT_POINT 29 | } else { 30 | icons::DOT 31 | } 32 | } 33 | 34 | impl tabs::Tab for OutlinerTab { 35 | fn title(&self) -> &str { 36 | &self.name 37 | } 38 | 39 | fn ui(&mut self, ui: &mut egui::Ui, ctx: &mut MyContext) { 40 | egui::Frame::none().inner_margin(4.0).show(ui, |ui| { 41 | egui_extras::TableBuilder::new(ui) 42 | .sense(egui::Sense::click_and_drag()) 43 | .striped(true) 44 | .column(egui_extras::Column::exact(20.0)) 45 | .column(egui_extras::Column::exact(150.0)) 46 | .column(egui_extras::Column::remainder()) 47 | .header(20.0, |mut header| { 48 | header.col(|_| {}); 49 | header.col(|ui| { 50 | ui.label("name"); 51 | }); 52 | header.col(|ui| { 53 | ui.label("id"); 54 | }); 55 | }) 56 | .body(|mut body| { 57 | let mut cmds = ecs::Commands::new(); 58 | for (entity, name) in &ctx.world.query::<(Entity, &Name)>() { 59 | body.row(16.0, |mut row| { 60 | row.set_selected(ctx.selection.contains(&entity)); 61 | 62 | row.col(|ui| { 63 | ui.label( 64 | egui::RichText::new(format!( 65 | "{}", 66 | icon_for_entity(&ctx.world, entity) 67 | )) 68 | .color(egui::Color32::from_rgb(193, 133, 84)) 69 | .size(18.0), 70 | ); 71 | }); 72 | row.col(|ui| { 73 | ui.label(name.name.to_string()); 74 | }); 75 | row.col(|ui| { 76 | ui.label(format!("({},{})", entity.index(), entity.generation())); 77 | }); 78 | 79 | let res = row.response(); 80 | 81 | if res.clicked() { 82 | ctx.selection.clear(); 83 | ctx.selection.insert(entity); 84 | } 85 | 86 | res.context_menu(|ui| { 87 | if ui.button("Delete").clicked() { 88 | ctx.selection.remove(&entity); 89 | cmds.despawn(entity); 90 | ui.close_menu(); 91 | } 92 | }); 93 | }) 94 | } 95 | cmds.execute(&mut ctx.world); 96 | }); 97 | }); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /crates/math/src/primitives/measure.rs: -------------------------------------------------------------------------------- 1 | use super::shapes::*; 2 | use std::f32::consts::PI; 3 | 4 | pub trait Measure { 5 | /// Get the surface area of the shape. 6 | fn area(&self) -> f32; 7 | /// Get the volume of the shape. 8 | fn volume(&self) -> f32; 9 | } 10 | 11 | impl Measure for Sphere { 12 | fn area(&self) -> f32 { 13 | 4.0 * PI * self.radius * self.radius 14 | } 15 | 16 | fn volume(&self) -> f32 { 17 | (4.0 / 3.0) * PI * self.radius * self.radius * self.radius 18 | } 19 | } 20 | 21 | impl Measure for Cylinder { 22 | fn area(&self) -> f32 { 23 | 2.0 * PI * self.radius * (self.radius + 2.0 * self.half_height) 24 | } 25 | 26 | fn volume(&self) -> f32 { 27 | 2.0 * PI * self.radius * self.radius * self.half_height 28 | } 29 | } 30 | 31 | impl Measure for Capsule { 32 | fn area(&self) -> f32 { 33 | 4.0 * PI * self.radius * (self.half_length + self.radius) 34 | } 35 | 36 | fn volume(&self) -> f32 { 37 | PI * self.radius * self.radius * (2.0 * self.half_length + (4.0 / 3.0) * self.radius) 38 | } 39 | } 40 | 41 | impl Measure for Cuboid { 42 | fn area(&self) -> f32 { 43 | 8.0 * (self.half_size.x * self.half_size.y 44 | + self.half_size.y * self.half_size.z 45 | + self.half_size.x * self.half_size.z) 46 | } 47 | 48 | fn volume(&self) -> f32 { 49 | 8.0 * self.half_size.x * self.half_size.y * self.half_size.z 50 | } 51 | } 52 | 53 | impl Measure for TriMesh<'_> { 54 | fn area(&self) -> f32 { 55 | self.indices 56 | .chunks_exact(3) 57 | .map(|indices| { 58 | let a = self.vertices[indices[0]]; 59 | let b = self.vertices[indices[1]]; 60 | let c = self.vertices[indices[2]]; 61 | 62 | (b - a).cross(c - a).length() / 2.0 63 | }) 64 | .sum() 65 | } 66 | 67 | fn volume(&self) -> f32 { 68 | self.indices 69 | .chunks_exact(3) 70 | .map(|indices| { 71 | let a = self.vertices[indices[0]]; 72 | let b = self.vertices[indices[1]]; 73 | let c = self.vertices[indices[2]]; 74 | 75 | a.dot(b.cross(c)) / 6.0 76 | }) 77 | .sum() 78 | } 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::*; 84 | use crate::Vec3; 85 | 86 | #[test] 87 | fn sphere() { 88 | let sphere = Sphere { radius: 2.0 }; 89 | 90 | assert_eq!(sphere.area(), 50.265484, "Incorrect area"); 91 | assert_eq!(sphere.volume(), 33.510323, "Incorrect volume"); 92 | } 93 | 94 | #[test] 95 | fn cylinder() { 96 | let cylinder = Cylinder { 97 | radius: 2.0, 98 | half_height: 1.5, 99 | }; 100 | 101 | assert_eq!(cylinder.area(), 62.831856, "Incorrect area"); 102 | assert_eq!(cylinder.volume(), 37.699112, "Incorrect volume"); 103 | } 104 | 105 | #[test] 106 | fn capsule() { 107 | let capsule = Capsule { 108 | radius: 2.0, 109 | half_length: 1.5, 110 | }; 111 | 112 | assert_eq!(capsule.area(), 87.9646, "Incorrect area"); 113 | assert_eq!(capsule.volume(), 71.20944, "Incorrect volume"); 114 | } 115 | 116 | #[test] 117 | fn cuboid() { 118 | let cuboid = Cuboid { 119 | half_size: Vec3::new(1.0, 1.5, 2.0), 120 | }; 121 | 122 | assert_eq!(cuboid.area(), 52.0, "Incorrect area"); 123 | assert_eq!(cuboid.volume(), 24.0, "Incorrect volume"); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /editor/windows/inspector.rs: -------------------------------------------------------------------------------- 1 | use crate::editor::MyContext; 2 | use crate::icons; 3 | use crate::tabs; 4 | use ecs::Name; 5 | 6 | use egui::DragValue; 7 | 8 | pub struct InspectorTab { 9 | value: (f32, f32, f32), 10 | name: String, 11 | } 12 | 13 | impl InspectorTab { 14 | pub fn new() -> Self { 15 | InspectorTab { 16 | value: Default::default(), 17 | name: format!("{} Inspector", icons::PROPERTIES), 18 | } 19 | } 20 | } 21 | 22 | impl tabs::Tab for InspectorTab { 23 | fn title(&self) -> &str { 24 | &self.name 25 | } 26 | 27 | fn ui(&mut self, ui: &mut egui::Ui, ctx: &mut MyContext) { 28 | const WRAP_WIDTH: f32 = 235.0; 29 | const PERCENT: f32 = 0.35; 30 | let wrapping = ui.available_width() > WRAP_WIDTH; 31 | 32 | egui::Frame::none().inner_margin(4.0).show(ui, |ui| { 33 | if ctx.selection.is_empty() { 34 | ui.vertical_centered(|ui| { 35 | ui.label("Nothing selected"); 36 | }); 37 | 38 | return; 39 | } 40 | 41 | let width = ui.available_width(); 42 | let height = 18.0; 43 | let pad = 16.0; 44 | let level = 0; 45 | 46 | let label = |ui: &mut egui::Ui, label: &str| { 47 | let indent = pad * level as f32; 48 | let _ = ui.allocate_space(egui::vec2(indent, height)); 49 | let (id, space) = ui.allocate_space(egui::vec2(width * PERCENT - indent, height)); 50 | let layout = egui::Layout::left_to_right(egui::emath::Align::LEFT); 51 | let mut ui = ui.child_ui_with_id_source(space, layout, id); 52 | ui.label(label); 53 | }; 54 | 55 | if let Some(selection) = ctx.selection.iter().next() 56 | && let Some(name) = ctx.world.entity_mut(*selection).get_mut::() 57 | { 58 | ui.add(egui::TextEdit::singleline(&mut name.name).desired_width(f32::INFINITY)); 59 | } 60 | 61 | ui.collapsing("Transform", |ui| { 62 | ui.horizontal(|ui| { 63 | label(ui, "Translation"); 64 | let num = if wrapping { 3 } else { 1 }; 65 | ui.columns(num, |ui| { 66 | ui[0.min(num - 1)] 67 | .add(DragValue::new(&mut self.value.0).speed(0.1).suffix(" m")); 68 | ui[1.min(num - 1)] 69 | .add(DragValue::new(&mut self.value.1).speed(0.1).suffix(" m")); 70 | ui[2.min(num - 1)] 71 | .add(DragValue::new(&mut self.value.2).speed(0.1).suffix(" m")); 72 | }); 73 | }); 74 | 75 | ui.horizontal(|ui| { 76 | label(ui, "Rotation"); 77 | let num = if wrapping { 3 } else { 1 }; 78 | ui.columns(num, |ui| { 79 | ui[0.min(num - 1)] 80 | .add(DragValue::new(&mut self.value.0).speed(0.1).suffix(" °")); 81 | ui[1.min(num - 1)] 82 | .add(DragValue::new(&mut self.value.1).speed(0.1).suffix(" °")); 83 | ui[2.min(num - 1)] 84 | .add(DragValue::new(&mut self.value.2).speed(0.1).suffix(" °")); 85 | }); 86 | }); 87 | 88 | ui.horizontal(|ui| { 89 | label(ui, "Scale"); 90 | let num = if wrapping { 3 } else { 1 }; 91 | ui.columns(num, |ui| { 92 | ui[0.min(num - 1)].add(DragValue::new(&mut self.value.0).speed(0.1)); 93 | ui[1.min(num - 1)].add(DragValue::new(&mut self.value.1).speed(0.1)); 94 | ui[2.min(num - 1)].add(DragValue::new(&mut self.value.2).speed(0.1)); 95 | }); 96 | }); 97 | }); 98 | }); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /crates/graphics/src/camera.rs: -------------------------------------------------------------------------------- 1 | use math::{Mat4, Vec3, transform::Transform3}; 2 | 3 | #[derive(Clone, Copy)] 4 | pub struct Camera { 5 | /// Focal length in millimeters. 6 | pub focal_length: f32, 7 | /// Focus distance in meters. 8 | pub focus_distance: f32, 9 | 10 | // full-frame is 36x24mm 11 | pub sensor_width: f32, 12 | pub sensor_height: f32, 13 | 14 | pub depth_of_field: bool, 15 | pub f_stop: f32, 16 | pub shutter_speed: f32, 17 | pub iso: f32, 18 | } 19 | 20 | impl Default for Camera { 21 | fn default() -> Self { 22 | Self { 23 | focal_length: 50.0, 24 | focus_distance: 3.0, 25 | 26 | sensor_width: 36.0, 27 | sensor_height: 36.0 / (16.0 / 9.0), //24.0, 28 | 29 | depth_of_field: false, 30 | f_stop: 22.0, // 1.4 31 | shutter_speed: 1.0 / 125.0, 32 | iso: 100.0, 33 | } 34 | } 35 | } 36 | 37 | impl Camera { 38 | pub fn projection_matrix(&self) -> Mat4 { 39 | let near_clip = 0.01; 40 | let far_clip = 1000.0; 41 | 42 | let inv_x = 2.0 * self.focal_length / self.sensor_width; 43 | let inv_y = 2.0 * self.focal_length / self.sensor_height; 44 | let inv_z = far_clip / (near_clip - far_clip); 45 | 46 | Mat4::from_array([ 47 | inv_x, 48 | 0.0, 49 | 0.0, 50 | 0.0, 51 | 0.0, 52 | inv_y, 53 | 0.0, 54 | 0.0, 55 | 0.0, 56 | 0.0, 57 | inv_z, 58 | near_clip * inv_z, 59 | 0.0, 60 | 0.0, 61 | -1.0, 62 | 0.0, 63 | ]) 64 | } 65 | } 66 | 67 | #[repr(C)] 68 | pub struct GpuCamera { 69 | pub u: Vec3, 70 | pub scale_u: f32, 71 | pub v: Vec3, 72 | pub scale_v: f32, 73 | pub w: Vec3, 74 | pub scale_w: f32, 75 | pub position: Vec3, 76 | pub aperture_radius: f32, 77 | } 78 | 79 | impl GpuCamera { 80 | pub fn from_camera(camera: &Camera, transform: &Transform3) -> Self { 81 | let u = transform.rotation * Vec3::X; 82 | let v = transform.rotation * Vec3::Y; 83 | let w = transform.rotation * -Vec3::Z; 84 | 85 | let scale_u = 0.5 * camera.sensor_width / camera.focal_length * camera.focus_distance; 86 | let scale_v = 0.5 * camera.sensor_height / camera.focal_length * camera.focus_distance; 87 | let scale_w = camera.focus_distance; 88 | 89 | let position = transform.translation; 90 | 91 | let aperture_radius = if camera.depth_of_field { 92 | 0.5 * (camera.focal_length / camera.f_stop) * 0.001 93 | } else { 94 | 0.0 95 | }; 96 | 97 | Self { 98 | u: *u, 99 | scale_u, 100 | v: *v, 101 | scale_v, 102 | w: *w, 103 | scale_w, 104 | position, 105 | aperture_radius, 106 | } 107 | } 108 | } 109 | 110 | enum LensUnit { 111 | /// Field of view in radians. 112 | FieldOfView(f32), 113 | /// Focal length in millimeters. 114 | FocalLength(f32), 115 | } 116 | 117 | // f-stop = focal_length (mm) / aperture diameter (mm) 118 | 119 | fn focal_length_to_fov(focal_length: f32, sensor_size: f32) -> f32 { 120 | 2.0 * (0.5 * sensor_size / focal_length).atan() 121 | } 122 | 123 | fn fov_to_focal_length(fov: f32, sensor_size: f32) -> f32 { 124 | 0.5 * sensor_size / (0.5 * fov).tan() 125 | } 126 | 127 | fn compute_ev_100(aperture: f32, shutter_time: f32, iso: f32) -> f32 { 128 | (aperture * aperture / shutter_time * 100.0 / iso).log2() 129 | } 130 | 131 | fn exposure_from_ev_100(ev_100: f32) -> f32 { 132 | 1.0 / 2.0f32.powf(ev_100) 133 | } 134 | -------------------------------------------------------------------------------- /crates/geometry/src/primitives/platonic.rs: -------------------------------------------------------------------------------- 1 | use super::super::mesh::{Mesh, MeshBuilder}; 2 | 3 | pub fn tetrahedron() -> Mesh { 4 | let mut mesh = MeshBuilder::new(); 5 | mesh.reserve(4, 6, 4); 6 | 7 | // Coordinates on the unit sphere 8 | let a = 1.0 / 3.0; 9 | let b = (8.0_f32 / 9.0).sqrt(); 10 | let c = (2.0_f32 / 9.0).sqrt(); 11 | let d = (2.0_f32 / 3.0).sqrt(); 12 | 13 | let v0 = mesh.add_vertex([0.0, 0.0, 1.0]); 14 | let v1 = mesh.add_vertex([-c, d, -a]); 15 | let v2 = mesh.add_vertex([-c, -d, -a]); 16 | let v3 = mesh.add_vertex([b, 0.0, -a]); 17 | 18 | mesh.add_triangle(v0, v1, v2); 19 | mesh.add_triangle(v0, v2, v3); 20 | mesh.add_triangle(v0, v3, v1); 21 | mesh.add_triangle(v3, v2, v1); 22 | 23 | mesh.build() 24 | } 25 | 26 | pub fn hexahedron() -> Mesh { 27 | let mut mesh = MeshBuilder::new(); 28 | mesh.reserve(8, 12, 6); 29 | 30 | // Coordinates on the unit sphere 31 | let a = 1.0 / 3.0_f32.sqrt(); 32 | 33 | let v0 = mesh.add_vertex([-a, -a, -a]); 34 | let v1 = mesh.add_vertex([a, -a, -a]); 35 | let v2 = mesh.add_vertex([a, a, -a]); 36 | let v3 = mesh.add_vertex([-a, a, -a]); 37 | let v4 = mesh.add_vertex([-a, -a, a]); 38 | let v5 = mesh.add_vertex([a, -a, a]); 39 | let v6 = mesh.add_vertex([a, a, a]); 40 | let v7 = mesh.add_vertex([-a, a, a]); 41 | 42 | mesh.add_quad(v3, v2, v1, v0); 43 | mesh.add_quad(v2, v6, v5, v1); 44 | mesh.add_quad(v5, v6, v7, v4); 45 | mesh.add_quad(v0, v4, v7, v3); 46 | mesh.add_quad(v3, v7, v6, v2); 47 | mesh.add_quad(v1, v5, v4, v0); 48 | 49 | mesh.build() 50 | } 51 | 52 | pub fn octahedron() -> Mesh { 53 | let mut mesh = MeshBuilder::new(); 54 | mesh.reserve(6, 12, 8); 55 | 56 | let v0 = mesh.add_vertex([0.0, 1.0, 0.0]); 57 | let v1 = mesh.add_vertex([1.0, 0.0, 0.0]); 58 | let v2 = mesh.add_vertex([0.0, -1.0, 0.0]); 59 | let v3 = mesh.add_vertex([-1.0, 0.0, 0.0]); 60 | let v4 = mesh.add_vertex([0.0, 0.0, 1.0]); 61 | let v5 = mesh.add_vertex([0.0, 0.0, -1.0]); 62 | 63 | mesh.add_triangle(v1, v0, v4); 64 | mesh.add_triangle(v0, v3, v4); 65 | mesh.add_triangle(v3, v2, v4); 66 | mesh.add_triangle(v2, v1, v4); 67 | mesh.add_triangle(v1, v5, v0); 68 | mesh.add_triangle(v0, v5, v3); 69 | mesh.add_triangle(v3, v5, v2); 70 | mesh.add_triangle(v2, v5, v1); 71 | 72 | mesh.build() 73 | } 74 | 75 | pub fn icosahedron() -> Mesh { 76 | let mut mesh = MeshBuilder::new(); 77 | mesh.reserve(12, 30, 20); 78 | 79 | // Coordinates on the unit sphere 80 | let phi = (1.0 + 5.0_f32.sqrt()) / 2.0; 81 | let scale = (1.0 + phi * phi).sqrt(); 82 | 83 | let a = 1.0 / scale; 84 | let b = phi / scale; 85 | 86 | let vertices = [ 87 | [-a, 0.0, b], 88 | [a, 0.0, b], 89 | [-a, 0.0, -b], 90 | [a, 0.0, -b], 91 | [0.0, b, a], 92 | [0.0, b, -a], 93 | [0.0, -b, a], 94 | [0.0, -b, -a], 95 | [b, a, 0.0], 96 | [-b, a, 0.0], 97 | [b, -a, 0.0], 98 | [-b, -a, 0.0], 99 | ]; 100 | 101 | let faces = [ 102 | [0, 1, 4], 103 | [0, 4, 9], 104 | [9, 4, 5], 105 | [4, 8, 5], 106 | [4, 1, 8], 107 | [8, 1, 10], 108 | [8, 10, 3], 109 | [5, 8, 3], 110 | [5, 3, 2], 111 | [2, 3, 7], 112 | [7, 3, 10], 113 | [7, 10, 6], 114 | [7, 6, 11], 115 | [11, 6, 0], 116 | [0, 6, 1], 117 | [6, 10, 1], 118 | [9, 11, 0], 119 | [9, 2, 11], 120 | [9, 5, 2], 121 | [7, 11, 2], 122 | ]; 123 | 124 | vertices.iter().for_each(|v| { 125 | mesh.add_vertex(*v); 126 | }); 127 | 128 | faces.iter().for_each(|f| { 129 | mesh.add_triangle(f[0], f[1], f[2]); 130 | }); 131 | 132 | mesh.build() 133 | } 134 | -------------------------------------------------------------------------------- /shaders/pathtracer/microfacet.slang: -------------------------------------------------------------------------------- 1 | import math; 2 | import onb; 3 | import sample_generator; 4 | import sampling; 5 | 6 | /// Converts isotropic alpha and anisotropy factor into anisotropic Trowbridge-Reitz alpha. 7 | /// @param alpha Squared perceptual roughness. 8 | /// @param anisotropy Factor in range [0, 1] which increases roughness along tangent and decreases it along bitangent. 9 | /// @return Alpha along tangent and bitangent. 10 | func anisotropic_roughness(float alpha, float anisotropy) -> float2 { 11 | // [Kulla 2017, *Revisiting Physically Based Shading at Imageworks*] 12 | return float2( 13 | max(1e-3, alpha * (1.0 + anisotropy)), 14 | max(1e-3, alpha * (1.0 - anisotropy)) 15 | ); 16 | } 17 | 18 | interface MicrofacetDistribution { 19 | /// Returns true if the distribution can't be sampled reliably. 20 | func is_singular() -> bool; 21 | 22 | /// Normal Distribution Function (NDF), differential area of microfacets with surface normal `wm`. 23 | func d(wm: float3) -> float; 24 | 25 | /// Invisible masked microfacet area per visible microfacet area. 26 | func lambda(w: float3) -> float; 27 | 28 | /// Samples the distribution of normals visible from direction `w`. 29 | func sample_wm(w: float3, inout sg: S) -> float3; 30 | 31 | /// Probability density for sampling normal `wm` from `d(w)`. 32 | func pdf(w: float3, wm: float3) -> float; 33 | } 34 | 35 | func g1(m: M, w: float3) -> float { 36 | return 1.0 / (1.0 + m.lambda(w)); 37 | } 38 | 39 | func g2(m: M, wo: float3, wi: float3) -> float { 40 | return 1.0 / (1.0 + m.lambda(wo) + m.lambda(wi)); 41 | } 42 | 43 | /// Anisotropic Trowbridge-Reitz (GGX) Microfacet Distribution. 44 | /// [Walter et al. 2007, *Microfacet Models for Refraction through Rough Surfaces*] 45 | struct TrowbridgeReitzDistribution: MicrofacetDistribution { 46 | float2 alpha; 47 | 48 | func is_singular() -> bool { 49 | return all(alpha < 1e-3); 50 | } 51 | 52 | func d(wm: float3) -> float { 53 | let tan2_theta = onb::tan2_theta(wm); 54 | if (isinf(tan2_theta)) { 55 | return 0.0; 56 | } 57 | 58 | let cos4_theta = sqr(onb::cos2_theta(wm)); 59 | if (cos4_theta < 1e-16) { 60 | return 0.0; 61 | } 62 | 63 | let e = tan2_theta * (sqr(onb::cos_phi(wm) / alpha.x) + sqr(onb::sin_phi(wm) / alpha.y)); 64 | return 1.0 / (PI * alpha.x * alpha.y * cos4_theta * sqr(1.0 + e)); 65 | } 66 | 67 | func lambda(w: float3) -> float { 68 | let tan2_theta = onb::tan2_theta(w); 69 | if (isinf(tan2_theta)) { 70 | return 0.0; 71 | } 72 | 73 | let alpha2 = sqr(onb::cos_phi(w) * alpha.x) + sqr(onb::sin_phi(w) * alpha.y); 74 | return (sqrt(1.0 + alpha2 * tan2_theta) - 1.0) / 2.0; 75 | } 76 | 77 | func sample_wm(w: float3, inout sg: S) -> float3 { 78 | let u = sample_next_2d(sg); 79 | 80 | // Transform `w` to hemisphere configuration. 81 | let wh = normalize(float3(w.xy * alpha, w.z)); 82 | 83 | // Sample the visible hemisphere as half vectors. 84 | // [Dupey & Benyoub 2023, *Sampling Visible GGX Normals with Spherical Caps*](https://arxiv.org/pdf/2306.05044.pdf) 85 | let phi = 2.0 * PI * u[0]; 86 | let z = (1.0 - u[1]) * (1.0 + wh.z) - wh.z; 87 | let sin_theta = safe_sqrt(1.0 - sqr(z)); 88 | let c = float3(sin_theta * cos(phi), sin_theta * sin(phi), z); 89 | let h = c + wh; 90 | 91 | // Transform back to ellipsoid configuration. 92 | return normalize(float3(h.xy * alpha, h.z)); 93 | } 94 | 95 | func pdf(w: float3, wm: float3) -> float { 96 | return g1(this, w) / onb::abs_cos_theta(w) * d(wm) * abs(dot(w, wm)); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /crates/math/src/transform.rs: -------------------------------------------------------------------------------- 1 | use super::num::Number; 2 | use super::{Mat4, Vector2, Vector3}; 3 | use super::{UnitComplex, UnitQuaternion}; 4 | use std::ops::Mul; 5 | 6 | #[derive(Clone, Copy, PartialEq)] 7 | pub struct Transform { 8 | pub translation: T, 9 | pub rotation: R, 10 | pub scale: S, 11 | } 12 | 13 | pub type Transform2 = Transform, UnitComplex, T>; 14 | pub type Transform3 = Transform, UnitQuaternion, Vector3>; 15 | 16 | impl Transform3 { 17 | pub const IDENTITY: Self = Self { 18 | translation: Vector3::ZERO, 19 | rotation: UnitQuaternion::IDENTITY, 20 | scale: Vector3::ONE, 21 | }; 22 | 23 | pub const fn from_translation(translation: Vector3) -> Self { 24 | Self { 25 | translation, 26 | ..Self::IDENTITY 27 | } 28 | } 29 | 30 | pub const fn from_rotation(rotation: UnitQuaternion) -> Self { 31 | Self { 32 | rotation, 33 | ..Self::IDENTITY 34 | } 35 | } 36 | 37 | pub const fn from_scale(scale: Vector3) -> Self { 38 | Self { 39 | scale, 40 | ..Self::IDENTITY 41 | } 42 | } 43 | 44 | pub const fn with_translation(self, translation: Vector3) -> Self { 45 | Self { 46 | translation, 47 | ..self 48 | } 49 | } 50 | 51 | pub const fn with_rotation(self, rotation: UnitQuaternion) -> Self { 52 | Self { rotation, ..self } 53 | } 54 | 55 | pub const fn with_scale(self, scale: Vector3) -> Self { 56 | Self { scale, ..self } 57 | } 58 | } 59 | 60 | impl Transform3 { 61 | pub fn inv(&self) -> Self { 62 | let scale = Vector3::new(1.0 / self.scale.x, 1.0 / self.scale.y, 1.0 / self.scale.z); 63 | let rotation = self.rotation.inv(); 64 | let translation = rotation * -self.translation.cmul(scale); 65 | 66 | Self { 67 | translation, 68 | rotation, 69 | scale, 70 | } 71 | } 72 | 73 | /// Translates, rotates and scales a point by this transform. 74 | pub fn transform_point(&self, point: Vector3) -> Vector3 { 75 | self.rotation * self.scale.cmul(point) + self.translation 76 | } 77 | 78 | /// Rotates and scales a vector by this transform. 79 | pub fn transform_vector(&self, vector: Vector3) -> Vector3 { 80 | self.rotation * self.scale.cmul(vector) 81 | } 82 | 83 | /// Rotates a direction by this transform. 84 | pub fn transform_direction(&self, direction: Vector3) -> Vector3 { 85 | self.rotation * direction 86 | } 87 | } 88 | 89 | impl Mul for Transform3 { 90 | type Output = Self; 91 | 92 | fn mul(self, rhs: Self) -> Self::Output { 93 | let translation = self.rotation * self.scale.cmul(rhs.translation) + self.translation; 94 | let rotation = self.rotation * rhs.rotation; 95 | let scale = self.scale.cmul(rhs.scale); 96 | 97 | Self { 98 | translation, 99 | rotation, 100 | scale, 101 | } 102 | } 103 | } 104 | 105 | impl From for Mat4 { 106 | fn from(transform: Transform3) -> Self { 107 | let translation = transform.translation; 108 | let rotation = transform.rotation.to_matrix3(); 109 | let scale = transform.scale; 110 | 111 | Mat4::from_array([ 112 | rotation[0] * scale.x, 113 | rotation[1] * scale.y, 114 | rotation[2] * scale.z, 115 | translation.x, 116 | rotation[3] * scale.x, 117 | rotation[4] * scale.y, 118 | rotation[5] * scale.z, 119 | translation.y, 120 | rotation[6] * scale.x, 121 | rotation[7] * scale.y, 122 | rotation[8] * scale.z, 123 | translation.z, 124 | 0.0, 125 | 0.0, 126 | 0.0, 127 | 1.0, 128 | ]) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /shaders/pathtracer/sampling.slang: -------------------------------------------------------------------------------- 1 | import math; 2 | 3 | /// Uniform sampling of the unit disk using polar coordinates. 4 | func sample_uniform_disk_polar(u: float2) -> float2 { 5 | let r = sqrt(u[0]); 6 | let theta = 2.0 * PI * u[1]; 7 | return float2(r * cos(theta), r * sin(theta)); 8 | } 9 | 10 | /// Uniform sampling of the unit disk using Shirley's concentric mapping. 11 | func sample_uniform_disk_concentric(u: float2) -> float2 { 12 | let uo = 2.0 * u - 1.0; 13 | if (uo[0] == 0.0 && uo[1] == 0.0) return 0.0; 14 | 15 | var theta: float; 16 | var r: float; 17 | 18 | if (abs(uo[0]) > abs(uo[1])) { 19 | r = uo[0]; 20 | theta = (PI / 4.0) * (uo[1] / uo[0]); 21 | } else { 22 | r = uo[1]; 23 | theta = (PI / 2.0) - (PI / 4.0) * (uo[0] / uo[1]); 24 | } 25 | 26 | return float2(r * cos(theta), r * sin(theta)); 27 | } 28 | 29 | /// Uniform sampling of the unit sphere using spherical coordinates. 30 | func sample_uniform_sphere(u: float2) -> float3 { 31 | let z = 1.0 - 2.0 * u[0]; 32 | let r = safe_sqrt(1.0 - sqr(z)); 33 | let phi = 2.0 * PI * u[1]; 34 | return float3(r * cos(phi), r * sin(phi), z); 35 | } 36 | 37 | /// PDF for `sample_uniform_sphere()`. 38 | func uniform_sphere_pdf() -> float { 39 | return 1.0 / (4.0 * PI); 40 | } 41 | 42 | /// Uniform sampling of the unit hemisphere using spherical coordinates. 43 | func sample_uniform_hemisphere(u: float2) -> float3 { 44 | let z = u[0]; 45 | let r = safe_sqrt(1.0 - sqr(z)); 46 | let phi = 2.0 * PI * u[1]; 47 | return float3(r * cos(phi), r * sin(phi), z); 48 | } 49 | 50 | /// PDF for `sample_uniform_hemisphere()`. 51 | func uniform_hemisphere_pdf() -> float { 52 | return 1.0 / (2.0 * PI); 53 | } 54 | 55 | /// Cosine-weighted sampling of the unit hemisphere using Shirley's concentric maping. 56 | func sample_cosine_hemisphere(u: float2) -> float3 { 57 | let d = sample_uniform_disk_concentric(u); 58 | let z = safe_sqrt(1.0 - sqr(d.x) - sqr(d.y)); 59 | return float3(d.x, d.y, z); 60 | } 61 | 62 | /// PDF for `sample_cosine_hemisphere()`. 63 | func cosine_hemisphere_pdf(cos_theta: float) -> float { 64 | return cos_theta / PI; 65 | } 66 | 67 | /// Uniform sampling of direction within a cone. 68 | func sample_uniform_cone(u: float2, cos_theta_max: float) -> float3 { 69 | let z = u[0] * (1.0 - cos_theta_max) + cos_theta_max; 70 | let r = safe_sqrt(1.0 - sqr(z)); 71 | let phi = 2.0 * PI * u[1]; 72 | return float3(r * cos(phi), r * sin(phi), z); 73 | } 74 | 75 | /// PDF for `sample_uniform_cone()`. 76 | func uniform_cone_pdf(cos_theta_max: float) -> float { 77 | return 1.0 / (2.0 * PI * (1.0 - cos_theta_max)); 78 | } 79 | 80 | /// Uniform sampling of point on a triangle. 81 | /// [Heitz 2019, *A Low-Distortion Map Between Triangle and Square*](https://hal.science/hal-02073696v2/document) 82 | func sample_uniform_triangle(u: float2) -> float3 { 83 | var b0: float; 84 | var b1: float; 85 | 86 | if (u[0] < u[1]) { 87 | b0 = u[0] / 2.0; 88 | b1 = u[1] - b0; 89 | } else { 90 | b1 = u[1] / 2.0; 91 | b0 = u[0] - b1; 92 | } 93 | 94 | return float3(1.0 - b0 - b1, b0, b1); 95 | } 96 | 97 | /// Uniform sampling of point on a regular polygon. 98 | func sample_uniform_regular_polygon(corners: uint, rotation: float, u: float2) -> float2 { 99 | var v = u[1]; 100 | var u = u[0]; 101 | 102 | // Sample corner number and reuse u 103 | let corner = floor(u * corners); 104 | u = u * corners - corner; 105 | 106 | // Uniform sampled triangle weights 107 | u = sqrt(u); 108 | v = v * u; 109 | u = 1.0 - u; 110 | 111 | // Point in triangle 112 | let angle = PI / corners; 113 | float2 p = float2((u + v) * cos(angle), (u - v) * sin(angle)); 114 | 115 | // Rotate 116 | let rot = rotation + corner * 2.0 * angle; 117 | 118 | let cr = cos(rot); 119 | let sr = sin(rot); 120 | 121 | return float2(p.x * cr - p.y * sr, p.x * sr + p.y * cr); 122 | } 123 | -------------------------------------------------------------------------------- /crates/graphics/src/env_map.rs: -------------------------------------------------------------------------------- 1 | use super::mipgen::MipGen; 2 | use gpu::{self, CmdListImpl, DeviceImpl, TextureImpl}; 3 | 4 | const RESOLUTION: usize = 512; 5 | const SAMPLES_PER_PIXEL: usize = 64; 6 | 7 | pub struct ImportanceMap { 8 | pub importance_map: gpu::Texture, 9 | prepare_pipeline: gpu::ComputePipeline, 10 | uavs: Vec, 11 | mipgen: MipGen, 12 | dirty: bool, 13 | } 14 | 15 | #[repr(C)] 16 | struct PushConstants { 17 | env_map_id: u32, 18 | importance_map_id: u32, 19 | 20 | output_res: [u32; 2], 21 | output_res_in_samples: [u32; 2], 22 | num_samples: [u32; 2], 23 | inv_samples: f32, 24 | } 25 | 26 | impl ImportanceMap { 27 | pub fn setup(device: &mut gpu::Device, shader_compiler: &gpu::ShaderCompiler) -> Self { 28 | // Setup env map prepare shader. 29 | 30 | let shader = 31 | shader_compiler.compile("shaders/pathtracer/kernels/env-map-prepare.slang", "main"); 32 | 33 | let descriptor_layout = gpu::DescriptorLayout { 34 | push_constants: Some(gpu::PushConstantBinding { 35 | size: size_of::() as u32, 36 | }), 37 | bindings: Some(vec![ 38 | gpu::DescriptorBinding::bindless_srv(1), 39 | gpu::DescriptorBinding::bindless_uav(2), 40 | ]), 41 | static_samplers: Some(vec![gpu::SamplerBinding { 42 | shader_register: 0, 43 | register_space: 0, 44 | sampler_desc: gpu::SamplerDesc { 45 | filter_min: gpu::FilterMode::Linear, 46 | filter_mag: gpu::FilterMode::Linear, 47 | filter_mip: gpu::FilterMode::Linear, 48 | ..Default::default() 49 | }, 50 | }]), 51 | }; 52 | 53 | let compute_pipeline = device 54 | .create_compute_pipeline(&gpu::ComputePipelineDesc { 55 | cs: &shader, 56 | descriptor_layout: &descriptor_layout, 57 | }) 58 | .unwrap(); 59 | 60 | // Setup importance map. 61 | 62 | let mip_levels = gpu::max_mip_level(RESOLUTION as u32) + 1; 63 | 64 | let importance_map = device 65 | .create_texture(&gpu::TextureDesc { 66 | width: RESOLUTION as u64, 67 | height: RESOLUTION as u64, 68 | depth: 1, 69 | array_size: 1, 70 | mip_levels, 71 | format: gpu::Format::R32Float, 72 | usage: gpu::TextureUsage::SHADER_RESOURCE | gpu::TextureUsage::UNORDERED_ACCESS, 73 | layout: gpu::TextureLayout::ShaderResource, 74 | }) 75 | .unwrap(); 76 | 77 | let uavs = (1..mip_levels) 78 | .map(|i| { 79 | device.create_texture_view( 80 | &gpu::TextureViewDesc { 81 | first_mip_level: i, 82 | mip_level_count: 1, 83 | }, 84 | &importance_map, 85 | ) 86 | }) 87 | .collect::>(); 88 | 89 | Self { 90 | importance_map, 91 | prepare_pipeline: compute_pipeline, 92 | uavs, 93 | mipgen: MipGen::setup(device, shader_compiler), 94 | dirty: true, 95 | } 96 | } 97 | 98 | pub fn update(&mut self, cmd: &mut gpu::CmdList, env_map_srv_index: u32) { 99 | if !self.dirty { 100 | return; 101 | } 102 | 103 | assert!(RESOLUTION.is_power_of_two()); 104 | assert!(SAMPLES_PER_PIXEL.is_power_of_two()); 105 | 106 | let dimension = RESOLUTION as u32; 107 | let samples = SAMPLES_PER_PIXEL as u32; 108 | 109 | let samples_x = (samples as f32).sqrt().max(1.0) as u32; 110 | let samples_y = samples / samples_x; 111 | assert_eq!(samples, samples_x * samples_y); 112 | 113 | // Transform the env map to the importance map. 114 | let push_constants = PushConstants { 115 | env_map_id: env_map_srv_index, 116 | importance_map_id: self.importance_map.uav_index().unwrap(), 117 | 118 | output_res: [dimension; 2], 119 | output_res_in_samples: [dimension * samples_x, dimension * samples_y], 120 | num_samples: [samples_x, samples_y], 121 | inv_samples: 1.0 / (samples_x * samples_y) as f32, 122 | }; 123 | 124 | cmd.set_compute_pipeline(&self.prepare_pipeline); 125 | cmd.compute_push_constants(0, gpu::as_u8_slice(&push_constants)); 126 | 127 | cmd.dispatch([dimension.div_ceil(16), dimension.div_ceil(16), 1]); 128 | 129 | cmd.barriers(&gpu::Barriers::global()); 130 | 131 | // Generate mips. 132 | self.mipgen 133 | .generate_mips(cmd, &self.importance_map, dimension, &self.uavs); 134 | 135 | self.dirty = false; 136 | } 137 | 138 | pub fn base_mip(&self) -> u32 { 139 | gpu::max_mip_level(RESOLUTION as u32) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /editor/editor.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use crate::time::Time; 4 | use ecs::{Entity, World}; 5 | 6 | use super::tabs; 7 | use super::windows; 8 | 9 | pub struct MyContext { 10 | pub world: World, 11 | pub selection: HashSet, 12 | pub viewport_texture_srv: u32, 13 | } 14 | 15 | pub struct Editor { 16 | pub egui_ctx: egui::Context, 17 | pub context: MyContext, 18 | tree: tabs::Tree, 19 | } 20 | 21 | impl Editor { 22 | pub fn new() -> Self { 23 | // TODO: Move earlier into main.rs 24 | log::set_logger(&windows::Log {}) 25 | .map(|()| log::set_max_level(log::LevelFilter::Trace)) 26 | .unwrap(); 27 | 28 | let egui_ctx = egui::Context::default(); 29 | 30 | egui_extras::install_image_loaders(&egui_ctx); 31 | 32 | let default_font = 33 | egui::FontData::from_static(include_bytes!("../resources/Inter-Regular.ttf")); 34 | let icon_font = egui::FontData::from_static(include_bytes!("../resources/icon.ttf")); 35 | 36 | let mut fonts = egui::FontDefinitions::empty(); 37 | 38 | fonts 39 | .font_data 40 | .insert("Inter-Regular".to_owned(), default_font); 41 | fonts.font_data.insert("icons".to_owned(), icon_font); 42 | 43 | if let Some(family) = fonts.families.get_mut(&egui::FontFamily::Proportional) { 44 | family.push("Inter-Regular".to_owned()); 45 | family.push("icons".to_owned()); 46 | } 47 | 48 | if let Some(family) = fonts.families.get_mut(&egui::FontFamily::Monospace) { 49 | family.push("Inter-Regular".to_owned()); // TODO: this is not monospace 50 | family.push("icons".to_owned()); 51 | } 52 | 53 | egui_ctx.set_fonts(fonts); 54 | 55 | let mut world = World::new(); 56 | world.add_singleton(Time::new()); 57 | 58 | Self { 59 | egui_ctx, 60 | context: MyContext { 61 | world, 62 | selection: HashSet::new(), 63 | viewport_texture_srv: 0, 64 | }, 65 | tree: Self::setup_tree(), 66 | } 67 | } 68 | 69 | pub fn run(&mut self, raw_input: egui::RawInput) -> egui::FullOutput { 70 | if let Some(time) = self.context.world.get_singleton_mut::