├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── build.rs ├── crates ├── asset │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── ecs │ ├── Cargo.toml │ └── src │ │ ├── commands.rs │ │ ├── lib.rs │ │ ├── name.rs │ │ ├── query.rs │ │ └── world.rs ├── geometry │ ├── Cargo.toml │ └── src │ │ ├── bone_deform.rs │ │ ├── lib.rs │ │ ├── mesh.rs │ │ └── primitives │ │ ├── cylinder.rs │ │ ├── grid.rs │ │ ├── platonic.rs │ │ ├── sphere.rs │ │ └── torus.rs ├── gpu │ ├── Cargo.toml │ └── src │ │ ├── d3d12 │ │ ├── cmd.rs │ │ └── mod.rs │ │ ├── lib.rs │ │ ├── shader_compiler.rs │ │ └── vulkan │ │ ├── cmd.rs │ │ └── mod.rs ├── graphics │ ├── Cargo.toml │ └── src │ │ ├── acceleration_structure.rs │ │ ├── camera.rs │ │ ├── env_map.rs │ │ ├── lib.rs │ │ ├── mipgen.rs │ │ ├── pathtracer.rs │ │ └── scene.rs ├── math │ ├── Cargo.toml │ └── src │ │ ├── complex.rs │ │ ├── dual.rs │ │ ├── isometry.rs │ │ ├── lib.rs │ │ ├── matrix.rs │ │ ├── num.rs │ │ ├── primitives │ │ ├── measure.rs │ │ ├── mod.rs │ │ ├── sample.rs │ │ └── shapes.rs │ │ ├── quaternion.rs │ │ ├── transform.rs │ │ └── unit.rs ├── os │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── win32.rs └── usd │ ├── Cargo.toml │ └── src │ └── lib.rs ├── editor ├── camera.rs ├── editor.rs ├── egui_impl.rs ├── gizmo.rs ├── icons.rs ├── main.rs ├── tabs │ ├── mod.rs │ ├── tab.rs │ └── tree.rs └── windows │ ├── console.rs │ ├── content.rs │ ├── inspector.rs │ ├── mod.rs │ ├── outliner.rs │ └── viewport.rs ├── shaders ├── compositor.slang ├── editor │ ├── egui.slang │ └── gizmo.slang ├── geometry │ └── bone-deform.slang ├── math.slang ├── mipgen.slang ├── pathtracer │ ├── bxdf.slang │ ├── bxdfs │ │ ├── conductor.slang │ │ └── diffuse.slang │ ├── fresnel.slang │ ├── importance-map.slang │ ├── kernel.slang │ ├── kernels │ │ └── env-map-prepare.slang │ ├── light.slang │ ├── lights │ │ ├── dome-light.slang │ │ ├── rect-light.slang │ │ └── sphere-light.slang │ ├── microfacet.slang │ ├── onb.slang │ ├── sample-generator.slang │ ├── sampling.slang │ └── spectrum.slang ├── util.slang └── view-transform.slang └── src ├── lib.rs ├── scene.rs └── time.rs /.gitignore: -------------------------------------------------------------------------------- 1 | debug/ 2 | target/ 3 | Cargo.lock 4 | 5 | .cargo/ 6 | .vscode/ 7 | 8 | resources/ 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rustc-link-arg=/EXPORT:D3D12SDKVersion,DATA"); 3 | println!("cargo:rustc-link-arg=/EXPORT:D3D12SDKPath,DATA"); 4 | } 5 | -------------------------------------------------------------------------------- /crates/asset/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "asset" 3 | version = "0.1.0" 4 | edition = "2024" 5 | -------------------------------------------------------------------------------- /crates/asset/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | pub struct AssetServer { 4 | id: u64, 5 | assets: HashMap>, 6 | } 7 | 8 | impl AssetServer { 9 | pub fn new() -> Self { 10 | Self { 11 | id: 0, 12 | assets: HashMap::new(), 13 | } 14 | } 15 | 16 | pub fn insert(&mut self, asset: T) -> AssetId { 17 | let handle = AssetId { 18 | id: self.id, 19 | phantom: std::marker::PhantomData, 20 | }; 21 | self.assets.insert(handle.id, Box::new(asset)); 22 | self.id += 1; 23 | handle 24 | } 25 | 26 | pub fn get(&self, handle: &AssetId) -> Option<&T> { 27 | self.assets.get(&handle.id).and_then(|asset| asset.downcast_ref::()) 28 | } 29 | 30 | pub fn get_mut(&mut self, handle: &AssetId) -> Option<&mut T> { 31 | self.assets.get_mut(&handle.id).and_then(|asset| asset.downcast_mut::()) 32 | } 33 | } 34 | 35 | pub trait Asset: std::any::Any {} 36 | 37 | pub struct AssetId { 38 | id: u64, 39 | phantom: std::marker::PhantomData, 40 | } 41 | 42 | impl AssetId { 43 | pub fn id(&self) -> UntypedAssetId { 44 | self.id 45 | } 46 | } 47 | 48 | impl Clone for AssetId { 49 | fn clone(&self) -> Self { 50 | Self { 51 | id: self.id, 52 | phantom: std::marker::PhantomData, 53 | } 54 | } 55 | } 56 | 57 | impl Copy for AssetId {} 58 | 59 | pub type UntypedAssetId = u64; 60 | -------------------------------------------------------------------------------- /crates/ecs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ecs" 3 | version = "0.1.0" 4 | edition = "2024" 5 | -------------------------------------------------------------------------------- /crates/ecs/src/commands.rs: -------------------------------------------------------------------------------- 1 | use super::{Entity, World}; 2 | 3 | pub struct Commands { 4 | commands: Vec>, // TODO: Optimize 5 | } 6 | 7 | impl Commands { 8 | pub fn new() -> Self { 9 | Self { 10 | commands: Vec::new(), 11 | } 12 | } 13 | 14 | pub fn push(&mut self, command: C) { 15 | self.commands.push(Box::new(command)); 16 | } 17 | 18 | pub fn despawn(&mut self, entity: Entity) { 19 | self.push(Despawn { entity }) 20 | } 21 | 22 | pub fn execute(&mut self, world: &mut World) { 23 | for mut command in self.commands.drain(..) { 24 | command.execute(world); 25 | } 26 | } 27 | } 28 | 29 | pub trait Command { 30 | fn execute(&mut self, world: &mut World); 31 | } 32 | 33 | pub struct Despawn { 34 | pub entity: Entity, 35 | } 36 | 37 | impl Command for Despawn { 38 | fn execute(&mut self, world: &mut World) { 39 | world.entity_mut(self.entity).despawn(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/ecs/src/query.rs: -------------------------------------------------------------------------------- 1 | use crate::recursive; 2 | use super::{Archetype, Component, Entity, World}; 3 | use std::{cell::UnsafeCell, marker::PhantomData, mem::MaybeUninit, iter::FusedIterator}; 4 | 5 | /// Type that can be fetched from a [`World`] using a [`Query`]. 6 | pub trait QueryParam { 7 | /// The item type returned by this [`QueryParam`]. 8 | type Item<'a>; 9 | 10 | /// Per archetype state used by this [`QueryParam`] to fetch [`QueryParam::Item`]. 11 | type Fetch<'a>; 12 | 13 | fn matches_archetype(world: &World, archetype: &Archetype) -> bool; 14 | fn fetch<'a>(world: &'a World, archetype: &'a Archetype) -> Self::Fetch<'a>; 15 | fn item<'a>(fetch: &mut Self::Fetch<'a>, index: usize) -> Self::Item<'a>; 16 | } 17 | 18 | impl QueryParam for Entity { 19 | type Item<'a> = Entity; 20 | type Fetch<'a> = (&'a World, &'a Archetype); 21 | 22 | fn matches_archetype(_world: &World, _archetype: &Archetype) -> bool { 23 | true 24 | } 25 | 26 | fn fetch<'a>(world: &'a World, archetype: &'a Archetype) -> Self::Fetch<'a> { 27 | (world, archetype) 28 | } 29 | 30 | #[inline(always)] 31 | fn item<'a>(fetch: &mut Self::Fetch<'a>, index: usize) -> Self::Item<'a> { 32 | let (world, archetype) = fetch; 33 | let entity_index = archetype.entities[index]; 34 | let entity_generation = world.entities[entity_index as usize].generation; 35 | Entity::new(entity_index, entity_generation) 36 | } 37 | } 38 | 39 | impl QueryParam for &T { 40 | type Item<'a> = &'a T; 41 | type Fetch<'a> = &'a [UnsafeCell]; 42 | 43 | fn matches_archetype(world: &World, archetype: &Archetype) -> bool { 44 | world.component_id::().map_or(false, |id| archetype.contains(id)) 45 | } 46 | 47 | fn fetch<'a>(world: &'a World, archetype: &'a Archetype) -> Self::Fetch<'a> { 48 | let component = archetype.component_index(world.component_id::().unwrap()).unwrap(); 49 | unsafe { archetype.get_slice(component) } 50 | } 51 | 52 | #[inline(always)] 53 | fn item<'a>(fetch: &mut Self::Fetch<'a>, index: usize) -> Self::Item<'a> { 54 | unsafe { &*fetch[index].get() } 55 | } 56 | } 57 | 58 | impl QueryParam for &mut T { 59 | type Item<'a> = &'a mut T; 60 | type Fetch<'a> = &'a [UnsafeCell]; 61 | 62 | fn matches_archetype(world: &World, archetype: &Archetype) -> bool { 63 | world.component_id::().map_or(false, |id| archetype.contains(id)) 64 | } 65 | 66 | fn fetch<'a>(world: &'a World, archetype: &'a Archetype) -> Self::Fetch<'a> { 67 | let component = archetype.component_index(world.component_id::().unwrap()).unwrap(); 68 | unsafe { archetype.get_slice(component) } 69 | } 70 | 71 | #[inline(always)] 72 | fn item<'a>(fetch: &mut Self::Fetch<'a>, index: usize) -> Self::Item<'a> { 73 | unsafe { &mut *fetch[index].get() } 74 | } 75 | } 76 | 77 | /// Returns a bool that indicates if the entity has the component `C`. 78 | pub struct Has(PhantomData); 79 | 80 | impl QueryParam for Has { 81 | type Item<'a> = bool; 82 | type Fetch<'a> = bool; 83 | 84 | fn matches_archetype(_world: &World, _archetype: &Archetype) -> bool { 85 | true 86 | } 87 | 88 | fn fetch<'a>(world: &'a World, archetype: &'a Archetype) -> Self::Fetch<'a> { 89 | world.component_id::().map_or(false, |id| archetype.contains(id)) 90 | } 91 | 92 | #[inline(always)] 93 | fn item<'a>(fetch: &mut Self::Fetch<'a>, _index: usize) -> Self::Item<'a> { 94 | *fetch 95 | } 96 | } 97 | 98 | impl QueryParam for Option { 99 | type Item<'a> = Option>; 100 | type Fetch<'a> = Option>; 101 | 102 | fn matches_archetype(_world: &World, _archetype: &Archetype) -> bool { 103 | true 104 | } 105 | 106 | fn fetch<'a>(world: &'a World, archetype: &'a Archetype) -> Self::Fetch<'a> { 107 | T::matches_archetype(world, archetype).then(|| T::fetch(world, archetype)) 108 | } 109 | 110 | #[inline(always)] 111 | fn item<'a>(fetch: &mut Self::Fetch<'a>, index: usize) -> Self::Item<'a> { 112 | fetch.as_mut().map(|fetch| T::item(fetch, index)) 113 | } 114 | } 115 | 116 | macro_rules! query_params_impl { 117 | ($($name: ident),*) => { 118 | impl<$($name: QueryParam,)*> QueryParam for ($($name,)*) { 119 | type Item<'a> = ($($name::Item<'a>,)*); 120 | type Fetch<'a> = ($($name::Fetch<'a>,)*); 121 | 122 | fn matches_archetype(world: &World, archetype: &Archetype) -> bool { 123 | $($name::matches_archetype(world, archetype))&&* 124 | } 125 | 126 | fn fetch<'a>(world: &'a World, archetype: &'a Archetype) -> Self::Fetch<'a> { 127 | ($($name::fetch(world, archetype),)*) 128 | } 129 | 130 | #[inline(always)] 131 | fn item<'a>(fetch: &mut Self::Fetch<'a>, index: usize) -> Self::Item<'a> { 132 | #[allow(non_snake_case)] 133 | let ($($name,)*) = fetch; 134 | ($($name::item($name, index),)*) 135 | } 136 | } 137 | }; 138 | } 139 | 140 | recursive! (query_params_impl, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P); 141 | 142 | /// An [`Iterator`] over the items returned by a [`Query`]. 143 | pub struct QueryIter<'w, Q: QueryParam> { 144 | world: &'w World, 145 | archetypes: core::ops::Range, 146 | 147 | // State for the current archetype. 148 | fetch: MaybeUninit>, 149 | row: usize, 150 | len: usize, 151 | } 152 | 153 | impl<'w, Q: QueryParam> QueryIter<'w, Q> { 154 | fn new(world: &'w World) -> Self { 155 | Self { 156 | world, 157 | archetypes: 0..world.archetypes.len(), 158 | fetch: MaybeUninit::uninit(), 159 | row: 0, 160 | len: 0, 161 | } 162 | } 163 | } 164 | 165 | impl<'w, Q: QueryParam> Iterator for QueryIter<'w, Q> { 166 | type Item = Q::Item<'w>; 167 | 168 | #[inline(always)] 169 | fn next(&mut self) -> Option { 170 | loop { 171 | // First iteration or reached the end of the current archetype. 172 | if self.row == self.len { 173 | let archetype_idx = self.archetypes.next()?; 174 | let archetype = &self.world.archetypes[archetype_idx]; 175 | if archetype.is_empty() || !Q::matches_archetype(self.world, archetype) { 176 | continue; 177 | } 178 | self.fetch = MaybeUninit::new(Q::fetch(self.world, archetype)); 179 | self.row = 0; 180 | self.len = archetype.len(); 181 | } 182 | 183 | // SAFETY: `fetch` was initialized prior. 184 | let item = unsafe { Q::item(self.fetch.assume_init_mut(), self.row) }; 185 | 186 | self.row += 1; 187 | return Some(item); 188 | } 189 | } 190 | 191 | fn size_hint(&self) -> (usize, Option) { 192 | let len = self.archetypes 193 | .clone() 194 | .map(|i| &self.world.archetypes[i]) 195 | .filter(|archetype| !archetype.is_empty() && Q::matches_archetype(self.world, archetype)) 196 | .map(|archetype| archetype.len()) 197 | .sum::() + (self.len - self.row); 198 | 199 | (len, Some(len)) 200 | } 201 | } 202 | 203 | impl<'w, Q: QueryParam> ExactSizeIterator for QueryIter<'w, Q> {} 204 | impl<'w, Q: QueryParam> FusedIterator for QueryIter<'w, Q> {} 205 | 206 | pub struct Query<'w, T: QueryParam> { 207 | world: &'w World, 208 | _phantom: PhantomData, 209 | } 210 | 211 | impl<'w, T: QueryParam> Query<'w, T> { 212 | pub fn new(world: &'w World) -> Self { 213 | Self { 214 | world, 215 | _phantom: PhantomData, 216 | } 217 | } 218 | 219 | pub fn iter(&self) -> QueryIter<'w, T> { 220 | QueryIter::new(self.world) 221 | } 222 | } 223 | 224 | impl<'w, T: QueryParam> IntoIterator for &'w Query<'w, T> { 225 | type Item = T::Item<'w>; 226 | type IntoIter = QueryIter<'w, T>; 227 | 228 | fn into_iter(self) -> Self::IntoIter { 229 | self.iter() 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /crates/geometry/src/bone_deform.rs: -------------------------------------------------------------------------------- 1 | use gpu::{self, DeviceImpl, CmdListImpl, BufferImpl}; 2 | use math::{Vec3, Mat3x4}; 3 | use crate::mesh::{AttributeGroup, Mesh}; 4 | 5 | fn to_gpu_data(vertex_groups: &AttributeGroup) -> (Vec, Vec) { 6 | let lookup = vertex_groups.lookup.iter().map(|&i| { 7 | i as u32 8 | }).collect::>(); 9 | 10 | let values = vertex_groups.values.iter().map(|&(attribute_id, value)| { 11 | (attribute_id as u32) | ((value * 65535.0) as u32) << 16 12 | }).collect::>(); 13 | 14 | (lookup, values) 15 | } 16 | 17 | #[repr(C)] 18 | struct PushConstants { 19 | num_vertices: u32, 20 | 21 | in_vertices: u32, 22 | out_vertices: u32, 23 | 24 | lookup_stream: u32, 25 | weight_stream: u32, 26 | bone_matrices: u32, 27 | } 28 | 29 | pub struct Vertex { 30 | pub p: Vec3, 31 | pub n: Vec3, 32 | } 33 | 34 | pub struct BoneDeform { 35 | num_vertices: usize, 36 | lookup_buffer: gpu::Buffer, 37 | weights_buffer: gpu::Buffer, 38 | bone_transforms_buffer: gpu::Buffer, 39 | transformed_vertex_buffer: gpu::Buffer, 40 | compute_pipeline: gpu::ComputePipeline, 41 | } 42 | 43 | impl BoneDeform { 44 | pub fn new(device: &mut gpu::Device, shader_compiler: &gpu::ShaderCompiler, mesh: &Mesh, bone_count: usize) -> Self { 45 | let shader = shader_compiler.compile("shaders/geometry/bone-deform.slang", "main"); 46 | 47 | let descriptor_layout = gpu::DescriptorLayout { 48 | push_constants: Some(gpu::PushConstantBinding { 49 | size: size_of::() as u32, 50 | }), 51 | bindings: Some(vec![ 52 | gpu::DescriptorBinding::bindless_srv(1), 53 | gpu::DescriptorBinding::bindless_uav(2), 54 | ]), 55 | static_samplers: None, 56 | }; 57 | 58 | let compute_pipeline = device.create_compute_pipeline(&gpu::ComputePipelineDesc { 59 | cs: &shader, 60 | descriptor_layout: &descriptor_layout, 61 | }).unwrap(); 62 | 63 | let (lookup, values) = to_gpu_data(&mesh.vertex_groups); 64 | 65 | let lookup_buffer = device.create_buffer(&gpu::BufferDesc { 66 | size: size_of::() * lookup.len(), 67 | usage: gpu::BufferUsage::SHADER_RESOURCE, 68 | memory: gpu::Memory::GpuOnly, 69 | }).unwrap(); 70 | 71 | let weights_buffer = device.create_buffer(&gpu::BufferDesc { 72 | size: size_of::() * values.len(), 73 | usage: gpu::BufferUsage::SHADER_RESOURCE, 74 | memory: gpu::Memory::GpuOnly, 75 | }).unwrap(); 76 | 77 | gpu::upload_buffer(device, &lookup_buffer, gpu::slice_as_u8_slice(&lookup)); 78 | gpu::upload_buffer(device, &weights_buffer, gpu::slice_as_u8_slice(&values)); 79 | 80 | let bone_transforms_buffer = device.create_buffer(&gpu::BufferDesc { 81 | size: size_of::() * bone_count, 82 | usage: gpu::BufferUsage::SHADER_RESOURCE, 83 | memory: gpu::Memory::CpuToGpu, 84 | }).unwrap(); 85 | 86 | let transformed_vertex_buffer = device.create_buffer(&gpu::BufferDesc { 87 | size: size_of::() * mesh.vertices.len(), 88 | usage: gpu::BufferUsage::SHADER_RESOURCE | gpu::BufferUsage::UNORDERED_ACCESS, 89 | memory: gpu::Memory::GpuOnly, 90 | }).unwrap(); 91 | 92 | Self { 93 | num_vertices: mesh.vertices.len(), 94 | lookup_buffer, 95 | weights_buffer, 96 | bone_transforms_buffer, 97 | transformed_vertex_buffer, 98 | compute_pipeline, 99 | } 100 | } 101 | 102 | pub fn update_bone_transforms(&mut self, transforms: &[Mat3x4]) { 103 | let ptr = self.bone_transforms_buffer.cpu_ptr() as *mut Mat3x4; 104 | unsafe { std::ptr::copy_nonoverlapping(transforms.as_ptr(), ptr, transforms.len()); } 105 | } 106 | 107 | pub fn execute(&self, cmd: &gpu::CmdList, vertex_srv: u32) { 108 | let push_constants = PushConstants { 109 | num_vertices: self.num_vertices as u32, 110 | in_vertices: vertex_srv, 111 | out_vertices: self.transformed_vertex_buffer.uav_index().unwrap(), 112 | lookup_stream: self.lookup_buffer.srv_index().unwrap(), 113 | weight_stream: self.weights_buffer.srv_index().unwrap(), 114 | bone_matrices: self.bone_transforms_buffer.srv_index().unwrap(), 115 | }; 116 | 117 | cmd.set_compute_pipeline(&self.compute_pipeline); 118 | cmd.compute_push_constants(0, gpu::as_u8_slice(&push_constants)); 119 | 120 | cmd.dispatch([push_constants.num_vertices.div_ceil(32), 1, 1]); 121 | 122 | cmd.barriers(&gpu::Barriers::global()); 123 | } 124 | 125 | pub fn get_transformed_vertex_buffer(&self) -> &gpu::Buffer { 126 | &self.transformed_vertex_buffer 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /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/src/mesh.rs: -------------------------------------------------------------------------------- 1 | use asset::Asset; 2 | use math::Vec3; 3 | 4 | pub struct Mesh { 5 | pub vertices: Vec, 6 | pub indices: Vec, 7 | pub vertex_groups: VertexGroups, 8 | } 9 | 10 | pub struct Vertex { 11 | pub p: Vec3, 12 | pub n: Vec3, 13 | } 14 | 15 | #[derive(Default)] 16 | pub struct AttributeGroup { 17 | /// Attribute names. 18 | pub names: Vec, 19 | /// Offset to index into `values` for each primitive + 1. 20 | pub lookup: Vec, 21 | /// Tuples of (attribute_id, value) for all attribute values of all primitives. 22 | pub values: Vec<(usize, T)>, 23 | } 24 | 25 | pub type VertexGroups = AttributeGroup; 26 | 27 | impl Mesh { 28 | pub fn new() -> Self { 29 | Self { 30 | vertices: Vec::new(), 31 | indices: Vec::new(), 32 | vertex_groups: VertexGroups::default(), 33 | } 34 | } 35 | } 36 | 37 | pub struct MeshBuilder { 38 | pub mesh: Mesh, 39 | } 40 | 41 | impl MeshBuilder { 42 | pub fn new() -> Self { 43 | Self { 44 | mesh: Mesh::new(), 45 | } 46 | } 47 | 48 | pub fn reserve(&mut self, vertex_count: usize, _edge_count: usize, _face_count: usize) { 49 | self.mesh.vertices.reserve(vertex_count); 50 | } 51 | 52 | pub fn add_vertex(&mut self, coords: [f32; 3]) -> usize { 53 | let index = self.mesh.vertices.len(); 54 | self.mesh.vertices.push(Vertex { p: Vec3::new(coords[0], coords[1], coords[2]), n: Vec3::ZERO }); 55 | index 56 | } 57 | 58 | pub fn add_triangle(&mut self, v0: usize, v1: usize, v2: usize) { 59 | self.mesh.indices.extend_from_slice(&[ 60 | v0, v1, v2 61 | ]); 62 | } 63 | 64 | pub fn add_quad(&mut self, v0: usize, v1: usize, v2: usize, v3: usize) { 65 | self.mesh.indices.extend_from_slice(&[ 66 | v0, v1, v2, 67 | v0, v2, v3, 68 | ]); 69 | } 70 | 71 | pub fn build(self) -> Mesh { 72 | let mut mesh = self.mesh; 73 | calculate_vert_normals(&mut mesh); 74 | mesh 75 | } 76 | } 77 | 78 | pub fn calculate_vert_normals(mesh: &mut Mesh) { 79 | for vertex in &mut mesh.vertices { 80 | vertex.n = Vec3::ZERO; 81 | } 82 | 83 | for face in mesh.indices.chunks_exact(3) { 84 | let v0 = &mesh.vertices[face[0]]; 85 | let v1 = &mesh.vertices[face[1]]; 86 | let v2 = &mesh.vertices[face[2]]; 87 | 88 | let e0 = v1.p - v0.p; 89 | let e1 = v2.p - v0.p; 90 | 91 | let normal = e0.cross(e1); 92 | 93 | for i in 0..3 { 94 | mesh.vertices[face[i]].n += normal; 95 | } 96 | } 97 | 98 | for vertex in &mut mesh.vertices { 99 | vertex.n = *vertex.n.normalize(); 100 | } 101 | } 102 | 103 | impl Asset for Mesh {} 104 | -------------------------------------------------------------------------------- /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 = resolution * (segments + 1) + resolution * segments + if caps { 2 * resolution } else { 0 }; 17 | let face_count = resolution * segments + if caps { 2 * resolution } else { 0 }; 18 | 19 | mesh.reserve(vertex_count, edge_count, face_count); 20 | 21 | let delta_phi = 2.0 * PI / resolution as f32; 22 | let delta_z = height / segments as f32; 23 | let shift_z = height / 2.0; 24 | 25 | for s in 0..=segments { 26 | let z = shift_z - s as f32 * delta_z; 27 | 28 | for r in 0..resolution { 29 | let phi = r as f32 * delta_phi; 30 | let x = radius * phi.cos(); 31 | let y = radius * phi.sin(); 32 | 33 | mesh.add_vertex([x, y, z]); 34 | } 35 | } 36 | 37 | for s in 0..segments { 38 | let idx0 = s * resolution; 39 | let idx1 = (s + 1) * resolution; 40 | 41 | for r in 0..resolution { 42 | let i0 = idx0 + r; 43 | let i1 = idx1 + r; 44 | let i2 = idx1 + (r + 1) % resolution; 45 | let i3 = idx0 + (r + 1) % resolution; 46 | mesh.add_quad(i0, i1, i2, i3); 47 | } 48 | } 49 | 50 | if caps { 51 | // TODO: Ensure that the normals are flat for the caps. 52 | let v_top = mesh.add_vertex([0.0, 0.0, shift_z]); 53 | let v_bottom = mesh.add_vertex([0.0, 0.0, -shift_z]); 54 | 55 | for r in 0..resolution { 56 | let i0 = r; 57 | let i1 = (r + 1) % resolution; 58 | mesh.add_triangle(v_top, i0, i1); 59 | } 60 | 61 | for r in 0..resolution { 62 | let i0 = r + resolution * segments; 63 | let i1 = (r + 1) % resolution + resolution * segments; 64 | mesh.add_triangle(v_bottom, i1, i0); 65 | } 66 | } 67 | 68 | mesh.build() 69 | } 70 | -------------------------------------------------------------------------------- /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([x as f32 * delta_x - shift_x, y as f32 * delta_y - shift_y, 0.0]); 26 | } 27 | } 28 | 29 | for y in 0..faces_y { 30 | for x in 0..faces_x { 31 | let i0 = x + y * vertices_x; 32 | let i1 = i0 + 1; 33 | let i2 = i1 + vertices_x; 34 | let i3 = i0 + vertices_x; 35 | mesh.add_quad(i0, i1, i2, i3); 36 | } 37 | } 38 | 39 | mesh.build() 40 | } 41 | -------------------------------------------------------------------------------- /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| { mesh.add_vertex(*v); }); 125 | faces.iter().for_each(|f| { mesh.add_triangle(f[0], f[1], f[2]); }); 126 | 127 | mesh.build() 128 | } 129 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | slang = { git = "https://github.com/FloatyMonkey/slang-rs.git" } 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 | -------------------------------------------------------------------------------- /crates/gpu/src/shader_compiler.rs: -------------------------------------------------------------------------------- 1 | use slang::{self, 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.create_composite_component_type(&[ 47 | module.downcast().clone(), entry_point.downcast().clone(), 48 | ]).unwrap(); 49 | 50 | let linked_program = program.link().unwrap(); 51 | 52 | let code = linked_program.entry_point_code(0, 0).unwrap().as_slice().to_vec(); 53 | 54 | code 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /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/graphics/src/acceleration_structure.rs: -------------------------------------------------------------------------------- 1 | use gpu::{self, AccelerationStructureImpl, BufferImpl, DeviceImpl, CmdListImpl}; 2 | 3 | pub struct Blas { 4 | pub accel: gpu::AccelerationStructure, 5 | pub build_inputs: gpu::AccelerationStructureBuildInputs, 6 | pub buffer: gpu::Buffer, 7 | pub scratch_buffer: gpu::Buffer, 8 | } 9 | 10 | impl Blas { 11 | pub fn create(device: &mut gpu::Device, vertex_buffer: &gpu::Buffer, index_buffer: &gpu::Buffer, vertex_count: usize, index_count: usize, vertex_stride: usize) -> Self { 12 | let geo = gpu::AccelerationStructureTriangles { 13 | vertex_buffer: vertex_buffer.gpu_ptr(), 14 | vertex_format: gpu::Format::RGB32Float, 15 | vertex_count, 16 | vertex_stride, 17 | index_buffer: index_buffer.gpu_ptr(), 18 | index_format: gpu::Format::R32UInt, 19 | index_count, 20 | transform: gpu::GpuPtr::NULL, 21 | flags: gpu::AccelerationStructureGeometryFlags::OPAQUE, 22 | }; 23 | 24 | let build_inputs = gpu::AccelerationStructureBuildInputs { 25 | flags: gpu::AccelerationStructureBuildFlags::PREFER_FAST_TRACE, 26 | entries: gpu::AccelerationStructureEntries::Triangles(vec![geo]), 27 | }; 28 | 29 | let sizes = device.acceleration_structure_sizes(&build_inputs); 30 | 31 | let buffer = device.create_buffer(&gpu::BufferDesc { 32 | size: sizes.acceleration_structure_size, 33 | usage: gpu::BufferUsage::ACCELERATION_STRUCTURE, 34 | memory: gpu::Memory::GpuOnly, 35 | }).unwrap(); 36 | 37 | let scratch_buffer = device.create_buffer(&gpu::BufferDesc { 38 | size: sizes.build_scratch_size, 39 | usage: gpu::BufferUsage::UNORDERED_ACCESS, 40 | memory: gpu::Memory::GpuOnly, 41 | }).unwrap(); 42 | 43 | let accel = device.create_acceleration_structure(&gpu::AccelerationStructureDesc { 44 | ty: gpu::AccelerationStructureType::BottomLevel, 45 | buffer: &buffer, 46 | offset: 0, 47 | size: sizes.acceleration_structure_size, 48 | }).unwrap(); 49 | 50 | Self { 51 | accel, 52 | build_inputs, 53 | buffer, 54 | scratch_buffer, 55 | } 56 | } 57 | 58 | pub fn set_vertex_buffer(&mut self, vertex_buffer: &gpu::Buffer) { 59 | if let gpu::AccelerationStructureEntries::Triangles(geo) = &mut self.build_inputs.entries { 60 | geo[0].vertex_buffer = vertex_buffer.gpu_ptr(); 61 | } 62 | } 63 | 64 | pub fn build(&mut self, cmd: &gpu::CmdList) { 65 | cmd.build_acceleration_structure(&gpu::AccelerationStructureBuildDesc { 66 | inputs: &self.build_inputs, 67 | src: None, 68 | dst: &self.accel, 69 | scratch_data: self.scratch_buffer.gpu_ptr(), 70 | }); 71 | } 72 | } 73 | 74 | pub struct Tlas { 75 | pub accel: gpu::AccelerationStructure, 76 | pub build_inputs: gpu::AccelerationStructureBuildInputs, 77 | pub buffer: gpu::Buffer, 78 | pub scratch_buffer: gpu::Buffer, 79 | 80 | pub instance_buffer: gpu::Buffer, 81 | } 82 | 83 | impl Tlas { 84 | pub fn create(device: &mut gpu::Device, count: usize) -> Self { 85 | let instance_descriptor_size = gpu::AccelerationStructure::instance_descriptor_size(); 86 | 87 | let instance_buffer = device.create_buffer(&gpu::BufferDesc { 88 | size: instance_descriptor_size * count, 89 | usage: gpu::BufferUsage::SHADER_RESOURCE, 90 | memory: gpu::Memory::CpuToGpu, 91 | }).unwrap(); 92 | 93 | let build_inputs = gpu::AccelerationStructureBuildInputs { 94 | flags: gpu::AccelerationStructureBuildFlags::PREFER_FAST_TRACE, 95 | entries: gpu::AccelerationStructureEntries::Instances( 96 | gpu::AccelerationStructureInstances { data: instance_buffer.gpu_ptr(), count } 97 | ), 98 | }; 99 | 100 | let sizes = device.acceleration_structure_sizes(&build_inputs); 101 | 102 | let buffer = device.create_buffer(&gpu::BufferDesc { 103 | size: sizes.acceleration_structure_size, 104 | usage: gpu::BufferUsage::ACCELERATION_STRUCTURE, 105 | memory: gpu::Memory::GpuOnly, 106 | }).unwrap(); 107 | 108 | let scratch_buffer = device.create_buffer(&gpu::BufferDesc { 109 | size: sizes.build_scratch_size, 110 | usage: gpu::BufferUsage::UNORDERED_ACCESS, 111 | memory: gpu::Memory::GpuOnly, 112 | }).unwrap(); 113 | 114 | let accel = device.create_acceleration_structure(&gpu::AccelerationStructureDesc { 115 | ty: gpu::AccelerationStructureType::TopLevel, 116 | buffer: &buffer, 117 | offset: 0, 118 | size: sizes.acceleration_structure_size, 119 | }).unwrap(); 120 | 121 | Self { 122 | accel, 123 | build_inputs, 124 | buffer, 125 | scratch_buffer, 126 | instance_buffer, 127 | } 128 | } 129 | 130 | pub fn build(&mut self, cmd: &gpu::CmdList) { 131 | cmd.build_acceleration_structure(&gpu::AccelerationStructureBuildDesc { 132 | inputs: &self.build_inputs, 133 | src: None, 134 | dst: &self.accel, 135 | scratch_data: self.scratch_buffer.gpu_ptr(), 136 | }); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /crates/graphics/src/camera.rs: -------------------------------------------------------------------------------- 1 | use math::{transform::Transform3, Mat4, Vec3}; 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, 0.0, 0.0, 0.0, 48 | 0.0, inv_y, 0.0, 0.0, 49 | 0.0, 0.0, inv_z, near_clip * inv_z, 50 | 0.0, 0.0, -1.0, 0.0, 51 | ]) 52 | } 53 | } 54 | 55 | #[repr(C)] 56 | pub struct GpuCamera { 57 | pub u: Vec3, 58 | pub scale_u: f32, 59 | pub v: Vec3, 60 | pub scale_v: f32, 61 | pub w: Vec3, 62 | pub scale_w: f32, 63 | pub position: Vec3, 64 | pub aperture_radius: f32, 65 | } 66 | 67 | impl GpuCamera { 68 | pub fn from_camera(camera: &Camera, transform: &Transform3) -> Self { 69 | let u = transform.rotation * Vec3::X; 70 | let v = transform.rotation * Vec3::Y; 71 | let w = transform.rotation * -Vec3::Z; 72 | 73 | let scale_u = 0.5 * camera.sensor_width / camera.focal_length * camera.focus_distance; 74 | let scale_v = 0.5 * camera.sensor_height / camera.focal_length * camera.focus_distance; 75 | let scale_w = camera.focus_distance; 76 | 77 | let position = transform.translation; 78 | 79 | let aperture_radius = if camera.depth_of_field { 80 | 0.5 * (camera.focal_length / camera.f_stop) * 0.001 81 | } else { 82 | 0.0 83 | }; 84 | 85 | Self { u: *u, scale_u, v: *v, scale_v, w: *w, scale_w, position, aperture_radius } 86 | } 87 | } 88 | 89 | enum LensUnit { 90 | /// Field of view in radians. 91 | FieldOfView(f32), 92 | /// Focal length in millimeters. 93 | FocalLength(f32), 94 | } 95 | 96 | // f-stop = focal_length (mm) / aperture diameter (mm) 97 | 98 | fn focal_length_to_fov(focal_length: f32, sensor_size: f32) -> f32 { 99 | 2.0 * (0.5 * sensor_size / focal_length).atan() 100 | } 101 | 102 | fn fov_to_focal_length(fov: f32, sensor_size: f32) -> f32 { 103 | 0.5 * sensor_size / (0.5 * fov).tan() 104 | } 105 | 106 | fn compute_ev_100(aperture: f32, shutter_time: f32, iso: f32) -> f32 { 107 | (aperture * aperture / shutter_time * 100.0 / iso).log2() 108 | } 109 | 110 | fn exposure_from_ev_100(ev_100: f32) -> f32 { 111 | 1.0 / 2.0f32.powf(ev_100) 112 | } 113 | -------------------------------------------------------------------------------- /crates/graphics/src/env_map.rs: -------------------------------------------------------------------------------- 1 | use super::mipgen::MipGen; 2 | use gpu::{self, DeviceImpl, TextureImpl, CmdListImpl}; 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 = shader_compiler.compile("shaders/pathtracer/kernels/env-map-prepare.slang", "main"); 31 | 32 | let descriptor_layout = gpu::DescriptorLayout { 33 | push_constants: Some(gpu::PushConstantBinding { 34 | size: size_of::() as u32, 35 | }), 36 | bindings: Some(vec![ 37 | gpu::DescriptorBinding::bindless_srv(1), 38 | gpu::DescriptorBinding::bindless_uav(2), 39 | ]), 40 | static_samplers: Some(vec![ 41 | 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 | 54 | let compute_pipeline = device.create_compute_pipeline(&gpu::ComputePipelineDesc { 55 | cs: &shader, 56 | descriptor_layout: &descriptor_layout, 57 | }).unwrap(); 58 | 59 | // Setup importance map. 60 | 61 | let mip_levels = gpu::max_mip_level(RESOLUTION as u32) + 1; 62 | 63 | let importance_map = device.create_texture(&gpu::TextureDesc { 64 | width: RESOLUTION as u64, 65 | height: RESOLUTION as u64, 66 | depth: 1, 67 | array_size: 1, 68 | mip_levels, 69 | format: gpu::Format::R32Float, 70 | usage: gpu::TextureUsage::SHADER_RESOURCE | gpu::TextureUsage::UNORDERED_ACCESS, 71 | layout: gpu::TextureLayout::ShaderResource, 72 | }).unwrap(); 73 | 74 | let uavs = (1..mip_levels).map(|i| { 75 | device.create_texture_view(&gpu::TextureViewDesc { 76 | first_mip_level: i, 77 | mip_level_count: 1, 78 | }, &importance_map) 79 | }).collect::>(); 80 | 81 | Self { 82 | importance_map, 83 | prepare_pipeline: compute_pipeline, 84 | uavs, 85 | mipgen: MipGen::setup(device, shader_compiler), 86 | dirty: true, 87 | } 88 | } 89 | 90 | pub fn update(&mut self, cmd: &mut gpu::CmdList, env_map_srv_index: u32) { 91 | if !self.dirty { 92 | return; 93 | } 94 | 95 | assert!(RESOLUTION.is_power_of_two()); 96 | assert!(SAMPLES_PER_PIXEL.is_power_of_two()); 97 | 98 | let dimension = RESOLUTION as u32; 99 | let samples = SAMPLES_PER_PIXEL as u32; 100 | 101 | let samples_x = (samples as f32).sqrt().max(1.0) as u32; 102 | let samples_y = samples / samples_x; 103 | assert_eq!(samples, samples_x * samples_y); 104 | 105 | // Transform the env map to the importance map. 106 | let push_constants = PushConstants { 107 | env_map_id: env_map_srv_index, 108 | importance_map_id: self.importance_map.uav_index().unwrap(), 109 | 110 | output_res: [dimension; 2], 111 | output_res_in_samples: [dimension * samples_x, dimension * samples_y], 112 | num_samples: [samples_x, samples_y], 113 | inv_samples: 1.0 / (samples_x * samples_y) as f32, 114 | }; 115 | 116 | cmd.set_compute_pipeline(&self.prepare_pipeline); 117 | cmd.compute_push_constants(0, gpu::as_u8_slice(&push_constants)); 118 | 119 | cmd.dispatch([dimension.div_ceil(16), dimension.div_ceil(16), 1]); 120 | 121 | cmd.barriers(&gpu::Barriers::global()); 122 | 123 | // Generate mips. 124 | self.mipgen.generate_mips(cmd, &self.importance_map, dimension, &self.uavs); 125 | 126 | self.dirty = false; 127 | } 128 | 129 | pub fn base_mip(&self) -> u32 { 130 | gpu::max_mip_level(RESOLUTION as u32) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /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/graphics/src/mipgen.rs: -------------------------------------------------------------------------------- 1 | use gpu::{self, DeviceImpl, TextureImpl, CmdListImpl}; 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![ 28 | gpu::SamplerBinding { 29 | shader_register: 0, 30 | register_space: 0, 31 | sampler_desc: gpu::SamplerDesc { 32 | filter_min: gpu::FilterMode::Linear, 33 | filter_mag: gpu::FilterMode::Linear, 34 | filter_mip: gpu::FilterMode::Linear, 35 | ..Default::default() 36 | }, 37 | }, 38 | ]), 39 | }; 40 | 41 | let compute_pipeline = device.create_compute_pipeline(&gpu::ComputePipelineDesc { 42 | cs: &shader, 43 | descriptor_layout: &descriptor_layout, 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/graphics/src/pathtracer.rs: -------------------------------------------------------------------------------- 1 | use gpu::{self, AccelerationStructureImpl, BufferImpl, CmdListImpl, DeviceImpl, RaytracingPipelineImpl, TextureImpl}; 2 | use super::camera::GpuCamera; 3 | use super::scene; 4 | use rand::{RngCore, SeedableRng, rngs::StdRng}; 5 | 6 | #[repr(C)] 7 | struct PushConstants { 8 | camera: GpuCamera, 9 | tlas_index: u32, 10 | color_pass_index: u32, 11 | depth_pass_index: u32, 12 | instance_data_index: u32, 13 | light_data_index: u32, 14 | light_count: u32, 15 | infinite_light_count: u32, 16 | seed: u32, 17 | accumulation_factor: f32, 18 | } 19 | 20 | // TODO: Document depth pass: 21 | // View-space depth, perpendicular distance from camera plane to intersection point, ie. not distance between camera position and intersection point 22 | pub struct PathTracer { 23 | pipeline: gpu::RaytracingPipeline, 24 | shader_table: gpu::Buffer, 25 | sample_index: usize, 26 | rng: StdRng, 27 | 28 | resolution: [u32; 2], 29 | pub color_pass_texture: gpu::Texture, 30 | pub depth_pass_texture: gpu::Texture, 31 | } 32 | 33 | impl PathTracer { 34 | pub fn new(device: &mut gpu::Device, shader_compiler: &gpu::ShaderCompiler) -> Self { 35 | // Create the pipeline 36 | 37 | let shader_raygen = shader_compiler.compile("shaders/pathtracer/kernel.slang", "raygen"); 38 | let shader_miss = shader_compiler.compile("shaders/pathtracer/kernel.slang", "miss"); 39 | let shader_closesthit = shader_compiler.compile("shaders/pathtracer/kernel.slang", "closesthit"); 40 | 41 | let libraries = vec![ 42 | gpu::ShaderLibrary { ty: gpu::ShaderType::Raygen, entry: "raygen".to_string(), shader: shader_raygen }, 43 | gpu::ShaderLibrary { ty: gpu::ShaderType::Miss, entry: "miss".to_string(), shader: shader_miss }, 44 | gpu::ShaderLibrary { ty: gpu::ShaderType::ClosestHit, entry: "closesthit".to_string(), shader: shader_closesthit }, 45 | ]; 46 | 47 | let groups = vec![ 48 | gpu::ShaderGroup::general("raygen", 0), 49 | gpu::ShaderGroup::general("miss", 1), 50 | gpu::ShaderGroup::triangles("hit_group", Some(2), None), 51 | ]; 52 | 53 | let descriptor_layout = gpu::DescriptorLayout { 54 | push_constants: Some(gpu::PushConstantBinding { 55 | size: size_of::() as u32, 56 | }), 57 | bindings: Some(vec![ 58 | gpu::DescriptorBinding::bindless_srv(1), // Buffers 59 | gpu::DescriptorBinding::bindless_srv(2), // Acceleration structures 60 | gpu::DescriptorBinding::bindless_srv(3), // Textures 2D Float4 61 | gpu::DescriptorBinding::bindless_srv(4), // Textures 2D Float 62 | gpu::DescriptorBinding::bindless_uav(5), // RWTextures 2D Float4 63 | gpu::DescriptorBinding::bindless_uav(6), // RWTextures 2D Float 64 | ]), 65 | static_samplers: Some(vec![ 66 | gpu::SamplerBinding { 67 | shader_register: 0, 68 | register_space: 0, 69 | sampler_desc: gpu::SamplerDesc { 70 | filter_min: gpu::FilterMode::Linear, 71 | filter_mag: gpu::FilterMode::Linear, 72 | filter_mip: gpu::FilterMode::Linear, 73 | ..Default::default() 74 | }, 75 | }, 76 | ]), 77 | }; 78 | 79 | let pipeline_desc = gpu::RaytracingPipelineDesc { 80 | max_trace_recursion_depth: 3, // TODO: Too high? 81 | max_attribute_size: 8, 82 | max_payload_size: 128, // TODO: Arbitrary 83 | libraries, 84 | groups, 85 | descriptor_layout, 86 | }; 87 | 88 | let pipeline = device.create_raytracing_pipeline(&pipeline_desc).unwrap(); 89 | 90 | // Create the shader table 91 | 92 | // TODO: Ensure alignment 93 | // Address must be aligned to 64 bytes (D3D12_RAYTRACING_SHADER_TABLE_BYTE_ALIGNMENT) 94 | // The stride must be aligned to 32 bytes (D3D12_RAYTRACING_SHADER_RECORD_BYTE_ALIGNMENT) 95 | // let shader_identifier_size = pipeline.get_shader_identifier_size(); 96 | 97 | let table_alignment = 64; 98 | let mut shader_table_data = vec![0u8; table_alignment * 3]; // Works for now with single entry per table 99 | 100 | pipeline.write_shader_identifier(0, &mut shader_table_data[table_alignment * 0..]); 101 | pipeline.write_shader_identifier(1, &mut shader_table_data[table_alignment * 1..]); 102 | pipeline.write_shader_identifier(2, &mut shader_table_data[table_alignment * 2..]); 103 | 104 | let shader_table = device.create_buffer(&gpu::BufferDesc { 105 | size: shader_table_data.len(), 106 | usage: gpu::BufferUsage::empty(), 107 | memory: gpu::Memory::GpuOnly, 108 | }).unwrap(); 109 | gpu::upload_buffer(device, &shader_table, &shader_table_data); 110 | 111 | // Create the output texture 112 | 113 | let resolution = [1920_u32, 1080_u32]; // TODO: Hardcoded 114 | 115 | let color_pass_texture = device.create_texture(&gpu::TextureDesc { 116 | width: resolution[0] as _, 117 | height: resolution[1] as _, 118 | depth: 1, 119 | array_size: 1, 120 | mip_levels: 1, 121 | format: gpu::Format::RGBA32Float, 122 | usage: gpu::TextureUsage::SHADER_RESOURCE | gpu::TextureUsage::UNORDERED_ACCESS, 123 | layout: gpu::TextureLayout::ShaderResource, 124 | }).unwrap(); 125 | 126 | let depth_pass_texture = device.create_texture(&gpu::TextureDesc { 127 | width: resolution[0] as _, 128 | height: resolution[1] as _, 129 | depth: 1, 130 | array_size: 1, 131 | mip_levels: 1, 132 | format: gpu::Format::R32Float, 133 | usage: gpu::TextureUsage::SHADER_RESOURCE | gpu::TextureUsage::UNORDERED_ACCESS, 134 | layout: gpu::TextureLayout::ShaderResource, 135 | }).unwrap(); 136 | 137 | Self { 138 | pipeline, 139 | shader_table, 140 | resolution, 141 | color_pass_texture, 142 | depth_pass_texture, 143 | sample_index: 0, 144 | rng: StdRng::seed_from_u64(0), 145 | } 146 | } 147 | 148 | fn render(&mut self, cmd: &mut gpu::CmdList, scene: &scene::Scene) { 149 | let push_constants = PushConstants { 150 | camera: GpuCamera::from_camera(&scene.camera, &scene.camera_transform), 151 | tlas_index: scene.tlas.accel.srv_index().unwrap(), 152 | color_pass_index: self.color_pass_texture.uav_index().unwrap(), 153 | depth_pass_index: self.depth_pass_texture.uav_index().unwrap(), 154 | instance_data_index: scene.instance_data_buffer.srv_index().unwrap(), 155 | light_data_index: scene.light_data_buffer.srv_index().unwrap(), 156 | light_count: scene.light_count as _, 157 | infinite_light_count: scene.infinite_light_count as _, 158 | seed: self.rng.next_u32(), 159 | accumulation_factor: 1.0 / (self.sample_index + 1) as f32, 160 | }; 161 | 162 | cmd.set_raytracing_pipeline(&self.pipeline); 163 | cmd.compute_push_constants(0, gpu::as_u8_slice(&push_constants)); 164 | 165 | cmd.dispatch_rays(&gpu::DispatchRaysDesc { 166 | size: [self.resolution[0], self.resolution[1], 1], 167 | raygen: Some(gpu::ShaderTable { 168 | ptr: self.shader_table.gpu_ptr().offset(64 * 0), 169 | size: 32, 170 | stride: 32, 171 | }), 172 | miss: Some(gpu::ShaderTable { 173 | ptr: self.shader_table.gpu_ptr().offset(64 * 1), 174 | size: 32, 175 | stride: 32, 176 | }), 177 | hit_group: Some(gpu::ShaderTable { 178 | ptr: self.shader_table.gpu_ptr().offset(64 * 2), 179 | size: 32, 180 | stride: 32, 181 | }), 182 | callable: None, 183 | }); 184 | 185 | cmd.barriers(&gpu::Barriers::global()); 186 | 187 | self.sample_index += 1; 188 | } 189 | 190 | fn reset(&mut self) { 191 | self.rng = StdRng::seed_from_u64(0); 192 | self.sample_index = 0; 193 | } 194 | 195 | pub fn run(&mut self, cmd: &mut gpu::CmdList, scene: &scene::Scene, samples: usize) { 196 | self.reset(); 197 | 198 | cmd.barriers(&gpu::Barriers::texture(&[ 199 | gpu::TextureBarrier { 200 | texture: &self.color_pass_texture, 201 | old_layout: gpu::TextureLayout::ShaderResource, 202 | new_layout: gpu::TextureLayout::UnorderedAccess, 203 | }, gpu::TextureBarrier { 204 | texture: &self.depth_pass_texture, 205 | old_layout: gpu::TextureLayout::ShaderResource, 206 | new_layout: gpu::TextureLayout::UnorderedAccess, 207 | }, 208 | ])); 209 | 210 | for _ in 0..samples { 211 | self.render(cmd, scene); 212 | } 213 | 214 | cmd.barriers(&gpu::Barriers::texture(&[ 215 | gpu::TextureBarrier { 216 | texture: &self.color_pass_texture, 217 | old_layout: gpu::TextureLayout::UnorderedAccess, 218 | new_layout: gpu::TextureLayout::ShaderResource, 219 | }, gpu::TextureBarrier { 220 | texture: &self.depth_pass_texture, 221 | old_layout: gpu::TextureLayout::UnorderedAccess, 222 | new_layout: gpu::TextureLayout::ShaderResource, 223 | }, 224 | ])); 225 | } 226 | } 227 | 228 | #[repr(C)] 229 | struct CompositorPushConstants { 230 | input_id: u32, 231 | output_id: u32, 232 | output_res: [u32; 2], 233 | overlay_id: u32, 234 | } 235 | 236 | pub struct Compositor { 237 | res: [u32; 2], 238 | texture: gpu::Texture, 239 | pipeline: gpu::ComputePipeline, 240 | } 241 | 242 | impl Compositor { 243 | pub fn new(res: [u32; 2], device: &mut gpu::Device, shader_compiler: &gpu::ShaderCompiler) -> Self { 244 | let shader = shader_compiler.compile("shaders/compositor.slang", "main"); 245 | 246 | let descriptor_layout = gpu::DescriptorLayout { 247 | push_constants: Some(gpu::PushConstantBinding { 248 | size: size_of::() as u32, 249 | }), 250 | bindings: Some(vec![ 251 | gpu::DescriptorBinding::bindless_srv(1), 252 | gpu::DescriptorBinding::bindless_uav(2), 253 | ]), 254 | static_samplers: None, 255 | }; 256 | 257 | let pipeline = device.create_compute_pipeline(&gpu::ComputePipelineDesc { 258 | cs: &shader, 259 | descriptor_layout: &descriptor_layout, 260 | }).unwrap(); 261 | 262 | let texture = device.create_texture(&gpu::TextureDesc { 263 | width: res[0] as _, 264 | height: res[1] as _, 265 | depth: 1, 266 | array_size: 1, 267 | mip_levels: 1, 268 | format: gpu::Format::RGBA32Float, 269 | usage: gpu::TextureUsage::SHADER_RESOURCE | gpu::TextureUsage::UNORDERED_ACCESS, 270 | layout: gpu::TextureLayout::ShaderResource, 271 | }).unwrap(); 272 | 273 | Self { 274 | res, 275 | texture, 276 | pipeline, 277 | } 278 | } 279 | 280 | pub fn process(&mut self, cmd: &mut gpu::CmdList, input: &gpu::Texture, overlay: &gpu::Texture) { 281 | cmd.barriers(&gpu::Barriers::texture(&[gpu::TextureBarrier { 282 | texture: &self.texture, 283 | old_layout: gpu::TextureLayout::ShaderResource, 284 | new_layout: gpu::TextureLayout::UnorderedAccess, 285 | }])); 286 | 287 | cmd.set_compute_pipeline(&self.pipeline); 288 | 289 | let push_constants = CompositorPushConstants { 290 | input_id: input.srv_index().unwrap(), 291 | output_id: self.texture.uav_index().unwrap(), 292 | output_res: self.res, 293 | overlay_id: overlay.srv_index().unwrap(), 294 | }; 295 | 296 | cmd.compute_push_constants(0, gpu::as_u8_slice(&push_constants)); 297 | cmd.dispatch([self.res[0].div_ceil(16), self.res[1].div_ceil(16), 1]); 298 | 299 | cmd.barriers(&gpu::Barriers::global()); 300 | 301 | cmd.barriers(&gpu::Barriers::texture(&[gpu::TextureBarrier { 302 | texture: &self.texture, 303 | old_layout: gpu::TextureLayout::UnorderedAccess, 304 | new_layout: gpu::TextureLayout::ShaderResource, 305 | }])); 306 | } 307 | 308 | pub fn texture(&self) -> &gpu::Texture { 309 | &self.texture 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /crates/graphics/src/scene.rs: -------------------------------------------------------------------------------- 1 | use asset::{Asset, UntypedAssetId, AssetId, AssetServer}; 2 | use ecs::World; 3 | use gpu::{self, BufferImpl, CmdListImpl, DeviceImpl, TextureImpl, AccelerationStructureImpl}; 4 | use math::{Vec3, Mat3x4, Mat4, transform::Transform3}; 5 | use geometry::mesh::Mesh; 6 | use super::acceleration_structure::{Blas, Tlas}; 7 | use super::camera::Camera; 8 | use super::env_map::ImportanceMap; 9 | 10 | pub struct Renderable { 11 | pub mesh: AssetId, 12 | } 13 | 14 | pub struct DomeLight { 15 | pub image: AssetId, 16 | } 17 | 18 | pub struct SphereLight { 19 | pub emission: [f32; 3], 20 | pub radius: f32, 21 | } 22 | 23 | pub struct RectLight { 24 | pub emission: [f32; 3], 25 | pub width: f32, 26 | pub height: f32, 27 | } 28 | 29 | const MAX_LIGHTS: usize = 100; 30 | const MAX_INSTANCES: usize = 1000; 31 | 32 | #[repr(C)] 33 | pub struct Vertex { 34 | position: Vec3, 35 | normal: Vec3, 36 | } 37 | 38 | #[repr(C)] 39 | struct Instance { 40 | vertex_buffer_id: u32, 41 | index_buffer_id: u32, 42 | material_offset: u32, 43 | } 44 | 45 | #[repr(C)] 46 | enum GpuLightType { 47 | Dome = 0, 48 | Rect = 1, 49 | Sphere = 2, 50 | } 51 | 52 | #[repr(C)] 53 | #[derive(Clone, Copy)] 54 | struct GpuDomeLight { 55 | ty: u32, 56 | env_map_id: u32, 57 | importance_map_id: u32, 58 | base_mip: u32, 59 | } 60 | 61 | #[repr(C)] 62 | #[derive(Clone, Copy)] 63 | struct GpuSphereLight { 64 | ty: u32, 65 | emission: Vec3, 66 | position: Vec3, 67 | radius: f32, 68 | } 69 | 70 | #[repr(C)] 71 | #[derive(Clone, Copy)] 72 | struct GpuRectLight { 73 | ty: u32, 74 | emission: Vec3, 75 | position: Vec3, 76 | area_scaled_normal: Vec3, 77 | x: Vec3, 78 | y: Vec3, 79 | } 80 | 81 | #[repr(C)] 82 | union GpuLight { 83 | dome: GpuDomeLight, 84 | rect: GpuRectLight, 85 | sphere: GpuSphereLight, 86 | } 87 | 88 | #[derive(Clone, Debug)] 89 | pub struct Image { 90 | pub width: u32, 91 | pub height: u32, 92 | pub data: Vec<[f32; 4]>, 93 | } 94 | 95 | impl Image { 96 | pub fn new(width: u32, height: u32, data: Vec<[f32; 4]>) -> Self { 97 | assert_eq!((width * height) as usize, data.len()); 98 | Self { width, height, data } 99 | } 100 | 101 | pub fn from_file(path: impl AsRef) -> Self 102 | where 103 | Self: Sized { 104 | exr::prelude::read_first_rgba_layer_from_file( 105 | path, 106 | |resolution, _| Image::new( 107 | resolution.width() as u32, 108 | resolution.height() as u32, 109 | vec![[0.0, 0.0, 0.0, 0.0]; resolution.width() * resolution.height()], 110 | ), 111 | |image, position, (r, g, b, a): (f32, f32, f32, f32)| { 112 | image.data[image.width as usize * position.y() + position.x()] = [r, g, b, a]; 113 | }, 114 | ).unwrap().layer_data.channel_data.pixels 115 | } 116 | } 117 | 118 | impl Asset for Image {} 119 | 120 | struct GpuMeshData { 121 | vertex_buffer: gpu::Buffer, 122 | index_buffer: gpu::Buffer, 123 | blas: Blas, 124 | } 125 | 126 | impl GpuMeshData { 127 | fn from_mesh(device: &mut gpu::Device, mesh: &Mesh) -> Self { 128 | let vertices: Vec = mesh.vertices.iter().map(|v| Vertex { position: v.p, normal: v.n }).collect(); 129 | let indices: Vec = mesh.indices.iter().map(|i| *i as u32).collect(); 130 | 131 | let vertex_buffer = device.create_buffer(&gpu::BufferDesc { 132 | size: size_of::() * vertices.len(), 133 | usage: gpu::BufferUsage::SHADER_RESOURCE, 134 | memory: gpu::Memory::GpuOnly, 135 | }).unwrap(); 136 | 137 | let index_buffer = device.create_buffer(&gpu::BufferDesc { 138 | size: size_of::() * indices.len(), 139 | usage: gpu::BufferUsage::SHADER_RESOURCE, 140 | memory: gpu::Memory::GpuOnly, 141 | }).unwrap(); 142 | 143 | gpu::upload_buffer(device, &vertex_buffer, gpu::slice_as_u8_slice(&vertices)); 144 | gpu::upload_buffer(device, &index_buffer, gpu::slice_as_u8_slice(&indices)); 145 | 146 | let blas = Blas::create(device, &vertex_buffer, &index_buffer, vertices.len(), indices.len(), size_of::()); 147 | 148 | Self { 149 | vertex_buffer, 150 | index_buffer, 151 | blas, 152 | } 153 | } 154 | } 155 | 156 | pub struct Scene { 157 | pub tlas: Tlas, 158 | pub camera: Camera, 159 | pub camera_transform: Transform3, 160 | pub instance_data_buffer: gpu::Buffer, 161 | pub light_data_buffer: gpu::Buffer, 162 | pub light_count: usize, 163 | pub infinite_light_count: usize, 164 | pub importance_map: ImportanceMap, 165 | 166 | texture_cache: std::collections::HashMap, 167 | mesh_cache: std::collections::HashMap, 168 | } 169 | 170 | impl Scene { 171 | pub fn new(device: &mut gpu::Device, shader_compiler: &gpu::ShaderCompiler) -> Self { 172 | let tlas = Tlas::create(device, MAX_INSTANCES); 173 | 174 | let instance_data_buffer = device.create_buffer(&gpu::BufferDesc { 175 | size: size_of::() * MAX_INSTANCES, 176 | usage: gpu::BufferUsage::SHADER_RESOURCE, 177 | memory: gpu::Memory::CpuToGpu, 178 | }).unwrap(); 179 | 180 | let light_data_buffer = device.create_buffer(&gpu::BufferDesc { 181 | size: size_of::() * MAX_LIGHTS, 182 | usage: gpu::BufferUsage::SHADER_RESOURCE, 183 | memory: gpu::Memory::CpuToGpu, 184 | }).unwrap(); 185 | 186 | let importance_map = ImportanceMap::setup(device, shader_compiler); 187 | 188 | Self { 189 | tlas, 190 | camera: Camera::default(), 191 | camera_transform: Transform3::IDENTITY, 192 | instance_data_buffer, 193 | light_data_buffer, 194 | light_count: 0, 195 | infinite_light_count: 0, 196 | importance_map, 197 | texture_cache: std::collections::HashMap::new(), 198 | mesh_cache: std::collections::HashMap::new(), 199 | } 200 | } 201 | 202 | pub fn update(&mut self, world: &mut World, assets: &AssetServer, device: &mut gpu::Device, cmd: &mut gpu::CmdList) { 203 | // CAMERA 204 | // TODO: Handle properly when there's no camera in the scene. 205 | if let Some((transform, camera)) = world.query::<(&Transform3, &Camera)>().iter().next() { 206 | self.camera = *camera; 207 | self.camera_transform = *transform; 208 | } 209 | 210 | // LIGHTS 211 | 212 | // Gpu assumes infinite lights are at the start of the array 213 | // light_count contains the number of all lights 214 | // infinite_light_count only contains the number of infinite lights 215 | 216 | let lights = unsafe { std::slice::from_raw_parts_mut(self.light_data_buffer.cpu_ptr() as *mut GpuLight, MAX_LIGHTS) }; 217 | let mut light_index = 0; 218 | let mut infinite_light_count = 0; 219 | 220 | for light in &world.query::<&DomeLight>() { 221 | let env_map_srv_index = self.get_texture_from_cache(&light.image, device, assets).srv_index().unwrap(); 222 | 223 | // TODO: Currently only supports single dome light 224 | self.importance_map.update(cmd, env_map_srv_index); 225 | 226 | lights[light_index].dome = GpuDomeLight { 227 | ty: GpuLightType::Dome as _, 228 | env_map_id: env_map_srv_index, 229 | importance_map_id: self.importance_map.importance_map.srv_index().unwrap(), 230 | base_mip: self.importance_map.base_mip(), 231 | }; 232 | 233 | light_index += 1; 234 | infinite_light_count += 1; 235 | } 236 | 237 | for (transform, light) in &world.query::<(&Transform3, &RectLight)>() { 238 | let x = transform.rotation * Vec3::X * transform.scale.x * light.width; 239 | let y = transform.rotation * Vec3::Y * transform.scale.y * light.height; 240 | let z = transform.rotation * -Vec3::Z * transform.scale.z.signum(); 241 | 242 | let area = light.width * light.height; 243 | 244 | lights[light_index].rect = GpuRectLight { 245 | ty: GpuLightType::Rect as _, 246 | emission: Vec3::new(light.emission[0], light.emission[1], light.emission[2]), 247 | position: transform.translation, 248 | area_scaled_normal: z * area, 249 | x, 250 | y, 251 | }; 252 | 253 | light_index += 1; 254 | } 255 | 256 | for (transform, light) in &world.query::<(&Transform3, &SphereLight)>() { 257 | lights[light_index].sphere = GpuSphereLight { 258 | ty: GpuLightType::Sphere as _, 259 | emission: Vec3::new(light.emission[0], light.emission[1], light.emission[2]), 260 | position: transform.translation, 261 | radius: light.radius, 262 | }; 263 | 264 | light_index += 1; 265 | } 266 | 267 | self.light_count = light_index; 268 | self.infinite_light_count = infinite_light_count; 269 | 270 | // INSTANCES 271 | 272 | let instance_descriptor_size = gpu::AccelerationStructure::instance_descriptor_size(); 273 | 274 | let instance_data = unsafe { std::slice::from_raw_parts_mut(self.instance_data_buffer.cpu_ptr() as *mut Instance, MAX_INSTANCES) }; 275 | let instance_descriptors = unsafe { std::slice::from_raw_parts_mut(self.tlas.instance_buffer.cpu_ptr(), MAX_INSTANCES * instance_descriptor_size) }; 276 | 277 | let mut instance_index = 0; 278 | 279 | for (transform, renderable) in &world.query::<(&Transform3, &Renderable)>() { 280 | let mesh_data = self.get_mesh_from_cache(&renderable.mesh, device, cmd, assets); 281 | 282 | instance_data[instance_index] = Instance { 283 | vertex_buffer_id: mesh_data.vertex_buffer.srv_index().unwrap(), 284 | index_buffer_id: mesh_data.index_buffer.srv_index().unwrap(), 285 | material_offset: 0, 286 | }; 287 | 288 | let instance_desc = gpu::AccelerationStructureInstance { 289 | transform: Mat3x4::from(Mat4::from(*transform)).data, 290 | user_id: 0, 291 | mask: 0xff, 292 | contribution_to_hit_group_index: 0, 293 | flags: gpu::AccelerationStructureInstanceFlags::empty(), 294 | bottom_level: mesh_data.blas.accel.gpu_ptr(), 295 | }; 296 | 297 | gpu::AccelerationStructure::write_instance_descriptor(&instance_desc, &mut instance_descriptors[instance_index * instance_descriptor_size..]); 298 | 299 | instance_index += 1; 300 | } 301 | 302 | self.tlas.build_inputs.entries = gpu::AccelerationStructureEntries::Instances( 303 | gpu::AccelerationStructureInstances { data: self.tlas.instance_buffer.gpu_ptr(), count: instance_index } 304 | ); 305 | 306 | // ACCELERATION STRUCTURES 307 | 308 | cmd.barriers(&gpu::Barriers::global()); // Global barrier to ensure the BLASes are visible to TLAS 309 | self.tlas.build(cmd); 310 | cmd.barriers(&gpu::Barriers::global()); // Global barrier to ensure the TLAS is visible to the raytracing pipeline 311 | } 312 | 313 | fn get_texture_from_cache(&mut self, asset: &AssetId, device: &mut gpu::Device, assets: &AssetServer) -> &gpu::Texture { 314 | self.texture_cache.entry(asset.id()).or_insert_with(|| { 315 | let image = assets.get(asset).unwrap(); 316 | 317 | let texture_desc = gpu::TextureDesc { 318 | width: image.width as u64, 319 | height: image.height as u64, 320 | depth: 1, 321 | array_size: 1, 322 | mip_levels: 1, 323 | format: gpu::Format::RGBA32Float, 324 | usage: gpu::TextureUsage::SHADER_RESOURCE, 325 | layout: gpu::TextureLayout::ShaderResource, 326 | }; 327 | 328 | let texture = device.create_texture(&texture_desc).unwrap(); 329 | gpu::upload_texture(device, &texture, &texture_desc, gpu::slice_as_u8_slice(&image.data)); 330 | 331 | texture 332 | }) 333 | } 334 | 335 | fn get_mesh_from_cache(&mut self, asset: &AssetId, device: &mut gpu::Device, cmd: &gpu::CmdList, assets: &AssetServer) -> &GpuMeshData { 336 | self.mesh_cache.entry(asset.id()).or_insert_with(|| { 337 | let mesh = assets.get(asset).unwrap(); 338 | let mut gpu_mesh_data = GpuMeshData::from_mesh(device, mesh); 339 | gpu_mesh_data.blas.build(cmd); 340 | gpu_mesh_data 341 | }) 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /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/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/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/math/src/isometry.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Mul; 2 | use super::matrix::{Vector2, Vector3}; 3 | use super::num::Number; 4 | use super::{UnitComplex, UnitQuaternion}; 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 { translation, ..Self::IDENTITY } 25 | } 26 | 27 | pub const fn from_rotation(rotation: UnitQuaternion) -> Self { 28 | Self { rotation, ..Self::IDENTITY } 29 | } 30 | 31 | pub const fn with_translation(self, translation: Vector3) -> Self { 32 | Self { translation, ..self } 33 | } 34 | 35 | pub const fn with_rotation(self, rotation: UnitQuaternion) -> Self { 36 | Self { rotation, ..self } 37 | } 38 | } 39 | 40 | impl Isometry3 { 41 | pub fn inv(&self) -> Self { 42 | let rotation = self.rotation.inv(); 43 | let translation = rotation * -self.translation; 44 | 45 | Self { translation, rotation } 46 | } 47 | 48 | /// Translates and rotates a point by this isometry. 49 | pub fn transform_point(&self, point: Vector3) -> Vector3 { 50 | self.rotation * point + self.translation 51 | } 52 | 53 | /// Translates and rotates a point by the inverse of this isometry. 54 | /// Shorthand for `inv()` followed by `transform_point()`. 55 | pub fn inv_transform_point(&self, point: Vector3) -> Vector3 { 56 | self.rotation.inv() * (point - self.translation) 57 | } 58 | 59 | /// Rotates a vector by this isometry. 60 | pub fn transform_vector(&self, vector: Vector3) -> Vector3 { 61 | self.rotation * vector 62 | } 63 | 64 | /// Rotates a vector by the inverse of this isometry. 65 | /// Shorthand for `inv()` followed by `transform_vector()`. 66 | pub fn inv_transform_vector(&self, vector: Vector3) -> Vector3 { 67 | self.rotation.inv() * vector 68 | } 69 | } 70 | 71 | impl Mul for Isometry3 { 72 | type Output = Self; 73 | 74 | fn mul(self, rhs: Self) -> Self::Output { 75 | let translation = self.rotation * rhs.translation + self.translation; 76 | let rotation = self.rotation * rhs.rotation; 77 | 78 | Self { translation, rotation } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /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::{cast, Number, NumberOps}; 15 | 16 | pub use complex::{Complex, UnitComplex}; 17 | pub use dual::Dual; 18 | pub use matrix::{ 19 | Matrix, Matrix2, Matrix3, Matrix4, 20 | Vector, Vector2, Vector3, Vector4, 21 | }; 22 | pub use quaternion::{Quaternion, UnitQuaternion}; 23 | pub use unit::Unit; 24 | 25 | pub type Vec2 = Vector2; 26 | pub type Vec3 = Vector3; 27 | 28 | pub type Mat3 = Matrix3; 29 | pub type Mat4 = Matrix4; 30 | pub type Mat3x4 = Matrix; 31 | 32 | pub const E: f32 = 2.71828182845904523536; 33 | pub const PI: f32 = 3.14159265358979323846; 34 | 35 | /// Clamps x to be in the range [min, max]. 36 | pub fn clamp>(x: T, min: T, max: T) -> T { 37 | T::max(min, T::min(max, x)) 38 | } 39 | 40 | /// Wraps x to be in the range [min, max]. 41 | pub fn wrap(mut x: T, min: T, max: T) -> T { 42 | let range = max - min; 43 | 44 | while x < min { x += range; } 45 | while x > max { x -= range; } 46 | 47 | x 48 | } 49 | 50 | /// Unwinds an angle in radians to the range [-pi, pi]. 51 | pub fn unwind_radians(radians: T) -> T { 52 | wrap(radians, cast(-PI), cast(PI)) 53 | } 54 | 55 | /// Unwinds an angle in degrees to the range [-180, 180]. 56 | pub fn unwind_degrees(degrees: T) -> T { 57 | wrap(degrees, cast(-180.0), cast(180.0)) 58 | } 59 | 60 | /// Remaps a value from one range to another. 61 | /// The minimum of either range may be larger or smaller than the maximum. 62 | pub fn map_range(x: T, min: T, max: T, new_min: T, new_max: T) -> T { 63 | (x - min) * (new_max - new_min) / (max - min) + new_min 64 | } 65 | -------------------------------------------------------------------------------- /crates/math/src/num.rs: -------------------------------------------------------------------------------- 1 | use core::ops::{Mul, Add, Sub, Div, Rem, Neg, Shl, Shr, BitOr, BitAnd, BitXor}; 2 | use core::ops::{MulAssign, AddAssign, SubAssign, DivAssign, RemAssign, ShlAssign, ShrAssign, BitOrAssign, BitAndAssign, BitXorAssign}; 3 | use core::cmp::{PartialEq, PartialOrd}; 4 | 5 | /// Forward a method to an inherent method or a base trait method. 6 | macro_rules! forward { 7 | ($( Self :: $method:ident ( self $( , $arg:ident : $ty:ty )* ) -> $ret:ty ; )*) => {$( 8 | #[inline] 9 | fn $method(self $( , $arg : $ty )* ) -> $ret { 10 | Self::$method(self $( , $arg )* ) 11 | } 12 | )*}; 13 | } 14 | 15 | pub trait Cast where Self: Sized { 16 | fn from_f64(v: f64) -> Self; 17 | fn as_f64(&self) -> f64; 18 | } 19 | 20 | pub fn cast(v: T) -> U { 21 | U::from_f64(v.as_f64()) // TODO: cast immediately to correct type 22 | } 23 | 24 | pub trait NumOps: 25 | Add + 26 | Sub + 27 | Mul + 28 | Div + 29 | Rem 30 | {} 31 | 32 | impl NumOps for T where T: 33 | Add + 34 | Sub + 35 | Mul + 36 | Div + 37 | Rem 38 | {} 39 | 40 | pub trait NumAssignOps: 41 | AddAssign + 42 | SubAssign + 43 | MulAssign + 44 | DivAssign + 45 | RemAssign 46 | {} 47 | 48 | impl NumAssignOps for T where T: 49 | AddAssign + 50 | SubAssign + 51 | MulAssign + 52 | DivAssign + 53 | RemAssign 54 | {} 55 | 56 | pub trait Base: Copy + NumOps + NumAssignOps + where Self: Sized { 57 | const ZERO: Self; 58 | const ONE: Self; 59 | const TWO: Self; 60 | 61 | const MIN: Self; 62 | const MAX: Self; 63 | } 64 | 65 | pub trait Number: Base + Cast + Default + PartialEq + PartialOrd {} 66 | 67 | pub trait NumberOps { 68 | fn min(a: Self, b: Self) -> Self; 69 | fn max(a: Self, b: Self) -> Self; 70 | } 71 | 72 | macro_rules! number_impl { 73 | ($t:ident) => { 74 | impl Base<$t> for $t { 75 | const ZERO: Self = 0 as Self; 76 | const ONE: Self = 1 as Self; 77 | const TWO: Self = 2 as Self; 78 | 79 | const MIN: Self = $t::MIN; 80 | const MAX: Self = $t::MAX; 81 | } 82 | 83 | impl Number for $t {} 84 | 85 | impl NumberOps<$t> for $t { 86 | fn min(a: Self, b: Self) -> Self { 87 | a.min(b) 88 | } 89 | 90 | fn max(a: Self, b: Self) -> Self { 91 | a.max(b) 92 | } 93 | } 94 | 95 | impl Cast<$t> for $t { 96 | fn from_f64(v: f64) -> Self { 97 | v as Self 98 | } 99 | 100 | fn as_f64(&self) -> f64 { 101 | *self as f64 102 | } 103 | } 104 | } 105 | } 106 | 107 | number_impl!(u8); 108 | number_impl!(i8); 109 | number_impl!(u16); 110 | number_impl!(i16); 111 | number_impl!(u32); 112 | number_impl!(i32); 113 | number_impl!(u64); 114 | number_impl!(i64); 115 | number_impl!(u128); 116 | number_impl!(i128); 117 | number_impl!(usize); 118 | number_impl!(isize); 119 | number_impl!(f32); 120 | number_impl!(f64); 121 | 122 | pub trait SignedNumber: Number + Neg { 123 | const MINUS_ONE: Self; 124 | } 125 | 126 | pub trait SignedNumberOps: Neg { 127 | fn signum(self) -> Self; 128 | fn abs(self) -> Self; 129 | } 130 | 131 | macro_rules! signed_number_impl { 132 | ($t:ident, $minus_one:literal) => { 133 | impl SignedNumber for $t { 134 | const MINUS_ONE: Self = $minus_one; 135 | } 136 | 137 | impl SignedNumberOps<$t> for $t { 138 | forward! { 139 | Self::signum(self) -> Self; 140 | Self::abs(self) -> Self; 141 | } 142 | } 143 | } 144 | } 145 | 146 | signed_number_impl!(i8, -1); 147 | signed_number_impl!(i16, -1); 148 | signed_number_impl!(i32, -1); 149 | signed_number_impl!(i64, -1); 150 | signed_number_impl!(i128, -1); 151 | signed_number_impl!(f32, -1.0); 152 | signed_number_impl!(f64, -1.0); 153 | 154 | pub trait Integer: Number + 155 | Shl + ShlAssign + 156 | Shr + ShrAssign + 157 | BitOr + BitOrAssign + 158 | BitAnd + BitAndAssign + 159 | BitXor + BitXorAssign { 160 | } 161 | 162 | pub trait IntegerOps { 163 | fn pow(self, exp: u32) -> Self; 164 | } 165 | 166 | macro_rules! integer_impl { 167 | ($t:ident) => { 168 | impl Integer for $t { 169 | } 170 | 171 | impl IntegerOps<$t> for $t { 172 | forward! { 173 | Self::pow(self, exp: u32) -> Self; 174 | } 175 | } 176 | } 177 | } 178 | 179 | integer_impl!(u8); 180 | integer_impl!(i8); 181 | integer_impl!(u16); 182 | integer_impl!(i16); 183 | integer_impl!(u32); 184 | integer_impl!(i32); 185 | integer_impl!(u64); 186 | integer_impl!(i64); 187 | integer_impl!(u128); 188 | integer_impl!(i128); 189 | 190 | pub trait Float: SignedNumber { 191 | const SMALL_EPSILON: Self; 192 | } 193 | 194 | pub trait FloatOps: where Self: Sized { 195 | fn acos(self) -> Self; 196 | fn approx(self, b: Self, eps: T) -> bool; 197 | fn asin(self) -> Self; 198 | fn atan(self) -> Self; 199 | fn atan2(self, x: Self) -> Self; 200 | fn ceil(self) -> Self; 201 | fn copysign(self, sign: T) -> Self; 202 | fn cos(self) -> Self; 203 | fn cosh(self) -> Self; 204 | fn exp(self) -> Self; 205 | fn exp2(self) -> Self; 206 | fn floor(self) -> Self; 207 | fn fract(self) -> Self; 208 | fn is_finite(self) -> bool; 209 | fn is_infinite(self) -> bool; 210 | fn is_nan(self) -> bool; 211 | fn log(self, base: T) -> Self; 212 | fn log10(self) -> Self; 213 | fn log2(self) -> Self; 214 | fn mul_add(self, a: Self, b: Self) -> Self; 215 | fn powf(self, exp: T) -> Self; 216 | fn powi(self, exp: i32) -> Self; 217 | fn recip(self) -> Self; 218 | fn round(self) -> Self; 219 | fn sin_cos(self) -> (Self, Self); 220 | fn sin(self) -> Self; 221 | fn sinh(self) -> Self; 222 | fn sqrt(self) -> Self; 223 | fn tan(self) -> Self; 224 | fn tanh(self) -> Self; 225 | fn to_degrees(self) -> Self; 226 | fn to_radians(self) -> Self; 227 | fn trunc(self) -> Self; 228 | } 229 | 230 | macro_rules! float_impl { 231 | ($t:ident) => { 232 | impl Float for $t { 233 | const SMALL_EPSILON: Self = 1e-30; 234 | } 235 | 236 | impl FloatOps<$t> for $t { 237 | fn approx(self, b: Self, eps: Self) -> bool { 238 | Self::abs(self - b) < eps 239 | } 240 | 241 | forward! { 242 | Self::acos(self) -> Self; 243 | Self::asin(self) -> Self; 244 | Self::atan(self) -> Self; 245 | Self::atan2(self, y: Self) -> Self; 246 | Self::ceil(self) -> Self; 247 | Self::copysign(self, sign: $t) -> Self; 248 | Self::cos(self) -> Self; 249 | Self::cosh(self) -> Self; 250 | Self::exp(self) -> Self; 251 | Self::exp2(self) -> Self; 252 | Self::floor(self) -> Self; 253 | Self::fract(self) -> Self; 254 | Self::is_finite(self) -> bool; 255 | Self::is_infinite(self) -> bool; 256 | Self::is_nan(self) -> bool; 257 | Self::log(self, base: Self) -> Self; 258 | Self::log10(self) -> Self; 259 | Self::log2(self) -> Self; 260 | Self::mul_add(self, a: Self, b: Self) -> Self; 261 | Self::powf(self, exp: $t) -> Self; 262 | Self::powi(self, exp: i32) -> Self; 263 | Self::recip(self) -> Self; 264 | Self::round(self) -> Self; 265 | Self::sin_cos(self) -> (Self, Self); 266 | Self::sin(self) -> Self; 267 | Self::sinh(self) -> Self; 268 | Self::sqrt(self) -> Self; 269 | Self::tan(self) -> Self; 270 | Self::tanh(self) -> Self; 271 | Self::to_degrees(self) -> Self; 272 | Self::to_radians(self) -> Self; 273 | Self::trunc(self) -> Self; 274 | } 275 | } 276 | } 277 | } 278 | 279 | float_impl!(f32); 280 | float_impl!(f64); 281 | -------------------------------------------------------------------------------- /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 + self.half_size.y * self.half_size.z + self.half_size.x * self.half_size.z) 44 | } 45 | 46 | fn volume(&self) -> f32 { 47 | 8.0 * self.half_size.x * self.half_size.y * self.half_size.z 48 | } 49 | } 50 | 51 | impl Measure for TriMesh<'_> { 52 | fn area(&self) -> f32 { 53 | self.indices 54 | .chunks_exact(3) 55 | .map(|indices| { 56 | let a = self.vertices[indices[0]]; 57 | let b = self.vertices[indices[1]]; 58 | let c = self.vertices[indices[2]]; 59 | 60 | (b - a).cross(c - a).length() / 2.0 61 | }) 62 | .sum() 63 | } 64 | 65 | fn volume(&self) -> f32 { 66 | self.indices 67 | .chunks_exact(3) 68 | .map(|indices| { 69 | let a = self.vertices[indices[0]]; 70 | let b = self.vertices[indices[1]]; 71 | let c = self.vertices[indices[2]]; 72 | 73 | a.dot(b.cross(c)) / 6.0 74 | }) 75 | .sum() 76 | } 77 | } 78 | 79 | #[cfg(test)] 80 | mod tests { 81 | use super::*; 82 | use crate::Vec3; 83 | 84 | #[test] 85 | fn sphere() { 86 | let sphere = Sphere { radius: 2.0 }; 87 | 88 | assert_eq!(sphere.area(), 50.265484, "Incorrect area"); 89 | assert_eq!(sphere.volume(), 33.510323, "Incorrect volume"); 90 | } 91 | 92 | #[test] 93 | fn cylinder() { 94 | let cylinder = Cylinder { radius: 2.0, half_height: 1.5 }; 95 | 96 | assert_eq!(cylinder.area(), 62.831856, "Incorrect area"); 97 | assert_eq!(cylinder.volume(), 37.699112, "Incorrect volume"); 98 | } 99 | 100 | #[test] 101 | fn capsule() { 102 | let capsule = Capsule { radius: 2.0, half_length: 1.5 }; 103 | 104 | assert_eq!(capsule.area(), 87.9646, "Incorrect area"); 105 | assert_eq!(capsule.volume(), 71.20944, "Incorrect volume"); 106 | } 107 | 108 | #[test] 109 | fn cuboid() { 110 | let cuboid = Cuboid { half_size: Vec3::new(1.0, 1.5, 2.0) }; 111 | 112 | assert_eq!(cuboid.area(), 52.0, "Incorrect area"); 113 | assert_eq!(cuboid.volume(), 24.0, "Incorrect volume"); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /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/math/src/primitives/sample.rs: -------------------------------------------------------------------------------- 1 | use super::shapes::*; 2 | use crate::{Vec2, Vec3}; 3 | use std::f32::consts::PI; 4 | use rand::Rng; 5 | 6 | pub trait ShapeSample { 7 | /// Uniformly sample a point on the boundary of this shape. 8 | fn sample_boundary(&self, rng: &mut R) -> Vec3; 9 | 10 | /// Uniformly sample a point in the interior of this shape. 11 | fn sample_interior(&self, rng: &mut R) -> Vec3; 12 | } 13 | 14 | impl ShapeSample for Sphere { 15 | fn sample_boundary(&self, rng: &mut R) -> Vec3 { 16 | sample_sphere_boundary(rng) * self.radius 17 | } 18 | 19 | fn sample_interior(&self, rng: &mut R) -> Vec3 { 20 | let r_cubed = rng.random_range(0.0..=(self.radius * self.radius * self.radius)); 21 | let r = r_cubed.cbrt(); 22 | sample_sphere_boundary(rng) * r 23 | } 24 | } 25 | 26 | impl ShapeSample for Cylinder { 27 | fn sample_boundary(&self, rng: &mut R) -> Vec3 { 28 | if rng.random_bool((self.radius / (self.radius + 2.0 * self.half_height)) as f64) { 29 | let circle = sample_circle_interior(rng) * self.radius; 30 | if rng.random() { 31 | Vec3::new(circle.x, circle.y, self.half_height) 32 | } else { 33 | Vec3::new(circle.x, circle.y, -self.half_height) 34 | } 35 | } else { 36 | let circle = sample_circle_boundary(rng) * self.radius; 37 | let z = rng.random_range(-self.half_height..=self.half_height); 38 | Vec3::new(circle.x, circle.y, z) 39 | } 40 | } 41 | 42 | fn sample_interior(&self, rng: &mut R) -> Vec3 { 43 | let xy = sample_circle_interior(rng) * self.radius; 44 | let z = rng.random_range(-self.half_height..=self.half_height); 45 | Vec3::new(xy.x, xy.y, z) 46 | } 47 | } 48 | 49 | impl ShapeSample for Capsule { 50 | fn sample_boundary(&self, rng: &mut R) -> Vec3 { 51 | let tube_area = 4.0 * PI * self.radius * self.half_length; 52 | let capsule_area = tube_area + 4.0 * PI * self.radius * self.radius; 53 | 54 | if rng.random_bool((tube_area / capsule_area) as f64) { 55 | let circle = sample_circle_interior(rng) * self.radius; 56 | let z = rng.random_range(-self.half_length..=self.half_length); 57 | Vec3::new(circle.x, circle.y, z) 58 | } else { 59 | let sphere = Sphere { radius: self.radius }; 60 | let point = sphere.sample_boundary(rng); 61 | point + (Vec3::Z * self.half_length) * point.z.signum() 62 | } 63 | } 64 | 65 | fn sample_interior(&self, rng: &mut R) -> Vec3 { 66 | let tube_volume = 2.0 * PI * self.radius * self.radius * self.half_length; 67 | let capsule_volume = tube_volume + 4.0 / 3.0 * PI * self.radius * self.radius * self.radius; 68 | 69 | if rng.random_bool((tube_volume / capsule_volume) as f64) { 70 | let cylinder = Cylinder { radius: self.radius, half_height: self.half_length }; 71 | cylinder.sample_interior(rng) 72 | } else { 73 | let sphere = Sphere { radius: self.radius }; 74 | let point = sphere.sample_interior(rng); 75 | point + (Vec3::Z * self.half_length) * point.z.signum() 76 | } 77 | } 78 | } 79 | 80 | impl ShapeSample for Cuboid { 81 | fn sample_boundary(&self, rng: &mut R) -> Vec3 { 82 | let u = rng.random_range(-1.0..1.0); 83 | let v = rng.random_range(-1.0..1.0); 84 | let w = if rng.random() { -1.0 } else { 1.0 }; 85 | 86 | // These are not the actual areas, because they use the half sizes. 87 | // This is fine, since we only need ratios for the probabilities. 88 | let area_xy = self.half_size.x * self.half_size.y; 89 | let area_yz = self.half_size.y * self.half_size.z; 90 | let area_xz = self.half_size.x * self.half_size.z; 91 | 92 | let area = area_xy + area_yz + area_xz; 93 | let p_xy = area_xy / area; 94 | let p_yz = area_yz / area; 95 | 96 | let r = rng.random_range(0.0..1.0); 97 | 98 | if r < p_xy { 99 | Vec3::new(u, v, w).cmul(self.half_size) 100 | } else if r < p_xy + p_yz { 101 | Vec3::new(w, u, v).cmul(self.half_size) 102 | } else { 103 | Vec3::new(u, w, v).cmul(self.half_size) 104 | } 105 | } 106 | 107 | fn sample_interior(&self, rng: &mut R) -> Vec3 { 108 | let x = rng.random_range(-self.half_size.x..self.half_size.x); 109 | let y = rng.random_range(-self.half_size.y..self.half_size.y); 110 | let z = rng.random_range(-self.half_size.z..self.half_size.z); 111 | 112 | Vec3::new(x, y, z) 113 | } 114 | } 115 | 116 | fn sample_circle_boundary(rng: &mut R) -> Vec2 { 117 | let theta = rng.random_range(0.0..2.0 * PI); 118 | let (sin, cos) = theta.sin_cos(); 119 | Vec2::new(cos, sin) 120 | } 121 | 122 | fn sample_circle_interior(rng: &mut R) -> Vec2 { 123 | let theta = rng.random_range(0.0..2.0 * PI); 124 | let r = (rng.random_range(0.0..=1.0) as f32).sqrt(); 125 | let (sin, cos) = theta.sin_cos(); 126 | Vec2::new(r * cos, r * sin) 127 | } 128 | 129 | fn sample_sphere_boundary(rng: &mut R) -> Vec3 { 130 | let z = rng.random_range(-1f32..=1f32); 131 | let (a_sin, a_cos) = rng.random_range(-PI..=PI).sin_cos(); 132 | let c = (1f32 - z * z).sqrt(); 133 | let x = a_sin * c; 134 | let y = a_cos * c; 135 | 136 | Vec3::new(x, y, z) 137 | } 138 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /crates/math/src/quaternion.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Add, Sub, Mul, Div, Neg}; 2 | 3 | use super::{Dual, Unit}; 4 | use super::num::{Float, FloatOps, Number}; 5 | 6 | use super::matrix::{Vector3, Matrix3}; 7 | use super::unwind_radians; 8 | 9 | /// A quaternion. See [`UnitQuaternion`] for a quaternion that may be used to represent a rotation. 10 | #[repr(C)] 11 | #[derive(Clone, Copy, PartialEq)] 12 | pub struct Quaternion { 13 | pub i: T, 14 | pub j: T, 15 | pub k: T, 16 | pub w: T, 17 | } 18 | 19 | impl Quaternion { 20 | pub const ZERO: Self = Self { 21 | i: T::ZERO, j: T::ZERO, k: T::ZERO, w: T::ZERO 22 | }; 23 | 24 | /// A quaternions multiplicative identity. 25 | pub const IDENTITY: Self = Self { 26 | i: T::ZERO, j: T::ZERO, k: T::ZERO, w: T::ONE 27 | }; 28 | 29 | /// Constructs a real quaternion. 30 | pub const fn from_real(real: T) -> Self { 31 | Self { i: T::ZERO, j: T::ZERO, k: T::ZERO, w: real } 32 | } 33 | 34 | /// Constructs a pure quaternion. 35 | pub fn from_imag(imag: Vector3) -> Self { 36 | Self { i: imag.x, j: imag.y, k: imag.z, w: T::ZERO } 37 | } 38 | 39 | /// Constructs a quaternion from its real/scalar and imaginary/vector parts. 40 | pub fn from_parts(real: T, imag: Vector3) -> Self { 41 | Self { i: imag.x, j: imag.y, k: imag.z, w: real } 42 | } 43 | } 44 | 45 | impl Quaternion { 46 | /// The real/scalar part `w` of this quaternion. 47 | pub fn real(&self) -> T { 48 | self.w 49 | } 50 | 51 | /// The imaginary/vector part `(i, j, k)` of this quaternion. 52 | pub fn imag(&self) -> Vector3 { 53 | Vector3::new(self.i, self.j, self.k) 54 | } 55 | } 56 | 57 | impl> Quaternion { 58 | /// Returns the dot product of `self` and `rhs`. 59 | /// This is equal to the cosine of the angle between the two quaternions, multiplied by their lengths. 60 | pub fn dot(&self, rhs: Self) -> T { 61 | self.i * rhs.i + self.j * rhs.j + self.k * rhs.k + self.w * rhs.w 62 | } 63 | 64 | /// Returns the squared length (L2 norm) of this quaternion. 65 | pub fn length_sq(&self) -> T { 66 | self.dot(*self) 67 | } 68 | 69 | /// Returns the length (L2 norm) of this quaternion. 70 | pub fn length(&self) -> T { 71 | self.length_sq().sqrt() 72 | } 73 | 74 | /// Returns the conjugate of this quaternion. 75 | pub fn conj(&self) -> Self { 76 | Self::from_parts(self.w, -self.imag()) 77 | } 78 | 79 | /// Returns the inverse of this quaternion. 80 | pub fn inv(&self) -> Option { 81 | let length_sq = self.length_sq(); 82 | (length_sq > T::ZERO).then(|| self.conj() / length_sq) 83 | } 84 | 85 | /// Normalizes this quaternion. 86 | pub fn normalize(&self) -> Unit { 87 | Unit::new_unchecked(*self / self.length()) 88 | } 89 | 90 | pub fn log(&self) -> Self { 91 | let length = self.imag().length(); 92 | 93 | if length < T::SMALL_EPSILON { 94 | Self::from_imag(self.imag()) 95 | } else { 96 | let half_angle = length.atan2(self.real()); 97 | let axis = self.imag() / length; 98 | 99 | Self::from_imag(axis * half_angle) 100 | } 101 | } 102 | 103 | pub fn exp(&self) -> Self { 104 | let half_angle = self.imag().length(); 105 | 106 | if half_angle < T::SMALL_EPSILON { 107 | Self::from_imag(self.imag()) 108 | } else { 109 | let (sin, cos) = half_angle.sin_cos(); 110 | let axis = self.imag() / half_angle; 111 | 112 | Self::from_parts(cos, axis * sin) 113 | } 114 | } 115 | } 116 | 117 | /// A unit quaternion. May be used to represent a 3D rotation. 118 | pub type UnitQuaternion = Unit>; 119 | 120 | impl UnitQuaternion { 121 | pub const IDENTITY: Self = Self::new_unchecked(Quaternion::IDENTITY); 122 | } 123 | 124 | impl> UnitQuaternion { 125 | pub fn conj(&self) -> Self { 126 | Self::new_unchecked(self.as_ref().conj()) 127 | } 128 | 129 | pub fn inv(&self) -> Self { 130 | self.conj() 131 | } 132 | 133 | /// Returns the shortest equivalent of the rotation. 134 | /// Ensures the quaternion is on the hemisphere closest to the identity quaternion. 135 | pub fn abs(&self) -> Self { 136 | if self.w < T::ZERO { 137 | -*self 138 | } else { 139 | *self 140 | } 141 | } 142 | 143 | /// Creates a new unit quaternion from a vector. 144 | /// The vectors direction represents the rotation axis and its length the rotation angle. 145 | pub fn from_scaled_axis(scaled_axis: Vector3) -> Self { 146 | let q = Quaternion::from_imag(scaled_axis / T::TWO); 147 | Self::new_unchecked(q.exp()) 148 | } 149 | 150 | /// Returns a vector where the direction represents the rotation axis and its length the rotation angle. 151 | pub fn scaled_axis(&self) -> Vector3 { 152 | let log = self.as_ref().log(); 153 | log.imag() * T::TWO 154 | } 155 | 156 | /// Creates a new unit quaternion from a unit vector (rotation axis) and an angle (rotation angle). 157 | pub fn from_axis_angle(axis: Unit>, angle: T) -> Self { 158 | let (sin, cos) = (angle / T::TWO).sin_cos(); 159 | Self::new_unchecked(Quaternion::from_parts(cos, *axis * sin)) 160 | } 161 | 162 | /// Creates a new unit quaternion from Euler angles. 163 | pub fn from_euler(pitch: T, roll: T, yaw: T) -> Self { 164 | let (sx, cx) = (pitch / T::TWO).sin_cos(); 165 | let (sy, cy) = (roll / T::TWO).sin_cos(); 166 | let (sz, cz) = (yaw / T::TWO).sin_cos(); 167 | 168 | Self::new_unchecked(Quaternion { 169 | i: sx * cy * cz - cx * sy * sz, 170 | j: cx * sy * cz + sx * cy * sz, 171 | k: cx * cy * sz - sx * sy * cz, 172 | w: cx * cy * cz + sx * sy * sz, 173 | }) 174 | } 175 | 176 | /// Converts this unit quaternion to a 3x3 rotation matrix. 177 | pub fn to_matrix3(&self) -> Matrix3 { 178 | let x2 = self.i + self.i; 179 | let y2 = self.j + self.j; 180 | let z2 = self.k + self.k; 181 | let x2w = x2 * self.w; 182 | let y2w = y2 * self.w; 183 | let z2w = z2 * self.w; 184 | let x2x = x2 * self.i; 185 | let y2x = y2 * self.i; 186 | let z2x = z2 * self.i; 187 | let y2y = y2 * self.j; 188 | let z2y = z2 * self.j; 189 | let z2z = z2 * self.k; 190 | 191 | Matrix3::from_array([ 192 | T::ONE - (y2y + z2z), y2x - z2w, z2x + y2w, 193 | y2x + z2w, T::ONE - (x2x + z2z), z2y - x2w, 194 | z2x - y2w, z2y + x2w, T::ONE - (x2x + y2y), 195 | ]) 196 | } 197 | 198 | /// The euler angles, returned in the form (roll, pitch, yaw). 199 | pub fn euler(&self) -> (T, T, T) { 200 | ( 201 | T::atan2(T::TWO * (self.j * self.k + self.w * self.i), self.w * self.w - self.i * self.i - self.j * self.j + self.k * self.k), 202 | T::asin(-T::TWO * (self.i * self.k - self.w * self.j)), 203 | T::atan2(T::TWO * (self.i * self.j + self.w * self.k), self.w * self.w + self.i * self.i - self.j * self.j - self.k * self.k), 204 | ) 205 | } 206 | 207 | /// The rotation angle in [0, pi]. 208 | pub fn angle(&self) -> T { 209 | T::TWO * self.w.acos() 210 | } 211 | 212 | /// The rotation axis or `None` if the rotation is zero. 213 | pub fn axis(&self) -> Option>> { 214 | let length_sq = T::ONE - self.w * self.w; 215 | 216 | if length_sq <= T::ZERO { 217 | return None; 218 | } 219 | 220 | Some(Unit::new_unchecked(self.imag() / length_sq.sqrt())) 221 | } 222 | 223 | /// The rotation axis and angle in (0, pi] or `None` if the rotation is zero. 224 | pub fn axis_angle(&self) -> Option<(Unit>, T)> { 225 | self.axis().map(|axis| (axis, self.angle())) 226 | } 227 | 228 | /// The rotation angle around `twist_axis`. 229 | pub fn twist_angle(&self, twist_axis: Unit>) -> T { 230 | unwind_radians(T::TWO * self.imag().dot(*twist_axis).atan2(self.real())) 231 | } 232 | 233 | /// Decomposes this quaternion such that `q = swing * twist` where: 234 | /// * swing = rotation around axis perpendicular to `twist_axis` 235 | /// * twist = rotation around `twist_axis` 236 | pub fn swing_twist(&self, twist_axis: Unit>) -> (Self, Self) { 237 | let projection = self.imag().project_onto(twist_axis); 238 | 239 | let twist = Quaternion::from_parts(self.real(), projection); 240 | 241 | let twist = if twist.length_sq() < T::SMALL_EPSILON { 242 | Self::IDENTITY 243 | } else { 244 | twist.normalize() 245 | }; 246 | 247 | let swing = *self * twist.inv(); 248 | 249 | (swing, twist) 250 | } 251 | 252 | /// Returns the shortest rotation for transforming `from` to `to`. 253 | pub fn between(from: Unit>, to: Unit>) -> Self { 254 | let cross = from.cross(*to); 255 | let dot = from.dot(*to); 256 | 257 | Quaternion::from_parts(T::ONE + dot, cross).normalize() 258 | } 259 | 260 | /// Performs a linear interpolation between `self` and `rhs` along the shortest path. 261 | pub fn nlerp(&self, rhs: Self, t: T) -> Self { 262 | let dot = self.dot(*rhs); 263 | let sign = if dot < T::ZERO { -T::ONE } else { T::ONE }; 264 | (**self + (rhs * sign - **self) * t).normalize() 265 | } 266 | } 267 | 268 | /// A dual quaternion. May be used to represent a 3D isometry. 269 | pub type DualQuaternion = Dual>; 270 | 271 | impl DualQuaternion { 272 | pub const IDENTITY: Self = Self::new(Quaternion::IDENTITY, Quaternion::ZERO); 273 | } 274 | 275 | impl> Neg for Quaternion { 276 | type Output = Quaternion; 277 | 278 | fn neg(self) -> Self::Output { 279 | Self { 280 | i: -self.i, 281 | j: -self.j, 282 | k: -self.k, 283 | w: -self.w, 284 | } 285 | } 286 | } 287 | 288 | impl> Add for Quaternion { 289 | type Output = Quaternion; 290 | 291 | fn add(self, rhs: Self) -> Self::Output { 292 | Self { 293 | i: self.i + rhs.i, 294 | j: self.j + rhs.j, 295 | k: self.k + rhs.k, 296 | w: self.w + rhs.w, 297 | } 298 | } 299 | } 300 | 301 | impl> Sub for Quaternion { 302 | type Output = Quaternion; 303 | 304 | fn sub(self, rhs: Self) -> Self::Output { 305 | Self { 306 | i: self.i - rhs.i, 307 | j: self.j - rhs.j, 308 | k: self.k - rhs.k, 309 | w: self.w - rhs.w, 310 | } 311 | } 312 | } 313 | 314 | impl + Copy> Mul for Quaternion { 315 | type Output = Quaternion; 316 | 317 | fn mul(self, rhs: T) -> Self::Output { 318 | Self { 319 | i: self.i * rhs, 320 | j: self.j * rhs, 321 | k: self.k * rhs, 322 | w: self.w * rhs, 323 | } 324 | } 325 | } 326 | 327 | impl + Copy> Div for Quaternion { 328 | type Output = Quaternion; 329 | 330 | fn div(self, rhs: T) -> Self::Output { 331 | Self { 332 | i: self.i / rhs, 333 | j: self.j / rhs, 334 | k: self.k / rhs, 335 | w: self.w / rhs, 336 | } 337 | } 338 | } 339 | 340 | impl Mul> for Quaternion { 341 | type Output = Quaternion; 342 | 343 | fn mul(self, rhs: Self) -> Self::Output { 344 | Self { 345 | i: self.w * rhs.i + self.i * rhs.w + self.j * rhs.k - self.k * rhs.j, 346 | j: self.w * rhs.j + self.j * rhs.w + self.k * rhs.i - self.i * rhs.k, 347 | k: self.w * rhs.k + self.k * rhs.w + self.i * rhs.j - self.j * rhs.i, 348 | w: self.w * rhs.w - self.i * rhs.i - self.j * rhs.j - self.k * rhs.k, 349 | } 350 | } 351 | } 352 | 353 | impl Mul> for UnitQuaternion { 354 | type Output = UnitQuaternion; 355 | 356 | fn mul(self, rhs: Self) -> Self::Output { 357 | UnitQuaternion::new_unchecked(self * *rhs) 358 | } 359 | } 360 | 361 | impl> Mul> for UnitQuaternion { 362 | type Output = Vector3; 363 | 364 | fn mul(self, rhs: Vector3) -> Self::Output { 365 | let t = self.imag().cross(rhs) * T::TWO; 366 | rhs + t * self.real() + self.imag().cross(t) 367 | } 368 | } 369 | 370 | impl> Mul>> for UnitQuaternion { 371 | type Output = Unit>; 372 | 373 | fn mul(self, rhs: Unit>) -> Self::Output { 374 | Unit::new_unchecked(self * *rhs) 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /crates/math/src/transform.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Mul; 2 | use super::{Mat4, Vector2, Vector3}; 3 | use super::num::Number; 4 | use super::{UnitComplex, UnitQuaternion}; 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 { translation, ..Self::IDENTITY } 25 | } 26 | 27 | pub const fn from_rotation(rotation: UnitQuaternion) -> Self { 28 | Self { rotation, ..Self::IDENTITY } 29 | } 30 | 31 | pub const fn from_scale(scale: Vector3) -> Self { 32 | Self { scale, ..Self::IDENTITY } 33 | } 34 | 35 | pub const fn with_translation(self, translation: Vector3) -> Self { 36 | Self { translation, ..self } 37 | } 38 | 39 | pub const fn with_rotation(self, rotation: UnitQuaternion) -> Self { 40 | Self { rotation, ..self } 41 | } 42 | 43 | pub const fn with_scale(self, scale: Vector3) -> Self { 44 | Self { scale, ..self } 45 | } 46 | } 47 | 48 | impl Transform3 { 49 | pub fn inv(&self) -> Self { 50 | let scale = Vector3::new(1.0 / self.scale.x, 1.0 / self.scale.y, 1.0 / self.scale.z); 51 | let rotation = self.rotation.inv(); 52 | let translation = rotation * -self.translation.cmul(scale); 53 | 54 | Self { translation, rotation, scale } 55 | } 56 | 57 | /// Translates, rotates and scales a point by this transform. 58 | pub fn transform_point(&self, point: Vector3) -> Vector3 { 59 | self.rotation * self.scale.cmul(point) + self.translation 60 | } 61 | 62 | /// Rotates and scales a vector by this transform. 63 | pub fn transform_vector(&self, vector: Vector3) -> Vector3 { 64 | self.rotation * self.scale.cmul(vector) 65 | } 66 | 67 | /// Rotates a direction by this transform. 68 | pub fn transform_direction(&self, direction: Vector3) -> Vector3 { 69 | self.rotation * direction 70 | } 71 | } 72 | 73 | impl Mul for Transform3 { 74 | type Output = Self; 75 | 76 | fn mul(self, rhs: Self) -> Self::Output { 77 | let translation = self.rotation * self.scale.cmul(rhs.translation) + self.translation; 78 | let rotation = self.rotation * rhs.rotation; 79 | let scale = self.scale.cmul(rhs.scale); 80 | 81 | Self { translation, rotation, scale } 82 | } 83 | } 84 | 85 | impl From for Mat4 { 86 | fn from(transform: Transform3) -> Self { 87 | let translation = transform.translation; 88 | let rotation = transform.rotation.to_matrix3(); 89 | let scale = transform.scale; 90 | 91 | Mat4::from_array([ 92 | rotation[0] * scale.x, rotation[1] * scale.y, rotation[2] * scale.z, translation.x, 93 | rotation[3] * scale.x, rotation[4] * scale.y, rotation[5] * scale.z, translation.y, 94 | rotation[6] * scale.x, rotation[7] * scale.y, rotation[8] * scale.z, translation.z, 95 | 0.0, 0.0, 0.0, 1.0, 96 | ]) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/os/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod win32; 2 | 3 | #[cfg(target_os = "windows")] 4 | pub use win32 as platform; 5 | 6 | #[derive(Clone, Copy)] 7 | pub struct Rect { 8 | pub x: T, 9 | pub y: T, 10 | pub width: T, 11 | pub height: T, 12 | } 13 | 14 | #[derive(Clone, Copy)] 15 | pub struct Point { 16 | pub x: T, 17 | pub y: T, 18 | } 19 | 20 | impl From> for [T; 2] { 21 | fn from(p: Point) -> Self { 22 | [p.x, p.y] 23 | } 24 | } 25 | 26 | pub type Size = Point; 27 | 28 | #[derive(Clone, Copy)] 29 | pub enum MouseButton { 30 | Left, 31 | Middle, 32 | Right, 33 | } 34 | 35 | #[derive(Clone, Copy)] 36 | pub enum Key { 37 | A, B, C, D, E, F, G, H, I, J, K, L, M, 38 | N, O, P, Q, R, S, T, U, V, W, X, Y, Z, 39 | 40 | ArrowLeft, 41 | ArrowRight, 42 | ArrowUp, 43 | ArrowDown, 44 | 45 | Escape, 46 | Tab, 47 | Backspace, 48 | Enter, 49 | Space, 50 | 51 | Insert, 52 | Delete, 53 | Home, 54 | End, 55 | PageUp, 56 | PageDown, 57 | 58 | Minus, 59 | Plus, 60 | 61 | Num0, Num1, Num2, Num3, Num4, 62 | Num5, Num6, Num7, Num8, Num9, 63 | 64 | F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, 65 | F11, F12, F13, F14, F15, F16, F17, F18, F19, F20, 66 | } 67 | 68 | #[derive(Clone, Copy)] 69 | pub enum Event { 70 | Key { key: Key, pressed: bool }, 71 | Text { character: char }, 72 | MouseButton { button: MouseButton, pressed: bool }, 73 | MouseWheel { delta: [f32; 2] }, 74 | } 75 | 76 | #[derive(Clone, Copy, Eq, PartialEq, Debug)] 77 | pub enum Cursor { 78 | None, 79 | Arrow, 80 | Crosshair, 81 | Hand, 82 | Help, 83 | Text, 84 | Wait, 85 | ResizeAll, 86 | ResizeEw, 87 | ResizeNs, 88 | ResizeNeSw, 89 | ResizeNwSe, 90 | NotAllowed, 91 | } 92 | 93 | #[derive(Clone)] 94 | pub struct MonitorInfo { 95 | pub name: String, 96 | pub rect: Rect, 97 | pub scale_factor: f32, 98 | pub primary: bool, 99 | } 100 | 101 | #[derive(Clone)] 102 | pub struct WindowDesc { 103 | pub title: String, 104 | pub rect: Rect, 105 | } 106 | 107 | pub struct NativeHandle(pub u64); 108 | 109 | pub trait App { 110 | type Window: Window; 111 | 112 | /// Creates a new app instance. 113 | fn new() -> Self; 114 | 115 | /// Creates a new window. 116 | fn create_window(&mut self, desc: &WindowDesc) -> Self::Window; 117 | 118 | /// Should be called every frame to update app and windows state. 119 | /// Returns false when the app is requested to close. 120 | fn run(&mut self) -> bool; 121 | 122 | /// Returns the events that have occured since the last call to [`App::run`]. 123 | fn events(&self) -> Vec; 124 | 125 | /// Returns the mouse position relative to the top-left corner of the desktop. 126 | fn mouse_pos(&self) -> Point; 127 | 128 | /// Returns info about all monitors connected to the system. 129 | fn enumerate_monitors() -> Vec; 130 | 131 | /// Sets the mouse cursor icon. 132 | fn set_cursor(&mut self, cursor: &Cursor); 133 | } 134 | 135 | pub trait Window { 136 | fn title(&self) -> String; 137 | fn set_title(&self, title: &str); 138 | 139 | /// Returns the position of the top-left corner of the window relative to the top-left corner of the desktop. 140 | fn position(&self) -> Point; 141 | 142 | /// Sets the position of the top-left corner of the window relative to the top-left corner of the desktop. 143 | fn set_position(&self, pos: Point); 144 | 145 | /// Returns the size of the window's client area. 146 | fn size(&self) -> Size; 147 | 148 | /// Sets the size of the window's client area. 149 | fn set_size(&self, size: Size); 150 | 151 | fn is_minimized(&self) -> bool; 152 | fn minimize(&self); 153 | 154 | fn is_maximized(&self) -> bool; 155 | fn maximize(&self); 156 | 157 | /// Returns true if the window has keyboard focus. 158 | fn is_focused(&self) -> bool; 159 | 160 | /// Brings the window to the front and sets keyboard focus. 161 | fn focus(&self); 162 | 163 | /// Returns the mouse position relative to the top-left corner of the window's client area. 164 | fn mouse_pos_client(&self, mouse_pos: Point) -> Point; 165 | 166 | /// Returns the dpi scale factor for the monitor the window is currently on. 167 | fn scale_factor(&self) -> f32; 168 | 169 | /// Returns the platform native handle for the window. 170 | fn native_handle(&self) -> NativeHandle; 171 | } 172 | -------------------------------------------------------------------------------- /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 = { git = "https://github.com/FloatyMonkey/openusd-rs.git" } 14 | -------------------------------------------------------------------------------- /crates/usd/src/lib.rs: -------------------------------------------------------------------------------- 1 | use asset::AssetServer; 2 | use ecs::{Name, World}; 3 | use geometry::mesh::{Mesh, Vertex, VertexGroups}; 4 | use graphics::scene::{DomeLight, Image, RectLight, Renderable, SphereLight}; 5 | use math::{transform::Transform3, Quaternion, Unit, UnitQuaternion, Vec3}; 6 | 7 | use openusd::{gf, sdf, usd, usd_geom, usd_lux}; 8 | 9 | fn convert_mesh(mesh: &usd_geom::Mesh) -> Mesh { 10 | let points = mesh.points_attr().get::>(); 11 | let normals = mesh.normals_attr().get::>(); 12 | 13 | let vertices = points.iter().zip(normals.iter()).map(|(p, n)| { 14 | Vertex { 15 | p: Vec3::new(p.x, p.y, p.z), 16 | n: Vec3::new(n.x, n.y, n.z), 17 | } 18 | }).collect(); 19 | 20 | let triangles = usd_geom::triangulate(&mesh); 21 | 22 | let indices = triangles.iter().map(|i| { 23 | *i as usize 24 | }).collect(); 25 | 26 | let mut mesh = Mesh { 27 | vertices, 28 | indices, 29 | vertex_groups: VertexGroups::default(), 30 | }; 31 | 32 | // TODO: Use normals from USD mesh. 33 | geometry::mesh::calculate_vert_normals(&mut mesh); 34 | 35 | mesh 36 | } 37 | 38 | fn traverse_recurse( 39 | stage_path: &str, 40 | stage: &usd::Stage, 41 | world: &mut World, 42 | transform_stack: &mut Vec, 43 | assets: &mut AssetServer, 44 | prim: &usd::Prim, 45 | depth: usize 46 | ) { 47 | let xform = usd_geom::XformOp::get_local_transform(prim); 48 | 49 | if let Some(xform) = xform { 50 | transform_stack.push(from_usd_transform3d(xform)); 51 | } 52 | 53 | let get_transform = |stack: &mut Vec| 54 | stack.iter().fold(Transform3::::IDENTITY, |acc, xform| acc * *xform); 55 | 56 | match prim.type_name().as_str() { 57 | "Mesh" => { 58 | let mesh = usd_geom::Mesh::define(stage, prim.path().clone()); 59 | let mesh = convert_mesh(&mesh); 60 | let mesh = assets.insert(mesh); 61 | 62 | world.spawn(( 63 | Name::new(prim.path()), 64 | get_transform(transform_stack), 65 | Renderable { mesh }, 66 | )); 67 | }, 68 | "SphereLight" => { 69 | let light = usd_lux::SphereLight::define(stage, prim.path().clone()); 70 | 71 | let color = from_usd_vec3f(light.color_attr().get::()); 72 | let intensity = light.intensity_attr().get::(); 73 | 74 | world.spawn(( 75 | Name::new(prim.path()), 76 | get_transform(transform_stack), 77 | SphereLight { 78 | emission: (color * intensity).into(), 79 | radius: light.radius_attr().get::(), 80 | }, 81 | )); 82 | }, 83 | "RectLight" => { 84 | let light = usd_lux::RectLight::define(stage, prim.path().clone()); 85 | 86 | let color = from_usd_vec3f(light.color_attr().get::()); 87 | let intensity = light.intensity_attr().get::(); 88 | 89 | world.spawn(( 90 | Name::new(prim.path()), 91 | get_transform(transform_stack), 92 | RectLight { 93 | emission: (color * intensity).into(), 94 | width: light.width_attr().get::(), 95 | height: light.height_attr().get::(), 96 | }, 97 | )); 98 | }, 99 | "DomeLight" => { 100 | let light = usd_lux::DomeLight::define(stage, prim.path().clone()); 101 | 102 | // TODO: Handle at least intensity (and maybe color?) 103 | //let color = from_usd_vec3f(light.color_attr().get::()); 104 | //let intensity = light.intensity_attr().get::(); 105 | 106 | let texture_file_ref = light.texture_file_attr().get::(); 107 | 108 | let root_path = std::path::Path::new(stage_path); 109 | let parent_path = root_path.parent().unwrap_or(root_path); 110 | let texuture_path = parent_path.join(texture_file_ref.asset_path.clone()); 111 | 112 | let texture = Image::from_file(texuture_path); 113 | let texture_asset = assets.insert(texture); 114 | 115 | world.spawn(( 116 | Name::new(prim.path()), 117 | get_transform(transform_stack), 118 | DomeLight { 119 | image: texture_asset, 120 | }, 121 | )); 122 | } 123 | _ => {}, 124 | } 125 | 126 | for child in prim.children() { 127 | traverse_recurse(stage_path, stage, world, transform_stack, assets, &child, depth + 1); 128 | } 129 | 130 | if xform.is_some() { 131 | transform_stack.pop(); 132 | } 133 | } 134 | 135 | pub fn populate_world_from_usd(filepath: &str, world: &mut World, assets: &mut AssetServer) { 136 | let stage = usd::Stage::open(filepath); 137 | 138 | let pseudo_root = stage.pseudo_root(); 139 | 140 | let mut transform_stack: Vec = Vec::new(); 141 | 142 | traverse_recurse(&filepath, &stage, world, &mut transform_stack, assets, &pseudo_root, 0); 143 | } 144 | 145 | fn from_usd_vec3f(v: gf::Vec3f) -> Vec3 { 146 | Vec3::new(v.x, v.y, v.z) 147 | } 148 | 149 | fn from_usd_vec3d(v: gf::Vec3d) -> Vec3 { 150 | Vec3::new(v.x as f32, v.y as f32, v.z as f32) 151 | } 152 | 153 | fn from_usd_quatd(q: gf::Quatd) -> UnitQuaternion { 154 | Unit::new_unchecked(Quaternion { i: q.i as f32, j: q.j as f32, k: q.k as f32, w: q.w as f32 }) 155 | } 156 | 157 | fn from_usd_transform3d(t: gf::Transform3d) -> Transform3 { 158 | Transform3 { 159 | translation: from_usd_vec3d(t.translation), 160 | rotation: from_usd_quatd(t.rotation), 161 | scale: from_usd_vec3d(t.scale), 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /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)) { movement -= *Vec3::Z; } // TODO: Prevent dereferencing these 38 | if ui.input(|i| i.key_down(egui::Key::S)) { movement += *Vec3::Z; } 39 | if ui.input(|i| i.key_down(egui::Key::Q)) { movement -= *Vec3::X; } 40 | if ui.input(|i| i.key_down(egui::Key::D)) { movement += *Vec3::X; } 41 | 42 | if movement != Vec3::ZERO { 43 | transform.translation += transform.rotation * *movement.normalize() * dt * MOVE_SPEED; 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /editor/editor.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use ecs::{Entity, World}; 4 | use crate::time::Time; 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 {}).map(|()| log::set_max_level(log::LevelFilter::Trace)).unwrap(); 25 | 26 | let egui_ctx = egui::Context::default(); 27 | 28 | egui_extras::install_image_loaders(&egui_ctx); 29 | 30 | let default_font = egui::FontData::from_static(include_bytes!("../resources/Inter-Regular.ttf")); 31 | let icon_font = egui::FontData::from_static(include_bytes!("../resources/icon.ttf")); 32 | 33 | let mut fonts = egui::FontDefinitions::empty(); 34 | 35 | fonts.font_data.insert("Inter-Regular".to_owned(), default_font); 36 | fonts.font_data.insert("icons".to_owned(), icon_font); 37 | 38 | if let Some(family) = fonts.families.get_mut(&egui::FontFamily::Proportional) { 39 | family.push("Inter-Regular".to_owned()); 40 | family.push("icons".to_owned()); 41 | } 42 | 43 | if let Some(family) = fonts.families.get_mut(&egui::FontFamily::Monospace) { 44 | family.push("Inter-Regular".to_owned()); // TODO: this is not monospace 45 | family.push("icons".to_owned()); 46 | } 47 | 48 | egui_ctx.set_fonts(fonts); 49 | 50 | let mut world = World::new(); 51 | world.add_singleton(Time::new()); 52 | 53 | Self { 54 | egui_ctx, 55 | context: MyContext { 56 | world, 57 | selection: HashSet::new(), 58 | viewport_texture_srv: 0, 59 | }, 60 | tree: Self::setup_tree(), 61 | } 62 | } 63 | 64 | pub fn run(&mut self, raw_input: egui::RawInput) -> egui::FullOutput { 65 | if let Some(time) = self.context.world.get_singleton_mut::