├── .github └── workflows │ └── check.yaml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── baryon-core ├── Cargo.toml └── src │ ├── color.rs │ ├── lib.rs │ ├── load.rs │ ├── mesh.rs │ └── space.rs ├── etc ├── cubeception.png └── pickachu.png ├── examples ├── assets │ ├── Duck │ │ ├── Duck.gltf │ │ ├── Duck0.bin │ │ └── DuckCM.png │ ├── car.mtl │ ├── car.obj │ └── pickachu.png ├── cubes.rs ├── empty.rs ├── load-gltf.rs ├── load-obj.rs ├── phong.rs ├── scene.rs ├── shapes.rs └── sprite.rs ├── src ├── asset │ ├── gltf.rs │ ├── mod.rs │ └── obj.rs ├── geometry │ ├── cuboid.rs │ ├── mod.rs │ ├── plane.rs │ ├── shape.rs │ └── sphere.rs ├── lib.rs ├── pass │ ├── flat.rs │ ├── flat.wgsl │ ├── mod.rs │ ├── phong.rs │ ├── phong.wgsl │ ├── real.rs │ ├── real.wgsl │ ├── solid.rs │ └── solid.wgsl └── window.rs └── tests └── parse-shaders.rs /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | name: check 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: actions-rs/cargo@v1 12 | 13 | - name: Select Rust channel 14 | uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: stable 17 | 18 | - name: Check Basics 19 | run: cargo check --workspace --no-default-features 20 | 21 | - name: Test All 22 | run: cargo test --workspace --all-features 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## baryon-0.4 (TBD) 4 | - gltf lights 5 | - plane geometry 6 | - `lyon` integration for 2D shapes 7 | 8 | ## baryon-0.3 (2021-09-18) 9 | - based on `baryon-core-0.1` 10 | - solid-color pass 11 | - cube-ception example 12 | 13 | ## baryon-core-0.1 (2021-09-18) 14 | - spatial hierarchy 15 | - meshes 16 | - pass/material composability 17 | 18 | ## v0.2 (2021-09-08) 19 | - initial release, nothing in here 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "baryon" 3 | version = "0.3.0" 4 | edition = "2018" 5 | resolver = "2" 6 | license = "MIT" 7 | description = "Fast prototyping 3D engine" 8 | repository = "https://github.com/kvark/baryon" 9 | keywords = ["3d", "graphics", "wgpu"] 10 | categories = ["game-development", "graphics", "rendering::engine"] 11 | exclude = [ 12 | "examples/*", 13 | "tests/*", 14 | ] 15 | 16 | [workspace] 17 | members = ["baryon-core"] 18 | 19 | [features] 20 | default = ["window"] 21 | window = ["raw-window-handle", "winit"] 22 | shape = ["lyon"] 23 | # obj, gltf 24 | # pass = glam, fxhash, mint, wgpu 25 | # factory = 26 | 27 | [[example]] 28 | name = "load-obj" 29 | required-features = ["obj"] 30 | 31 | [[example]] 32 | name = "load-gltf" 33 | required-features = ["gltf"] 34 | 35 | [[example]] 36 | name = "shapes" 37 | required-features = ["shape"] 38 | 39 | [[example]] 40 | name = "scene" 41 | 42 | [dependencies.bc] 43 | package = "baryon-core" 44 | path = "baryon-core" 45 | version = "0.1" 46 | 47 | [dependencies] 48 | # public 49 | mint = "0.5" 50 | # private 51 | bitflags = "1.0" 52 | bytemuck = { version = "1.4", features = ["derive"] } 53 | glam = "0.20" 54 | gltf = { version = "1", features = ["names", "utils", "KHR_lights_punctual"], optional = true } 55 | fxhash = "0.2" 56 | log = "0.4" 57 | obj = { version = "0.10", optional = true } 58 | raw-window-handle = { version = "0.4", optional = true } 59 | winit = { version = "0.26", optional = true } 60 | wgpu = "0.12" 61 | lyon = { version = "0.17", optional = true } 62 | 63 | [dev-dependencies] 64 | naga = { version = "0.8", features = ["wgsl-in"] } 65 | pollster = "0.2" 66 | 67 | [package.metadata.docs.rs] 68 | all-features = true 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Dzmitry Malyshau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # baryon 2 | [![Build Status](https://github.com/kvark/baryon/workflows/check/badge.svg)](https://github.com/kvark/baryon/actions) 3 | [![Docs](https://docs.rs/baryon/badge.svg)](https://docs.rs/baryon) 4 | [![Crates.io](https://img.shields.io/crates/v/baryon.svg?maxAge=2592000)](https://crates.io/crates/baryon) 5 | 6 | Baryon is a compact 3D engine focused on fast prototyping in code. 7 | No big dependencies, no fancy run-times, GUI editors, or other magic. 8 | It's heavily ECS-oriented, and could be used as a foundation for high-level engines. 9 | 10 | See [the slides](https://hackmd.io/@kvark/baryon/) from the Rust-GameDev [talk on Feb 2022](https://youtu.be/adt63Gqt6yA?t=1907). 11 | 12 | Dependency highlights: 13 | - [wgpu](https://github.com/gfx-rs/wgpu) for GPU access 14 | - [winit](https://github.com/rust-windowing/winit) for windowing 15 | - [hecs](https://github.com/Ralith/hecs) for material ECS 16 | 17 | For a similar but more complete experience, with bigger focus on ECS, consider [Dotrix](https://github.com/lowenware/dotrix). 18 | For more features and bigger community, consider [Bevy](https://github.com/bevyengine/bevy). 19 | For better performance and experimental high-tech, consider [Rend3](https://github.com/BVE-Reborn/rend3/). 20 | 21 | ![cube-ception](etc/cubeception.png) 22 | ![pickachu](etc/pickachu.png) 23 | -------------------------------------------------------------------------------- /baryon-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "baryon-core" 3 | version = "0.1.0" 4 | edition = "2018" 5 | resolver = "2" 6 | license = "MIT" 7 | description = "Core API of Baryon 3D-engine" 8 | repository = "https://github.com/kvark/baryon" 9 | 10 | [features] 11 | default = [] 12 | 13 | [dependencies] 14 | # public 15 | bytemuck = "1.4" 16 | hecs = "=0.7.1" 17 | mint = "0.5" 18 | raw-window-handle = "0.4" 19 | wgpu = "0.12" 20 | # private 21 | ddsfile = "0.5" 22 | glam = { version = "0.20", features = ["mint"] } 23 | image = { version = "0.23", default-features = false, features = ["jpeg", "png", "bmp", "hdr", "dds"] } 24 | log = "0.4" 25 | -------------------------------------------------------------------------------- /baryon-core/src/color.rs: -------------------------------------------------------------------------------- 1 | /// Can be specified as 0xAARRGGBB 2 | #[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd)] 3 | pub struct Color(pub u32); 4 | 5 | const GAMMA: f32 = 2.2; 6 | 7 | impl Color { 8 | pub const BLACK_TRANSPARENT: Self = Self(0x0); 9 | pub const BLACK_OPAQUE: Self = Self(0xFF000000); 10 | pub const RED: Self = Self(0xFFFF0000); 11 | pub const GREEN: Self = Self(0xFF00FF00); 12 | pub const BLUE: Self = Self(0xFF0000FF); 13 | 14 | fn import(value: f32) -> u32 { 15 | (value.clamp(0.0, 1.0) * 255.0) as u32 16 | } 17 | 18 | pub fn new(red: f32, green: f32, blue: f32, alpha: f32) -> Self { 19 | Self( 20 | (Self::import(alpha) << 24) 21 | | (Self::import(red) << 16) 22 | | (Self::import(green) << 8) 23 | | Self::import(blue), 24 | ) 25 | } 26 | 27 | pub fn from_rgba(d: [f32; 4]) -> Self { 28 | Self::new(d[0], d[1], d[2], d[3]) 29 | } 30 | pub fn from_rgb_alpha(d: [f32; 3], alpha: f32) -> Self { 31 | Self::new(d[0], d[1], d[2], alpha) 32 | } 33 | 34 | fn export(self, index: u32) -> f32 { 35 | ((self.0 >> (index << 3)) & 0xFF) as f32 / 255.0 36 | } 37 | pub fn red(self) -> f32 { 38 | self.export(2) 39 | } 40 | pub fn green(self) -> f32 { 41 | self.export(1) 42 | } 43 | pub fn blue(self) -> f32 { 44 | self.export(0) 45 | } 46 | pub fn alpha(self) -> f32 { 47 | self.export(3) 48 | } 49 | pub fn into_vec4(self) -> [f32; 4] { 50 | [self.red(), self.green(), self.blue(), self.alpha()] 51 | } 52 | pub fn into_vec4_gamma(self) -> [f32; 4] { 53 | [ 54 | self.red().powf(GAMMA), 55 | self.green().powf(GAMMA), 56 | self.blue().powf(GAMMA), 57 | self.alpha().powf(GAMMA), 58 | ] 59 | } 60 | } 61 | 62 | impl From for wgpu::Color { 63 | fn from(c: Color) -> Self { 64 | Self { 65 | r: c.red() as f64, 66 | g: c.green() as f64, 67 | b: c.blue() as f64, 68 | a: c.alpha() as f64, 69 | } 70 | } 71 | } 72 | 73 | impl Default for Color { 74 | fn default() -> Self { 75 | Color::BLACK_OPAQUE 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /baryon-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow( 2 | // We use loops for getting early-out of scope without closures. 3 | clippy::never_loop, 4 | // We don't use syntax sugar where it's not necessary. 5 | clippy::match_like_matches_macro, 6 | // Redundant matching is more explicit. 7 | clippy::redundant_pattern_matching, 8 | // Explicit lifetimes are often easier to reason about. 9 | clippy::needless_lifetimes, 10 | // No need for defaults in the internal types. 11 | clippy::new_without_default, 12 | // For some reason `rustc` can warn about these in const generics even 13 | // though they are required. 14 | unused_braces, 15 | )] 16 | #![warn( 17 | trivial_casts, 18 | trivial_numeric_casts, 19 | unused_extern_crates, 20 | unused_qualifications, 21 | // We don't match on a reference, unless required. 22 | clippy::pattern_type_mismatch, 23 | )] 24 | 25 | mod color; 26 | mod load; 27 | mod mesh; 28 | mod space; 29 | 30 | use raw_window_handle::HasRawWindowHandle; 31 | use std::{mem, ops}; 32 | 33 | pub use color::Color; 34 | pub use mesh::{IndexStream, Mesh, MeshBuilder, Prototype, Vertex, VertexStream}; 35 | pub use space::{Camera, Projection, RawSpace}; 36 | 37 | pub trait HasWindow: HasRawWindowHandle { 38 | fn size(&self) -> mint::Vector2; 39 | } 40 | 41 | struct SurfaceContext { 42 | raw: wgpu::Surface, 43 | config: wgpu::SurfaceConfiguration, 44 | } 45 | 46 | pub struct Target { 47 | pub view: wgpu::TextureView, 48 | pub format: wgpu::TextureFormat, 49 | pub size: wgpu::Extent3d, 50 | } 51 | 52 | impl Target { 53 | pub fn aspect(&self) -> f32 { 54 | self.size.width as f32 / self.size.height as f32 55 | } 56 | } 57 | 58 | /// Parameters of a texture target that affect its pipeline compatibility. 59 | #[derive(Clone, Copy, Debug, PartialEq)] 60 | pub struct TargetInfo { 61 | pub format: wgpu::TextureFormat, 62 | pub sample_count: u32, 63 | pub aspect_ratio: f32, 64 | } 65 | 66 | #[derive(Clone, Copy, Debug, PartialEq)] 67 | pub struct TargetRef(u8); 68 | 69 | pub struct Image { 70 | pub view: wgpu::TextureView, 71 | pub size: wgpu::Extent3d, 72 | } 73 | 74 | pub struct ImageInfo { 75 | pub size: mint::Vector2, 76 | } 77 | 78 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 79 | pub struct ImageRef(u32); 80 | 81 | pub struct Context { 82 | #[allow(unused)] 83 | instance: wgpu::Instance, 84 | surface: Option, 85 | device: wgpu::Device, 86 | queue: wgpu::Queue, 87 | targets: Vec, 88 | images: Vec, 89 | meshes: Vec, 90 | } 91 | 92 | #[derive(Default, Debug)] 93 | pub struct ContextBuilder { 94 | power_preference: wgpu::PowerPreference, 95 | software: bool, 96 | } 97 | 98 | impl ContextBuilder { 99 | pub fn power_hungry(self, hungry: bool) -> Self { 100 | Self { 101 | power_preference: if hungry { 102 | wgpu::PowerPreference::HighPerformance 103 | } else { 104 | wgpu::PowerPreference::LowPower 105 | }, 106 | ..self 107 | } 108 | } 109 | 110 | pub fn software(self, software: bool) -> Self { 111 | Self { software, ..self } 112 | } 113 | 114 | pub async fn build_offscreen(self) -> Context { 115 | let instance = wgpu::Instance::new(wgpu::Backends::PRIMARY); 116 | let adapter = instance 117 | .request_adapter(&wgpu::RequestAdapterOptions { 118 | power_preference: self.power_preference, 119 | force_fallback_adapter: self.software, 120 | compatible_surface: None, 121 | }) 122 | .await 123 | .unwrap(); 124 | 125 | let (device, queue) = adapter 126 | .request_device(&wgpu::DeviceDescriptor::default(), None) 127 | .await 128 | .unwrap(); 129 | 130 | Context { 131 | instance, 132 | surface: None, 133 | device, 134 | queue, 135 | targets: Vec::new(), 136 | images: Vec::new(), 137 | meshes: Vec::new(), 138 | } 139 | } 140 | 141 | pub async fn build(self, window: &W) -> Context { 142 | let instance = wgpu::Instance::new(wgpu::Backends::PRIMARY); 143 | 144 | let size = window.size(); 145 | let mut surface = SurfaceContext { 146 | raw: unsafe { instance.create_surface(window) }, 147 | config: wgpu::SurfaceConfiguration { 148 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT, 149 | // using an erroneous format, it is changed before used 150 | format: wgpu::TextureFormat::Depth24Plus, 151 | width: size.x, 152 | height: size.y, 153 | present_mode: wgpu::PresentMode::Mailbox, 154 | }, 155 | }; 156 | 157 | let adapter = instance 158 | .request_adapter(&wgpu::RequestAdapterOptions { 159 | power_preference: self.power_preference, 160 | force_fallback_adapter: self.software, 161 | compatible_surface: Some(&surface.raw), 162 | }) 163 | .await 164 | .unwrap(); 165 | 166 | let (device, queue) = adapter 167 | .request_device(&wgpu::DeviceDescriptor::default(), None) 168 | .await 169 | .unwrap(); 170 | 171 | let format = surface.raw.get_preferred_format(&adapter).unwrap(); 172 | surface.config.format = format; 173 | surface.raw.configure(&device, &surface.config); 174 | 175 | Context { 176 | instance, 177 | surface: Some(surface), 178 | device, 179 | queue, 180 | targets: Vec::new(), 181 | images: Vec::new(), 182 | meshes: Vec::new(), 183 | } 184 | } 185 | } 186 | 187 | impl Context { 188 | pub fn init() -> ContextBuilder { 189 | ContextBuilder::default() 190 | } 191 | 192 | pub fn resize(&mut self, width: u32, height: u32) { 193 | let surface = match self.surface { 194 | Some(ref mut suf) => suf, 195 | None => return, 196 | }; 197 | if (surface.config.width, surface.config.height) == (width, height) { 198 | return; 199 | } 200 | surface.config.width = width; 201 | surface.config.height = height; 202 | surface.raw.configure(&self.device, &surface.config); 203 | } 204 | 205 | pub fn present(&mut self, pass: &mut P, scene: &Scene, camera: &Camera) { 206 | let surface = self.surface.as_mut().expect("No screen is configured!"); 207 | let frame = surface.raw.get_current_texture().unwrap(); 208 | let view = frame 209 | .texture 210 | .create_view(&wgpu::TextureViewDescriptor::default()); 211 | 212 | let tr = TargetRef(self.targets.len() as _); 213 | self.targets.push(Target { 214 | view, 215 | format: surface.config.format, 216 | size: wgpu::Extent3d { 217 | width: surface.config.width, 218 | height: surface.config.height, 219 | depth_or_array_layers: 1, 220 | }, 221 | }); 222 | 223 | pass.draw(&[tr], scene, camera, self); 224 | 225 | self.targets.pop(); 226 | frame.present(); 227 | } 228 | 229 | pub fn add_mesh(&mut self) -> MeshBuilder { 230 | MeshBuilder::new(self) 231 | } 232 | 233 | pub fn surface_info(&self) -> Option { 234 | self.surface.as_ref().map(|s| TargetInfo { 235 | format: s.config.format, 236 | sample_count: 1, 237 | aspect_ratio: s.config.width as f32 / s.config.height as f32, 238 | }) 239 | } 240 | 241 | pub fn get_image_info(&self, image_ref: ImageRef) -> ImageInfo { 242 | let image = &self.images[image_ref.0 as usize]; 243 | ImageInfo { 244 | size: [image.size.width as i16, image.size.height as i16].into(), 245 | } 246 | } 247 | } 248 | 249 | impl Drop for Context { 250 | fn drop(&mut self) { 251 | // Do we need explicit cleanup? 252 | } 253 | } 254 | 255 | /// Trait that exposes `Context` details that depend on `wgpu` 256 | pub trait ContextDetail { 257 | fn get_target(&self, tr: TargetRef) -> &Target; 258 | fn get_mesh(&self, mr: MeshRef) -> &Mesh; 259 | fn get_image(&self, ir: ImageRef) -> &Image; 260 | fn device(&self) -> &wgpu::Device; 261 | fn queue(&self) -> &wgpu::Queue; 262 | } 263 | 264 | impl ContextDetail for Context { 265 | fn get_target(&self, tr: TargetRef) -> &Target { 266 | &self.targets[tr.0 as usize] 267 | } 268 | fn get_mesh(&self, mr: MeshRef) -> &Mesh { 269 | &self.meshes[mr.0 as usize] 270 | } 271 | fn get_image(&self, ir: ImageRef) -> &Image { 272 | &self.images[ir.0 as usize] 273 | } 274 | fn device(&self) -> &wgpu::Device { 275 | &self.device 276 | } 277 | fn queue(&self) -> &wgpu::Queue { 278 | &self.queue 279 | } 280 | } 281 | 282 | pub trait Pass { 283 | fn draw(&mut self, targets: &[TargetRef], scene: &Scene, camera: &Camera, context: &Context); 284 | } 285 | 286 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 287 | pub struct NodeRef(u32); 288 | 289 | #[derive(Default, Debug, PartialEq)] 290 | pub struct Node { 291 | parent: NodeRef, 292 | local: space::Space, 293 | } 294 | 295 | pub type EntityRef = hecs::Entity; 296 | 297 | pub struct Array(Vec); 298 | 299 | pub struct Scene { 300 | pub world: hecs::World, 301 | pub nodes: Array, 302 | pub lights: Array, 303 | } 304 | 305 | impl ops::Index for Array { 306 | type Output = Node; 307 | fn index(&self, node: NodeRef) -> &Node { 308 | &self.0[node.0 as usize] 309 | } 310 | } 311 | impl ops::IndexMut for Array { 312 | fn index_mut(&mut self, node: NodeRef) -> &mut Node { 313 | &mut self.0[node.0 as usize] 314 | } 315 | } 316 | impl ops::Index for Scene { 317 | type Output = Node; 318 | fn index(&self, node: NodeRef) -> &Node { 319 | &self.nodes.0[node.0 as usize] 320 | } 321 | } 322 | impl ops::IndexMut for Scene { 323 | fn index_mut(&mut self, node: NodeRef) -> &mut Node { 324 | &mut self.nodes.0[node.0 as usize] 325 | } 326 | } 327 | impl ops::Index for Array { 328 | type Output = Light; 329 | fn index(&self, light: LightRef) -> &Light { 330 | &self.0[light.0 as usize] 331 | } 332 | } 333 | impl ops::IndexMut for Array { 334 | fn index_mut(&mut self, light: LightRef) -> &mut Light { 335 | &mut self.0[light.0 as usize] 336 | } 337 | } 338 | 339 | pub struct BakedScene { 340 | spaces: Box<[RawSpace]>, 341 | } 342 | 343 | impl ops::Index for BakedScene { 344 | type Output = RawSpace; 345 | fn index(&self, node: NodeRef) -> &RawSpace { 346 | &self.spaces[node.0 as usize] 347 | } 348 | } 349 | 350 | pub struct EntityBuilder { 351 | raw: hecs::EntityBuilder, 352 | mesh: MeshRef, 353 | } 354 | 355 | pub struct LightBuilder { 356 | color: Color, 357 | intensity: f32, 358 | kind: LightKind, 359 | } 360 | 361 | pub struct SpriteBuilder { 362 | raw: hecs::EntityBuilder, 363 | image: ImageRef, 364 | uv: Option, 365 | } 366 | 367 | impl Scene { 368 | pub fn new() -> Self { 369 | Self { 370 | world: Default::default(), 371 | nodes: Array(vec![Node::default()]), 372 | lights: Array(Vec::new()), 373 | } 374 | } 375 | 376 | fn add_node_impl(&mut self, node: &mut Node) -> NodeRef { 377 | let index = self.nodes.0.len(); 378 | self.nodes.0.push(mem::take(node)); 379 | NodeRef(index as u32) 380 | } 381 | 382 | pub fn add_node(&mut self) -> ObjectBuilder<()> { 383 | ObjectBuilder { 384 | scene: self, 385 | node: Node::default(), 386 | kind: (), 387 | } 388 | } 389 | 390 | pub fn add_entity(&mut self, prototype: &Prototype) -> ObjectBuilder { 391 | let mut raw = hecs::EntityBuilder::new(); 392 | raw.add_bundle(prototype); 393 | ObjectBuilder { 394 | scene: self, 395 | node: Node::default(), 396 | kind: EntityBuilder { 397 | raw, 398 | mesh: prototype.reference, 399 | }, 400 | } 401 | } 402 | 403 | pub fn add_sprite(&mut self, image: ImageRef) -> ObjectBuilder { 404 | let raw = hecs::EntityBuilder::new(); 405 | ObjectBuilder { 406 | scene: self, 407 | node: Node::default(), 408 | kind: SpriteBuilder { 409 | raw, 410 | image, 411 | uv: None, 412 | }, 413 | } 414 | } 415 | 416 | pub fn add_light(&mut self, kind: LightKind) -> ObjectBuilder { 417 | ObjectBuilder { 418 | scene: self, 419 | node: Node::default(), 420 | kind: LightBuilder { 421 | color: Color(0xFFFFFFFF), 422 | intensity: 1.0, 423 | kind, 424 | }, 425 | } 426 | } 427 | 428 | pub fn add_directional_light(&mut self) -> ObjectBuilder { 429 | self.add_light(LightKind::Directional) 430 | } 431 | 432 | pub fn add_point_light(&mut self) -> ObjectBuilder { 433 | self.add_light(LightKind::Point) 434 | } 435 | 436 | pub fn lights<'a>(&'a self) -> impl Iterator { 437 | self.lights 438 | .0 439 | .iter() 440 | .enumerate() 441 | .map(|(i, light)| (LightRef(i as u32), light)) 442 | } 443 | 444 | pub fn bake(&self) -> BakedScene { 445 | let mut spaces: Vec = Vec::with_capacity(self.nodes.0.len()); 446 | for n in self.nodes.0.iter() { 447 | let space = if n.parent == NodeRef::default() { 448 | n.local.clone() 449 | } else { 450 | let parent_space = spaces[n.parent.0 as usize].to_space(); 451 | parent_space.combine(&n.local) 452 | }; 453 | spaces.push(space.into()); 454 | } 455 | BakedScene { 456 | spaces: spaces.into_boxed_slice(), 457 | } 458 | } 459 | } 460 | 461 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 462 | pub struct MeshRef(u32); 463 | 464 | pub struct Entity { 465 | pub node: NodeRef, 466 | pub mesh: MeshRef, 467 | } 468 | 469 | pub type UvRange = ops::Range>; 470 | 471 | pub struct Sprite { 472 | pub node: NodeRef, 473 | pub image: ImageRef, 474 | pub uv: Option, 475 | } 476 | 477 | #[derive(Clone, Copy, Debug)] 478 | pub enum LightKind { 479 | Directional, 480 | Point, 481 | } 482 | 483 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 484 | pub struct LightRef(u32); 485 | 486 | #[derive(Debug)] 487 | pub struct Light { 488 | pub node: NodeRef, 489 | pub color: Color, 490 | pub intensity: f32, 491 | pub kind: LightKind, 492 | } 493 | 494 | pub struct ObjectBuilder<'a, T> { 495 | scene: &'a mut Scene, 496 | node: Node, 497 | kind: T, 498 | } 499 | 500 | impl ObjectBuilder<'_, T> { 501 | pub fn parent(&mut self, parent: NodeRef) -> &mut Self { 502 | self.node.parent = parent; 503 | self 504 | } 505 | } 506 | 507 | impl ObjectBuilder<'_, ()> { 508 | pub fn build(&mut self) -> NodeRef { 509 | self.scene.add_node_impl(&mut self.node) 510 | } 511 | } 512 | 513 | impl ObjectBuilder<'_, EntityBuilder> { 514 | /// Register a new material component with this entity. 515 | /// 516 | /// The following components are recognized by the library: 517 | /// - [`Color`] 518 | pub fn component(&mut self, component: T) -> &mut Self { 519 | self.kind.raw.add(component); 520 | self 521 | } 522 | 523 | pub fn build(&mut self) -> EntityRef { 524 | let entity = Entity { 525 | node: if self.node.local == space::Space::default() { 526 | self.node.parent 527 | } else { 528 | self.scene.add_node_impl(&mut self.node) 529 | }, 530 | mesh: self.kind.mesh, 531 | }; 532 | let built = self.kind.raw.add(entity).build(); 533 | self.scene.world.spawn(built) 534 | } 535 | } 536 | 537 | impl ObjectBuilder<'_, SpriteBuilder> { 538 | pub fn uv(&mut self, uv: UvRange) -> &mut Self { 539 | self.kind.uv = Some(uv); 540 | self 541 | } 542 | 543 | /// Register additional data for this sprite. 544 | pub fn component(&mut self, component: T) -> &mut Self { 545 | self.kind.raw.add(component); 546 | self 547 | } 548 | 549 | pub fn build(&mut self) -> EntityRef { 550 | let sprite = Sprite { 551 | node: if self.node.local == space::Space::default() { 552 | self.node.parent 553 | } else { 554 | self.scene.add_node_impl(&mut self.node) 555 | }, 556 | image: self.kind.image, 557 | uv: self.kind.uv.take(), 558 | }; 559 | let built = self.kind.raw.add(sprite).build(); 560 | self.scene.world.spawn(built) 561 | } 562 | } 563 | 564 | impl ObjectBuilder<'_, LightBuilder> { 565 | pub fn intensity(&mut self, intensity: f32) -> &mut Self { 566 | self.kind.intensity = intensity; 567 | self 568 | } 569 | 570 | pub fn color(&mut self, color: Color) -> &mut Self { 571 | self.kind.color = color; 572 | self 573 | } 574 | 575 | pub fn build(&mut self) -> LightRef { 576 | let light = Light { 577 | node: if self.node.local == space::Space::default() { 578 | self.node.parent 579 | } else { 580 | self.scene.add_node_impl(&mut self.node) 581 | }, 582 | color: self.kind.color, 583 | intensity: self.kind.intensity, 584 | kind: self.kind.kind, 585 | }; 586 | let index = self.scene.lights.0.len(); 587 | self.scene.lights.0.push(light); 588 | LightRef(index as u32) 589 | } 590 | } 591 | -------------------------------------------------------------------------------- /baryon-core/src/load.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io, path::Path}; 2 | use wgpu::util::DeviceExt as _; 3 | 4 | impl super::Context { 5 | pub fn add_image_from_raw( 6 | &mut self, 7 | texture: wgpu::Texture, 8 | size: wgpu::Extent3d, 9 | ) -> super::ImageRef { 10 | let index = self.images.len(); 11 | let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); 12 | self.images.push(super::Image { view, size }); 13 | super::ImageRef(index as u32) 14 | } 15 | 16 | pub fn add_image_from_data( 17 | &mut self, 18 | desc: &wgpu::TextureDescriptor, 19 | data: &[u8], 20 | ) -> super::ImageRef { 21 | let texture = self 22 | .device 23 | .create_texture_with_data(&self.queue, desc, data); 24 | self.add_image_from_raw(texture, desc.size) 25 | } 26 | 27 | pub fn load_image(&mut self, path_ref: impl AsRef) -> super::ImageRef { 28 | let path = path_ref.as_ref(); 29 | let image_format = image::ImageFormat::from_extension(path.extension().unwrap()) 30 | .unwrap_or_else(|| panic!("Unrecognized image extension: {:?}", path.extension())); 31 | 32 | let label = path.display().to_string(); 33 | let file = File::open(path) 34 | .unwrap_or_else(|e| panic!("Unable to open {}: {:?}", path.display(), e)); 35 | let mut buf_reader = io::BufReader::new(file); 36 | 37 | let (texture, size) = if image_format == image::ImageFormat::Dds { 38 | let dds = ddsfile::Dds::read(&mut buf_reader) 39 | .unwrap_or_else(|e| panic!("Unable to read {}: {:?}", path.display(), e)); 40 | 41 | println!("Header {:?}", dds.header); 42 | let mip_level_count = dds.get_num_mipmap_levels(); 43 | let (dimension, depth_or_array_layers) = match dds.header10 { 44 | Some(ref h) => match h.resource_dimension { 45 | ddsfile::D3D10ResourceDimension::Texture2D => { 46 | (wgpu::TextureDimension::D2, h.array_size) 47 | } 48 | ddsfile::D3D10ResourceDimension::Texture3D => { 49 | (wgpu::TextureDimension::D3, dds.get_depth()) 50 | } 51 | other => panic!("Unsupported resource dimension {:?}", other), 52 | }, 53 | None => match dds.header.depth { 54 | None | Some(1) => (wgpu::TextureDimension::D2, 1), 55 | Some(other) => (wgpu::TextureDimension::D3, other), 56 | }, 57 | }; 58 | 59 | let format = if let Some(fourcc) = dds.header.spf.fourcc { 60 | match fourcc.0 { 61 | ddsfile::FourCC::BC1_UNORM => wgpu::TextureFormat::Bc1RgbaUnormSrgb, 62 | ddsfile::FourCC::BC2_UNORM => wgpu::TextureFormat::Bc2RgbaUnormSrgb, 63 | ddsfile::FourCC::BC3_UNORM => wgpu::TextureFormat::Bc3RgbaUnormSrgb, 64 | ddsfile::FourCC::BC4_UNORM => wgpu::TextureFormat::Bc4RUnorm, 65 | ddsfile::FourCC::BC4_SNORM => wgpu::TextureFormat::Bc4RSnorm, 66 | ddsfile::FourCC::BC5_UNORM => wgpu::TextureFormat::Bc5RgUnorm, 67 | ddsfile::FourCC::BC5_SNORM => wgpu::TextureFormat::Bc5RgSnorm, 68 | ref other => panic!("Unsupported DDS FourCC {:?}", other), 69 | } 70 | } else { 71 | assert_eq!(dds.header.spf.rgb_bit_count, Some(32)); 72 | wgpu::TextureFormat::Rgba8UnormSrgb 73 | }; 74 | 75 | let desc = wgpu::TextureDescriptor { 76 | label: Some(&label), 77 | size: wgpu::Extent3d { 78 | width: dds.header.width, 79 | height: dds.header.height, 80 | depth_or_array_layers, 81 | }, 82 | mip_level_count, 83 | sample_count: 1, 84 | dimension, 85 | format, 86 | usage: wgpu::TextureUsages::TEXTURE_BINDING, 87 | }; 88 | let texture = self 89 | .device 90 | .create_texture_with_data(&self.queue, &desc, &dds.data); 91 | 92 | (texture, desc.size) 93 | } else { 94 | let img = image::load(buf_reader, image_format) 95 | .unwrap_or_else(|e| panic!("Unable to decode {}: {:?}", path.display(), e)) 96 | .to_rgba8(); 97 | 98 | let (width, height) = img.dimensions(); 99 | let size = wgpu::Extent3d { 100 | width, 101 | height, 102 | depth_or_array_layers: 1, 103 | }; 104 | let desc = wgpu::TextureDescriptor { 105 | label: Some(&label), 106 | size, 107 | mip_level_count: 1, //TODO: generate `size.max_mips()` mipmaps 108 | sample_count: 1, 109 | dimension: wgpu::TextureDimension::D2, 110 | format: wgpu::TextureFormat::Rgba8UnormSrgb, 111 | usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING, 112 | }; 113 | let texture = self.device.create_texture(&desc); 114 | 115 | self.queue.write_texture( 116 | texture.as_image_copy(), 117 | &img, 118 | wgpu::ImageDataLayout { 119 | offset: 0, 120 | bytes_per_row: std::num::NonZeroU32::new(width * 4), 121 | rows_per_image: None, 122 | }, 123 | size, 124 | ); 125 | (texture, size) 126 | }; 127 | 128 | self.add_image_from_raw(texture, size) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /baryon-core/src/mesh.rs: -------------------------------------------------------------------------------- 1 | use std::{any::TypeId, marker::PhantomData, mem}; 2 | use wgpu::util::DeviceExt as _; 3 | 4 | /// A freshly created Mesh that comes with metadata, 5 | /// which is necessary to instantiate it. 6 | pub struct Prototype { 7 | pub reference: super::MeshRef, 8 | type_ids: Box<[TypeId]>, 9 | type_infos: Box<[hecs::TypeInfo]>, 10 | } 11 | 12 | pub struct IndexStream { 13 | pub offset: wgpu::BufferAddress, 14 | pub format: wgpu::IndexFormat, 15 | pub count: u32, 16 | } 17 | 18 | pub struct VertexStream { 19 | type_id: TypeId, 20 | pub offset: wgpu::BufferAddress, 21 | pub stride: wgpu::BufferAddress, 22 | } 23 | 24 | //HACK: `hecs` doesn't want anybody to implement this, but we have no choice. 25 | unsafe impl<'a> hecs::DynamicBundle for &'a Prototype { 26 | fn with_ids(&self, f: impl FnOnce(&[TypeId]) -> T) -> T { 27 | f(&self.type_ids) 28 | } 29 | fn type_info(&self) -> Vec { 30 | self.type_infos.to_vec() 31 | } 32 | unsafe fn put(self, mut f: impl FnMut(*mut u8, hecs::TypeInfo)) { 33 | const DUMMY_SIZE: usize = 1; 34 | let mut v = [0u8; DUMMY_SIZE]; 35 | assert!(mem::size_of::>() <= DUMMY_SIZE); 36 | for ts in self.type_infos.iter() { 37 | f(v.as_mut_ptr(), ts.clone()); 38 | } 39 | } 40 | } 41 | 42 | pub struct Mesh { 43 | pub buffer: wgpu::Buffer, 44 | pub index_stream: Option, 45 | vertex_streams: Box<[VertexStream]>, 46 | pub vertex_count: u32, 47 | pub bound_radius: f32, 48 | } 49 | 50 | impl Mesh { 51 | pub fn vertex_stream(&self) -> Option<&VertexStream> { 52 | self.vertex_streams 53 | .iter() 54 | .find(|vs| vs.type_id == TypeId::of::()) 55 | } 56 | 57 | pub fn vertex_slice(&self) -> wgpu::BufferSlice { 58 | let stream = self.vertex_stream::().unwrap(); 59 | self.buffer.slice(stream.offset..) 60 | } 61 | } 62 | 63 | pub struct Vertex(PhantomData); 64 | 65 | pub struct MeshBuilder<'a> { 66 | context: &'a mut super::Context, 67 | name: String, 68 | data: Vec, // could be moved up to the context 69 | index_stream: Option, 70 | vertex_streams: Vec, 71 | type_infos: Vec, 72 | vertex_count: usize, 73 | bound_radius: f32, 74 | } 75 | 76 | impl<'a> MeshBuilder<'a> { 77 | pub fn new(context: &'a mut super::Context) -> Self { 78 | Self { 79 | context, 80 | name: String::new(), 81 | data: Vec::new(), 82 | index_stream: None, 83 | vertex_streams: Vec::new(), 84 | type_infos: Vec::new(), 85 | vertex_count: 0, 86 | bound_radius: 0.0, 87 | } 88 | } 89 | 90 | pub fn name<'s>(&'a mut self, name: &str) -> &'s mut Self { 91 | self.name = name.to_string(); 92 | self 93 | } 94 | 95 | fn append(&mut self, data: &[T]) -> wgpu::BufferAddress { 96 | let offset = self.data.len(); 97 | self.data.extend(bytemuck::cast_slice(data)); 98 | offset as _ 99 | } 100 | 101 | pub fn index<'s>(&'s mut self, data: &[u16]) -> &'s mut Self { 102 | assert!(self.index_stream.is_none()); 103 | let offset = self.append(data); 104 | self.index_stream = Some(IndexStream { 105 | offset, 106 | format: wgpu::IndexFormat::Uint16, 107 | count: data.len() as u32, 108 | }); 109 | self 110 | } 111 | 112 | pub fn vertex<'s, T: bytemuck::Pod>(&'s mut self, data: &[T]) -> &'s mut Self { 113 | let offset = self.append(data); 114 | if self.vertex_count == 0 { 115 | self.vertex_count = data.len(); 116 | } else { 117 | assert_eq!(self.vertex_count, data.len()); 118 | } 119 | self.vertex_streams.push(VertexStream { 120 | type_id: TypeId::of::(), 121 | offset, 122 | stride: mem::size_of::() as _, 123 | }); 124 | self.type_infos.push(hecs::TypeInfo::of::>()); 125 | self 126 | } 127 | 128 | pub fn radius(&mut self, radius: f32) -> &mut Self { 129 | self.bound_radius = radius; 130 | self 131 | } 132 | 133 | pub fn build(&mut self) -> Prototype { 134 | let index = self.context.meshes.len(); 135 | 136 | let mut usage = wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::VERTEX; 137 | usage.set(wgpu::BufferUsages::INDEX, self.index_stream.is_some()); 138 | let buffer = self 139 | .context 140 | .device 141 | .create_buffer_init(&wgpu::util::BufferInitDescriptor { 142 | label: if self.name.is_empty() { 143 | None 144 | } else { 145 | Some(&self.name) 146 | }, 147 | contents: &self.data, 148 | usage, 149 | }); 150 | 151 | let type_ids = self 152 | .vertex_streams 153 | .iter() 154 | .map(|vs| vs.type_id) 155 | .collect::>() 156 | .into_boxed_slice(); 157 | self.context.meshes.push(Mesh { 158 | buffer, 159 | index_stream: self.index_stream.take(), 160 | vertex_streams: mem::take(&mut self.vertex_streams).into_boxed_slice(), 161 | vertex_count: self.vertex_count as u32, 162 | bound_radius: self.bound_radius, 163 | }); 164 | 165 | Prototype { 166 | reference: super::MeshRef(index as u32), 167 | type_ids, 168 | type_infos: mem::take(&mut self.type_infos).into_boxed_slice(), 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /baryon-core/src/space.rs: -------------------------------------------------------------------------------- 1 | use std::ops; 2 | 3 | #[derive(Clone, Debug, PartialEq)] 4 | pub(super) struct Space { 5 | position: glam::Vec3, 6 | scale: f32, 7 | orientation: glam::Quat, 8 | } 9 | 10 | impl Default for Space { 11 | fn default() -> Self { 12 | Self { 13 | position: glam::Vec3::ZERO, 14 | scale: 1.0, 15 | orientation: glam::Quat::IDENTITY, 16 | } 17 | } 18 | } 19 | 20 | impl Space { 21 | pub(super) fn combine(&self, other: &Self) -> Self { 22 | Self { 23 | scale: self.scale * other.scale, 24 | orientation: self.orientation * other.orientation, 25 | position: self.scale * (self.orientation * other.position) + self.position, 26 | } 27 | } 28 | 29 | fn inverse(&self) -> Self { 30 | let scale = 1.0 / self.scale; 31 | let orientation = self.orientation.inverse(); 32 | let position = -scale * (orientation * self.position); 33 | Self { 34 | position, 35 | scale, 36 | orientation, 37 | } 38 | } 39 | 40 | fn to_matrix(&self) -> glam::Mat4 { 41 | glam::Mat4::from_scale_rotation_translation( 42 | glam::Vec3::splat(self.scale), 43 | self.orientation, 44 | self.position, 45 | ) 46 | } 47 | } 48 | 49 | impl super::ObjectBuilder<'_, T> { 50 | //TODO: should we accept `V: Into` here? 51 | pub fn position(&mut self, position: mint::Vector3) -> &mut Self { 52 | self.node.local.position = position.into(); 53 | self 54 | } 55 | 56 | pub fn scale(&mut self, scale: f32) -> &mut Self { 57 | self.node.local.scale = scale; 58 | self 59 | } 60 | 61 | pub fn orientation_around(&mut self, axis: mint::Vector3, angle_deg: f32) -> &mut Self { 62 | self.node.local.orientation = 63 | glam::Quat::from_axis_angle(axis.into(), angle_deg.to_radians()); 64 | self 65 | } 66 | 67 | pub fn orientation(&mut self, quat: mint::Quaternion) -> &mut Self { 68 | self.node.local.orientation = quat.into(); 69 | self 70 | } 71 | 72 | pub fn look_at(&mut self, target: mint::Vector3, up: mint::Vector3) -> &mut Self { 73 | /* // This path just doesn't work well 74 | let dir = (glam::Vec3::from(target) - self.node.local.position).normalize(); 75 | self.node.local.orientation = glam::Quat::from_rotation_arc(-glam::Vec3::Z, dir); 76 | * glam::Quat::from_rotation_arc(glam::Vec3::Y, up.into()); 77 | let temp = glam::Quat::from_rotation_arc(glam::Vec3::Y, up.into(); 78 | let new_dir = temp * -glam::Vec3::Z; 79 | self.node.local.orientation = glam::Quat::from_rotation_arc(-glam::Vec3::Z, dir); 80 | */ 81 | 82 | let affine = glam::Affine3A::look_at_rh(self.node.local.position, target.into(), up.into()); 83 | let (_, rot, _) = affine.inverse().to_scale_rotation_translation(); 84 | // translation here is expected to match `self.node.local.position` 85 | self.node.local.orientation = rot; 86 | 87 | /* // Blocked on https://github.com/bitshifter/glam-rs/issues/235 88 | let dir = self.node.local.position - glam::Vec3::from(target); 89 | let f = dir.normalize(); 90 | let s = glam::Vec3::from(up).cross(f).normalize(); 91 | let u = f.cross(s); 92 | self.node.local.orientation = glam::Quat::from_rotation_axes(s, u, f); 93 | */ 94 | self 95 | } 96 | } 97 | 98 | impl super::Node { 99 | pub fn get_position(&self) -> mint::Vector3 { 100 | self.local.position.into() 101 | } 102 | pub fn set_position(&mut self, pos: mint::Vector3) { 103 | self.local.position = pos.into(); 104 | } 105 | pub fn pre_move(&mut self, offset: mint::Vector3) { 106 | let other = Space { 107 | position: offset.into(), 108 | scale: 1.0, 109 | orientation: glam::Quat::IDENTITY, 110 | }; 111 | self.local = other.combine(&self.local); 112 | } 113 | pub fn post_move(&mut self, offset: mint::Vector3) { 114 | self.local.position += glam::Vec3::from(offset); 115 | } 116 | 117 | pub fn get_rotation(&self) -> (mint::Vector3, f32) { 118 | let (axis, angle) = self.local.orientation.to_axis_angle(); 119 | (axis.into(), angle.to_degrees()) 120 | } 121 | pub fn set_rotation(&mut self, axis: mint::Vector3, angle_deg: f32) { 122 | self.local.orientation = glam::Quat::from_axis_angle(axis.into(), angle_deg.to_radians()); 123 | } 124 | pub fn pre_rotate(&mut self, axis: mint::Vector3, angle_deg: f32) { 125 | self.local.orientation = self.local.orientation 126 | * glam::Quat::from_axis_angle(axis.into(), angle_deg.to_radians()); 127 | } 128 | pub fn post_rotate(&mut self, axis: mint::Vector3, angle_deg: f32) { 129 | let other = Space { 130 | position: glam::Vec3::ZERO, 131 | scale: 1.0, 132 | orientation: glam::Quat::from_axis_angle(axis.into(), angle_deg.to_radians()), 133 | }; 134 | self.local = other.combine(&self.local); 135 | } 136 | 137 | pub fn get_scale(&self) -> f32 { 138 | self.local.scale 139 | } 140 | pub fn set_scale(&mut self, scale: f32) { 141 | self.local.scale = scale; 142 | } 143 | } 144 | 145 | #[derive(Debug)] 146 | pub struct RawSpace { 147 | pub pos_scale: [f32; 4], 148 | pub rot: [f32; 4], 149 | } 150 | 151 | impl From for RawSpace { 152 | fn from(s: Space) -> Self { 153 | Self { 154 | pos_scale: [s.position.x, s.position.y, s.position.z, s.scale], 155 | rot: s.orientation.into(), 156 | } 157 | } 158 | } 159 | 160 | impl RawSpace { 161 | pub(super) fn to_space(&self) -> Space { 162 | Space { 163 | position: glam::Vec3::new(self.pos_scale[0], self.pos_scale[1], self.pos_scale[2]), 164 | scale: self.pos_scale[3], 165 | orientation: glam::Quat::from_array(self.rot), 166 | } 167 | } 168 | 169 | pub fn inverse_matrix(&self) -> mint::ColumnMatrix4 { 170 | self.to_space().inverse().to_matrix().into() 171 | } 172 | } 173 | 174 | #[derive(Clone, Debug)] 175 | pub enum Projection { 176 | Orthographic { 177 | /// The center of the projection. 178 | center: mint::Vector2, 179 | /// Vertical extent from the center point. The height is double the extent. 180 | /// The width is derived from the height based on the current aspect ratio. 181 | extent_y: f32, 182 | }, 183 | Perspective { 184 | /// Vertical field of view, in degrees. 185 | /// Note: the horizontal FOV is computed based on the aspect. 186 | fov_y: f32, 187 | }, 188 | } 189 | 190 | #[derive(Clone, Debug)] 191 | pub struct Camera { 192 | pub projection: Projection, 193 | /// Specify the depth range as seen by the camera. 194 | /// `depth.start` maps to 0.0, and `depth.end` maps to 1.0. 195 | pub depth: ops::Range, 196 | pub node: super::NodeRef, 197 | pub background: super::Color, 198 | } 199 | 200 | impl Default for Camera { 201 | fn default() -> Self { 202 | Self { 203 | projection: Projection::Orthographic { 204 | center: mint::Vector2 { x: 0.0, y: 0.0 }, 205 | extent_y: 1.0, 206 | }, 207 | depth: 0.0..1.0, 208 | node: super::NodeRef::default(), 209 | background: super::Color::default(), 210 | } 211 | } 212 | } 213 | 214 | impl Camera { 215 | pub fn projection_matrix(&self, aspect: f32) -> mint::ColumnMatrix4 { 216 | let matrix = match self.projection { 217 | Projection::Orthographic { center, extent_y } => { 218 | let extent_x = aspect * extent_y; 219 | glam::Mat4::orthographic_rh( 220 | center.x - extent_x, 221 | center.x + extent_x, 222 | center.y - extent_y, 223 | center.y + extent_y, 224 | self.depth.start, 225 | self.depth.end, 226 | ) 227 | } 228 | Projection::Perspective { fov_y } => { 229 | let fov = fov_y.to_radians(); 230 | if self.depth.end == f32::INFINITY { 231 | assert!(self.depth.start.is_finite()); 232 | glam::Mat4::perspective_infinite_rh(fov, aspect, self.depth.start) 233 | } else if self.depth.start == f32::INFINITY { 234 | glam::Mat4::perspective_infinite_reverse_rh(fov, aspect, self.depth.end) 235 | } else { 236 | glam::Mat4::perspective_rh(fov, aspect, self.depth.start, self.depth.end) 237 | } 238 | } 239 | }; 240 | matrix.into() 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /etc/cubeception.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/baryon/e4dff61567b2bd6a22549525d1ff9e1eaf8fb704/etc/cubeception.png -------------------------------------------------------------------------------- /etc/pickachu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/baryon/e4dff61567b2bd6a22549525d1ff9e1eaf8fb704/etc/pickachu.png -------------------------------------------------------------------------------- /examples/assets/Duck/Duck.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset": { 3 | "generator": "COLLADA2GLTF", 4 | "version": "2.0" 5 | }, 6 | "scene": 0, 7 | "scenes": [ 8 | { 9 | "nodes": [ 10 | 0 11 | ] 12 | } 13 | ], 14 | "nodes": [ 15 | { 16 | "children": [ 17 | 2, 18 | 1 19 | ], 20 | "matrix": [ 21 | 0.009999999776482582, 22 | 0.0, 23 | 0.0, 24 | 0.0, 25 | 0.0, 26 | 0.009999999776482582, 27 | 0.0, 28 | 0.0, 29 | 0.0, 30 | 0.0, 31 | 0.009999999776482582, 32 | 0.0, 33 | 0.0, 34 | 0.0, 35 | 0.0, 36 | 1.0 37 | ] 38 | }, 39 | { 40 | "matrix": [ 41 | -0.7289686799049377, 42 | 0.0, 43 | -0.6845470666885376, 44 | 0.0, 45 | -0.4252049028873444, 46 | 0.7836934328079224, 47 | 0.4527972936630249, 48 | 0.0, 49 | 0.5364750623703003, 50 | 0.6211478114128113, 51 | -0.571287989616394, 52 | 0.0, 53 | 400.1130065917969, 54 | 463.2640075683594, 55 | -431.0780334472656, 56 | 1.0 57 | ], 58 | "camera": 0 59 | }, 60 | { 61 | "mesh": 0 62 | } 63 | ], 64 | "cameras": [ 65 | { 66 | "perspective": { 67 | "aspectRatio": 1.5, 68 | "yfov": 0.6605925559997559, 69 | "zfar": 10000.0, 70 | "znear": 1.0 71 | }, 72 | "type": "perspective" 73 | } 74 | ], 75 | "meshes": [ 76 | { 77 | "primitives": [ 78 | { 79 | "attributes": { 80 | "NORMAL": 1, 81 | "POSITION": 2, 82 | "TEXCOORD_0": 3 83 | }, 84 | "indices": 0, 85 | "mode": 4, 86 | "material": 0 87 | } 88 | ], 89 | "name": "LOD3spShape" 90 | } 91 | ], 92 | "accessors": [ 93 | { 94 | "bufferView": 0, 95 | "byteOffset": 0, 96 | "componentType": 5123, 97 | "count": 12636, 98 | "max": [ 99 | 2398 100 | ], 101 | "min": [ 102 | 0 103 | ], 104 | "type": "SCALAR" 105 | }, 106 | { 107 | "bufferView": 1, 108 | "byteOffset": 0, 109 | "componentType": 5126, 110 | "count": 2399, 111 | "max": [ 112 | 0.9995989799499512, 113 | 0.999580979347229, 114 | 0.9984359741210938 115 | ], 116 | "min": [ 117 | -0.9990839958190918, 118 | -1.0, 119 | -0.9998319745063782 120 | ], 121 | "type": "VEC3" 122 | }, 123 | { 124 | "bufferView": 1, 125 | "byteOffset": 28788, 126 | "componentType": 5126, 127 | "count": 2399, 128 | "max": [ 129 | 96.17990112304688, 130 | 163.97000122070313, 131 | 53.92519760131836 132 | ], 133 | "min": [ 134 | -69.29850006103516, 135 | 9.929369926452637, 136 | -61.32819747924805 137 | ], 138 | "type": "VEC3" 139 | }, 140 | { 141 | "bufferView": 2, 142 | "byteOffset": 0, 143 | "componentType": 5126, 144 | "count": 2399, 145 | "max": [ 146 | 0.9833459854125976, 147 | 0.9800369739532472 148 | ], 149 | "min": [ 150 | 0.026409000158309938, 151 | 0.01996302604675293 152 | ], 153 | "type": "VEC2" 154 | } 155 | ], 156 | "materials": [ 157 | { 158 | "pbrMetallicRoughness": { 159 | "baseColorTexture": { 160 | "index": 0 161 | }, 162 | "metallicFactor": 0.0 163 | }, 164 | "emissiveFactor": [ 165 | 0.0, 166 | 0.0, 167 | 0.0 168 | ], 169 | "name": "blinn3-fx" 170 | } 171 | ], 172 | "textures": [ 173 | { 174 | "sampler": 0, 175 | "source": 0 176 | } 177 | ], 178 | "images": [ 179 | { 180 | "uri": "DuckCM.png" 181 | } 182 | ], 183 | "samplers": [ 184 | { 185 | "magFilter": 9729, 186 | "minFilter": 9986, 187 | "wrapS": 10497, 188 | "wrapT": 10497 189 | } 190 | ], 191 | "bufferViews": [ 192 | { 193 | "buffer": 0, 194 | "byteOffset": 76768, 195 | "byteLength": 25272, 196 | "target": 34963 197 | }, 198 | { 199 | "buffer": 0, 200 | "byteOffset": 0, 201 | "byteLength": 57576, 202 | "byteStride": 12, 203 | "target": 34962 204 | }, 205 | { 206 | "buffer": 0, 207 | "byteOffset": 57576, 208 | "byteLength": 19192, 209 | "byteStride": 8, 210 | "target": 34962 211 | } 212 | ], 213 | "buffers": [ 214 | { 215 | "byteLength": 102040, 216 | "uri": "Duck0.bin" 217 | } 218 | ] 219 | } 220 | -------------------------------------------------------------------------------- /examples/assets/Duck/Duck0.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/baryon/e4dff61567b2bd6a22549525d1ff9e1eaf8fb704/examples/assets/Duck/Duck0.bin -------------------------------------------------------------------------------- /examples/assets/Duck/DuckCM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/baryon/e4dff61567b2bd6a22549525d1ff9e1eaf8fb704/examples/assets/Duck/DuckCM.png -------------------------------------------------------------------------------- /examples/assets/car.mtl: -------------------------------------------------------------------------------- 1 | # WaveFront *.mtl file (generated by CINEMA 4D) 2 | 3 | newmtl Mat.13 4 | Kd 0.57999998331070 0.57999998331070 0.57999998331070 5 | Ks 1.00000000000000 1.00000000000000 1.00000000000000 6 | Ns 100 7 | illum 7 8 | 9 | newmtl Mat.12 10 | Kd 0.15000000596046 0.15000000596046 0.15000000596046 11 | Ks 1.00000000000000 1.00000000000000 1.00000000000000 12 | Ns 100 13 | illum 7 14 | 15 | newmtl Mat.16 16 | Kd 0.40999999642372 0.40999999642372 0.40999999642372 17 | Ks 1.00000000000000 1.00000000000000 1.00000000000000 18 | Ns 100 19 | illum 7 20 | 21 | newmtl Mat.11 22 | Ka 1.00000000000000 0.30000001192093 0.30000001192093 23 | Kd 0.80000001192093 0.00000000000000 0.00000000000000 24 | Ks 1.00000000000000 1.00000000000000 1.00000000000000 25 | Ns 100 26 | illum 7 27 | 28 | newmtl Mat.18 29 | Kd 0.80000001192093 0.44800001382828 0.44800001382828 30 | Ks 1.00000000000000 1.00000000000000 1.00000000000000 31 | Ns 100 32 | illum 7 33 | 34 | newmtl Mat.4 35 | Kd 0.80000001192093 0.64639997482300 0.28799998760223 36 | Ks 1.00000000000000 1.00000000000000 1.00000000000000 37 | Ns 100 38 | illum 7 39 | 40 | newmtl Mat.1 41 | Kd 0.80000001192093 0.49200001358986 0.18400000035763 42 | Ks 1.00000000000000 1.00000000000000 1.00000000000000 43 | Ns 100 44 | illum 7 45 | 46 | newmtl Mat.2 47 | Kd 0.00000000000000 0.00000000000000 0.00000000000000 48 | Ks 1.00000000000000 1.00000000000000 1.00000000000000 49 | Ns 100 50 | illum 7 51 | 52 | newmtl Mat.6 53 | Kd 0.80000001192093 0.80000001192093 0.80000001192093 54 | Ks 1.00000000000000 1.00000000000000 1.00000000000000 55 | Ns 100 56 | illum 7 57 | 58 | newmtl Mat.5 59 | Kd 0.28000000119209 0.28000000119209 0.28000000119209 60 | Ks 1.00000000000000 1.00000000000000 1.00000000000000 61 | Ns 100 62 | illum 7 63 | 64 | newmtl Mat.8 65 | Kd 0.68999999761581 0.68999999761581 0.68999999761581 66 | Ks 1.00000000000000 1.00000000000000 1.00000000000000 67 | Ns 100 68 | illum 7 69 | 70 | newmtl Mat.9 71 | Kd 0.49000000953674 0.49000000953674 0.49000000953674 72 | Ks 1.00000000000000 1.00000000000000 1.00000000000000 73 | Ns 100 74 | illum 7 75 | 76 | newmtl Mat.7 77 | Ka 1.00000000000000 0.00000000000000 0.00000000000000 78 | Kd 0.80000001192093 0.20800000429153 0.20800000429153 79 | Ks 1.00000000000000 1.00000000000000 1.00000000000000 80 | Ns 100 81 | illum 7 82 | 83 | newmtl Mat.10 84 | Kd 0.36800000071526 0.52640002965927 0.80000001192093 85 | Ks 1.00000000000000 1.00000000000000 1.00000000000000 86 | Ns 100 87 | illum 7 88 | 89 | newmtl Mat.19 90 | Kd 0.18000000715256 0.18000000715256 0.18000000715256 91 | Ks 1.00000000000000 1.00000000000000 1.00000000000000 92 | Ns 100 93 | illum 7 94 | 95 | newmtl Mat.17 96 | Ka 0.96666663885117 1.00000000000000 0.00000000000000 97 | Kd 0.80000001192093 0.77439999580383 0.28799998760223 98 | illum 7 99 | 100 | newmtl Mat 101 | Kd 0.80000001192093 0.80000001192093 0.80000001192093 102 | Ks 1.00000000000000 1.00000000000000 1.00000000000000 103 | Ns 100 104 | illum 7 105 | 106 | -------------------------------------------------------------------------------- /examples/assets/pickachu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/baryon/e4dff61567b2bd6a22549525d1ff9e1eaf8fb704/examples/assets/pickachu.png -------------------------------------------------------------------------------- /examples/cubes.rs: -------------------------------------------------------------------------------- 1 | use std::time; 2 | 3 | struct Cube { 4 | node: baryon::NodeRef, 5 | level: u8, 6 | } 7 | 8 | const SCALE_ROOT: f32 = 2.0; 9 | const SCALE_LEVEL: f32 = 0.4; 10 | 11 | struct Level { 12 | color: baryon::Color, 13 | speed: f32, 14 | } 15 | 16 | fn fill_scene( 17 | levels: &[Level], 18 | scene: &mut baryon::Scene, 19 | prototype: &baryon::Prototype, 20 | ) -> Vec { 21 | let root_node = scene.add_node().scale(SCALE_ROOT).build(); 22 | scene 23 | .add_entity(prototype) 24 | .parent(root_node) 25 | .component(levels[0].color) 26 | .build(); 27 | let mut list = vec![Cube { 28 | node: root_node, 29 | level: 0, 30 | }]; 31 | 32 | struct Stack { 33 | parent: baryon::NodeRef, 34 | level: u8, 35 | } 36 | let mut stack = vec![Stack { 37 | parent: root_node, 38 | level: 1, 39 | }]; 40 | 41 | let children = [ 42 | mint::Vector3 { 43 | x: 0.0, 44 | y: 0.0, 45 | z: 1.0, 46 | }, 47 | mint::Vector3 { 48 | x: 1.0, 49 | y: 0.0, 50 | z: 0.0, 51 | }, 52 | mint::Vector3 { 53 | x: -1.0, 54 | y: 0.0, 55 | z: 0.0, 56 | }, 57 | mint::Vector3 { 58 | x: 0.0, 59 | y: 1.0, 60 | z: 0.0, 61 | }, 62 | mint::Vector3 { 63 | x: 0.0, 64 | y: -1.0, 65 | z: 0.0, 66 | }, 67 | ]; 68 | 69 | while let Some(next) = stack.pop() { 70 | let level = match levels.get(next.level as usize) { 71 | Some(level) => level, 72 | None => continue, 73 | }; 74 | for &child in children.iter() { 75 | let node = scene 76 | .add_node() 77 | .position(mint::Vector3 { 78 | x: 0.0, 79 | y: 0.0, 80 | z: 1.0 + SCALE_LEVEL, 81 | }) 82 | .scale(SCALE_LEVEL) 83 | .parent(next.parent) 84 | .build(); 85 | scene[node].post_rotate(child, 90.0); 86 | 87 | scene 88 | .add_entity(prototype) 89 | .parent(node) 90 | .component(level.color) 91 | .build(); 92 | list.push(Cube { 93 | node, 94 | level: next.level, 95 | }); 96 | 97 | stack.push(Stack { 98 | parent: node, 99 | level: next.level + 1, 100 | }); 101 | } 102 | } 103 | 104 | list 105 | } 106 | 107 | const LEVELS: &[Level] = &[ 108 | Level { 109 | color: baryon::Color(0xFFFFFF80), 110 | speed: 20.0, 111 | }, 112 | Level { 113 | color: baryon::Color(0xFF8080FF), 114 | speed: -30.0, 115 | }, 116 | Level { 117 | color: baryon::Color(0xFF80FF80), 118 | speed: 40.0, 119 | }, 120 | Level { 121 | color: baryon::Color(0xFFFF8080), 122 | speed: -60.0, 123 | }, 124 | Level { 125 | color: baryon::Color(0xFF80FFFF), 126 | speed: 80.0, 127 | }, 128 | Level { 129 | color: baryon::Color(0xFFFF80FF), 130 | speed: -100.0, 131 | }, 132 | ]; 133 | 134 | fn main() { 135 | use baryon::{ 136 | geometry::{Geometry, Streams}, 137 | window::{Event, Window}, 138 | }; 139 | 140 | let window = Window::new().title("Cubeception").build(); 141 | let mut context = pollster::block_on(baryon::Context::init().build(&window)); 142 | let mut scene = baryon::Scene::new(); 143 | 144 | let camera = baryon::Camera { 145 | projection: baryon::Projection::Perspective { fov_y: 45.0 }, 146 | depth: 1.0..10.0, 147 | node: scene 148 | .add_node() 149 | .position([1.8f32, -8.0, 3.0].into()) 150 | .look_at([0f32; 3].into(), [0f32, 0.0, 1.0].into()) 151 | .build(), 152 | background: baryon::Color(0xFF203040), 153 | }; 154 | 155 | let prototype = Geometry::cuboid( 156 | Streams::empty(), 157 | mint::Vector3 { 158 | x: 1.0, 159 | y: 1.0, 160 | z: 1.0, 161 | }, 162 | ) 163 | .bake(&mut context); 164 | let cubes = fill_scene(&LEVELS[..5], &mut scene, &prototype); 165 | println!("Initialized {} cubes", cubes.len()); 166 | 167 | let mut pass = baryon::pass::Solid::new( 168 | &baryon::pass::SolidConfig { 169 | cull_back_faces: true, 170 | }, 171 | &context, 172 | ); 173 | 174 | let mut moment = time::Instant::now(); 175 | 176 | window.run(move |event| match event { 177 | Event::Resize { width, height } => { 178 | context.resize(width, height); 179 | } 180 | Event::Draw => { 181 | let delta = moment.elapsed().as_secs_f32(); 182 | moment = time::Instant::now(); 183 | for cube in cubes.iter() { 184 | let level = &LEVELS[cube.level as usize]; 185 | scene[cube.node].pre_rotate( 186 | mint::Vector3 { 187 | x: 0.0, 188 | y: 0.0, 189 | z: 1.0, 190 | }, 191 | delta * level.speed, 192 | ); 193 | } 194 | 195 | context.present(&mut pass, &scene, &camera); 196 | } 197 | _ => {} 198 | }) 199 | } 200 | -------------------------------------------------------------------------------- /examples/empty.rs: -------------------------------------------------------------------------------- 1 | // Custom implementation of a `Pass` that just clears. 2 | pub struct Clear; 3 | impl baryon::Pass for Clear { 4 | fn draw( 5 | &mut self, 6 | targets: &[baryon::TargetRef], 7 | _scene: &baryon::Scene, 8 | camera: &baryon::Camera, 9 | context: &baryon::Context, 10 | ) { 11 | use bc::ContextDetail as _; 12 | 13 | let target = context.get_target(targets[0]); 14 | let mut encoder = context 15 | .device() 16 | .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 17 | 18 | { 19 | let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 20 | label: Some("clear"), 21 | color_attachments: &[wgpu::RenderPassColorAttachment { 22 | view: &target.view, 23 | resolve_target: None, 24 | ops: wgpu::Operations { 25 | load: wgpu::LoadOp::Clear(camera.background.into()), 26 | store: true, 27 | }, 28 | }], 29 | depth_stencil_attachment: None, 30 | }); 31 | } 32 | 33 | context.queue().submit(Some(encoder.finish())); 34 | } 35 | } 36 | 37 | fn main() { 38 | use baryon::window::{Event, Window}; 39 | 40 | let window = Window::new().title("Empty").build(); 41 | let mut context = pollster::block_on(baryon::Context::init().build(&window)); 42 | let scene = baryon::Scene::new(); 43 | let mut camera = baryon::Camera::default(); 44 | camera.background = baryon::Color(0xFF203040); 45 | let mut pass = Clear; 46 | 47 | window.run(move |event| match event { 48 | Event::Resize { width, height } => { 49 | context.resize(width, height); 50 | } 51 | Event::Draw => { 52 | context.present(&mut pass, &scene, &camera); 53 | } 54 | _ => {} 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /examples/load-gltf.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | use baryon::window::{Event, Window}; 3 | 4 | let window = Window::new().title("Load GLTF").build(); 5 | let mut context = pollster::block_on(baryon::Context::init().build(&window)); 6 | let mut scene = baryon::Scene::new(); 7 | 8 | let node = scene.add_node().build(); 9 | let module = baryon::asset::load_gltf( 10 | format!( 11 | //"{}/examples/assets/Duck/Duck.gltf", 12 | "{}/../rendering-demo-scenes/pbr-test/pbr-test.glb", 13 | env!("CARGO_MANIFEST_DIR") 14 | ), 15 | &mut scene, 16 | node, 17 | &mut context, 18 | ); 19 | 20 | let mut pass = baryon::pass::Real::new( 21 | &baryon::pass::RealConfig { 22 | cull_back_faces: true, 23 | max_lights: 4, 24 | }, 25 | &context, 26 | ); 27 | 28 | window.run(move |event| match event { 29 | Event::Resize { width, height } => { 30 | context.resize(width, height); 31 | } 32 | Event::Draw => { 33 | let camera = module.cameras.find("Camera").unwrap(); 34 | context.present(&mut pass, &scene, camera); 35 | } 36 | _ => {} 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /examples/load-obj.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | use baryon::window::{Event, Window}; 3 | 4 | let window = Window::new().title("Load OBJ").build(); 5 | let mut context = pollster::block_on(baryon::Context::init().build(&window)); 6 | let mut scene = baryon::Scene::new(); 7 | 8 | let camera = baryon::Camera { 9 | projection: baryon::Projection::Perspective { fov_y: 45.0 }, 10 | depth: 1.0..10.0, 11 | node: scene 12 | .add_node() 13 | .position([-3f32, 2.0, 5.0].into()) 14 | .look_at([1.0, 0.0, 0.0].into(), [0f32, 1.0, 0.0].into()) 15 | .build(), 16 | background: baryon::Color(0xFF203040), 17 | }; 18 | 19 | let _entities = baryon::asset::load_obj( 20 | format!("{}/examples/assets/car.obj", env!("CARGO_MANIFEST_DIR")), 21 | &mut scene, 22 | baryon::NodeRef::default(), 23 | &mut context, 24 | ); 25 | 26 | let mut pass = baryon::pass::Solid::new( 27 | &baryon::pass::SolidConfig { 28 | cull_back_faces: true, 29 | }, 30 | &context, 31 | ); 32 | 33 | window.run(move |event| match event { 34 | Event::Resize { width, height } => { 35 | context.resize(width, height); 36 | } 37 | Event::Draw => { 38 | context.present(&mut pass, &scene, &camera); 39 | } 40 | _ => {} 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /examples/phong.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | use baryon::{ 3 | geometry::{Geometry, Streams}, 4 | window::{Event, Window}, 5 | }; 6 | 7 | let window = Window::new().title("Phong").build(); 8 | let mut context = pollster::block_on(baryon::Context::init().build(&window)); 9 | let mut scene = baryon::Scene::new(); 10 | 11 | let camera = baryon::Camera { 12 | projection: baryon::Projection::Perspective { fov_y: 45.0 }, 13 | depth: 1.0..10.0, 14 | node: scene 15 | .add_node() 16 | .position([-1.8f32, 5.0, 2.0].into()) 17 | .look_at([0f32; 3].into(), [0f32, 0.0, 1.0].into()) 18 | .build(), 19 | background: baryon::Color(0xFF203040), 20 | }; 21 | 22 | let _point_light = scene 23 | .add_point_light() 24 | .position([3.0, 3.0, 3.0].into()) 25 | .color(baryon::Color(0xFFFF8080)) 26 | .build(); 27 | let _dir_light = scene 28 | .add_directional_light() 29 | .position([0.0, 0.0, 5.0].into()) 30 | .intensity(3.0) 31 | .color(baryon::Color(0xFF8080FF)) 32 | .build(); 33 | 34 | let prototype = Geometry::sphere(Streams::NORMAL, 1.0, 4).bake(&mut context); 35 | 36 | let _m_flat = scene 37 | .add_entity(&prototype) 38 | .position([-2.5, 0.0, 0.0].into()) 39 | .component(baryon::Color(0xFF808080)) 40 | .component(baryon::pass::Shader::Gouraud { flat: true }) 41 | .build(); 42 | let _m_gouraud = scene 43 | .add_entity(&prototype) 44 | .position([0.0, 0.0, 0.0].into()) 45 | .component(baryon::Color(0xFF808080)) 46 | .component(baryon::pass::Shader::Gouraud { flat: false }) 47 | .build(); 48 | let _m_phong = scene 49 | .add_entity(&prototype) 50 | .position([2.5, 0.0, 0.0].into()) 51 | .component(baryon::Color(0xFF808080)) 52 | .component(baryon::pass::Shader::Phong { glossiness: 10 }) 53 | .build(); 54 | 55 | let mut pass = baryon::pass::Phong::new( 56 | &baryon::pass::PhongConfig { 57 | cull_back_faces: true, 58 | max_lights: 10, 59 | ambient: baryon::pass::Ambient { 60 | color: baryon::Color(0xFFFFFFFF), 61 | intensity: 0.2, 62 | }, 63 | }, 64 | &context, 65 | ); 66 | 67 | window.run(move |event| match event { 68 | Event::Resize { width, height } => { 69 | context.resize(width, height); 70 | } 71 | Event::Draw => { 72 | context.present(&mut pass, &scene, &camera); 73 | } 74 | _ => {} 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /examples/scene.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | use baryon::{ 3 | geometry::{Geometry, Streams}, 4 | pass::{Phong, PhongConfig, Shader}, 5 | window::{Event, Window}, 6 | Camera, Color, Context, Projection, Scene, 7 | }; 8 | 9 | let window = Window::new().title("Scene").build(); 10 | let mut context = pollster::block_on(Context::init().build(&window)); 11 | let mut scene = Scene::new(); 12 | 13 | let camera = Camera { 14 | projection: Projection::Perspective { fov_y: 45.0 }, 15 | depth: 1.0..10.0, 16 | node: scene 17 | .add_node() 18 | .position([-2.0, 2.5, 5.0].into()) 19 | .look_at([0f32; 3].into(), [0f32, 1.0, 0.0].into()) 20 | .build(), 21 | background: Color::BLACK_OPAQUE, 22 | }; 23 | 24 | scene 25 | .add_point_light() 26 | .position([4.0, 8.0, 4.0].into()) 27 | .color(Color(0x00AAAAAA)) 28 | .build(); 29 | scene 30 | .add_entity(&Geometry::plane(5.0).bake(&mut context)) 31 | .position([0.0, 0.0, 0.0].into()) 32 | .component(Color(0xFF006400)) 33 | .component(Shader::Phong { glossiness: 100 }) 34 | .build(); 35 | scene 36 | .add_entity(&Geometry::cuboid(Streams::NORMAL, [0.5, 0.5, 0.5].into()).bake(&mut context)) 37 | .position([0.0, 0.25, 0.0].into()) 38 | .component(Color::new(0.8, 0.7, 0.6, 1.0)) 39 | .component(Shader::Phong { glossiness: 100 }) 40 | .build(); 41 | 42 | let mut pass = Phong::new( 43 | &PhongConfig { 44 | ..Default::default() 45 | }, 46 | &context, 47 | ); 48 | 49 | window.run(move |event| match event { 50 | Event::Resize { width, height } => { 51 | context.resize(width, height); 52 | } 53 | Event::Draw => { 54 | context.present(&mut pass, &scene, &camera); 55 | } 56 | _ => {} 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /examples/shapes.rs: -------------------------------------------------------------------------------- 1 | // https://nical.github.io/posts/lyon-intro.html 2 | 3 | use baryon::{ 4 | geometry::Geometry, 5 | pass, 6 | window::{Event, Window}, 7 | Camera, Color, Projection, 8 | }; 9 | use lyon::{algorithms::aabb::bounding_rect, math::point, path::Path, tessellation::StrokeOptions}; 10 | 11 | fn main() { 12 | let window = Window::new().title("Shapes").build(); 13 | let mut context = pollster::block_on(baryon::Context::init().build(&window)); 14 | let mut scene = baryon::Scene::new(); 15 | 16 | // Build a Path. 17 | let mut builder = Path::builder(); 18 | builder.begin(point(5.0, 5.0)); 19 | builder.cubic_bezier_to(point(5.0, 5.0), point(4.0, 0.0), point(0.0, 0.0)); 20 | builder.cubic_bezier_to(point(-6.0, 0.0), point(-6.0, 7.0), point(-6.0, 7.0)); 21 | builder.cubic_bezier_to(point(-6.0, 11.0), point(-3.0, 15.4), point(5.0, 19.0)); 22 | builder.cubic_bezier_to(point(12.0, 15.4), point(16.0, 11.0), point(16.0, 7.0)); 23 | builder.cubic_bezier_to(point(16.0, 7.0), point(16.0, 0.0), point(10.0, 0.0)); 24 | builder.cubic_bezier_to(point(7.0, 0.0), point(5.0, 5.0), point(5.0, 5.0)); 25 | builder.end(true); 26 | let path = builder.build(); 27 | let bbox = bounding_rect(path.iter()); 28 | let pos = mint::Vector3 { 29 | x: bbox.size.width / -4.0, 30 | y: bbox.size.height / -2.0, 31 | z: 0.0, 32 | }; 33 | scene 34 | .add_entity(&Geometry::stroke(&path, &StrokeOptions::default()).bake(&mut context)) 35 | .component(Color::from_rgba([1.0, 1.0, 1.0, 1.0])) 36 | .position(pos) 37 | .build(); 38 | scene 39 | .add_entity(&Geometry::fill(&path).bake(&mut context)) 40 | .component(Color::RED) 41 | .position(pos) 42 | .build(); 43 | 44 | let camera = Camera { 45 | projection: Projection::Perspective { fov_y: 70.0 }, 46 | depth: 1.0..100.0, 47 | node: scene 48 | .add_node() 49 | .position([0.0f32, 0.0, -30.0].into()) 50 | .look_at([0f32; 3].into(), [0f32, -1.0, 0.0].into()) 51 | .build(), 52 | background: baryon::Color::BLACK_OPAQUE, 53 | }; 54 | 55 | let mut pass = pass::Solid::new( 56 | &pass::SolidConfig { 57 | cull_back_faces: false, 58 | }, 59 | &context, 60 | ); 61 | 62 | window.run(move |event| match event { 63 | Event::Resize { width, height } => { 64 | context.resize(width, height); 65 | } 66 | Event::Draw => { 67 | context.present(&mut pass, &scene, &camera); 68 | } 69 | _ => {} 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /examples/sprite.rs: -------------------------------------------------------------------------------- 1 | use std::time; 2 | 3 | //TODO: a mechanism like this should be a part of the engine 4 | struct Animator { 5 | map: baryon::asset::SpriteMap, 6 | cell_counts: mint::Vector2, 7 | duration: time::Duration, 8 | sprite: baryon::EntityRef, 9 | current: mint::Point2, 10 | moment: time::Instant, 11 | } 12 | 13 | #[repr(usize)] 14 | #[derive(Clone, Copy, Debug, PartialEq)] 15 | enum State { 16 | Idle = 0, 17 | MoveRight = 9, 18 | MoveLeft = 8, 19 | Kick = 4, 20 | Jump = 10, 21 | Lie = 12, 22 | } 23 | impl Default for State { 24 | fn default() -> Self { 25 | Self::Idle 26 | } 27 | } 28 | 29 | impl Animator { 30 | fn update_uv(&mut self, scene: &mut baryon::Scene) { 31 | let uv_range = self.map.at(self.current); 32 | scene 33 | .world 34 | .get_mut::(self.sprite) 35 | .unwrap() 36 | .uv = Some(uv_range); 37 | } 38 | 39 | fn switch(&mut self, state: State, scene: &mut baryon::Scene) { 40 | self.moment = time::Instant::now(); 41 | self.current.x = 0; 42 | self.current.y = state as usize; 43 | self.update_uv(scene); 44 | } 45 | 46 | fn tick(&mut self, scene: &mut baryon::Scene) { 47 | if self.moment.elapsed() < self.duration { 48 | return; 49 | } 50 | 51 | self.current.x += 1; 52 | self.moment = time::Instant::now(); 53 | if self.current.x == self.cell_counts.x { 54 | self.current.x = 0; 55 | self.current.y = State::Idle as usize; 56 | // don't update the scene here, so that 57 | // input can have a chance to transition 58 | // to something other than `Idle`. 59 | } else { 60 | self.update_uv(scene); 61 | } 62 | } 63 | } 64 | 65 | fn main() { 66 | use baryon::window::{Event, Key, Window}; 67 | 68 | let window = Window::new().title("Sprite").build(); 69 | let mut context = pollster::block_on(baryon::Context::init().build(&window)); 70 | let mut scene = baryon::Scene::new(); 71 | let camera = baryon::Camera { 72 | projection: baryon::Projection::Orthographic { 73 | // the sprite configuration is not centered 74 | center: [0.0, -10.0].into(), 75 | extent_y: 40.0, 76 | }, 77 | ..Default::default() 78 | }; 79 | let mut pass = baryon::pass::Flat::new(&context); 80 | 81 | let image = context.load_image(format!( 82 | "{}/examples/assets/pickachu.png", 83 | env!("CARGO_MANIFEST_DIR") 84 | )); 85 | let sprite = scene.add_sprite(image).build(); 86 | 87 | let mut anim = Animator { 88 | map: baryon::asset::SpriteMap { 89 | origin: mint::Point2 { x: 0, y: 0 }, 90 | cell_size: mint::Vector2 { x: 96, y: 96 }, 91 | }, 92 | cell_counts: mint::Vector2 { x: 5, y: 13 }, 93 | duration: time::Duration::from_secs_f64(0.1), 94 | current: mint::Point2 { x: 0, y: 0 }, 95 | moment: time::Instant::now(), 96 | sprite, 97 | }; 98 | anim.switch(State::Idle, &mut scene); 99 | 100 | window.run(move |event| match event { 101 | Event::Resize { width, height } => { 102 | context.resize(width, height); 103 | } 104 | Event::Keyboard { key, pressed: true } => { 105 | let new_state = match key { 106 | Key::Up => Some(State::Jump), 107 | Key::Down => Some(State::Lie), 108 | Key::Space => Some(State::Kick), 109 | Key::Left => Some(State::MoveLeft), 110 | Key::Right => Some(State::MoveRight), 111 | _ => None, 112 | }; 113 | if let Some(state) = new_state { 114 | if anim.current.y != state as usize || state == State::Kick { 115 | anim.switch(state, &mut scene); 116 | } 117 | } 118 | } 119 | Event::Draw => { 120 | anim.tick(&mut scene); 121 | context.present(&mut pass, &scene, &camera); 122 | } 123 | _ => {} 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /src/asset/gltf.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::VecDeque, ops, path::Path}; 2 | 3 | #[derive(Default)] 4 | struct MeshScratch { 5 | indices: Vec, 6 | positions: Vec, 7 | tex_coords: Vec, 8 | normals: Vec, 9 | } 10 | 11 | struct Texture { 12 | image: crate::ImageRef, 13 | } 14 | 15 | struct Primitive { 16 | prototype: crate::Prototype, 17 | color: crate::Color, 18 | shader: crate::pass::Shader, 19 | material: crate::pass::Material, 20 | } 21 | 22 | fn load_texture(mut data: gltf::image::Data, context: &mut crate::Context) -> Texture { 23 | let format = match data.format { 24 | gltf::image::Format::R8 => wgpu::TextureFormat::R8Unorm, 25 | gltf::image::Format::R8G8 => wgpu::TextureFormat::Rg8Unorm, 26 | gltf::image::Format::R8G8B8 | gltf::image::Format::B8G8R8 => { 27 | log::warn!( 28 | "Converting {}x{} texture from RGB to RGBA...", 29 | data.width, 30 | data.height 31 | ); 32 | let original = data.pixels; 33 | data.pixels = Vec::with_capacity(original.len() * 4 / 3); 34 | for chunk in original.chunks(3) { 35 | data.pixels.push(chunk[0]); 36 | data.pixels.push(chunk[1]); 37 | data.pixels.push(chunk[2]); 38 | data.pixels.push(0xFF); 39 | } 40 | if data.format == gltf::image::Format::R8G8B8 { 41 | wgpu::TextureFormat::Rgba8UnormSrgb 42 | } else { 43 | wgpu::TextureFormat::Bgra8UnormSrgb 44 | } 45 | } 46 | gltf::image::Format::R16G16B16 => panic!("RGB16 is outdated"), 47 | gltf::image::Format::R8G8B8A8 => wgpu::TextureFormat::Rgba8UnormSrgb, 48 | gltf::image::Format::B8G8R8A8 => wgpu::TextureFormat::Bgra8UnormSrgb, 49 | gltf::image::Format::R16 => wgpu::TextureFormat::R16Float, 50 | gltf::image::Format::R16G16 => wgpu::TextureFormat::Rg16Float, 51 | gltf::image::Format::R16G16B16A16 => wgpu::TextureFormat::Rgba16Float, 52 | }; 53 | 54 | let desc = wgpu::TextureDescriptor { 55 | label: None, 56 | size: wgpu::Extent3d { 57 | width: data.width, 58 | height: data.height, 59 | depth_or_array_layers: 1, 60 | }, 61 | mip_level_count: 1, 62 | sample_count: 1, 63 | dimension: wgpu::TextureDimension::D2, 64 | format, 65 | usage: wgpu::TextureUsages::TEXTURE_BINDING, 66 | }; 67 | let image = context.add_image_from_data(&desc, &data.pixels); 68 | Texture { image } 69 | } 70 | 71 | fn load_primitive<'a>( 72 | primitive: gltf::Primitive<'a>, 73 | buffers: &[gltf::buffer::Data], 74 | textures: &[Texture], 75 | context: &mut crate::Context, 76 | scratch: &mut MeshScratch, 77 | ) -> Primitive { 78 | let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()].0)); 79 | let mut mesh_builder = context.add_mesh(); 80 | 81 | if let Some(indices) = reader.read_indices() { 82 | scratch.indices.clear(); 83 | scratch.indices.extend(indices.into_u32().map(|i| i as u16)); 84 | mesh_builder.index(&scratch.indices); 85 | } 86 | 87 | if let Some(positions) = reader.read_positions() { 88 | scratch.positions.clear(); 89 | scratch.positions.extend(positions.map(crate::Position)); 90 | mesh_builder.vertex(&scratch.positions); 91 | } 92 | 93 | if let Some(tex_coords) = reader.read_tex_coords(0) { 94 | scratch.tex_coords.clear(); 95 | scratch 96 | .tex_coords 97 | .extend(tex_coords.into_u16().map(crate::TexCoords)); 98 | mesh_builder.vertex(&scratch.tex_coords); 99 | } 100 | 101 | if let Some(normals) = reader.read_normals() { 102 | scratch.normals.clear(); 103 | scratch.normals.extend(normals.map(crate::Normal)); 104 | mesh_builder.vertex(&scratch.normals); 105 | } 106 | 107 | let mat = primitive.material(); 108 | let pbr = mat.pbr_metallic_roughness(); 109 | let base_color = pbr.base_color_factor(); 110 | let material = crate::pass::Material { 111 | base_color_map: pbr 112 | .base_color_texture() 113 | .map(|t| textures[t.texture().index()].image), 114 | emissive_color: crate::Color::from_rgb_alpha(mat.emissive_factor(), 0.0), 115 | metallic_factor: pbr.metallic_factor(), 116 | roughness_factor: pbr.roughness_factor(), 117 | normal_scale: 1.0, 118 | occlusion_strength: 1.0, 119 | }; 120 | 121 | Primitive { 122 | prototype: mesh_builder.build(), 123 | color: crate::Color::from_rgba(base_color), 124 | shader: crate::pass::Shader::Gouraud { flat: true }, //TODO 125 | material, 126 | } 127 | } 128 | 129 | #[derive(Debug)] 130 | struct Named { 131 | data: T, 132 | name: Option, 133 | } 134 | 135 | #[derive(Debug)] 136 | pub struct NamedVec(Vec>); 137 | 138 | impl Default for NamedVec { 139 | fn default() -> Self { 140 | Self(Vec::new()) 141 | } 142 | } 143 | 144 | impl ops::Index for NamedVec { 145 | type Output = T; 146 | fn index(&self, index: usize) -> &T { 147 | &self.0[index].data 148 | } 149 | } 150 | 151 | impl NamedVec { 152 | pub fn iter(&self) -> impl Iterator { 153 | self.0.iter().map(|elem| &elem.data) 154 | } 155 | 156 | pub fn find(&self, name: &str) -> Option<&T> { 157 | self.0 158 | .iter() 159 | .find(|elem| elem.name.as_deref() == Some(name)) 160 | .map(|elem| &elem.data) 161 | } 162 | } 163 | 164 | #[derive(Default)] 165 | pub struct Module { 166 | pub entities: NamedVec, 167 | pub cameras: NamedVec, 168 | pub lights: NamedVec, 169 | } 170 | 171 | /// Load mesh from glTF 2.0 format. 172 | pub fn load_gltf( 173 | path: impl AsRef, 174 | scene: &mut crate::Scene, 175 | global_parent: crate::NodeRef, 176 | context: &mut crate::Context, 177 | ) -> Module { 178 | let mut module = Module::default(); 179 | let (gltf, buffers, images) = gltf::import(path).expect("invalid glTF 2.0"); 180 | 181 | let mut textures = Vec::with_capacity(images.len()); 182 | for (_texture, data) in gltf.textures().zip(images.into_iter()) { 183 | let texture = load_texture(data, context); 184 | textures.push(texture); 185 | } 186 | 187 | let mut prototypes = Vec::with_capacity(gltf.meshes().len()); 188 | let mut scratch = MeshScratch::default(); 189 | for gltf_mesh in gltf.meshes() { 190 | let mut primitives = Vec::new(); 191 | for gltf_primitive in gltf_mesh.primitives() { 192 | let primitive = 193 | load_primitive(gltf_primitive, &buffers, &textures, context, &mut scratch); 194 | primitives.push(primitive); 195 | } 196 | prototypes.push(primitives); 197 | } 198 | 199 | struct PreNode<'a> { 200 | gltf_node: gltf::Node<'a>, 201 | parent: crate::NodeRef, 202 | } 203 | 204 | let mut deque = VecDeque::new(); 205 | for gltf_scene in gltf.scenes() { 206 | deque.extend(gltf_scene.nodes().map(|gltf_node| PreNode { 207 | gltf_node, 208 | parent: global_parent, 209 | })); 210 | } 211 | 212 | while let Some(PreNode { gltf_node, parent }) = deque.pop_front() { 213 | log::debug!("Node {:?}", gltf_node.name()); 214 | 215 | let (translation, rotation, scale) = gltf_node.transform().decomposed(); 216 | let uniform_scale = if scale[1] != scale[0] || scale[2] != scale[0] { 217 | log::warn!( 218 | "Node[{}] scale {:?} is non-uniform", 219 | gltf_node.index(), 220 | scale 221 | ); 222 | (scale[0] + scale[1] + scale[2]) / 3.0 223 | } else { 224 | scale[0] 225 | }; 226 | 227 | let node = scene 228 | .add_node() 229 | .parent(parent) 230 | .position(translation.into()) 231 | .orientation(rotation.into()) 232 | .scale(uniform_scale) 233 | .build(); 234 | 235 | for gltf_child in gltf_node.children() { 236 | deque.push_back(PreNode { 237 | gltf_node: gltf_child, 238 | parent: node, 239 | }); 240 | } 241 | 242 | if let Some(gltf_mesh) = gltf_node.mesh() { 243 | log::debug!("Mesh {:?}", gltf_mesh.name()); 244 | for primitive in prototypes[gltf_mesh.index()].iter() { 245 | let entity = scene 246 | .add_entity(&primitive.prototype) 247 | .component(primitive.color) 248 | .component(primitive.shader) 249 | .component(primitive.material) 250 | .parent(node) 251 | .build(); 252 | module.entities.0.push(Named { 253 | data: entity, 254 | name: gltf_mesh.name().map(str::to_string), 255 | }); 256 | } 257 | } 258 | 259 | if let Some(gltf_camera) = gltf_node.camera() { 260 | let (depth, projection) = match gltf_camera.projection() { 261 | gltf::camera::Projection::Orthographic(p) => ( 262 | p.znear()..p.zfar(), 263 | bc::Projection::Orthographic { 264 | center: [0.0; 2].into(), 265 | //Note: p.xmag() is ignored 266 | extent_y: p.ymag(), 267 | }, 268 | ), 269 | gltf::camera::Projection::Perspective(p) => ( 270 | p.znear()..p.zfar().unwrap_or(f32::INFINITY), 271 | bc::Projection::Perspective { 272 | fov_y: p.yfov().to_degrees(), 273 | }, 274 | ), 275 | }; 276 | log::debug!( 277 | "Camera {:?} depth {:?} proj {:?} at {:?}", 278 | gltf_camera.name(), 279 | depth, 280 | projection, 281 | scene[node] 282 | ); 283 | module.cameras.0.push(Named { 284 | data: bc::Camera { 285 | projection, 286 | depth, 287 | node, 288 | background: bc::Color::default(), 289 | }, 290 | name: gltf_camera.name().map(str::to_string), 291 | }); 292 | } 293 | 294 | if let Some(gltf_light) = gltf_node.light() { 295 | use gltf::khr_lights_punctual::Kind as LightKind; 296 | let kind = match gltf_light.kind() { 297 | LightKind::Directional => bc::LightKind::Directional, 298 | LightKind::Point => bc::LightKind::Point, 299 | LightKind::Spot { .. } => { 300 | log::warn!("Spot lights are not supported: {:?}", gltf_light.name()); 301 | continue; 302 | } 303 | }; 304 | let light = scene 305 | .add_light(kind) 306 | .intensity(gltf_light.intensity()) 307 | .color(crate::Color::from_rgb_alpha(gltf_light.color(), 0.0)) 308 | .parent(node) 309 | .build(); 310 | module.lights.0.push(Named { 311 | data: light, 312 | name: gltf_light.name().map(str::to_string), 313 | }); 314 | } 315 | } 316 | 317 | module 318 | } 319 | -------------------------------------------------------------------------------- /src/asset/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "gltf")] 2 | mod gltf; 3 | #[cfg(feature = "obj")] 4 | mod obj; 5 | 6 | #[cfg(feature = "gltf")] 7 | pub use self::gltf::load_gltf; 8 | #[cfg(feature = "obj")] 9 | pub use self::obj::load_obj; 10 | 11 | /// A common ancestor of "sprite sheet", "tile map". 12 | pub struct SpriteMap { 13 | pub origin: mint::Point2, 14 | pub cell_size: mint::Vector2, 15 | } 16 | 17 | impl SpriteMap { 18 | pub fn at(&self, index: mint::Point2) -> crate::UvRange { 19 | let begin = mint::Point2 { 20 | x: index.x as i16 * self.cell_size.x as i16, 21 | y: index.y as i16 * self.cell_size.y as i16, 22 | }; 23 | let end = mint::Point2 { 24 | x: begin.x + self.cell_size.x as i16, 25 | y: begin.y + self.cell_size.y as i16, 26 | }; 27 | begin..end 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/asset/obj.rs: -------------------------------------------------------------------------------- 1 | use std::{iter, path::Path}; 2 | 3 | /// Load entities from Wavefront Obj format. 4 | pub fn load_obj( 5 | path: impl AsRef, 6 | scene: &mut crate::Scene, 7 | node: crate::NodeRef, 8 | context: &mut crate::Context, 9 | ) -> fxhash::FxHashMap { 10 | let mut obj = obj::Obj::load(path).unwrap(); 11 | obj.load_mtls().unwrap(); 12 | 13 | let mut entities = fxhash::FxHashMap::default(); 14 | let mut positions = Vec::new(); 15 | let mut normals = Vec::new(); 16 | 17 | for object in obj.data.objects { 18 | for group in object.groups { 19 | positions.clear(); 20 | normals.clear(); 21 | 22 | for poly in group.polys.iter() { 23 | let tr0 = [0usize, 1, 2]; 24 | let tr1 = if poly.0.len() > 3 { 25 | if poly.0.len() > 4 { 26 | log::warn!("Object geometry contains pentagons!"); 27 | } 28 | Some([2usize, 3, 0]) 29 | } else { 30 | None 31 | }; 32 | for triangle in iter::once(tr0).chain(tr1) { 33 | for &elem_index in triangle.iter() { 34 | let obj::IndexTuple(pos_id, _tex_id, nor_id) = poly.0[elem_index]; 35 | positions.push(crate::Position(obj.data.position[pos_id])); 36 | if let Some(index) = nor_id { 37 | normals.push(crate::Normal(obj.data.normal[index])); 38 | } 39 | } 40 | } 41 | } 42 | 43 | let mut mesh_builder = context.add_mesh(); 44 | mesh_builder.vertex(&positions); 45 | if !normals.is_empty() { 46 | mesh_builder.vertex(&normals); 47 | } 48 | let prototype = mesh_builder.build(); 49 | let mut entity_builder = scene.add_entity(&prototype); 50 | entity_builder.parent(node); 51 | 52 | log::info!( 53 | "\tmaterial {} with {} positions and {} normals", 54 | group.name, 55 | positions.len(), 56 | normals.len() 57 | ); 58 | if let Some(obj::ObjMaterial::Mtl(ref mat)) = group.material { 59 | if let Some(cf) = mat.kd { 60 | let color = cf.iter().fold(0xFF, |u, c| { 61 | (u << 8) + (c * 255.0).max(0.0).min(255.0) as u32 62 | }); 63 | entity_builder.component(crate::Color(color)); 64 | } 65 | if !normals.is_empty() { 66 | if let Some(glossiness) = mat.ns { 67 | entity_builder.component(crate::pass::Shader::Phong { 68 | glossiness: glossiness as u8, 69 | }) 70 | } else { 71 | entity_builder.component(crate::pass::Shader::Gouraud { flat: false }) 72 | }; 73 | } 74 | } 75 | 76 | entities.insert(group.name, (entity_builder.build(), prototype)); 77 | } 78 | } 79 | 80 | entities 81 | } 82 | -------------------------------------------------------------------------------- /src/geometry/cuboid.rs: -------------------------------------------------------------------------------- 1 | use std::iter; 2 | 3 | impl super::Geometry { 4 | pub fn cuboid(streams: super::Streams, half_extent: mint::Vector3) -> Self { 5 | let pos = |x, y, z| { 6 | crate::Position([ 7 | (x as f32) * half_extent.x, 8 | (y as f32) * half_extent.y, 9 | (z as f32) * half_extent.z, 10 | ]) 11 | }; 12 | 13 | // bounding radius is half of the diagonal length 14 | let radius = (half_extent.x * half_extent.x 15 | + half_extent.y * half_extent.y 16 | + half_extent.z * half_extent.z) 17 | .sqrt(); 18 | 19 | if streams.contains(super::Streams::NORMAL) { 20 | let positions = vec![ 21 | // top (0, 0, 1) 22 | pos(-1, -1, 1), 23 | pos(1, -1, 1), 24 | pos(1, 1, 1), 25 | pos(-1, 1, 1), 26 | // bottom (0, 0, -1) 27 | pos(-1, 1, -1), 28 | pos(1, 1, -1), 29 | pos(1, -1, -1), 30 | pos(-1, -1, -1), 31 | // right (1, 0, 0) 32 | pos(1, -1, -1), 33 | pos(1, 1, -1), 34 | pos(1, 1, 1), 35 | pos(1, -1, 1), 36 | // left (-1, 0, 0) 37 | pos(-1, -1, 1), 38 | pos(-1, 1, 1), 39 | pos(-1, 1, -1), 40 | pos(-1, -1, -1), 41 | // front (0, 1, 0) 42 | pos(1, 1, -1), 43 | pos(-1, 1, -1), 44 | pos(-1, 1, 1), 45 | pos(1, 1, 1), 46 | // back (0, -1, 0) 47 | pos(1, -1, 1), 48 | pos(-1, -1, 1), 49 | pos(-1, -1, -1), 50 | pos(1, -1, -1), 51 | ]; 52 | 53 | let normals = [ 54 | crate::Normal([0.0, 0.0, 1.0]), 55 | crate::Normal([0.0, 0.0, -1.0]), 56 | crate::Normal([1.0, 0.0, 0.0]), 57 | crate::Normal([-1.0, 0.0, 0.0]), 58 | crate::Normal([0.0, 1.0, 0.0]), 59 | crate::Normal([0.0, -1.0, 0.0]), 60 | ] 61 | .iter() 62 | .flat_map(|&n| iter::repeat(n).take(4)) 63 | .collect::>(); 64 | 65 | let indices = vec![ 66 | 0u16, 1, 2, 2, 3, 0, // top 67 | 4, 5, 6, 6, 7, 4, // bottom 68 | 8, 9, 10, 10, 11, 8, // right 69 | 12, 13, 14, 14, 15, 12, // left 70 | 16, 17, 18, 18, 19, 16, // front 71 | 20, 21, 22, 22, 23, 20, // back 72 | ]; 73 | 74 | Self { 75 | radius, 76 | positions, 77 | normals: Some(normals), 78 | indices: Some(indices), 79 | } 80 | } else { 81 | let positions = vec![ 82 | // top (0, 0, 1) 83 | pos(-1, -1, 1), 84 | pos(1, -1, 1), 85 | pos(1, 1, 1), 86 | pos(-1, 1, 1), 87 | // bottom (0, 0, -1) 88 | pos(-1, 1, -1), 89 | pos(1, 1, -1), 90 | pos(1, -1, -1), 91 | pos(-1, -1, -1), 92 | ]; 93 | 94 | let indices = vec![ 95 | 0u16, 1, 2, 2, 3, 0, // top 96 | 4, 5, 6, 6, 7, 4, // bottom 97 | 6, 5, 2, 2, 1, 6, // right 98 | 0, 3, 4, 4, 7, 0, // left 99 | 5, 4, 3, 3, 2, 5, // front 100 | 1, 0, 7, 7, 6, 1, // back 101 | ]; 102 | 103 | Self { 104 | radius, 105 | positions, 106 | normals: None, 107 | indices: Some(indices), 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/geometry/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cuboid; 2 | pub mod plane; 3 | #[cfg(feature = "shape")] 4 | pub mod shape; 5 | pub mod sphere; 6 | 7 | bitflags::bitflags!( 8 | /// Types of optional vertex streams. 9 | pub struct Streams: u32 { 10 | const NORMAL = 1 << 1; 11 | } 12 | ); 13 | 14 | pub struct Geometry { 15 | pub positions: Vec, 16 | pub normals: Option>, 17 | pub indices: Option>, 18 | pub radius: f32, 19 | } 20 | 21 | impl Geometry { 22 | pub fn bake(&self, context: &mut bc::Context) -> bc::Prototype { 23 | let mut mb = context.add_mesh(); 24 | mb.radius(self.radius); 25 | mb.vertex(&self.positions); 26 | if let Some(ref stream) = self.normals { 27 | mb.vertex(stream); 28 | } 29 | if let Some(ref indices) = self.indices { 30 | mb.index(indices); 31 | } 32 | mb.build() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/geometry/plane.rs: -------------------------------------------------------------------------------- 1 | use crate::{Normal, Position}; 2 | 3 | impl super::Geometry { 4 | pub fn plane(size: f32) -> Self { 5 | let extent = size / 2.0; 6 | let extent2 = extent.powf(2.0); 7 | let radius = (extent2 + extent2).sqrt(); 8 | 9 | let vertices = [ 10 | ([extent, 0.0, -extent], [0.0, 1.0, 0.0], [1.0, 1.0]), 11 | ([extent, 0.0, extent], [0.0, 1.0, 0.0], [1.0, 0.0]), 12 | ([-extent, 0.0, extent], [0.0, 1.0, 0.0], [0.0, 0.0]), 13 | ([-extent, 0.0, -extent], [0.0, 1.0, 0.0], [0.0, 1.0]), 14 | ]; 15 | 16 | let indices = vec![0, 2, 1, 0, 3, 2]; 17 | 18 | let mut positions = Vec::new(); 19 | let mut normals = Vec::new(); 20 | // let mut uvs = Vec::new(); 21 | for (position, normal, _uv) in vertices.iter() { 22 | positions.push(Position(*position)); 23 | normals.push(Normal(*normal)); 24 | // uvs.push(*uv); 25 | } 26 | 27 | Self { 28 | radius, 29 | positions, 30 | normals: Some(normals), 31 | indices: Some(indices), 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/geometry/shape.rs: -------------------------------------------------------------------------------- 1 | use lyon::path::Path; 2 | use lyon::tessellation::*; 3 | 4 | type PositionBuilder = VertexBuffers; 5 | 6 | fn fill_position(vertex: FillVertex) -> crate::Position { 7 | let p = vertex.position(); 8 | crate::Position([p.x, p.y, 0.0]) 9 | } 10 | 11 | fn stroke_position(vertex: StrokeVertex) -> crate::Position { 12 | let p = vertex.position(); 13 | crate::Position([p.x, p.y, 0.0]) 14 | } 15 | 16 | fn bounding_radius(path: &Path) -> f32 { 17 | path.iter().fold(0.0, |accum, item| { 18 | let p = item.from(); 19 | accum.max(p.x.abs().max(p.y.abs())) 20 | }) 21 | } 22 | 23 | impl super::Geometry { 24 | pub fn fill(path: &Path) -> Self { 25 | let mut buffer = PositionBuilder::new(); 26 | let builder = &mut BuffersBuilder::new(&mut buffer, fill_position); 27 | let mut tessellator = FillTessellator::new(); 28 | tessellator 29 | .tessellate_path(path, &FillOptions::default(), builder) 30 | .unwrap(); 31 | 32 | let radius = bounding_radius(path); 33 | 34 | Self { 35 | positions: buffer.vertices, 36 | indices: Some(buffer.indices), 37 | normals: None, 38 | radius, 39 | } 40 | } 41 | 42 | pub fn stroke(path: &Path, options: &StrokeOptions) -> Self { 43 | let mut buffer = PositionBuilder::new(); 44 | let builder = &mut BuffersBuilder::new(&mut buffer, stroke_position); 45 | let mut tessellator = StrokeTessellator::new(); 46 | tessellator.tessellate_path(path, options, builder).unwrap(); 47 | 48 | let radius = bounding_radius(path); 49 | 50 | Self { 51 | positions: buffer.vertices, 52 | indices: Some(buffer.indices), 53 | normals: None, 54 | radius, 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/geometry/sphere.rs: -------------------------------------------------------------------------------- 1 | // See https://github.com/gfx-rs/genmesh/blob/master/src/icosphere.rs 2 | 3 | const F: f32 = 1.618034; // 0.5 * (1.0 + 5f32.sqrt()); 4 | 5 | // Base icosahedron positions 6 | const BASE_POSITIONS: [[f32; 3]; 12] = [ 7 | [-1.0, F, 0.0], 8 | [1.0, F, 0.0], 9 | [-1.0, -F, 0.0], 10 | [1.0, -F, 0.0], 11 | [0.0, -1.0, F], 12 | [0.0, 1.0, F], 13 | [0.0, -1.0, -F], 14 | [0.0, 1.0, -F], 15 | [F, 0.0, -1.0], 16 | [F, 0.0, 1.0], 17 | [-F, 0.0, -1.0], 18 | [-F, 0.0, 1.0], 19 | ]; 20 | 21 | // Base icosahedron faces 22 | const BASE_FACES: [[u16; 3]; 20] = [ 23 | [0, 11, 5], 24 | [0, 5, 1], 25 | [0, 1, 7], 26 | [0, 7, 10], 27 | [0, 10, 11], 28 | [11, 10, 2], 29 | [5, 11, 4], 30 | [1, 5, 9], 31 | [7, 1, 8], 32 | [10, 7, 6], 33 | [3, 9, 4], 34 | [3, 4, 2], 35 | [3, 2, 6], 36 | [3, 6, 8], 37 | [3, 8, 9], 38 | [9, 8, 1], 39 | [4, 9, 5], 40 | [2, 4, 11], 41 | [6, 2, 10], 42 | [8, 6, 7], 43 | ]; 44 | 45 | impl super::Geometry { 46 | pub fn sphere(streams: super::Streams, radius: f32, detail: usize) -> Self { 47 | assert!(detail < 30); // just a sanity check 48 | let mut lookup = fxhash::FxHashMap::default(); 49 | let mut prev_faces = Vec::new(); 50 | let mut vertices = BASE_POSITIONS 51 | .iter() 52 | .map(|p| glam::Vec3::from_slice(p)) 53 | .collect::>(); 54 | let mut faces = BASE_FACES.to_vec(); 55 | 56 | for _ in 1..detail { 57 | lookup.clear(); 58 | prev_faces.clear(); 59 | prev_faces.append(&mut faces); 60 | 61 | for face in prev_faces.iter() { 62 | let mut mid = [0u16; 3]; 63 | for (pair, index) in face 64 | .iter() 65 | .cloned() 66 | .zip(face[1..].iter().chain(face.first()).cloned()) 67 | .zip(mid.iter_mut()) 68 | { 69 | *index = match lookup.get(&pair) { 70 | Some(i) => *i, 71 | None => { 72 | let i = vertices.len() as u16; 73 | lookup.insert(pair, i); 74 | lookup.insert((pair.1, pair.0), i); 75 | let v = 0.5 * (vertices[pair.0 as usize] + vertices[pair.1 as usize]); 76 | vertices.push(v); 77 | i 78 | } 79 | }; 80 | } 81 | 82 | faces.push([face[0], mid[0], mid[2]]); 83 | faces.push([face[1], mid[1], mid[0]]); 84 | faces.push([face[2], mid[2], mid[1]]); 85 | faces.push([mid[0], mid[1], mid[2]]); 86 | } 87 | } 88 | 89 | let indices = faces.into_iter().flat_map(|face| face).collect::>(); 90 | let mut positions = Vec::with_capacity(vertices.len()); 91 | let mut normals = if streams.contains(super::Streams::NORMAL) { 92 | Some(Vec::with_capacity(vertices.len())) 93 | } else { 94 | None 95 | }; 96 | 97 | for v in vertices { 98 | let n = v.normalize(); 99 | positions.push(crate::Position((n * radius).into())); 100 | if let Some(ref mut normals) = normals { 101 | normals.push(crate::Normal(n.into())); 102 | } 103 | } 104 | 105 | Self { 106 | positions, 107 | normals, 108 | radius, 109 | indices: Some(indices), 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use bc::{ 2 | Camera, Color, Context, Entity, EntityRef, ImageRef, Light, LightBuilder, LightRef, 3 | MeshBuilder, MeshRef, Node, NodeRef, Pass, Projection, Prototype, Scene, Sprite, SpriteBuilder, 4 | TargetInfo, TargetRef, UvRange, 5 | }; 6 | use std::mem; 7 | 8 | pub mod asset; 9 | pub mod geometry; 10 | pub mod pass; 11 | #[cfg(feature = "window")] 12 | pub mod window; 13 | 14 | #[repr(transparent)] 15 | #[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)] 16 | pub struct Position(pub [f32; 3]); 17 | 18 | #[repr(transparent)] 19 | #[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)] 20 | pub struct Normal(pub [f32; 3]); 21 | 22 | #[repr(transparent)] 23 | #[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)] 24 | pub struct TexCoords(pub [u16; 2]); 25 | 26 | impl Position { 27 | const fn layout() -> wgpu::VertexBufferLayout<'static> { 28 | wgpu::VertexBufferLayout { 29 | array_stride: mem::size_of::() as u64, 30 | step_mode: wgpu::VertexStepMode::Vertex, 31 | attributes: &[wgpu::VertexAttribute { 32 | format: wgpu::VertexFormat::Float32x3, 33 | offset: 0, 34 | shader_location: LOCATION, 35 | }], 36 | } 37 | } 38 | } 39 | 40 | impl Normal { 41 | const fn layout() -> wgpu::VertexBufferLayout<'static> { 42 | wgpu::VertexBufferLayout { 43 | array_stride: mem::size_of::() as u64, 44 | step_mode: wgpu::VertexStepMode::Vertex, 45 | attributes: &[wgpu::VertexAttribute { 46 | format: wgpu::VertexFormat::Float32x3, 47 | offset: 0, 48 | shader_location: LOCATION, 49 | }], 50 | } 51 | } 52 | } 53 | 54 | impl TexCoords { 55 | const fn layout() -> wgpu::VertexBufferLayout<'static> { 56 | wgpu::VertexBufferLayout { 57 | array_stride: mem::size_of::() as u64, 58 | step_mode: wgpu::VertexStepMode::Vertex, 59 | attributes: &[wgpu::VertexAttribute { 60 | format: wgpu::VertexFormat::Unorm16x2, 61 | offset: 0, 62 | shader_location: LOCATION, 63 | }], 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/pass/flat.rs: -------------------------------------------------------------------------------- 1 | use bc::ContextDetail as _; 2 | use std::mem; 3 | 4 | #[repr(C)] 5 | #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] 6 | struct Globals { 7 | view_proj: [[f32; 4]; 4], 8 | } 9 | 10 | #[repr(C)] 11 | #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] 12 | struct Locals { 13 | pos_scale: [f32; 4], 14 | rot: [f32; 4], 15 | // x0,y0, x1,y1 16 | bounds: [f32; 4], 17 | // u0,v0, u1,v1 18 | tex_coords: [f32; 4], 19 | } 20 | 21 | #[derive(Eq, Hash, PartialEq)] 22 | struct LocalKey { 23 | uniform_buf_index: usize, 24 | image: crate::ImageRef, 25 | } 26 | 27 | struct Pipelines { 28 | transparent: wgpu::RenderPipeline, 29 | } 30 | 31 | struct Instance { 32 | camera_distance: f32, 33 | locals_bl: super::BufferLocation, 34 | image: crate::ImageRef, 35 | } 36 | 37 | pub struct Flat { 38 | global_uniform_buf: wgpu::Buffer, 39 | global_bind_group: wgpu::BindGroup, 40 | local_bind_group_layout: wgpu::BindGroupLayout, 41 | local_bind_groups: fxhash::FxHashMap, 42 | uniform_pool: super::BufferPool, 43 | pipelines: Pipelines, 44 | temp: Vec, 45 | } 46 | 47 | impl Flat { 48 | pub fn new(context: &crate::Context) -> Self { 49 | Self::new_offscreen(context.surface_info().unwrap(), context) 50 | } 51 | pub fn new_offscreen(target_info: crate::TargetInfo, context: &crate::Context) -> Self { 52 | let d = context.device(); 53 | let shader_module = d.create_shader_module(&wgpu::ShaderModuleDescriptor { 54 | label: Some("flat"), 55 | source: wgpu::ShaderSource::Wgsl(include_str!("flat.wgsl").into()), 56 | }); 57 | 58 | let globals_size = mem::size_of::() as wgpu::BufferAddress; 59 | let global_bgl = d.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 60 | label: Some("flat globals"), 61 | entries: &[ 62 | wgpu::BindGroupLayoutEntry { 63 | binding: 0, 64 | visibility: wgpu::ShaderStages::VERTEX, 65 | ty: wgpu::BindingType::Buffer { 66 | ty: wgpu::BufferBindingType::Uniform, 67 | has_dynamic_offset: false, 68 | min_binding_size: wgpu::BufferSize::new(globals_size), 69 | }, 70 | count: None, 71 | }, 72 | wgpu::BindGroupLayoutEntry { 73 | binding: 1, 74 | visibility: wgpu::ShaderStages::FRAGMENT, 75 | ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), 76 | count: None, 77 | }, 78 | ], 79 | }); 80 | let global_uniform_buf = d.create_buffer(&wgpu::BufferDescriptor { 81 | label: Some("flat globals"), 82 | size: globals_size, 83 | usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 84 | mapped_at_creation: false, 85 | }); 86 | let sampler = d.create_sampler(&wgpu::SamplerDescriptor { 87 | label: Some("flat sampler"), 88 | min_filter: wgpu::FilterMode::Linear, 89 | mag_filter: wgpu::FilterMode::Linear, 90 | ..Default::default() 91 | }); 92 | let global_bind_group = d.create_bind_group(&wgpu::BindGroupDescriptor { 93 | label: Some("flat globals"), 94 | layout: &global_bgl, 95 | entries: &[ 96 | wgpu::BindGroupEntry { 97 | binding: 0, 98 | resource: global_uniform_buf.as_entire_binding(), 99 | }, 100 | wgpu::BindGroupEntry { 101 | binding: 1, 102 | resource: wgpu::BindingResource::Sampler(&sampler), 103 | }, 104 | ], 105 | }); 106 | 107 | let locals_size = mem::size_of::() as wgpu::BufferAddress; 108 | let local_bgl = d.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 109 | label: Some("flat locals"), 110 | entries: &[ 111 | wgpu::BindGroupLayoutEntry { 112 | binding: 0, 113 | visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, 114 | ty: wgpu::BindingType::Buffer { 115 | ty: wgpu::BufferBindingType::Uniform, 116 | has_dynamic_offset: true, 117 | min_binding_size: wgpu::BufferSize::new(locals_size), 118 | }, 119 | count: None, 120 | }, 121 | wgpu::BindGroupLayoutEntry { 122 | binding: 1, 123 | visibility: wgpu::ShaderStages::FRAGMENT, 124 | ty: wgpu::BindingType::Texture { 125 | sample_type: wgpu::TextureSampleType::Float { filterable: true }, 126 | view_dimension: wgpu::TextureViewDimension::D2, 127 | multisampled: false, 128 | }, 129 | count: None, 130 | }, 131 | ], 132 | }); 133 | 134 | let pipeline_layout = d.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 135 | label: Some("flat"), 136 | bind_group_layouts: &[&global_bgl, &local_bgl], 137 | push_constant_ranges: &[], 138 | }); 139 | 140 | let pipelines = { 141 | let transparent = d.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 142 | label: Some("flat-transparent"), 143 | layout: Some(&pipeline_layout), 144 | vertex: wgpu::VertexState { 145 | buffers: &[], 146 | module: &shader_module, 147 | entry_point: "main_vs", 148 | }, 149 | primitive: wgpu::PrimitiveState { 150 | topology: wgpu::PrimitiveTopology::TriangleStrip, 151 | ..Default::default() 152 | }, 153 | depth_stencil: None, 154 | multisample: wgpu::MultisampleState { 155 | count: target_info.sample_count, 156 | ..Default::default() 157 | }, 158 | fragment: Some(wgpu::FragmentState { 159 | targets: &[wgpu::ColorTargetState { 160 | format: target_info.format, 161 | blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), 162 | write_mask: wgpu::ColorWrites::all(), 163 | }], 164 | module: &shader_module, 165 | entry_point: "main_fs", 166 | }), 167 | multiview: None, 168 | }); 169 | 170 | Pipelines { transparent } 171 | }; 172 | 173 | Self { 174 | global_uniform_buf, 175 | global_bind_group, 176 | local_bind_group_layout: local_bgl, 177 | local_bind_groups: Default::default(), 178 | uniform_pool: super::BufferPool::uniform("flat locals", d), 179 | pipelines, 180 | temp: Vec::new(), 181 | } 182 | } 183 | } 184 | 185 | impl bc::Pass for Flat { 186 | fn draw( 187 | &mut self, 188 | targets: &[crate::TargetRef], 189 | scene: &crate::Scene, 190 | camera: &crate::Camera, 191 | context: &crate::Context, 192 | ) { 193 | let target = context.get_target(targets[0]); 194 | let device = context.device(); 195 | 196 | let nodes = scene.bake(); 197 | let cam_node = &nodes[camera.node]; 198 | self.uniform_pool.reset(); 199 | let queue = context.queue(); 200 | 201 | { 202 | let m_proj = camera.projection_matrix(target.aspect()); 203 | let m_view_inv = cam_node.inverse_matrix(); 204 | let m_final = glam::Mat4::from(m_proj) * glam::Mat4::from(m_view_inv); 205 | let globals = Globals { 206 | view_proj: m_final.to_cols_array_2d(), 207 | }; 208 | queue.write_buffer(&self.global_uniform_buf, 0, bytemuck::bytes_of(&globals)); 209 | } 210 | 211 | // gather all sprites 212 | self.temp.clear(); 213 | self.uniform_pool.reset(); 214 | let cam_dir = glam::Quat::from_slice(&cam_node.rot) * -glam::Vec3::Z; 215 | 216 | for (_, (sprite,)) in scene.world.query::<(&bc::Sprite,)>().iter() { 217 | let space = &nodes[sprite.node]; 218 | let cam_vector = glam::Vec3::from_slice(&space.pos_scale) 219 | - glam::Vec3::from_slice(&cam_node.pos_scale); 220 | let camera_distance = cam_vector.dot(cam_dir); 221 | 222 | let image = context.get_image(sprite.image); 223 | let locals = Locals { 224 | pos_scale: space.pos_scale, 225 | rot: space.rot, 226 | bounds: { 227 | let (w, h) = match sprite.uv { 228 | Some(ref uv) => (uv.end.x - uv.start.x, uv.end.y - uv.start.y), 229 | None => (image.size.width as i16, image.size.height as i16), 230 | }; 231 | [ 232 | -0.5 * w as f32, 233 | -0.5 * h as f32, 234 | 0.5 * w as f32, 235 | 0.5 * w as f32, 236 | ] 237 | }, 238 | tex_coords: match sprite.uv { 239 | Some(ref uv) => [ 240 | uv.start.x as f32 / image.size.width as f32, 241 | uv.start.y as f32 / image.size.height as f32, 242 | uv.end.x as f32 / image.size.width as f32, 243 | uv.end.y as f32 / image.size.height as f32, 244 | ], 245 | None => [0.0, 0.0, 1.0, 1.0], 246 | }, 247 | }; 248 | let locals_bl = self.uniform_pool.alloc(&locals, queue); 249 | 250 | // pre-create local bind group, if needed 251 | let local_bgl = &self.local_bind_group_layout; 252 | let key = LocalKey { 253 | uniform_buf_index: locals_bl.index, 254 | image: sprite.image, 255 | }; 256 | let binding = self.uniform_pool.binding::(locals_bl.index); 257 | self.local_bind_groups.entry(key).or_insert_with(|| { 258 | device.create_bind_group(&wgpu::BindGroupDescriptor { 259 | label: Some("flat locals"), 260 | layout: local_bgl, 261 | entries: &[ 262 | wgpu::BindGroupEntry { 263 | binding: 0, 264 | resource: wgpu::BindingResource::Buffer(binding), 265 | }, 266 | wgpu::BindGroupEntry { 267 | binding: 1, 268 | resource: wgpu::BindingResource::TextureView(&image.view), 269 | }, 270 | ], 271 | }) 272 | }); 273 | 274 | self.temp.push(Instance { 275 | camera_distance, 276 | locals_bl, 277 | image: sprite.image, 278 | }); 279 | } 280 | 281 | // sort from back to front 282 | self.temp 283 | .sort_by_key(|s| (s.camera_distance * -1000.0) as i64); 284 | 285 | let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 286 | 287 | { 288 | let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 289 | label: Some("flat"), 290 | color_attachments: &[wgpu::RenderPassColorAttachment { 291 | view: &target.view, 292 | resolve_target: None, 293 | ops: wgpu::Operations { 294 | load: wgpu::LoadOp::Clear(camera.background.into()), 295 | store: true, 296 | }, 297 | }], 298 | depth_stencil_attachment: None, 299 | }); 300 | pass.set_pipeline(&self.pipelines.transparent); 301 | pass.set_bind_group(0, &self.global_bind_group, &[]); 302 | 303 | for inst in self.temp.drain(..) { 304 | let key = LocalKey { 305 | uniform_buf_index: inst.locals_bl.index, 306 | image: inst.image, 307 | }; 308 | let local_bg = &self.local_bind_groups[&key]; 309 | pass.set_bind_group(1, local_bg, &[inst.locals_bl.offset]); 310 | pass.draw(0..4, 0..1); 311 | } 312 | } 313 | 314 | queue.submit(Some(encoder.finish())); 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /src/pass/flat.wgsl: -------------------------------------------------------------------------------- 1 | struct Globals { 2 | view_proj: mat4x4; 3 | }; 4 | [[group(0), binding(0)]] 5 | var globals: Globals; 6 | 7 | [[group(0), binding(1)]] 8 | var sam: sampler; 9 | 10 | struct Locals { 11 | pos_scale: vec4; 12 | rot: vec4; 13 | bounds: vec4; 14 | tex_coords: vec4; 15 | }; 16 | [[group(1), binding(0)]] 17 | var locals: Locals; 18 | 19 | [[group(1), binding(1)]] 20 | var image: texture_2d; 21 | 22 | fn qrot(q: vec4, v: vec3) -> vec3 { 23 | return v + 2.0*cross(q.xyz, cross(q.xyz,v) + q.w*v); 24 | } 25 | 26 | struct Varyings { 27 | [[builtin(position)]] clip_pos: vec4; 28 | [[location(0)]] tc: vec2; 29 | }; 30 | 31 | [[stage(vertex)]] 32 | fn main_vs([[builtin(vertex_index)]] index: u32) -> Varyings { 33 | let tc = vec2( 34 | f32(i32(index) / 2), 35 | f32(i32(index) & 1), 36 | ); 37 | let pos = vec3( 38 | mix(locals.bounds.xw, locals.bounds.zy, tc), 39 | 0.0 40 | ); 41 | let world = locals.pos_scale.w * qrot(locals.rot, pos) + locals.pos_scale.xyz; 42 | let clip_pos = globals.view_proj * vec4(world, 1.0); 43 | 44 | let tc_sub = mix(locals.tex_coords.xy, locals.tex_coords.zw, tc); 45 | return Varyings( clip_pos, tc_sub ); 46 | } 47 | 48 | [[stage(fragment)]] 49 | fn main_fs([[location(0)]] tc: vec2) -> [[location(0)]] vec4 { 50 | let sample = textureSample(image, sam, tc); 51 | // pre-multiply the alpha 52 | return sample * vec4(sample.aaa, 1.0); 53 | } 54 | -------------------------------------------------------------------------------- /src/pass/mod.rs: -------------------------------------------------------------------------------- 1 | mod flat; 2 | mod phong; 3 | mod real; 4 | mod solid; 5 | 6 | pub use flat::Flat; 7 | pub use phong::{Ambient, Phong, PhongConfig, Shader}; 8 | pub use real::{Material, Real, RealConfig}; 9 | pub use solid::{Solid, SolidConfig}; 10 | 11 | use std::mem; 12 | 13 | fn align_up(offset: u32, align: u32) -> u32 { 14 | (offset + align - 1) & !(align - 1) 15 | } 16 | 17 | struct BufferPool { 18 | label: &'static str, 19 | usage: wgpu::BufferUsages, 20 | buffers: Vec, 21 | chunk_size: u32, 22 | last_index: usize, 23 | last_offset: u32, 24 | alignment: u32, 25 | } 26 | 27 | struct BufferLocation { 28 | index: usize, 29 | offset: u32, 30 | } 31 | 32 | impl BufferPool { 33 | fn uniform(label: &'static str, device: &wgpu::Device) -> Self { 34 | let chunk_size = 0x10000; 35 | let usage = wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM; 36 | Self { 37 | label, 38 | buffers: vec![device.create_buffer(&wgpu::BufferDescriptor { 39 | label: Some(label), 40 | size: chunk_size as wgpu::BufferAddress, 41 | usage, 42 | mapped_at_creation: false, 43 | })], 44 | chunk_size, 45 | last_index: 0, 46 | last_offset: 0, 47 | alignment: device.limits().min_uniform_buffer_offset_alignment, 48 | usage, 49 | } 50 | } 51 | 52 | fn prepare_for_count(&mut self, count: usize, device: &wgpu::Device) -> usize { 53 | if count == 0 { 54 | return 0; 55 | } 56 | let size_per_element = align_up(mem::size_of::() as u32, self.alignment); 57 | let elements_per_chunk = self.chunk_size / size_per_element; 58 | let buf_count = 1 + (count - 1) / (elements_per_chunk as usize); 59 | 60 | while self.buffers.len() < buf_count { 61 | self.buffers 62 | .push(device.create_buffer(&wgpu::BufferDescriptor { 63 | label: Some(self.label), 64 | size: self.chunk_size as wgpu::BufferAddress, 65 | usage: self.usage, 66 | mapped_at_creation: false, 67 | })); 68 | } 69 | buf_count 70 | } 71 | 72 | //TODO: consider lifting `T` up 73 | fn binding(&self, index: usize) -> wgpu::BufferBinding { 74 | wgpu::BufferBinding { 75 | buffer: &self.buffers[index], 76 | offset: 0, 77 | size: wgpu::BufferSize::new(mem::size_of::() as _), 78 | } 79 | } 80 | 81 | fn alloc(&mut self, object: &T, queue: &wgpu::Queue) -> BufferLocation { 82 | let size = mem::size_of::() as u32; 83 | assert!(size <= self.chunk_size); 84 | if self.last_offset + size > self.chunk_size { 85 | self.last_index += 1; 86 | self.last_offset = 0; 87 | } 88 | 89 | let offset = self.last_offset; 90 | let buffer = &self.buffers[self.last_index]; 91 | queue.write_buffer( 92 | buffer, 93 | offset as wgpu::BufferAddress, 94 | bytemuck::bytes_of(object), 95 | ); 96 | 97 | self.last_offset = align_up(offset + size, self.alignment); 98 | 99 | BufferLocation { 100 | index: self.last_index, 101 | offset, 102 | } 103 | } 104 | 105 | fn reset(&mut self) { 106 | self.last_index = 0; 107 | self.last_offset = 0; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/pass/phong.rs: -------------------------------------------------------------------------------- 1 | use bc::ContextDetail as _; 2 | use fxhash::FxHashMap; 3 | use std::mem; 4 | 5 | #[derive(Clone, Copy, Debug, PartialEq)] 6 | pub enum Shader { 7 | Gouraud { flat: bool }, 8 | Phong { glossiness: u8 }, 9 | } 10 | 11 | const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth24Plus; 12 | const INTENSITY_THRESHOLD: f32 = 0.1; 13 | const LIGHT_COUNT: usize = 4; 14 | 15 | #[repr(C)] 16 | #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] 17 | struct Globals { 18 | view_proj: [[f32; 4]; 4], 19 | ambient: [f32; 4], 20 | } 21 | 22 | #[repr(C)] 23 | #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] 24 | struct Light { 25 | pos: [f32; 4], 26 | rot: [f32; 4], 27 | color_intensity: [f32; 4], 28 | } 29 | 30 | #[repr(C)] 31 | #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] 32 | struct Locals { 33 | pos_scale: [f32; 4], 34 | rot: [f32; 4], 35 | color: [f32; 4], 36 | lights: [u32; LIGHT_COUNT], 37 | glossiness: f32, 38 | _pad: [f32; 3], 39 | } 40 | 41 | struct Pipelines { 42 | flat: wgpu::RenderPipeline, 43 | gouraud: wgpu::RenderPipeline, 44 | phong: wgpu::RenderPipeline, 45 | } 46 | 47 | #[derive(Eq, Hash, PartialEq)] 48 | struct LocalKey { 49 | uniform_buf_index: usize, 50 | } 51 | 52 | #[derive(Clone, Copy, Debug)] 53 | pub struct Ambient { 54 | pub color: crate::Color, 55 | pub intensity: f32, 56 | } 57 | 58 | impl Default for Ambient { 59 | fn default() -> Self { 60 | Self { 61 | color: crate::Color(0xFFFFFFFF), 62 | intensity: 0.0, 63 | } 64 | } 65 | } 66 | 67 | #[derive(Debug)] 68 | pub struct PhongConfig { 69 | pub cull_back_faces: bool, 70 | pub ambient: Ambient, 71 | pub max_lights: usize, 72 | } 73 | 74 | impl Default for PhongConfig { 75 | fn default() -> Self { 76 | Self { 77 | cull_back_faces: true, 78 | ambient: Ambient::default(), 79 | max_lights: 16, 80 | } 81 | } 82 | } 83 | 84 | pub struct Phong { 85 | depth_texture: Option<(wgpu::TextureView, wgpu::Extent3d)>, 86 | global_uniform_buf: wgpu::Buffer, 87 | light_buf: wgpu::Buffer, 88 | light_capacity: usize, 89 | global_bind_group: wgpu::BindGroup, 90 | local_bind_group_layout: wgpu::BindGroupLayout, 91 | local_bind_groups: FxHashMap, 92 | uniform_pool: super::BufferPool, 93 | pipelines: Pipelines, 94 | ambient: Ambient, 95 | temp_lights: Vec<(f32, u32)>, 96 | } 97 | 98 | impl Phong { 99 | pub fn new(config: &PhongConfig, context: &crate::Context) -> Self { 100 | Self::new_offscreen(config, context.surface_info().unwrap(), context) 101 | } 102 | pub fn new_offscreen( 103 | config: &PhongConfig, 104 | target_info: crate::TargetInfo, 105 | context: &crate::Context, 106 | ) -> Self { 107 | let d = context.device(); 108 | let shader_module = d.create_shader_module(&wgpu::ShaderModuleDescriptor { 109 | label: Some("phong"), 110 | source: wgpu::ShaderSource::Wgsl(include_str!("phong.wgsl").into()), 111 | }); 112 | 113 | let globals_size = mem::size_of::() as wgpu::BufferAddress; 114 | let global_bgl = d.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 115 | label: Some("phong globals"), 116 | entries: &[ 117 | wgpu::BindGroupLayoutEntry { 118 | binding: 0, 119 | visibility: wgpu::ShaderStages::VERTEX, 120 | ty: wgpu::BindingType::Buffer { 121 | ty: wgpu::BufferBindingType::Uniform, 122 | has_dynamic_offset: false, 123 | min_binding_size: wgpu::BufferSize::new(globals_size), 124 | }, 125 | count: None, 126 | }, 127 | wgpu::BindGroupLayoutEntry { 128 | binding: 1, 129 | visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, 130 | ty: wgpu::BindingType::Buffer { 131 | ty: wgpu::BufferBindingType::Storage { read_only: true }, 132 | has_dynamic_offset: false, 133 | min_binding_size: wgpu::BufferSize::new( 134 | mem::size_of::() as wgpu::BufferAddress 135 | ), 136 | }, 137 | count: None, 138 | }, 139 | ], 140 | }); 141 | let global_uniform_buf = d.create_buffer(&wgpu::BufferDescriptor { 142 | label: Some("phong globals"), 143 | size: globals_size, 144 | usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 145 | mapped_at_creation: false, 146 | }); 147 | let light_buf = d.create_buffer(&wgpu::BufferDescriptor { 148 | label: Some("phong lights"), 149 | size: (config.max_lights * mem::size_of::()) as wgpu::BufferAddress, 150 | usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, 151 | mapped_at_creation: false, 152 | }); 153 | let global_bind_group = d.create_bind_group(&wgpu::BindGroupDescriptor { 154 | label: Some("phong globals"), 155 | layout: &global_bgl, 156 | entries: &[ 157 | wgpu::BindGroupEntry { 158 | binding: 0, 159 | resource: global_uniform_buf.as_entire_binding(), 160 | }, 161 | wgpu::BindGroupEntry { 162 | binding: 1, 163 | resource: light_buf.as_entire_binding(), 164 | }, 165 | ], 166 | }); 167 | 168 | let locals_size = mem::size_of::() as wgpu::BufferAddress; 169 | let local_bgl = d.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 170 | label: Some("phong locals"), 171 | entries: &[wgpu::BindGroupLayoutEntry { 172 | binding: 0, 173 | visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, 174 | ty: wgpu::BindingType::Buffer { 175 | ty: wgpu::BufferBindingType::Uniform, 176 | has_dynamic_offset: true, 177 | min_binding_size: wgpu::BufferSize::new(locals_size), 178 | }, 179 | count: None, 180 | }], 181 | }); 182 | 183 | let pipelines = { 184 | let pipeline_layout = d.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 185 | label: Some("phong"), 186 | bind_group_layouts: &[&global_bgl, &local_bgl], 187 | push_constant_ranges: &[], 188 | }); 189 | let vertex_buffers = [crate::Position::layout::<0>(), crate::Normal::layout::<1>()]; 190 | let primitive = wgpu::PrimitiveState { 191 | cull_mode: if config.cull_back_faces { 192 | Some(wgpu::Face::Back) 193 | } else { 194 | None 195 | }, 196 | ..Default::default() 197 | }; 198 | let ds = Some(wgpu::DepthStencilState { 199 | format: DEPTH_FORMAT, 200 | depth_compare: wgpu::CompareFunction::LessEqual, 201 | depth_write_enabled: true, 202 | bias: Default::default(), 203 | stencil: Default::default(), 204 | }); 205 | let multisample = wgpu::MultisampleState { 206 | count: target_info.sample_count, 207 | ..Default::default() 208 | }; 209 | 210 | Pipelines { 211 | flat: d.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 212 | label: Some("phong/flat"), 213 | layout: Some(&pipeline_layout), 214 | vertex: wgpu::VertexState { 215 | buffers: &vertex_buffers, 216 | module: &shader_module, 217 | entry_point: "vs_flat", 218 | }, 219 | primitive, 220 | depth_stencil: ds.clone(), 221 | multisample, 222 | fragment: Some(wgpu::FragmentState { 223 | targets: &[target_info.format.into()], 224 | module: &shader_module, 225 | entry_point: "fs_flat", 226 | }), 227 | multiview: None, 228 | }), 229 | gouraud: d.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 230 | label: Some("phong/gouraud"), 231 | layout: Some(&pipeline_layout), 232 | vertex: wgpu::VertexState { 233 | buffers: &vertex_buffers, 234 | module: &shader_module, 235 | entry_point: "vs_flat", 236 | }, 237 | primitive, 238 | depth_stencil: ds.clone(), 239 | multisample, 240 | fragment: Some(wgpu::FragmentState { 241 | targets: &[target_info.format.into()], 242 | module: &shader_module, 243 | entry_point: "fs_gouraud", 244 | }), 245 | multiview: None, 246 | }), 247 | phong: d.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 248 | label: Some("phong"), 249 | layout: Some(&pipeline_layout), 250 | vertex: wgpu::VertexState { 251 | buffers: &vertex_buffers, 252 | module: &shader_module, 253 | entry_point: "vs_phong", 254 | }, 255 | primitive, 256 | depth_stencil: ds.clone(), 257 | multisample, 258 | fragment: Some(wgpu::FragmentState { 259 | targets: &[target_info.format.into()], 260 | module: &shader_module, 261 | entry_point: "fs_phong", 262 | }), 263 | multiview: None, 264 | }), 265 | } 266 | }; 267 | 268 | Self { 269 | depth_texture: None, 270 | global_uniform_buf, 271 | light_capacity: config.max_lights, 272 | light_buf, 273 | global_bind_group, 274 | local_bind_group_layout: local_bgl, 275 | local_bind_groups: Default::default(), 276 | uniform_pool: super::BufferPool::uniform("phong locals", d), 277 | pipelines, 278 | ambient: config.ambient, 279 | temp_lights: Vec::new(), 280 | } 281 | } 282 | } 283 | 284 | impl bc::Pass for Phong { 285 | fn draw( 286 | &mut self, 287 | targets: &[crate::TargetRef], 288 | scene: &crate::Scene, 289 | camera: &crate::Camera, 290 | context: &crate::Context, 291 | ) { 292 | let target = context.get_target(targets[0]); 293 | let device = context.device(); 294 | 295 | let reset_depth = match self.depth_texture { 296 | Some((_, size)) => size != target.size, 297 | None => true, 298 | }; 299 | if reset_depth { 300 | let texture = device.create_texture(&wgpu::TextureDescriptor { 301 | label: Some("depth"), 302 | dimension: wgpu::TextureDimension::D2, 303 | format: DEPTH_FORMAT, 304 | size: target.size, 305 | sample_count: 1, 306 | mip_level_count: 1, 307 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT, 308 | }); 309 | let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); 310 | self.depth_texture = Some((view, target.size)); 311 | } 312 | 313 | let nodes = scene.bake(); 314 | self.uniform_pool.reset(); 315 | let queue = context.queue(); 316 | 317 | { 318 | let m_proj = camera.projection_matrix(target.aspect()); 319 | let m_view_inv = nodes[camera.node].inverse_matrix(); 320 | let m_final = glam::Mat4::from(m_proj) * glam::Mat4::from(m_view_inv); 321 | let ambient = self.ambient.color.into_vec4(); 322 | let globals = Globals { 323 | view_proj: m_final.to_cols_array_2d(), 324 | ambient: [ 325 | ambient[0] * self.ambient.intensity, 326 | ambient[1] * self.ambient.intensity, 327 | ambient[2] * self.ambient.intensity, 328 | 0.0, 329 | ], 330 | }; 331 | queue.write_buffer(&self.global_uniform_buf, 0, bytemuck::bytes_of(&globals)); 332 | } 333 | 334 | let lights = scene 335 | .lights() 336 | .map(|(_, light)| { 337 | let space = &nodes[light.node]; 338 | let mut pos = space.pos_scale; 339 | pos[3] = match light.kind { 340 | bc::LightKind::Directional => 0.0, 341 | bc::LightKind::Point => 1.0, 342 | }; 343 | let mut color_intensity = light.color.into_vec4(); 344 | color_intensity[3] = light.intensity; 345 | Light { 346 | pos, 347 | rot: space.rot, 348 | color_intensity, 349 | } 350 | }) 351 | .collect::>(); 352 | let light_count = lights.len().min(self.light_capacity); 353 | queue.write_buffer( 354 | &self.light_buf, 355 | 0, 356 | bytemuck::cast_slice(&lights[..light_count]), 357 | ); 358 | 359 | // pre-create the bind groups so that we don't need to do it on the fly 360 | let local_bgl = &self.local_bind_group_layout; 361 | let entity_count = scene 362 | .world 363 | .query::<(&bc::Entity, &bc::Color, &Shader)>() 364 | .with::>() 365 | .with::>() 366 | .iter() 367 | .count(); 368 | let uniform_pool_size = self 369 | .uniform_pool 370 | .prepare_for_count::(entity_count, device); 371 | for uniform_buf_index in 0..uniform_pool_size { 372 | let key = LocalKey { uniform_buf_index }; 373 | let binding = self.uniform_pool.binding::(uniform_buf_index); 374 | 375 | self.local_bind_groups.entry(key).or_insert_with(|| { 376 | device.create_bind_group(&wgpu::BindGroupDescriptor { 377 | label: Some("phong locals"), 378 | layout: local_bgl, 379 | entries: &[wgpu::BindGroupEntry { 380 | binding: 0, 381 | resource: wgpu::BindingResource::Buffer(binding), 382 | }], 383 | }) 384 | }); 385 | } 386 | 387 | let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 388 | 389 | { 390 | let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 391 | label: Some("phong"), 392 | color_attachments: &[wgpu::RenderPassColorAttachment { 393 | view: &target.view, 394 | resolve_target: None, 395 | ops: wgpu::Operations { 396 | load: wgpu::LoadOp::Clear(camera.background.into()), 397 | store: true, 398 | }, 399 | }], 400 | depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { 401 | view: &self.depth_texture.as_ref().unwrap().0, 402 | depth_ops: Some(wgpu::Operations { 403 | load: wgpu::LoadOp::Clear(1.0), 404 | store: true, 405 | }), 406 | stencil_ops: None, 407 | }), 408 | }); 409 | 410 | pass.set_bind_group(0, &self.global_bind_group, &[]); 411 | 412 | for (_, (entity, &color, &shader)) in scene 413 | .world 414 | .query::<(&bc::Entity, &bc::Color, &Shader)>() 415 | .with::>() 416 | .with::>() 417 | .iter() 418 | { 419 | let space = &nodes[entity.node]; 420 | let mesh = context.get_mesh(entity.mesh); 421 | let entity_radius = mesh.bound_radius * space.pos_scale[3]; 422 | 423 | // collect the `LIGHT_COUNT` lights most affecting the entity 424 | self.temp_lights.clear(); 425 | let entity_pos = glam::Vec3::from_slice(&space.pos_scale[..3]); 426 | for (index, (_, light)) in scene.lights().enumerate() { 427 | let light_pos = glam::Vec3::from_slice(&nodes[light.node].pos_scale[..3]); 428 | let intensity = match light.kind { 429 | bc::LightKind::Point => { 430 | let distance = (entity_pos - light_pos).length(); 431 | if distance <= entity_radius { 432 | light.intensity 433 | } else { 434 | let bound_distance = (distance - entity_radius).max(1.0); 435 | light.intensity / bound_distance * bound_distance 436 | } 437 | } 438 | bc::LightKind::Directional => light.intensity, 439 | }; 440 | if intensity > INTENSITY_THRESHOLD { 441 | self.temp_lights.push((intensity, index as u32)); 442 | } 443 | } 444 | self.temp_lights 445 | .sort_by_key(|&(intensity, _)| (1.0 / intensity) as usize); 446 | let mut light_indices = [0u32; LIGHT_COUNT]; 447 | for (li, &(_, index)) in light_indices.iter_mut().zip(&self.temp_lights) { 448 | *li = index; 449 | } 450 | 451 | //TODO: check for texture coordinates 452 | pass.set_pipeline(match shader { 453 | Shader::Gouraud { flat: true } => &self.pipelines.flat, 454 | Shader::Gouraud { flat: false } => &self.pipelines.gouraud, 455 | Shader::Phong { .. } => &self.pipelines.phong, 456 | }); 457 | 458 | let locals = Locals { 459 | pos_scale: space.pos_scale, 460 | rot: space.rot, 461 | color: color.into_vec4_gamma(), 462 | lights: light_indices, 463 | glossiness: match shader { 464 | Shader::Phong { glossiness } => glossiness as f32, 465 | _ => 0.0, 466 | }, 467 | _pad: [0.0; 3], 468 | }; 469 | let bl = self.uniform_pool.alloc(&locals, queue); 470 | 471 | let key = LocalKey { 472 | uniform_buf_index: bl.index, 473 | }; 474 | let local_bg = &self.local_bind_groups[&key]; 475 | pass.set_bind_group(1, local_bg, &[bl.offset]); 476 | 477 | pass.set_vertex_buffer(0, mesh.vertex_slice::()); 478 | pass.set_vertex_buffer(1, mesh.vertex_slice::()); 479 | 480 | if let Some(ref is) = mesh.index_stream { 481 | pass.set_index_buffer(mesh.buffer.slice(is.offset..), is.format); 482 | pass.draw_indexed(0..is.count, 0, 0..1); 483 | } else { 484 | pass.draw(0..mesh.vertex_count, 0..1); 485 | } 486 | } 487 | } 488 | 489 | queue.submit(Some(encoder.finish())); 490 | } 491 | } 492 | -------------------------------------------------------------------------------- /src/pass/phong.wgsl: -------------------------------------------------------------------------------- 1 | struct Vertex { 2 | [[location(0)]] pos: vec3; 3 | [[location(1)]] normal: vec3; 4 | }; 5 | 6 | struct Globals { 7 | view_proj: mat4x4; 8 | ambient: vec4; 9 | }; 10 | [[group(0), binding(0)]] 11 | var globals: Globals; 12 | 13 | struct Light { 14 | pos: vec4; 15 | rot: vec4; 16 | color_intensity: vec4; 17 | }; 18 | struct LightArray { 19 | data: array; 20 | }; 21 | [[group(0), binding(1)]] 22 | var lights: LightArray; 23 | 24 | struct Locals { 25 | pos_scale: vec4; 26 | rot: vec4; 27 | color: vec4; 28 | lights: vec4; 29 | glossiness: f32; 30 | }; 31 | [[group(1), binding(0)]] 32 | var locals: Locals; 33 | 34 | fn qrot(q: vec4, v: vec3) -> vec3 { 35 | return v + 2.0*cross(q.xyz, cross(q.xyz,v) + q.w*v); 36 | } 37 | 38 | struct PhongVaryings { 39 | [[builtin(position)]] position: vec4; 40 | [[location(0)]] world: vec3; 41 | [[location(1)]] normal: vec3; 42 | [[location(2)]] color: vec3; 43 | [[location(3)]] half_vec0: vec3; 44 | [[location(4)]] half_vec1: vec3; 45 | [[location(5)]] half_vec2: vec3; 46 | [[location(6)]] half_vec3: vec3; 47 | }; 48 | 49 | fn compute_half(world: vec3, normal: vec3, index: u32) -> vec3 { 50 | let light_pos = lights.data[index].pos; 51 | let dir = light_pos.xyz - light_pos.w * world; 52 | return normalize(normal + normalize(dir)); 53 | } 54 | 55 | [[stage(vertex)]] 56 | fn vs_phong(in: Vertex) -> PhongVaryings { 57 | let world = locals.pos_scale.w * qrot(locals.rot, in.pos) + locals.pos_scale.xyz; 58 | let normal = qrot(locals.rot, normalize(in.normal)); 59 | 60 | var out: PhongVaryings; 61 | out.position = globals.view_proj * vec4(world, 1.0); 62 | out.world = world; 63 | out.normal = normal; 64 | out.color = globals.ambient.xyz; 65 | out.half_vec0 = compute_half(world, normal, locals.lights.x); 66 | out.half_vec1 = compute_half(world, normal, locals.lights.y); 67 | out.half_vec2 = compute_half(world, normal, locals.lights.z); 68 | out.half_vec3 = compute_half(world, normal, locals.lights.w); 69 | return out; 70 | } 71 | 72 | struct Evaluation { 73 | diffuse: vec3; 74 | specular: vec3; 75 | }; 76 | 77 | fn evaluate(world: vec3, normal: vec3, half_vec: vec3, index: u32) -> Evaluation { 78 | var ev = Evaluation(vec3(0.0), vec3(0.0)); 79 | let light = lights.data[index]; 80 | 81 | let dir = light.pos.xyz - light.pos.w * world; 82 | let dot_nl = dot(normal, normalize(dir)); 83 | 84 | let kd = light.color_intensity.w * max(0.0, dot_nl); 85 | ev.diffuse = kd * light.color_intensity.xyz; 86 | 87 | if (light.color_intensity.w > 0.01 && dot_nl > 0.0) { 88 | let ks = dot(normal, normalize(half_vec)); 89 | if (ks > 0.0) { 90 | ev.specular = pow(ks, locals.glossiness) * light.color_intensity.xyz; 91 | } 92 | } 93 | 94 | return ev; 95 | } 96 | 97 | [[stage(fragment)]] 98 | fn fs_phong(in: PhongVaryings) -> [[location(0)]] vec4 { 99 | let eval0 = evaluate(in.world, in.normal, in.half_vec0, locals.lights.x); 100 | let eval1 = evaluate(in.world, in.normal, in.half_vec1, locals.lights.y); 101 | let eval2 = evaluate(in.world, in.normal, in.half_vec2, locals.lights.z); 102 | let eval3 = evaluate(in.world, in.normal, in.half_vec3, locals.lights.w); 103 | let total = Evaluation( 104 | in.color + eval0.diffuse + eval1.diffuse + eval2.diffuse + eval3.diffuse, 105 | eval0.specular + eval1.specular + eval2.specular + eval3.specular, 106 | ); 107 | return vec4(total.diffuse, 0.0) * locals.color + vec4(total.specular, 0.0); 108 | } 109 | 110 | fn evaluate_flat(world: vec3, normal: vec3, index: u32) -> vec3 { 111 | let light = lights.data[index]; 112 | 113 | let dir = light.pos.xyz - light.pos.w * world; 114 | let dot_nl = dot(normal, normalize(dir)); 115 | 116 | let kd = light.color_intensity.w * max(0.0, dot_nl); 117 | return kd * light.color_intensity.xyz; 118 | } 119 | 120 | struct FlatVaryings { 121 | [[builtin(position)]] position: vec4; 122 | [[location(0), interpolate(flat)]] flat_color: vec3; 123 | [[location(1)]] color: vec3; 124 | }; 125 | 126 | [[stage(vertex)]] 127 | fn vs_flat(in: Vertex) -> FlatVaryings { 128 | let world = locals.pos_scale.w * qrot(locals.rot, in.pos) + locals.pos_scale.xyz; 129 | let normal = qrot(locals.rot, normalize(in.normal)); 130 | let diffuse = globals.ambient.xyz + 131 | evaluate_flat(world, normal, locals.lights.x) + 132 | evaluate_flat(world, normal, locals.lights.y) + 133 | evaluate_flat(world, normal, locals.lights.z) + 134 | evaluate_flat(world, normal, locals.lights.w); 135 | 136 | var out: FlatVaryings; 137 | out.position = globals.view_proj * vec4(world, 1.0); 138 | out.flat_color = diffuse * locals.color.xyz; 139 | out.color = diffuse * locals.color.xyz; 140 | return out; 141 | } 142 | 143 | [[stage(fragment)]] 144 | fn fs_flat(in: FlatVaryings) -> [[location(0)]] vec4 { 145 | return vec4(in.flat_color, 0.0); 146 | } 147 | 148 | [[stage(fragment)]] 149 | fn fs_gouraud(in: FlatVaryings) -> [[location(0)]] vec4 { 150 | return vec4(in.color, 0.0); 151 | } 152 | -------------------------------------------------------------------------------- /src/pass/real.rs: -------------------------------------------------------------------------------- 1 | use bc::ContextDetail as _; 2 | use fxhash::FxHashMap; 3 | use std::mem; 4 | use wgpu::util::DeviceExt as _; 5 | 6 | #[derive(Clone, Copy, Debug)] 7 | pub struct Material { 8 | pub base_color_map: Option, 9 | pub emissive_color: crate::Color, 10 | pub metallic_factor: f32, 11 | pub roughness_factor: f32, 12 | pub normal_scale: f32, 13 | pub occlusion_strength: f32, 14 | } 15 | 16 | impl Default for Material { 17 | fn default() -> Self { 18 | Self { 19 | base_color_map: None, 20 | emissive_color: crate::Color(0), 21 | metallic_factor: 1.0, 22 | roughness_factor: 0.0, 23 | normal_scale: 1.0, 24 | occlusion_strength: 1.0, 25 | } 26 | } 27 | } 28 | 29 | const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth24Plus; 30 | 31 | #[repr(C)] 32 | #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] 33 | struct Globals { 34 | view_proj: [[f32; 4]; 4], 35 | camera_pos: [f32; 4], 36 | } 37 | 38 | #[repr(C)] 39 | #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] 40 | struct Light { 41 | pos: [f32; 4], 42 | rot: [f32; 4], 43 | color_intensity: [f32; 4], 44 | } 45 | 46 | #[repr(C)] 47 | #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] 48 | struct Locals { 49 | pos_scale: [f32; 4], 50 | rot: [f32; 4], 51 | base_color_factor: [f32; 4], 52 | emissive_factor: [f32; 4], 53 | metallic_roughness_values: [f32; 2], 54 | normal_scale: f32, 55 | occlusion_strength: f32, 56 | } 57 | 58 | #[derive(Eq, Hash, PartialEq)] 59 | struct LocalKey { 60 | uniform_buf_index: usize, 61 | base_color_map: Option, 62 | } 63 | 64 | #[derive(Debug)] 65 | pub struct RealConfig { 66 | pub cull_back_faces: bool, 67 | pub max_lights: usize, 68 | } 69 | 70 | impl Default for RealConfig { 71 | fn default() -> Self { 72 | Self { 73 | cull_back_faces: true, 74 | max_lights: 16, 75 | } 76 | } 77 | } 78 | 79 | struct Pipelines { 80 | main: wgpu::RenderPipeline, 81 | } 82 | 83 | struct Instance { 84 | mesh: crate::MeshRef, 85 | locals_bl: super::BufferLocation, 86 | base_color_map: Option, 87 | } 88 | 89 | /// Realistic renderer. 90 | /// Follows Disney PBR. 91 | pub struct Real { 92 | depth_texture: Option<(wgpu::TextureView, wgpu::Extent3d)>, 93 | global_uniform_buf: wgpu::Buffer, 94 | light_buf: wgpu::Buffer, 95 | light_capacity: usize, 96 | global_bind_group: wgpu::BindGroup, 97 | local_bind_group_layout: wgpu::BindGroupLayout, 98 | local_bind_groups: FxHashMap, 99 | uniform_pool: super::BufferPool, 100 | pipelines: Pipelines, 101 | blank_color_view: wgpu::TextureView, 102 | instances: Vec, 103 | } 104 | 105 | impl Real { 106 | pub fn new(config: &RealConfig, context: &crate::Context) -> Self { 107 | Self::new_offscreen(config, context.surface_info().unwrap(), context) 108 | } 109 | pub fn new_offscreen( 110 | config: &RealConfig, 111 | target_info: crate::TargetInfo, 112 | context: &crate::Context, 113 | ) -> Self { 114 | let d = context.device(); 115 | let shader_module = d.create_shader_module(&wgpu::ShaderModuleDescriptor { 116 | label: Some("real"), 117 | source: wgpu::ShaderSource::Wgsl(include_str!("real.wgsl").into()), 118 | }); 119 | 120 | let globals_size = mem::size_of::() as wgpu::BufferAddress; 121 | let global_bgl = d.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 122 | label: Some("real globals"), 123 | entries: &[ 124 | wgpu::BindGroupLayoutEntry { 125 | binding: 0, 126 | visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, 127 | ty: wgpu::BindingType::Buffer { 128 | ty: wgpu::BufferBindingType::Uniform, 129 | has_dynamic_offset: false, 130 | min_binding_size: wgpu::BufferSize::new(globals_size), 131 | }, 132 | count: None, 133 | }, 134 | wgpu::BindGroupLayoutEntry { 135 | binding: 1, 136 | visibility: wgpu::ShaderStages::FRAGMENT, 137 | ty: wgpu::BindingType::Buffer { 138 | ty: wgpu::BufferBindingType::Storage { read_only: true }, 139 | has_dynamic_offset: false, 140 | min_binding_size: wgpu::BufferSize::new( 141 | mem::size_of::() as wgpu::BufferAddress 142 | ), 143 | }, 144 | count: None, 145 | }, 146 | wgpu::BindGroupLayoutEntry { 147 | binding: 2, 148 | visibility: wgpu::ShaderStages::FRAGMENT, 149 | ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), 150 | count: None, 151 | }, 152 | ], 153 | }); 154 | let global_uniform_buf = d.create_buffer(&wgpu::BufferDescriptor { 155 | label: Some("real globals"), 156 | size: globals_size, 157 | usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 158 | mapped_at_creation: false, 159 | }); 160 | let light_buf = d.create_buffer(&wgpu::BufferDescriptor { 161 | label: Some("real lights"), 162 | size: (config.max_lights * mem::size_of::()) as wgpu::BufferAddress, 163 | usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, 164 | mapped_at_creation: false, 165 | }); 166 | let sampler = d.create_sampler(&wgpu::SamplerDescriptor { 167 | label: Some("real sampler"), 168 | min_filter: wgpu::FilterMode::Linear, 169 | mag_filter: wgpu::FilterMode::Linear, 170 | ..Default::default() 171 | }); 172 | let global_bind_group = d.create_bind_group(&wgpu::BindGroupDescriptor { 173 | label: Some("real globals"), 174 | layout: &global_bgl, 175 | entries: &[ 176 | wgpu::BindGroupEntry { 177 | binding: 0, 178 | resource: global_uniform_buf.as_entire_binding(), 179 | }, 180 | wgpu::BindGroupEntry { 181 | binding: 1, 182 | resource: light_buf.as_entire_binding(), 183 | }, 184 | wgpu::BindGroupEntry { 185 | binding: 2, 186 | resource: wgpu::BindingResource::Sampler(&sampler), 187 | }, 188 | ], 189 | }); 190 | 191 | let locals_size = mem::size_of::() as wgpu::BufferAddress; 192 | let local_bgl = d.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 193 | label: Some("real locals"), 194 | entries: &[ 195 | wgpu::BindGroupLayoutEntry { 196 | binding: 0, 197 | visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, 198 | ty: wgpu::BindingType::Buffer { 199 | ty: wgpu::BufferBindingType::Uniform, 200 | has_dynamic_offset: true, 201 | min_binding_size: wgpu::BufferSize::new(locals_size), 202 | }, 203 | count: None, 204 | }, 205 | wgpu::BindGroupLayoutEntry { 206 | binding: 1, 207 | visibility: wgpu::ShaderStages::FRAGMENT, 208 | ty: wgpu::BindingType::Texture { 209 | sample_type: wgpu::TextureSampleType::Float { filterable: true }, 210 | view_dimension: wgpu::TextureViewDimension::D2, 211 | multisampled: false, 212 | }, 213 | count: None, 214 | }, 215 | ], 216 | }); 217 | 218 | let pipelines = { 219 | let pipeline_layout = d.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 220 | label: Some("real"), 221 | bind_group_layouts: &[&global_bgl, &local_bgl], 222 | push_constant_ranges: &[], 223 | }); 224 | let primitive = wgpu::PrimitiveState { 225 | cull_mode: if config.cull_back_faces { 226 | Some(wgpu::Face::Back) 227 | } else { 228 | None 229 | }, 230 | ..Default::default() 231 | }; 232 | let multisample = wgpu::MultisampleState { 233 | count: target_info.sample_count, 234 | ..Default::default() 235 | }; 236 | 237 | Pipelines { 238 | main: d.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 239 | label: Some("real"), 240 | layout: Some(&pipeline_layout), 241 | vertex: wgpu::VertexState { 242 | buffers: &[ 243 | crate::Position::layout::<0>(), 244 | crate::TexCoords::layout::<1>(), 245 | crate::Normal::layout::<2>(), 246 | ], 247 | module: &shader_module, 248 | entry_point: "main_vs", 249 | }, 250 | primitive, 251 | depth_stencil: Some(wgpu::DepthStencilState { 252 | format: DEPTH_FORMAT, 253 | depth_compare: wgpu::CompareFunction::LessEqual, 254 | depth_write_enabled: true, 255 | bias: Default::default(), 256 | stencil: Default::default(), 257 | }), 258 | multisample, 259 | fragment: Some(wgpu::FragmentState { 260 | targets: &[target_info.format.into()], 261 | module: &shader_module, 262 | entry_point: "main_fs", 263 | }), 264 | multiview: None, 265 | }), 266 | } 267 | }; 268 | 269 | let blank_color_view = { 270 | let desc = wgpu::TextureDescriptor { 271 | label: Some("dummy"), 272 | size: wgpu::Extent3d { 273 | width: 1, 274 | height: 1, 275 | depth_or_array_layers: 1, 276 | }, 277 | mip_level_count: 1, 278 | sample_count: 1, 279 | dimension: wgpu::TextureDimension::D2, 280 | format: wgpu::TextureFormat::Rgba8UnormSrgb, 281 | usage: wgpu::TextureUsages::TEXTURE_BINDING, 282 | }; 283 | let texture = d.create_texture_with_data(context.queue(), &desc, &[0xFF; 4]); 284 | texture.create_view(&wgpu::TextureViewDescriptor::default()) 285 | }; 286 | 287 | Self { 288 | depth_texture: None, 289 | global_uniform_buf, 290 | light_capacity: config.max_lights, 291 | light_buf, 292 | global_bind_group, 293 | local_bind_group_layout: local_bgl, 294 | local_bind_groups: Default::default(), 295 | uniform_pool: super::BufferPool::uniform("real locals", d), 296 | pipelines, 297 | blank_color_view, 298 | instances: Vec::new(), 299 | } 300 | } 301 | } 302 | 303 | impl bc::Pass for Real { 304 | fn draw( 305 | &mut self, 306 | targets: &[crate::TargetRef], 307 | scene: &crate::Scene, 308 | camera: &crate::Camera, 309 | context: &crate::Context, 310 | ) { 311 | let target = context.get_target(targets[0]); 312 | let device = context.device(); 313 | 314 | let reset_depth = match self.depth_texture { 315 | Some((_, size)) => size != target.size, 316 | None => true, 317 | }; 318 | //TODO: abstract this part away 319 | if reset_depth { 320 | let texture = device.create_texture(&wgpu::TextureDescriptor { 321 | label: Some("depth"), 322 | dimension: wgpu::TextureDimension::D2, 323 | format: DEPTH_FORMAT, 324 | size: target.size, 325 | sample_count: 1, 326 | mip_level_count: 1, 327 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT, 328 | }); 329 | let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); 330 | self.depth_texture = Some((view, target.size)); 331 | } 332 | 333 | let nodes = scene.bake(); 334 | self.uniform_pool.reset(); 335 | let queue = context.queue(); 336 | 337 | { 338 | let m_proj = camera.projection_matrix(target.aspect()); 339 | let node = &nodes[camera.node]; 340 | let m_view_inv = node.inverse_matrix(); 341 | let m_final = glam::Mat4::from(m_proj) * glam::Mat4::from(m_view_inv); 342 | let globals = Globals { 343 | view_proj: m_final.to_cols_array_2d(), 344 | camera_pos: node.pos_scale, 345 | }; 346 | queue.write_buffer(&self.global_uniform_buf, 0, bytemuck::bytes_of(&globals)); 347 | } 348 | 349 | let lights = scene 350 | .lights() 351 | .map(|(_, light)| { 352 | let space = &nodes[light.node]; 353 | let mut pos = space.pos_scale; 354 | pos[3] = match light.kind { 355 | bc::LightKind::Directional => 0.0, 356 | bc::LightKind::Point => 1.0, 357 | }; 358 | let mut color_intensity = light.color.into_vec4(); 359 | color_intensity[3] = light.intensity; 360 | Light { 361 | pos, 362 | rot: space.rot, 363 | color_intensity, 364 | } 365 | }) 366 | .collect::>(); 367 | let light_count = lights.len().min(self.light_capacity); 368 | queue.write_buffer( 369 | &self.light_buf, 370 | 0, 371 | bytemuck::cast_slice(&lights[..light_count]), 372 | ); 373 | 374 | //TODO: we can do everything in a single pass if we use 375 | // some arena-based hashmap. 376 | self.instances.clear(); 377 | 378 | for (_, (entity, &color, mat)) in scene 379 | .world 380 | .query::<(&bc::Entity, &bc::Color, &Material)>() 381 | .with::>() 382 | .with::>() 383 | .with::>() 384 | .iter() 385 | { 386 | let space = &nodes[entity.node]; 387 | 388 | let locals = Locals { 389 | pos_scale: space.pos_scale, 390 | rot: space.rot, 391 | base_color_factor: color.into_vec4(), 392 | emissive_factor: mat.emissive_color.into_vec4(), 393 | metallic_roughness_values: [mat.metallic_factor, mat.roughness_factor], 394 | normal_scale: mat.normal_scale, 395 | occlusion_strength: mat.occlusion_strength, 396 | }; 397 | let locals_bl = self.uniform_pool.alloc(&locals, queue); 398 | 399 | // pre-create local bind group, if needed 400 | let key = LocalKey { 401 | uniform_buf_index: locals_bl.index, 402 | base_color_map: mat.base_color_map, 403 | }; 404 | let binding = self.uniform_pool.binding::(locals_bl.index); 405 | let local_bgl = &self.local_bind_group_layout; 406 | let blank_color_view = &self.blank_color_view; 407 | 408 | self.local_bind_groups.entry(key).or_insert_with(|| { 409 | let base_color_view = match mat.base_color_map { 410 | Some(image) => &context.get_image(image).view, 411 | None => blank_color_view, 412 | }; 413 | device.create_bind_group(&wgpu::BindGroupDescriptor { 414 | label: Some("real locals"), 415 | layout: local_bgl, 416 | entries: &[ 417 | wgpu::BindGroupEntry { 418 | binding: 0, 419 | resource: wgpu::BindingResource::Buffer(binding), 420 | }, 421 | wgpu::BindGroupEntry { 422 | binding: 1, 423 | resource: wgpu::BindingResource::TextureView(base_color_view), 424 | }, 425 | ], 426 | }) 427 | }); 428 | 429 | self.instances.push(Instance { 430 | mesh: entity.mesh, 431 | locals_bl, 432 | base_color_map: mat.base_color_map, 433 | }); 434 | } 435 | 436 | let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 437 | { 438 | let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 439 | label: Some("real"), 440 | color_attachments: &[wgpu::RenderPassColorAttachment { 441 | view: &target.view, 442 | resolve_target: None, 443 | ops: wgpu::Operations { 444 | load: wgpu::LoadOp::Clear(camera.background.into()), 445 | store: true, 446 | }, 447 | }], 448 | depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { 449 | view: &self.depth_texture.as_ref().unwrap().0, 450 | depth_ops: Some(wgpu::Operations { 451 | load: wgpu::LoadOp::Clear(1.0), 452 | store: true, 453 | }), 454 | stencil_ops: None, 455 | }), 456 | }); 457 | 458 | pass.set_pipeline(&self.pipelines.main); 459 | pass.set_bind_group(0, &self.global_bind_group, &[]); 460 | 461 | for inst in self.instances.drain(..) { 462 | let mesh = context.get_mesh(inst.mesh); 463 | 464 | let key = LocalKey { 465 | uniform_buf_index: inst.locals_bl.index, 466 | base_color_map: inst.base_color_map, 467 | }; 468 | let local_bg = &self.local_bind_groups[&key]; 469 | pass.set_bind_group(1, local_bg, &[inst.locals_bl.offset]); 470 | 471 | pass.set_vertex_buffer(0, mesh.vertex_slice::()); 472 | pass.set_vertex_buffer(1, mesh.vertex_slice::()); 473 | pass.set_vertex_buffer(2, mesh.vertex_slice::()); 474 | 475 | if let Some(ref is) = mesh.index_stream { 476 | pass.set_index_buffer(mesh.buffer.slice(is.offset..), is.format); 477 | pass.draw_indexed(0..is.count, 0, 0..1); 478 | } else { 479 | pass.draw(0..mesh.vertex_count, 0..1); 480 | } 481 | } 482 | } 483 | 484 | queue.submit(Some(encoder.finish())); 485 | } 486 | } 487 | -------------------------------------------------------------------------------- /src/pass/real.wgsl: -------------------------------------------------------------------------------- 1 | struct Attributes { 2 | [[location(0)]] position: vec3; 3 | [[location(1)]] tex_coords: vec2; 4 | [[location(2)]] normal: vec3; 5 | }; 6 | 7 | struct Varyings { 8 | [[builtin(position)]] clip_position: vec4; 9 | [[location(0)]] world_pos: vec3; 10 | [[location(1)]] tex_coords: vec2; 11 | [[location(2)]] normal: vec3; 12 | }; 13 | 14 | struct Globals { 15 | view_proj: mat4x4; 16 | camerate_pos: vec4; 17 | }; 18 | [[group(0), binding(0)]] 19 | var globals: Globals; 20 | 21 | struct Locals { 22 | pos_scale: vec4; 23 | rot: vec4; 24 | base_color_factor: vec4; 25 | emissive_factor: vec4; 26 | metallic_roughness_values: vec2; 27 | normal_scale: f32; 28 | occlusion_strength: f32; 29 | }; 30 | [[group(1), binding(0)]] 31 | var locals: Locals; 32 | 33 | fn qrot(q: vec4, v: vec3) -> vec3 { 34 | return v + 2.0*cross(q.xyz, cross(q.xyz,v) + q.w*v); 35 | } 36 | 37 | [[stage(vertex)]] 38 | fn main_vs(in: Attributes) -> Varyings { 39 | let world = locals.pos_scale.w * qrot(locals.rot, in.position) + locals.pos_scale.xyz; 40 | let normal = qrot(locals.rot, in.normal); 41 | 42 | return Varyings( 43 | globals.view_proj * vec4(world, 1.0), 44 | world, 45 | in.tex_coords, 46 | normal, 47 | ); 48 | } 49 | 50 | let PI: f32 = 3.141592653589793; 51 | let MIN_ROUGHNESS: f32 = 0.04; 52 | let MAX_LIGHTS: u32 = 4u; 53 | 54 | struct Light { 55 | pos: vec4; 56 | rot: vec4; 57 | color_intensity: vec4; 58 | }; 59 | struct LightArray { 60 | data: array; 61 | }; 62 | [[group(0), binding(1)]] 63 | var lights: LightArray; 64 | 65 | [[group(0), binding(2)]] 66 | var sam: sampler; 67 | 68 | [[group(1), binding(1)]] 69 | var base_color_map: texture_2d; 70 | 71 | 72 | struct PbrInfo { 73 | ndotl: f32; 74 | ndotv: f32; 75 | ndoth: f32; 76 | ldoth: f32; 77 | vdoth: f32; 78 | perceptual_roughness: f32; 79 | metalness: f32; 80 | base_color: vec3; 81 | reflectance0: vec3; 82 | reflectance90: vec3; 83 | alpha_roughness: f32; 84 | }; 85 | 86 | fn smith(ndotv: f32, r: f32) -> f32 { 87 | let tan_sq = (1.0 - ndotv * ndotv) / max((ndotv * ndotv), 0.00001); 88 | return 2.0 / (1.0 + sqrt(1.0 + r * r * tan_sq)); 89 | } 90 | 91 | fn geometric_occlusion_smith_ggx(pbr: PbrInfo) -> f32 { 92 | return smith(pbr.ndotl, pbr.alpha_roughness) * smith(pbr.ndotv, pbr.alpha_roughness); 93 | } 94 | 95 | // Basic Lambertian diffuse, implementation from Lambert's Photometria 96 | // https://archive.org/details/lambertsphotome00lambgoog 97 | fn lambertian_diffuse(pbr: PbrInfo) -> vec3{ 98 | return pbr.base_color / PI; 99 | } 100 | 101 | // The following equations model the Fresnel reflectance term of the spec equation 102 | // (aka F()) implementation of fresnel from “An Inexpensive BRDF Model for Physically 103 | // based Rendering” by Christophe Schlick 104 | fn fresnel_schlick(pbr: PbrInfo) -> vec3 { 105 | return pbr.reflectance0 + (pbr.reflectance90 - pbr.reflectance0) * pow(clamp(1.0 - pbr.vdoth, 0.0, 1.0), 5.0); 106 | } 107 | 108 | // The following equation(s) model the distribution of microfacet normals across 109 | // the area being drawn (aka D()) 110 | // Implementation from “Average Irregularity Representation of a Roughened Surface 111 | // for Ray Reflection” by T. S. Trowbridge, and K. P. Reitz 112 | fn ggx(pbr: PbrInfo) -> f32 { 113 | let roughness_sq = pbr.alpha_roughness * pbr.alpha_roughness; 114 | let f = (pbr.ndoth * roughness_sq - pbr.ndoth) * pbr.ndoth + 1.0; 115 | return roughness_sq / (PI * f * f); 116 | } 117 | 118 | [[stage(fragment)]] 119 | fn main_fs(in: Varyings) -> [[location(0)]] vec4 { 120 | let v = normalize(globals.camerate_pos.xyz - in.world_pos); 121 | let n = normalize(in.normal); 122 | 123 | let perceptual_roughness = clamp(locals.metallic_roughness_values.y, MIN_ROUGHNESS, 1.0); 124 | let metallic = clamp(locals.metallic_roughness_values.x, 0.0, 1.0); 125 | 126 | let base_color = locals.base_color_factor * textureSample(base_color_map, sam, in.tex_coords); 127 | 128 | let f0 = 0.04; 129 | let diffuse_color = mix(base_color.xyz * (1.0 - f0), vec3(0.0), metallic); 130 | let specular_color = mix(vec3(f0), base_color.xyz, metallic); 131 | let reflectance = max(max(specular_color.x, specular_color.y), specular_color.z); 132 | 133 | // For typical incident reflectance range (between 4% to 100%) set the grazing 134 | // reflectance to 100% for typical fresnel effect. 135 | // For very low reflectance range on highly diffuse objects (below 4%), 136 | // incrementally reduce grazing reflecance to 0%. 137 | let reflectance90 = clamp(reflectance * 25.0, 0.0, 1.0); 138 | let specular_environment_r0 = specular_color; 139 | let specular_environment_r90 = vec3(1.0) * reflectance90; 140 | 141 | // Roughness is authored as perceptual roughness; as is convention, convert to 142 | // material roughness by squaring the perceptual roughness 143 | let alpha_roughness = perceptual_roughness * perceptual_roughness; 144 | 145 | var color = vec3(0.0); 146 | let num_lights = min(MAX_LIGHTS, arrayLength(&lights.data)); 147 | for (var i = 0u; i(color, base_color.a); 181 | } 182 | -------------------------------------------------------------------------------- /src/pass/solid.rs: -------------------------------------------------------------------------------- 1 | use bc::ContextDetail as _; 2 | use fxhash::FxHashMap; 3 | use std::mem; 4 | 5 | const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth24Plus; 6 | 7 | #[repr(C)] 8 | #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] 9 | struct Globals { 10 | view_proj: [[f32; 4]; 4], 11 | } 12 | 13 | #[repr(C)] 14 | #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] 15 | struct Locals { 16 | pos_scale: [f32; 4], 17 | rot: [f32; 4], 18 | color: [f32; 4], 19 | } 20 | 21 | #[derive(Eq, Hash, PartialEq)] 22 | struct LocalKey { 23 | uniform_buf_index: usize, 24 | } 25 | 26 | #[derive(Debug)] 27 | pub struct SolidConfig { 28 | pub cull_back_faces: bool, 29 | } 30 | 31 | impl Default for SolidConfig { 32 | fn default() -> Self { 33 | Self { 34 | cull_back_faces: true, 35 | } 36 | } 37 | } 38 | 39 | pub struct Solid { 40 | depth_texture: Option<(wgpu::TextureView, wgpu::Extent3d)>, 41 | global_uniform_buf: wgpu::Buffer, 42 | global_bind_group: wgpu::BindGroup, 43 | local_bind_group_layout: wgpu::BindGroupLayout, 44 | local_bind_groups: FxHashMap, 45 | uniform_pool: super::BufferPool, 46 | pipeline: wgpu::RenderPipeline, 47 | } 48 | 49 | impl Solid { 50 | pub fn new(config: &SolidConfig, context: &crate::Context) -> Self { 51 | Self::new_offscreen(config, context.surface_info().unwrap(), context) 52 | } 53 | pub fn new_offscreen( 54 | config: &SolidConfig, 55 | target_info: crate::TargetInfo, 56 | context: &crate::Context, 57 | ) -> Self { 58 | let d = context.device(); 59 | let shader_module = d.create_shader_module(&wgpu::ShaderModuleDescriptor { 60 | label: Some("solid"), 61 | source: wgpu::ShaderSource::Wgsl(include_str!("solid.wgsl").into()), 62 | }); 63 | 64 | let globals_size = mem::size_of::() as wgpu::BufferAddress; 65 | let global_bgl = d.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 66 | label: Some("solid globals"), 67 | entries: &[wgpu::BindGroupLayoutEntry { 68 | binding: 0, 69 | visibility: wgpu::ShaderStages::VERTEX, 70 | ty: wgpu::BindingType::Buffer { 71 | ty: wgpu::BufferBindingType::Uniform, 72 | has_dynamic_offset: false, 73 | min_binding_size: wgpu::BufferSize::new(globals_size), 74 | }, 75 | count: None, 76 | }], 77 | }); 78 | let global_uniform_buf = d.create_buffer(&wgpu::BufferDescriptor { 79 | label: Some("solid globals"), 80 | size: globals_size, 81 | usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 82 | mapped_at_creation: false, 83 | }); 84 | let global_bind_group = d.create_bind_group(&wgpu::BindGroupDescriptor { 85 | label: Some("solid globals"), 86 | layout: &global_bgl, 87 | entries: &[wgpu::BindGroupEntry { 88 | binding: 0, 89 | resource: global_uniform_buf.as_entire_binding(), 90 | }], 91 | }); 92 | 93 | let locals_size = mem::size_of::() as wgpu::BufferAddress; 94 | let local_bgl = d.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 95 | label: Some("solid locals"), 96 | entries: &[wgpu::BindGroupLayoutEntry { 97 | binding: 0, 98 | visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, 99 | ty: wgpu::BindingType::Buffer { 100 | ty: wgpu::BufferBindingType::Uniform, 101 | has_dynamic_offset: true, 102 | min_binding_size: wgpu::BufferSize::new(locals_size), 103 | }, 104 | count: None, 105 | }], 106 | }); 107 | 108 | let pipeline_layout = d.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 109 | label: Some("solid"), 110 | bind_group_layouts: &[&global_bgl, &local_bgl], 111 | push_constant_ranges: &[], 112 | }); 113 | let pipeline = d.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 114 | label: Some("solid"), 115 | layout: Some(&pipeline_layout), 116 | vertex: wgpu::VertexState { 117 | buffers: &[crate::Position::layout::<0>()], 118 | module: &shader_module, 119 | entry_point: "main_vs", 120 | }, 121 | primitive: wgpu::PrimitiveState { 122 | cull_mode: if config.cull_back_faces { 123 | Some(wgpu::Face::Back) 124 | } else { 125 | None 126 | }, 127 | ..Default::default() 128 | }, 129 | depth_stencil: Some(wgpu::DepthStencilState { 130 | format: DEPTH_FORMAT, 131 | depth_compare: wgpu::CompareFunction::LessEqual, 132 | depth_write_enabled: true, 133 | bias: Default::default(), 134 | stencil: Default::default(), 135 | }), 136 | multisample: wgpu::MultisampleState::default(), 137 | fragment: Some(wgpu::FragmentState { 138 | targets: &[target_info.format.into()], 139 | module: &shader_module, 140 | entry_point: "main_fs", 141 | }), 142 | multiview: None, 143 | }); 144 | 145 | Self { 146 | depth_texture: None, 147 | global_uniform_buf, 148 | global_bind_group, 149 | local_bind_group_layout: local_bgl, 150 | local_bind_groups: Default::default(), 151 | uniform_pool: super::BufferPool::uniform("solid locals", d), 152 | pipeline, 153 | } 154 | } 155 | } 156 | 157 | impl bc::Pass for Solid { 158 | fn draw( 159 | &mut self, 160 | targets: &[crate::TargetRef], 161 | scene: &crate::Scene, 162 | camera: &crate::Camera, 163 | context: &crate::Context, 164 | ) { 165 | let target = context.get_target(targets[0]); 166 | let device = context.device(); 167 | 168 | let reset_depth = match self.depth_texture { 169 | Some((_, size)) => size != target.size, 170 | None => true, 171 | }; 172 | if reset_depth { 173 | let texture = device.create_texture(&wgpu::TextureDescriptor { 174 | label: Some("depth"), 175 | dimension: wgpu::TextureDimension::D2, 176 | format: DEPTH_FORMAT, 177 | size: target.size, 178 | sample_count: 1, 179 | mip_level_count: 1, 180 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT, 181 | }); 182 | let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); 183 | self.depth_texture = Some((view, target.size)); 184 | } 185 | 186 | let nodes = scene.bake(); 187 | self.uniform_pool.reset(); 188 | let queue = context.queue(); 189 | 190 | { 191 | let m_proj = camera.projection_matrix(target.aspect()); 192 | let m_view_inv = nodes[camera.node].inverse_matrix(); 193 | let m_final = glam::Mat4::from(m_proj) * glam::Mat4::from(m_view_inv); 194 | let globals = Globals { 195 | view_proj: m_final.to_cols_array_2d(), 196 | }; 197 | queue.write_buffer(&self.global_uniform_buf, 0, bytemuck::bytes_of(&globals)); 198 | } 199 | 200 | // pre-create the bind groups so that we don't need to do it on the fly 201 | let local_bgl = &self.local_bind_group_layout; 202 | let entity_count = scene 203 | .world 204 | .query::<(&bc::Entity, &bc::Color)>() 205 | .with::>() 206 | .iter() 207 | .count(); 208 | let uniform_pool_size = self 209 | .uniform_pool 210 | .prepare_for_count::(entity_count, device); 211 | for uniform_buf_index in 0..uniform_pool_size { 212 | let key = LocalKey { uniform_buf_index }; 213 | let binding = self.uniform_pool.binding::(uniform_buf_index); 214 | 215 | self.local_bind_groups.entry(key).or_insert_with(|| { 216 | device.create_bind_group(&wgpu::BindGroupDescriptor { 217 | label: Some("solid locals"), 218 | layout: local_bgl, 219 | entries: &[wgpu::BindGroupEntry { 220 | binding: 0, 221 | resource: wgpu::BindingResource::Buffer(binding), 222 | }], 223 | }) 224 | }); 225 | } 226 | 227 | let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 228 | 229 | { 230 | let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 231 | label: Some("solid"), 232 | color_attachments: &[wgpu::RenderPassColorAttachment { 233 | view: &target.view, 234 | resolve_target: None, 235 | ops: wgpu::Operations { 236 | load: wgpu::LoadOp::Clear(camera.background.into()), 237 | store: true, 238 | }, 239 | }], 240 | depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { 241 | view: &self.depth_texture.as_ref().unwrap().0, 242 | depth_ops: Some(wgpu::Operations { 243 | load: wgpu::LoadOp::Clear(1.0), 244 | store: true, 245 | }), 246 | stencil_ops: None, 247 | }), 248 | }); 249 | pass.set_pipeline(&self.pipeline); 250 | pass.set_bind_group(0, &self.global_bind_group, &[]); 251 | 252 | for (_, (entity, color)) in scene 253 | .world 254 | .query::<(&bc::Entity, &bc::Color)>() 255 | .with::>() 256 | .iter() 257 | { 258 | let space = &nodes[entity.node]; 259 | let locals = Locals { 260 | pos_scale: space.pos_scale, 261 | rot: space.rot, 262 | color: color.into_vec4_gamma(), 263 | }; 264 | let bl = self.uniform_pool.alloc(&locals, queue); 265 | 266 | let key = LocalKey { 267 | uniform_buf_index: bl.index, 268 | }; 269 | let local_bg = &self.local_bind_groups[&key]; 270 | pass.set_bind_group(1, local_bg, &[bl.offset]); 271 | 272 | let mesh = context.get_mesh(entity.mesh); 273 | let pos_vs = mesh.vertex_stream::().unwrap(); 274 | pass.set_vertex_buffer(0, mesh.buffer.slice(pos_vs.offset..)); 275 | 276 | if let Some(ref is) = mesh.index_stream { 277 | pass.set_index_buffer(mesh.buffer.slice(is.offset..), is.format); 278 | pass.draw_indexed(0..is.count, 0, 0..1); 279 | } else { 280 | pass.draw(0..mesh.vertex_count, 0..1); 281 | } 282 | } 283 | } 284 | 285 | queue.submit(Some(encoder.finish())); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/pass/solid.wgsl: -------------------------------------------------------------------------------- 1 | struct Vertex { 2 | [[location(0)]] pos: vec3; 3 | }; 4 | 5 | struct Globals { 6 | view_proj: mat4x4; 7 | }; 8 | [[group(0), binding(0)]] 9 | var globals: Globals; 10 | 11 | struct Locals { 12 | pos_scale: vec4; 13 | rot: vec4; 14 | color: vec4; 15 | }; 16 | [[group(1), binding(0)]] 17 | var locals: Locals; 18 | 19 | fn qrot(q: vec4, v: vec3) -> vec3 { 20 | return v + 2.0*cross(q.xyz, cross(q.xyz,v) + q.w*v); 21 | } 22 | 23 | [[stage(vertex)]] 24 | fn main_vs(in: Vertex) -> [[builtin(position)]] vec4 { 25 | let world = locals.pos_scale.w * qrot(locals.rot, in.pos) + locals.pos_scale.xyz; 26 | return globals.view_proj * vec4(world, 1.0); 27 | } 28 | 29 | [[stage(fragment)]] 30 | fn main_fs() -> [[location(0)]] vec4 { 31 | return locals.color; 32 | } 33 | -------------------------------------------------------------------------------- /src/window.rs: -------------------------------------------------------------------------------- 1 | use raw_window_handle::{HasRawWindowHandle, RawWindowHandle}; 2 | 3 | const TARGET_FRAME_TIME: f64 = 1.0 / 120.0; 4 | 5 | pub struct Window { 6 | event_loop: winit::event_loop::EventLoop<()>, 7 | raw: winit::window::Window, 8 | } 9 | 10 | unsafe impl HasRawWindowHandle for Window { 11 | fn raw_window_handle(&self) -> RawWindowHandle { 12 | self.raw.raw_window_handle() 13 | } 14 | } 15 | 16 | impl bc::HasWindow for Window { 17 | fn size(&self) -> mint::Vector2 { 18 | let size = self.raw.inner_size(); 19 | mint::Vector2 { 20 | x: size.width, 21 | y: size.height, 22 | } 23 | } 24 | } 25 | 26 | #[derive(Default)] 27 | pub struct WindowBuilder { 28 | title: Option, 29 | size: Option, 30 | } 31 | 32 | #[derive(Clone, Copy, Debug, PartialEq)] 33 | pub enum Key { 34 | Digit(u8), 35 | Letter(char), 36 | Function(u8), 37 | Up, 38 | Down, 39 | Left, 40 | Right, 41 | Space, 42 | Escape, 43 | Other, 44 | } 45 | 46 | #[derive(Clone, Copy, Debug, PartialEq)] 47 | pub enum Button { 48 | Left, 49 | Middle, 50 | Right, 51 | Other(u16), 52 | } 53 | 54 | pub enum Event { 55 | Resize { width: u32, height: u32 }, 56 | Keyboard { key: Key, pressed: bool }, 57 | Pointer { position: mint::Vector2 }, 58 | Scroll { delta: mint::Vector2 }, 59 | Click { button: Button, pressed: bool }, 60 | Draw, 61 | Exit, 62 | } 63 | 64 | impl Window { 65 | pub fn new() -> WindowBuilder { 66 | WindowBuilder::default() 67 | } 68 | 69 | pub fn run(self, mut runner: impl 'static + FnMut(Event)) -> ! { 70 | use std::time; 71 | use winit::{ 72 | event::{ 73 | ElementState, Event as WinEvent, KeyboardInput, MouseButton, MouseScrollDelta, 74 | VirtualKeyCode as Vkc, WindowEvent, 75 | }, 76 | event_loop::ControlFlow, 77 | }; 78 | 79 | let mut last_update_inst = time::Instant::now(); 80 | let Self { 81 | event_loop, 82 | raw: window, 83 | } = self; 84 | 85 | event_loop.run(move |event, _, control_flow| { 86 | *control_flow = match event { 87 | WinEvent::WindowEvent { 88 | event: WindowEvent::Resized(size), 89 | .. 90 | } => { 91 | runner(Event::Resize { 92 | width: size.width, 93 | height: size.height, 94 | }); 95 | ControlFlow::Poll 96 | } 97 | WinEvent::WindowEvent { 98 | event: 99 | WindowEvent::KeyboardInput { 100 | input: 101 | KeyboardInput { 102 | state, 103 | virtual_keycode: Some(code), 104 | .. 105 | }, 106 | .. 107 | }, 108 | .. 109 | } => { 110 | runner(Event::Keyboard { 111 | key: if code >= Vkc::Key1 && code <= Vkc::Key0 { 112 | Key::Digit(code as u8 - Vkc::Key1 as u8) 113 | } else if code >= Vkc::A && code <= Vkc::Z { 114 | Key::Letter((code as u8 - Vkc::A as u8) as char) 115 | } else if code >= Vkc::F1 && code <= Vkc::F12 { 116 | Key::Function(code as u8 - Vkc::F1 as u8) 117 | } else { 118 | match code { 119 | Vkc::Left => Key::Left, 120 | Vkc::Right => Key::Right, 121 | Vkc::Up => Key::Up, 122 | Vkc::Down => Key::Down, 123 | Vkc::Space => Key::Space, 124 | Vkc::Escape => Key::Escape, 125 | _ => { 126 | log::debug!("Unrecognized key {:?}", code); 127 | Key::Other 128 | } 129 | } 130 | }, 131 | pressed: state == ElementState::Pressed, 132 | }); 133 | ControlFlow::Poll 134 | } 135 | WinEvent::WindowEvent { 136 | event: WindowEvent::CursorMoved { position, .. }, 137 | .. 138 | } => { 139 | runner(Event::Pointer { 140 | position: mint::Vector2 { 141 | x: position.x as f32, 142 | y: position.y as f32, 143 | }, 144 | }); 145 | ControlFlow::Poll 146 | } 147 | WinEvent::WindowEvent { 148 | event: WindowEvent::MouseInput { button, state, .. }, 149 | .. 150 | } => { 151 | runner(Event::Click { 152 | button: match button { 153 | MouseButton::Left => Button::Left, 154 | MouseButton::Middle => Button::Middle, 155 | MouseButton::Right => Button::Right, 156 | MouseButton::Other(code) => Button::Other(code), 157 | }, 158 | pressed: state == ElementState::Pressed, 159 | }); 160 | ControlFlow::Poll 161 | } 162 | WinEvent::WindowEvent { 163 | event: WindowEvent::MouseWheel { delta, .. }, 164 | .. 165 | } => { 166 | match delta { 167 | MouseScrollDelta::LineDelta(x, y) => { 168 | runner(Event::Scroll { 169 | delta: mint::Vector2 { x, y }, 170 | }); 171 | } 172 | MouseScrollDelta::PixelDelta(position) => { 173 | runner(Event::Scroll { 174 | delta: mint::Vector2 { 175 | x: position.x as f32, 176 | y: position.y as f32, 177 | }, 178 | }); 179 | } 180 | } 181 | ControlFlow::Poll 182 | } 183 | WinEvent::RedrawRequested(_) => { 184 | runner(Event::Draw); 185 | ControlFlow::Poll 186 | } 187 | WinEvent::RedrawEventsCleared => { 188 | let target_frametime = time::Duration::from_secs_f64(TARGET_FRAME_TIME); 189 | let now = time::Instant::now(); 190 | match target_frametime.checked_sub(last_update_inst.elapsed()) { 191 | Some(wait_time) => ControlFlow::WaitUntil(now + wait_time), 192 | None => { 193 | window.request_redraw(); 194 | last_update_inst = now; 195 | ControlFlow::Poll 196 | } 197 | } 198 | } 199 | WinEvent::WindowEvent { 200 | event: WindowEvent::CloseRequested, 201 | .. 202 | } => ControlFlow::Exit, 203 | WinEvent::LoopDestroyed => { 204 | runner(Event::Exit); 205 | ControlFlow::Exit 206 | } 207 | _ => ControlFlow::Poll, 208 | } 209 | }) 210 | } 211 | } 212 | 213 | impl WindowBuilder { 214 | pub fn title(self, title: &str) -> Self { 215 | Self { 216 | title: Some(title.to_string()), 217 | ..self 218 | } 219 | } 220 | 221 | pub fn size(self, width: u32, height: u32) -> Self { 222 | Self { 223 | size: Some(wgpu::Extent3d { 224 | width, 225 | height, 226 | depth_or_array_layers: 1, 227 | }), 228 | ..self 229 | } 230 | } 231 | 232 | pub fn build(self) -> Window { 233 | let event_loop = winit::event_loop::EventLoop::new(); 234 | let mut builder = winit::window::WindowBuilder::new() 235 | .with_min_inner_size(winit::dpi::Size::Logical((64, 64).into())); 236 | if let Some(title) = self.title { 237 | builder = builder.with_title(title); 238 | } 239 | if let Some(size) = self.size { 240 | builder = builder 241 | .with_inner_size(winit::dpi::Size::Logical((size.width, size.height).into())); 242 | } 243 | let raw = builder.build(&event_loop).unwrap(); 244 | Window { raw, event_loop } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /tests/parse-shaders.rs: -------------------------------------------------------------------------------- 1 | use naga::{front::wgsl, valid::Validator}; 2 | use std::{fs, path::PathBuf}; 3 | 4 | /// Runs through all pass shaders and ensures they are valid WGSL. 5 | #[test] 6 | fn parse_wgsl() { 7 | let read_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) 8 | .join("src") 9 | .join("pass") 10 | .read_dir() 11 | .unwrap(); 12 | 13 | for file_entry in read_dir { 14 | let shader = match file_entry { 15 | Ok(entry) => match entry.path().extension() { 16 | Some(ostr) if &*ostr == "wgsl" => { 17 | println!("Validating {:?}", entry.path()); 18 | fs::read_to_string(entry.path()).unwrap_or_default() 19 | } 20 | _ => continue, 21 | }, 22 | Err(e) => { 23 | log::warn!("Skipping file: {:?}", e); 24 | continue; 25 | } 26 | }; 27 | 28 | let module = match wgsl::parse_str(&shader) { 29 | Ok(module) => module, 30 | Err(e) => panic!("{}", e.emit_to_string(&shader)), 31 | }; 32 | //TODO: re-use the validator 33 | Validator::new( 34 | naga::valid::ValidationFlags::all(), 35 | naga::valid::Capabilities::empty(), 36 | ) 37 | .validate(&module) 38 | .unwrap(); 39 | } 40 | } 41 | --------------------------------------------------------------------------------