diff --git a/.gitignore b/.gitignore index dbe2827b..cf0b3ef6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ build/ crates/pill_standalone/Cargo.toml examples/*/Cargo.toml + +engine/Cargo.toml diff --git a/engine/.cargo/config.toml b/engine/.cargo/config.toml new file mode 100644 index 00000000..1938115a --- /dev/null +++ b/engine/.cargo/config.toml @@ -0,0 +1,16 @@ +[build] +# Enable SIMD optimizations for math operations +# This will auto-vectorize cgmath operations when possible +rustflags = ["-C", "target-cpu=native"] + +# Alternative: More conservative SIMD flags +# rustflags = ["-C", "target-feature=+avx2,+fma"] + +[target.x86_64-unknown-linux-gnu] +rustflags = ["-C", "target-cpu=native"] + +[target.x86_64-apple-darwin] +rustflags = ["-C", "target-cpu=native"] + +[target.x86_64-pc-windows-msvc] +rustflags = ["-C", "target-cpu=native"] diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 85551ef7..894ecd1e 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -3,11 +3,19 @@ resolver = "2" members = [ "pill_core", "pill_engine", - "pill_renderer", "pill_standalone", - "NO_PATH", ### Game project crate (This will be changed by Pill Launcher on build to allow proper compilation of game project) + "/Users/mk/dev/demo/pill_demo", ### Game project crate (This will be changed by Pill Launcher on build to allow proper compilation of game project) ] exclude = [ "pill_launcher", ] + +[profile.dev] +opt-level = 3 +incremental = true + +[profile.release] +opt-level = 3 +lto = "thin" +codegen-units = 1 diff --git a/engine/pill_core/src/handle.rs b/engine/pill_core/src/handle.rs new file mode 100644 index 00000000..8598dde2 --- /dev/null +++ b/engine/pill_core/src/handle.rs @@ -0,0 +1,116 @@ +use core::fmt; +use core::hash::{Hash, Hasher}; +use core::marker::PhantomData; + +#[repr(transparent)] +pub struct Handle { + raw: u64, + _marker: PhantomData, +} + +impl Handle { + #[inline] + pub const fn from_parts(index: u32, generation: u32) -> Self { + Self { + raw: ((generation as u64) << 32) | (index as u64), + _marker: PhantomData, + } + } + + #[inline] + pub const fn index(&self) -> u32 { self.raw as u32 } + + #[inline] + pub const fn generation(&self) -> u32 { (self.raw >> 32) as u32 } + + pub const INVALID: Self = Self { raw: u64::MAX, _marker: PhantomData }; + + #[inline] + pub const fn is_valid(&self) -> bool { self.raw != u64::MAX } +} + +impl Copy for Handle {} +impl Clone for Handle { fn clone(&self) -> Self { *self } } +impl PartialEq for Handle { fn eq(&self, other: &Self) -> bool { self.raw == other.raw } } +impl Eq for Handle {} +impl Hash for Handle { fn hash(&self, state: &mut H) { self.raw.hash(state) } } +impl fmt::Debug for Handle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Handle({:#x})", self.raw) } } + +struct Entry { + generation: u32, + resource: Option, +} + +pub struct ResourcePool { + entries: Vec>, + free_list: Vec, + _marker: PhantomData, +} + +impl ResourcePool { + pub fn new() -> Self { + Self { + entries: Vec::new(), + free_list: Vec::new(), + _marker: PhantomData, + } + } + + #[inline] + pub fn insert(&mut self, resource: V) -> Handle { + if let Some(idx) = self.free_list.pop() { + let e = &mut self.entries[idx as usize]; + e.generation = e.generation.wrapping_add(1); + e.resource = Some(resource); + Handle::from_parts(idx, e.generation) + } else { + let idx = self.entries.len() as u32; + self.entries.push(Entry { generation: 0, resource: Some(resource) }); + Handle::from_parts(idx, 0) + } + } + + #[inline] + pub fn remove(&mut self, h: Handle) -> Option { + let idx = h.index() as usize; + if idx >= self.entries.len() { + return None; + } + let e = &mut self.entries[idx]; + if e.generation != h.generation() { + return None; + } + let res = e.resource.take(); + e.generation = e.generation.wrapping_add(1); + self.free_list.push(h.index()); + res + } + + #[inline] + pub fn get(&self, h: Handle) -> Option<&V> { + let idx = h.index() as usize; + if idx >= self.entries.len() { + return None; + } + let e = &self.entries[idx]; + if e.generation != h.generation() { + return None; + } + e.resource.as_ref() + } + + #[inline] + pub fn get_mut(&mut self, h: Handle) -> Option<&mut V> { + let idx = h.index() as usize; + if idx >= self.entries.len() { + return None; + } + let e = &mut self.entries[idx]; + if e.generation != h.generation() { + return None; + } + e.resource.as_mut() + } +} + + diff --git a/engine/pill_core/src/lib.rs b/engine/pill_core/src/lib.rs index 8a06fac8..357470e5 100644 --- a/engine/pill_core/src/lib.rs +++ b/engine/pill_core/src/lib.rs @@ -9,6 +9,8 @@ mod pill_twinmap; mod pill_typemap; mod bitmask_utils; mod timer; +mod handle; +mod tags; // --- Use --- @@ -62,4 +64,19 @@ pub use utils::{ pub use timer::{ Timer, TimerRecord +}; + +pub use handle::{ + Handle, + ResourcePool, +}; + +pub use tags::{ + RendererBufferTag, + RendererCameraTag, + RendererMaterialTag, + RendererMeshTag, + RendererPipelineTag, + RendererPipelineV2Tag, + RendererTextureTag, }; \ No newline at end of file diff --git a/engine/pill_core/src/pill_slotmap.rs b/engine/pill_core/src/pill_slotmap.rs index 5f39ac6a..66d24ec8 100644 --- a/engine/pill_core/src/pill_slotmap.rs +++ b/engine/pill_core/src/pill_slotmap.rs @@ -20,7 +20,6 @@ // 3. This notice may not be removed or altered from any source distribution. - // --- PillSlotMap // This is slotmap crate modified by changing names of types, adding public key variables, removing iterators and adding possibility to restrict version limit // Original crate: https://crates.io/crates/SlotMap @@ -30,14 +29,14 @@ //! Contains the slot map implementation. use core::fmt; +use core::fmt::Debug; use core::marker::PhantomData; #[allow(unused_imports)] // MaybeUninit is only used on nightly at the moment. use core::mem::{ManuallyDrop, MaybeUninit}; use core::ops::{Index, IndexMut}; use std::fmt::Formatter; -use core::fmt::Debug; use std::iter::Enumerate; -use std::num::{ NonZeroU32}; +use std::num::NonZeroU32; // Storage inside a slot or metadata for the freelist when vacant. union SlotUnion { @@ -73,7 +72,7 @@ impl Slot { self.version % 2 > 0 } - pub fn get(&self) -> SlotContent { + pub fn get(&self) -> SlotContent<'_, T> { unsafe { if self.occupied() { Occupied(&*self.u.value) @@ -83,7 +82,7 @@ impl Slot { } } - pub fn get_mut(&mut self) -> SlotContentMut { + pub fn get_mut(&mut self) -> SlotContentMut<'_, T> { unsafe { if self.occupied() { OccupiedMut(&mut *self.u.value) @@ -142,7 +141,6 @@ pub struct PillSlotMap { } impl PillSlotMap { - pub fn with_key() -> Self { Self::with_capacity_and_key(0) } @@ -152,11 +150,14 @@ impl PillSlotMap { } // Warning: Version limit has to be odd value - pub fn with_capacity_and_key_and_version_limit(capacity: usize, version_limit: u32) -> Result { + pub fn with_capacity_and_key_and_version_limit( + capacity: usize, + version_limit: u32, + ) -> Result { if version_limit % 2 == 0 { - return Err(()) + return Err(()); } - + let mut slots = Vec::with_capacity(capacity + 1); slots.push(Slot { u: SlotUnion { next_free: 0 }, @@ -170,7 +171,7 @@ impl PillSlotMap { version_limit, _k: PhantomData, }; - + Ok(slotmap) } @@ -201,7 +202,6 @@ impl PillSlotMap { } pub fn get_next_free_slot_handle(&self) -> K { - let new_num_elems = self.num_elems + 1; if new_num_elems == core::u32::MAX { panic!("PillSlotMap number of elements overflow"); @@ -290,8 +290,11 @@ impl PillSlotMap { self.free_head = index as u32; self.num_elems -= 1; - - slot.version = if slot.version >= self.version_limit { 0 } else { slot.version + 1 }; + slot.version = if slot.version >= self.version_limit { + 0 + } else { + slot.version + 1 + }; value } @@ -366,7 +369,7 @@ impl PillSlotMap { .value } - pub fn iter(&self) -> Iter { + pub fn iter(&self) -> Iter<'_, K, V> { let mut it = self.slots.iter().enumerate(); it.next(); // Skip sentinel. Iter { @@ -376,7 +379,7 @@ impl PillSlotMap { } } - pub fn iter_mut(&mut self) -> IterMut { + pub fn iter_mut(&mut self) -> IterMut<'_, K, V> { let len = self.len(); let mut it = self.slots.iter_mut().enumerate(); it.next(); // Skip sentinel. @@ -528,8 +531,18 @@ impl Default for PillSlotMapKeyData { } } -pub unsafe trait PillSlotMapKey: - From + Copy + Clone + Default + Eq + PartialEq + Ord + PartialOrd + core::hash::Hash + core::fmt::Debug { +pub unsafe trait PillSlotMapKey: + From + + Copy + + Clone + + Default + + Eq + + PartialEq + + Ord + + PartialOrd + + core::hash::Hash + + core::fmt::Debug +{ fn null() -> Self { PillSlotMapKeyData::null().into() } diff --git a/engine/pill_core/src/pill_typemap.rs b/engine/pill_core/src/pill_typemap.rs index f568f4dd..6a8cb381 100644 --- a/engine/pill_core/src/pill_typemap.rs +++ b/engine/pill_core/src/pill_typemap.rs @@ -1,18 +1,14 @@ - // --- PillTypeMap // This is typemap_rev crate modified by changing names of types // Original crate: https://crates.io/crates/typemap_rev //! A hashmap whose keys are defined by types. - use std::any::{Any, TypeId}; -use std::collections::HashMap; use std::collections::hash_map::{ - Entry as HashMapEntry, - OccupiedEntry as HashMapOccupiedEntry, - VacantEntry as HashMapVacantEntry, + Entry as HashMapEntry, OccupiedEntry as HashMapOccupiedEntry, VacantEntry as HashMapVacantEntry, }; +use std::collections::HashMap; use std::marker::PhantomData; /// PillTypeMapKey is used to declare key types that are eligible for use @@ -30,8 +26,7 @@ pub trait PillTypeMapKey: Any { /// /// [`HashMap`]: std::collections::HashMap //pub struct PillTypeMap(HashMap>); -pub struct PillTypeMap(HashMap>); - +pub struct PillTypeMap(HashMap>); impl PillTypeMap { /// Creates a new instance of `PillTypeMap`. @@ -59,7 +54,7 @@ impl PillTypeMap { #[inline] pub fn contains_key(&self) -> bool where - T: PillTypeMapKey + T: PillTypeMapKey, { self.0.contains_key(&TypeId::of::()) } @@ -87,7 +82,7 @@ impl PillTypeMap { #[inline] pub fn insert(&mut self, value: T::Storage) where - T: PillTypeMapKey + T: PillTypeMapKey, { self.0.insert(TypeId::of::(), Box::new(value)); } @@ -98,7 +93,7 @@ impl PillTypeMap { #[inline] pub fn entry(&mut self) -> Entry<'_, T> where - T: PillTypeMapKey + T: PillTypeMapKey, { match self.0.entry(TypeId::of::()) { HashMapEntry::Occupied(entry) => Entry::Occupied(OccupiedEntry { @@ -108,7 +103,7 @@ impl PillTypeMap { HashMapEntry::Vacant(entry) => Entry::Vacant(VacantEntry { entry, _marker: PhantomData, - }) + }), } } @@ -134,7 +129,7 @@ impl PillTypeMap { #[inline] pub fn get(&self) -> Option<&T::Storage> where - T: PillTypeMapKey + T: PillTypeMapKey, { self.0 .get(&TypeId::of::()) @@ -165,7 +160,7 @@ impl PillTypeMap { #[inline] pub fn get_mut(&mut self) -> Option<&mut T::Storage> where - T: PillTypeMapKey + T: PillTypeMapKey, { self.0 .get_mut(&TypeId::of::()) @@ -192,7 +187,7 @@ impl PillTypeMap { #[inline] pub fn remove(&mut self) -> Option where - T: PillTypeMapKey + T: PillTypeMapKey, { self.0 .remove(&TypeId::of::()) @@ -239,7 +234,7 @@ where #[inline] pub fn or_insert_with(self, f: F) -> &'a mut K::Storage where - F: FnOnce() -> K::Storage + F: FnOnce() -> K::Storage, { match self { Entry::Occupied(entry) => entry.into_mut(), @@ -250,13 +245,13 @@ where #[inline] pub fn and_modify(self, f: F) -> Self where - F: FnOnce(&mut K::Storage) + F: FnOnce(&mut K::Storage), { match self { Entry::Occupied(mut entry) => { f(entry.get_mut()); Entry::Occupied(entry) - }, + } Entry::Vacant(entry) => Entry::Vacant(entry), } } @@ -265,7 +260,7 @@ where impl<'a, K> Entry<'a, K> where K: PillTypeMapKey, - K::Storage: Default + K::Storage: Default, { #[inline] pub fn or_default(self) -> &'a mut K::Storage { @@ -278,7 +273,7 @@ where K: PillTypeMapKey, { //entry: HashMapOccupiedEntry<'a, TypeId, Box<(dyn Any + Send + Sync)>>, - entry: HashMapOccupiedEntry<'a, TypeId, Box<(dyn Any + Send)>>, + entry: HashMapOccupiedEntry<'a, TypeId, Box>, _marker: PhantomData<&'a K::Storage>, } @@ -317,7 +312,7 @@ where K: PillTypeMapKey, { //entry: HashMapVacantEntry<'a, TypeId, Box<(dyn Any + Send + Sync)>>, - entry: HashMapVacantEntry<'a, TypeId, Box<(dyn Any + Send)>>, + entry: HashMapVacantEntry<'a, TypeId, Box>, _marker: PhantomData<&'a K::Storage>, } diff --git a/engine/pill_core/src/tags.rs b/engine/pill_core/src/tags.rs new file mode 100644 index 00000000..cfd219c7 --- /dev/null +++ b/engine/pill_core/src/tags.rs @@ -0,0 +1,7 @@ +pub struct RendererBufferTag; +pub struct RendererCameraTag; +pub struct RendererMaterialTag; +pub struct RendererMeshTag; +pub struct RendererPipelineTag; +pub struct RendererPipelineV2Tag; +pub struct RendererTextureTag; diff --git a/engine/pill_core/src/timer.rs b/engine/pill_core/src/timer.rs index acdd8ca8..5ac84886 100644 --- a/engine/pill_core/src/timer.rs +++ b/engine/pill_core/src/timer.rs @@ -1,10 +1,8 @@ use std::time::Instant; -use anyhow::{Context, Result, Error}; +use anyhow::{Context, Error, Result}; use indexmap::IndexMap; - - #[derive(Debug, Clone)] pub struct TimerRecord { pub label: String, @@ -26,6 +24,8 @@ pub struct Timer { pub records: Vec, current_label: Option, current_label_start: Option, + // Simple per-frame counters keyed by label + counters: IndexMap, } impl Timer { @@ -35,6 +35,7 @@ impl Timer { records: Vec::new(), current_label: None, current_label_start: None, + counters: IndexMap::new(), } } @@ -79,9 +80,11 @@ impl Timer { } fn flush_record(&mut self) { - if let (Some(label), Some(start), Some(current)) = - (&self.current_label, self.current_label_start, self.stack.last_mut()) - { + if let (Some(label), Some(start), Some(current)) = ( + &self.current_label, + self.current_label_start, + self.stack.last_mut(), + ) { let duration = start.elapsed().as_secs_f32() * 1000.0; current.subrecords.push(TimerRecord { label: label.clone(), @@ -98,6 +101,25 @@ impl Timer { self.records.iter().map(|r| r.duration).sum() } + // --- Counters --- + pub fn set_counter(&mut self, label: impl Into, value: u64) { + self.counters.insert(label.into(), value); + } + + // pub fn increment_counter(&mut self, label: impl Into, delta: u64) { + // let key = label.into(); + // let entry = self.counters.entry(key).or_insert(0); + // *entry = entry.saturating_add(delta); + // } + + pub fn get_counter(&self, label: &str) -> Option { + self.counters.get(label).copied() + } + + pub fn counters(&self) -> &IndexMap { + &self.counters + } + pub fn print(&self, indent: usize) { for record in &self.records { Self::print_record(record, indent); @@ -105,9 +127,15 @@ impl Timer { } fn print_record(record: &TimerRecord, indent: usize) { - println!("{:indent$}- {}: {:.3}ms", "", record.label, record.duration, indent = indent); + println!( + "{:indent$}- {}: {:.3}ms", + "", + record.label, + record.duration, + indent = indent + ); for sub in &record.subrecords { Self::print_record(sub, indent + 2); } } -} \ No newline at end of file +} diff --git a/engine/pill_core/src/utils.rs b/engine/pill_core/src/utils.rs index 50a910b7..15dcfb24 100644 --- a/engine/pill_core/src/utils.rs +++ b/engine/pill_core/src/utils.rs @@ -1,10 +1,9 @@ -use crate::{EngineError, define_new_pill_slotmap_key}; +use crate::{define_new_pill_slotmap_key, EngineError}; -use anyhow::{ Context, Result, Error }; -use boolinator::Boolinator; +use anyhow::{Context, Error, Result}; use colored::{ColoredString, Colorize}; -use std::{ any::type_name, collections::HashMap, hash::Hash, path::PathBuf }; use log::debug; +use std::{any::type_name, collections::HashMap, hash::Hash, path::PathBuf}; // --- Type to string utils --- @@ -52,7 +51,12 @@ impl PillStyle for &str { // To be used with large module objects (Engine, Renderer, Window, etc) - changes color and adds bold #[inline] fn mobj_style(self) -> ColoredString { - self.color(colored::Color::TrueColor { r: 180, g: 25, b: 100 }).bold() + self.color(colored::Color::TrueColor { + r: 180, + g: 25, + b: 100, + }) + .bold() } // To be used with general objects (Scene, Component, System, Resource, etc) - changes color and adds bold @@ -64,13 +68,21 @@ impl PillStyle for &str { // To be used with specific objects (CameraComponent, Texture, Mesh, etc) - changes color #[inline] fn sobj_style(self) -> ColoredString { - self.color(colored::Color::TrueColor { r: 95, g: 210, b: 90 }) + self.color(colored::Color::TrueColor { + r: 95, + g: 210, + b: 90, + }) } // To be used with names - changes color adds quotation marks #[inline] fn name_style(self) -> ColoredString { - format!("\"{}\"", self).color(colored::Color::TrueColor { r: 190, g: 220, b: 160 }) + format!("\"{}\"", self).color(colored::Color::TrueColor { + r: 190, + g: 220, + b: 160, + }) } // To be used with names - changes color adds bold @@ -93,30 +105,45 @@ pub trait MeshXImpl { // Check if path to asset is correct (exists and has supported format) pub fn validate_asset_path(path: &PathBuf, allowed_formats: &'static [&'static str]) -> Result<()> // Vec { - path.exists().ok_or(Error::new(EngineError::InvalidAssetPath(path.display().to_string())))?; + if !path.exists() { + return Err(Error::new(EngineError::InvalidAssetPath( + path.display().to_string(), + ))); + } match path.extension() { - Some(v) => match allowed_formats.contains(&v.to_str().unwrap()) { //} v.eq(allowed_format) { + Some(v) => match allowed_formats.contains(&v.to_str().unwrap()) { + //} v.eq(allowed_format) { true => return Ok(()), - false => return Err(Error::new(EngineError::InvalidAssetFormat(allowed_formats, v.to_str().unwrap().to_string()))), + false => { + return Err(Error::new(EngineError::InvalidAssetFormat( + allowed_formats, + v.to_str().unwrap().to_string(), + ))) + } }, - None => return Err(Error::new(EngineError::InvalidAssetPath(path.display().to_string()))), + None => { + return Err(Error::new(EngineError::InvalidAssetPath( + path.display().to_string(), + ))) + } } } // --- PillSlotMap utils --- -#[macro_export] macro_rules! define_component_handle { +#[macro_export] +macro_rules! define_component_handle { ( $(#[$outer:meta])* $vis:vis struct $name:ident; $($rest:tt)* ) => { - pill_core::define_new_pill_slotmap_key! { } - }; + pill_core::define_new_pill_slotmap_key! {} + }; } // --- Other --- #[inline] pub fn get_game_error_message(result: Result<()>) -> Option { - if result.is_err() { + if result.is_err() { let mut message = String::new(); for (i, error) in result.err().unwrap().chain().enumerate() { let message_part = match i == 0 { @@ -126,8 +153,7 @@ pub fn get_game_error_message(result: Result<()>) -> Option { message.push_str(message_part.as_str()); } Some(message) - } - else { + } else { None } } @@ -141,4 +167,4 @@ macro_rules! create_game { Box::into_raw(Box::new(game)) as *mut std::ffi::c_void } }; -} \ No newline at end of file +} diff --git a/engine/pill_engine/Cargo.toml b/engine/pill_engine/Cargo.toml index 453d496b..a5cf1ee2 100644 --- a/engine/pill_engine/Cargo.toml +++ b/engine/pill_engine/Cargo.toml @@ -18,12 +18,18 @@ env_logger = "0.9.0" log = "0.4" # Loaders -image = "0.23" +image = { version = "0.25.6", features = ["hdr"] } tobj = "3.0" +gltf = { version = "1.4", features = ["import", "names"] } # Audio rodio = { version = "0.20", features = ["wav", "mp3"] } +# Rendering +wgpu = { version = "0.20.1", features = [ "spirv" ] } +egui-wgpu = "0.28.1" +egui-winit = "0.28.1" + # Other readonly = "0.2" cgmath = "0.18" @@ -40,6 +46,8 @@ rand = "0.9.2" bincode = "1.0" gilrs = "0.11.0" once_cell = "1.21.3" +glam = { version = "0.27" } +pollster = "0.3" [build-dependencies] anyhow = "1.0" diff --git a/engine/pill_engine/res/textures/pill_logo_horizontal_white.png b/engine/pill_engine/res/textures/pill_logo_horizontal_white.png new file mode 100644 index 00000000..d4e5bc35 Binary files /dev/null and b/engine/pill_engine/res/textures/pill_logo_horizontal_white.png differ diff --git a/engine/pill_engine/src/config.rs b/engine/pill_engine/src/config.rs index c872e460..f084fa6d 100644 --- a/engine/pill_engine/src/config.rs +++ b/engine/pill_engine/src/config.rs @@ -1,13 +1,17 @@ use crate::{ - ecs::{ audio_system, deferred_update_system, input_system, rendering_system, time_system, AudioManagerComponent, DeferredUpdateComponent, EguiManagerComponent, InputComponent, SystemFunction, TimeComponent, UpdatePhase }, - graphics::{ RendererMaterialHandle, RendererTextureHandle }, - resources::{ MaterialHandle, TextureHandle, TextureType }, + ecs::{ + audio_system, deferred_update_system, input_system, rendering_system, time_system, + AudioManagerComponent, DeferredUpdateComponent, EguiManagerComponent, InputComponent, + RenderStateComponent, SystemFunction, TimeComponent, UpdatePhase, + }, + graphics::RendererTextureHandle, + resources::{TextureHandle, TextureType}, }; -use pill_core::PillSlotMapKeyData; +use pill_core::{Handle, PillSlotMapKeyData}; -use std::{num::NonZeroU32, any::TypeId}; use lazy_static::lazy_static; +use std::{any::TypeId, num::NonZeroU32}; // --- General --- @@ -57,14 +61,21 @@ pub const RENDERING_SYSTEM: SystemConfig = SystemConfig { }; // --- Resources --- - +// +// Diagnostics (glTF import): +// - Set `LOG_GLTF_IMPORT = true` in runtime config to log imported meshes/materials/texture bindings. +// - Run with `RUST_LOG=info RUST_BACKTRACE=1` to see logs. +// - If materials show no textures bound, the asset may use KTX2/Basis images; use PNG/JPG variants +// (e.g., glTF-Sample-Models “glTF-Binary”) or add Basis/KTX2 transcode support. +// pub const RESOURCE_VERSION_LIMIT: usize = 255; pub const MAX_PIPELINES: usize = 10; -pub const MAX_TEXTURES: usize = 10; -pub const MAX_MATERIALS: usize = 10; -pub const MAX_MESHES: usize = 10; +pub const MAX_TEXTURES: usize = 1000; +pub const MAX_MATERIALS: usize = 1000; +pub const MAX_MESHES: usize = 1000; pub const MAX_SOUNDS: usize = 10; +pub const MAX_MODELS: usize = 1000; // Convention: All resource names starting with "pill_default" are restricted, cannot be added and removed from game pub const DEFAULT_RESOURCE_PREFIX: &str = "pill_default"; @@ -91,46 +102,45 @@ pub const RENDER_QUEUE_KEY_MESH_INDEX_IDX: u8 = 3; pub const RENDER_QUEUE_KEY_MESH_VERSION_IDX: u8 = 4; // Default resource handle - Color texture -pub const DEFAULT_COLOR_TEXTURE_HANDLE: TextureHandle = TextureHandle { - 0: PillSlotMapKeyData { index: 1, version: unsafe { std::num::NonZeroU32::new_unchecked(1) } } -}; -pub const DEFAULT_RENDERER_COLOR_TEXTURE_HANDLE: RendererTextureHandle = RendererTextureHandle { - 0: PillSlotMapKeyData { index: 1, version: unsafe { std::num::NonZeroU32::new_unchecked(1) } } +pub const DEFAULT_COLOR_TEXTURE_HANDLE: TextureHandle = TextureHandle { + 0: PillSlotMapKeyData { + index: 1, + version: unsafe { std::num::NonZeroU32::new_unchecked(1) }, + }, }; +pub const DEFAULT_RENDERER_COLOR_TEXTURE_HANDLE: RendererTextureHandle = Handle::from_parts(1, 1); // Default resource handle - Normal texture -pub const DEFAULT_NORMAL_TEXTURE_HANDLE: TextureHandle = TextureHandle { - 0: PillSlotMapKeyData { index: 2, version: unsafe { std::num::NonZeroU32::new_unchecked(1) } } -}; -pub const DEFAULT_RENDERER_NORMAL_TEXTURE_HANDLE: RendererTextureHandle = RendererTextureHandle { - 0: PillSlotMapKeyData { index: 2, version: unsafe { std::num::NonZeroU32::new_unchecked(1) } } +pub const DEFAULT_NORMAL_TEXTURE_HANDLE: TextureHandle = TextureHandle { + 0: PillSlotMapKeyData { + index: 2, + version: unsafe { std::num::NonZeroU32::new_unchecked(1) }, + }, }; +pub const DEFAULT_RENDERER_NORMAL_TEXTURE_HANDLE: RendererTextureHandle = Handle::from_parts(2, 1); -pub fn get_default_texture_handles(texture_type: TextureType) -> (TextureHandle, RendererTextureHandle) { +pub fn get_default_texture_handles( + texture_type: TextureType, +) -> (TextureHandle, RendererTextureHandle) { match texture_type { - TextureType::Color => (DEFAULT_COLOR_TEXTURE_HANDLE, DEFAULT_RENDERER_COLOR_TEXTURE_HANDLE), - TextureType::Normal => (DEFAULT_NORMAL_TEXTURE_HANDLE, DEFAULT_RENDERER_NORMAL_TEXTURE_HANDLE), + TextureType::Gamma => ( + DEFAULT_COLOR_TEXTURE_HANDLE, + DEFAULT_RENDERER_COLOR_TEXTURE_HANDLE, + ), + TextureType::Linear => ( + DEFAULT_NORMAL_TEXTURE_HANDLE, + DEFAULT_RENDERER_NORMAL_TEXTURE_HANDLE, + ), } } -// Default resource handle - Material -pub const DEFAULT_MATERIAL_HANDLE: MaterialHandle = MaterialHandle { - 0: PillSlotMapKeyData { index: 1, version: unsafe { std::num::NonZeroU32::new_unchecked(1) } } -}; -pub const DEFAULT_RENDERER_MATERIAL_HANDLE: RendererMaterialHandle = RendererMaterialHandle { - 0: PillSlotMapKeyData { index: 1, version: unsafe { std::num::NonZeroU32::new_unchecked(1) } } -}; - -pub fn get_default_material_handles() -> (MaterialHandle, RendererMaterialHandle) { - (DEFAULT_MATERIAL_HANDLE, DEFAULT_RENDERER_MATERIAL_HANDLE) -} - lazy_static! { pub static ref ENGINE_GLOBAL_COMPONENTS: Vec = vec!( TypeId::of::(), TypeId::of::(), TypeId::of::(), TypeId::of::(), - TypeId::of::() + TypeId::of::(), + TypeId::of::() ); -} \ No newline at end of file +} diff --git a/engine/pill_engine/src/ecs/components/camera_component.rs b/engine/pill_engine/src/ecs/components/camera_component.rs index 377e4ade..44a032c8 100644 --- a/engine/pill_engine/src/ecs/components/camera_component.rs +++ b/engine/pill_engine/src/ecs/components/camera_component.rs @@ -1,7 +1,7 @@ use crate::{ engine::Engine, graphics::{ RenderQueueKey, compose_render_queue_key, RendererCameraHandle }, - resources::{ Material, MaterialHandle, Mesh, MeshHandle }, + resources::{ PBRMaterial, PBRMaterialHandle, Mesh, MeshHandle }, ecs::{ Component, ComponentStorage, EntityHandle, SceneHandle, DeferredUpdateManagerPointer, DeferredUpdateComponentRequest }, }; diff --git a/engine/pill_engine/src/ecs/components/deferred_update_component.rs b/engine/pill_engine/src/ecs/components/deferred_update_component.rs index e5246b5c..6bf71d25 100644 --- a/engine/pill_engine/src/ecs/components/deferred_update_component.rs +++ b/engine/pill_engine/src/ecs/components/deferred_update_component.rs @@ -2,7 +2,7 @@ use crate::{ engine::Engine, - resources::{ MeshHandle, Material, Resource, ResourceStorage }, + resources::{ MeshHandle, PBRMaterial, Resource, ResourceStorage }, ecs::{ Component, EntityHandle, ComponentStorage, SceneHandle, GlobalComponentStorage, GlobalComponent } }; diff --git a/engine/pill_engine/src/ecs/components/egui_manager_component.rs b/engine/pill_engine/src/ecs/components/egui_manager_component.rs index d688cdf2..521d3a4d 100644 --- a/engine/pill_engine/src/ecs/components/egui_manager_component.rs +++ b/engine/pill_engine/src/ecs/components/egui_manager_component.rs @@ -1,14 +1,19 @@ use std::collections::HashMap; use crate::{ - ecs::{components::{ Component, GlobalComponent, GlobalComponentStorage }, systems, UpdatePhase}, engine::Engine + ecs::{ + components::{Component, GlobalComponent, GlobalComponentStorage}, + systems, UpdatePhase, + }, + engine::Engine, }; +use crate::ecs::components::render_state_component::RenderStateComponent; use egui::Ui; use indexmap::IndexMap; use pill_core::{PillTypeMapKey, Timer, TimerRecord}; -use anyhow::{Result, Error, Context}; +use anyhow::{Context, Error, Result}; pub struct EguiManagerComponent { collapsing_state: HashMap, @@ -16,26 +21,71 @@ pub struct EguiManagerComponent { impl EguiManagerComponent { pub fn new() -> Self { - Self { - collapsing_state: HashMap::new(), + Self { + collapsing_state: HashMap::new(), } } - pub fn get_ui(engine: &mut Engine) -> Box { + pub fn get_ui(engine: &mut Engine) -> Box { + let entity_count = engine + .scene_manager + .get_active_scene() + .unwrap() + .entities + .len(); + let system_count = engine + .system_manager + .update_phases + .iter() + .map(|(_, systems)| systems.len()) + .sum::(); - let entity_count = engine.scene_manager.get_active_scene().unwrap().entities.len(); - let system_count = engine.system_manager.update_phases.iter().map(|(_, systems)| systems.len()).sum::(); - - let system_timers: Vec<(UpdatePhase, Vec<(String, Timer)>)> = engine.system_manager.update_phases + let system_timers: Vec<(UpdatePhase, Vec<(String, Timer)>)> = engine + .system_manager + .update_phases .iter() .map(|(update_phase, systems)| { - let system_timers = systems.iter().map(|(_, system)| (system.name.clone(), system.timer.clone().context(system.name.clone()).unwrap())).collect(); + let system_timers = systems + .iter() + .map(|(_, system)| { + ( + system.name.clone(), + system.timer.clone().context(system.name.clone()).unwrap(), + ) + }) + .collect(); (update_phase.clone(), system_timers) }) .collect::>(); - let total_systems_delta_time = system_timers.iter().map(|(_, timers)| timers.iter().map(|(_, timer)| timer.total_duration()).sum::()).sum::(); + let total_systems_delta_time = system_timers + .iter() + .map(|(_, timers)| { + timers + .iter() + .map(|(_, timer)| timer.total_duration()) + .sum::() + }) + .sum::(); let frame_delta_time = engine.frame_delta_time; + let window_w = engine.window_size.width; + let window_h = engine.window_size.height; + let post_process = engine + .get_global_component::() + .ok() + .map(|rs| rs.post_process.clone()) + .unwrap_or_else(|| engine.resource_manager.post_process.clone()); + + // Snapshot draw call counter from last frame + let total_draw_calls: Option = engine + .system_manager + .peek_system_timer( + crate::config::RENDERING_SYSTEM.name, + crate::config::RENDERING_SYSTEM.update_phase, + ) + .ok() + .and_then(|t| t) + .and_then(|t| t.get_counter("draw_calls")); let ui = Box::new(move |ui: &egui::Context| { egui::Window::new("PillEngine") @@ -44,69 +94,121 @@ impl EguiManagerComponent { .anchor(egui::Align2::LEFT_TOP, [0.0, 0.0]) .show(ui, |ui| { egui::ScrollArea::vertical() - .auto_shrink([false; 2]) // optional: prevent auto shrink - .show(ui, |ui| { - if ui.add(egui::Button::new("Click me")).clicked() { - println!("PRESSED"); - } - ui.add(egui::Label::new(format!("FPS {}", 1000.0 / frame_delta_time) )); - ui.add(egui::Label::new(format!("Frame Delta Time: {:.4} ms", frame_delta_time))); - ui.add(egui::Label::new(format!("Entities: {}", entity_count))); - ui.separator(); - ui.add(egui::Label::new(format!("Systems: {}, Total delta time: {:.3} ms", system_count, total_systems_delta_time))); - let mut phase_state = HashMap::new(); - for (update_phase, system_timers) in system_timers.iter() { - let phase_duration = system_timers - .iter() - .map(|(_, timer)| timer.total_duration()) - .sum::(); - - let phase_id = format!("phase_{}", update_phase); - let is_phase_open = *phase_state.get(&phase_id).unwrap_or(&true); - - let header = egui::CollapsingHeader::new(format!( - "Update Phase: {} {:.4} ms", - update_phase, phase_duration - )) - .id_source(&phase_id) - .default_open(is_phase_open) - .show(ui, |ui| { - for (system_name, timer) in system_timers { - let mut state = HashMap::new(); - for record in &timer.records { - Self::render_timer_tree_with_state(ui, record, &mut state); - } + .auto_shrink([false; 2]) // optional: prevent auto shrink + .show(ui, |ui| { + ui.horizontal(|ui| { + if ui.add(egui::Button::new("Click me")).clicked() { + println!("PRESSED"); } + ui.label(format!("FPS {:.1}", 1000.0 / frame_delta_time)); + ui.label(format!("{}x{}", window_w, window_h)); }); - - if header.header_response.clicked() { - phase_state.insert(phase_id, !is_phase_open); + // Focus controls + { + let mut pp = post_process.lock().unwrap(); + ui.add( + egui::Slider::new(&mut pp.focus_point, 0.1..=200.0) + .text("focusPoint (m)"), + ); + ui.add( + egui::Slider::new(&mut pp.focus_scale, 0.0..=50.0) + .text("focusScale"), + ); + ui.checkbox(&mut pp.mb_enabled, "Motion blur enabled"); + ui.checkbox(&mut pp.mb_debug_velocity, "MB debug overlay"); + ui.add( + egui::Slider::new(&mut pp.mb_strength, 0.0..=4.0) + .text("MB velocity scale"), + ); + ui.add( + egui::Slider::new(&mut pp.mb_min_speed, 0.0..=0.05) + .text("MB min speed (texels)"), + ); + ui.add( + egui::Slider::new(&mut pp.mb_depth_softness, 0.0..=200.0) + .text("MB depth softness"), + ); + let mut max_samples_i = pp.mb_max_samples as i32; + ui.add( + egui::Slider::new(&mut max_samples_i, 1..=64) + .text("MB max samples"), + ); + pp.mb_max_samples = max_samples_i as u32; } - } - }); + ui.add(egui::Label::new(format!( + "Frame Delta Time: {:.4} ms", + frame_delta_time + ))); + ui.add(egui::Label::new(format!("Entities: {}", entity_count))); + if let Some(dc) = total_draw_calls { + ui.add(egui::Label::new(format!("Draw calls: {}", dc))); + } + ui.separator(); + ui.add(egui::Label::new(format!( + "Systems: {}, Total delta time: {:.3} ms", + system_count, total_systems_delta_time + ))); + let mut phase_state = HashMap::new(); + for (update_phase, system_timers) in system_timers.iter() { + let phase_duration = system_timers + .iter() + .map(|(_, timer)| timer.total_duration()) + .sum::(); + + let phase_id = format!("phase_{}", update_phase); + let is_phase_open = *phase_state.get(&phase_id).unwrap_or(&true); + + let header = egui::CollapsingHeader::new(format!( + "Update Phase: {} {:.4} ms", + update_phase, phase_duration + )) + .id_source(&phase_id) + .default_open(is_phase_open) + .show(ui, |ui| { + for (system_name, timer) in system_timers { + let mut state = HashMap::new(); + for record in &timer.records { + Self::render_timer_tree_with_state( + ui, record, &mut state, + ); + } + } + }); + + if header.header_response.clicked() { + phase_state.insert(phase_id, !is_phase_open); + } + } + }); }); - }); + }) as Box; ui } - pub fn render_timer_tree_with_state(ui: &mut Ui, record: &TimerRecord, state: &mut HashMap) { + pub fn render_timer_tree_with_state( + ui: &mut Ui, + record: &TimerRecord, + state: &mut HashMap, + ) { let summary = format!("{:.3} ms - {}", record.duration, record.label); if record.subrecords.is_empty() { ui.label(summary); } else { let id = format!("_{}", record.label); let is_open = state.get(&id).copied().unwrap_or(false); - let response = egui::CollapsingHeader::new(egui::RichText::new(summary) - .text_style(egui::TextStyle::Body) - .color(ui.visuals().text_color())) - .id_source(&id) - .default_open(is_open) - .show(ui, |ui| { - for sub in &record.subrecords { - Self::render_timer_tree_with_state(ui, sub, state); - } - }); + let response = egui::CollapsingHeader::new( + egui::RichText::new(summary) + .text_style(egui::TextStyle::Body) + .color(ui.visuals().text_color()), + ) + .id_source(&id) + .default_open(is_open) + .show(ui, |ui| { + for sub in &record.subrecords { + Self::render_timer_tree_with_state(ui, sub, state); + } + }); let header_response = response.header_response; if header_response.clicked() { state.insert(id, !is_open); @@ -115,16 +217,12 @@ impl EguiManagerComponent { } pub(crate) fn update(&mut self, delta_time: f32) -> Result<()> { - - Ok(()) } } impl PillTypeMapKey for EguiManagerComponent { - type Storage = GlobalComponentStorage; + type Storage = GlobalComponentStorage; } -impl GlobalComponent for EguiManagerComponent { - -} \ No newline at end of file +impl GlobalComponent for EguiManagerComponent {} diff --git a/engine/pill_engine/src/ecs/components/mesh_rendering_component.rs b/engine/pill_engine/src/ecs/components/mesh_rendering_component.rs index 8714a3ad..358698f1 100644 --- a/engine/pill_engine/src/ecs/components/mesh_rendering_component.rs +++ b/engine/pill_engine/src/ecs/components/mesh_rendering_component.rs @@ -1,16 +1,17 @@ use crate::{ + ecs::{ + Component, ComponentStorage, DeferredUpdateComponent, DeferredUpdateComponentRequest, + DeferredUpdateManagerPointer, EntityHandle, SceneHandle, + }, engine::Engine, - graphics::{ RenderQueueKey, compose_render_queue_key }, - resources::{ Material, MaterialHandle, Mesh, MeshHandle, ResourceManager }, - ecs::{ EntityHandle, ComponentStorage, Component, SceneHandle, DeferredUpdateComponentRequest, DeferredUpdateManagerPointer, DeferredUpdateComponent }, - config::DEFAULT_MATERIAL_HANDLE, + graphics::{compose_render_queue_key, RenderQueueKey}, + resources::{Mesh, MeshHandle, PBRMaterial, PBRMaterialHandle, ResourceManager}, }; use cgmath::num_traits::Float; -use pill_core::{ PillTypeMap, PillTypeMapKey, PillStyle, get_type_name, PillSlotMapKey }; - -use anyhow::{ Result, Context, Error }; +use pill_core::{get_type_name, PillSlotMapKey, PillStyle, PillTypeMap, PillTypeMapKey}; +use anyhow::{Context, Error, Result}; const DEFERRED_REQUEST_VARIANT_UPDATE_RENDER_QUEUE: usize = 0; const DEFERRED_REQUEST_VARIANT_SET_MATERIAL: usize = 1; @@ -28,13 +29,13 @@ impl MeshRenderingComponentBuilder { component: MeshRenderingComponent::new(), } } - + pub fn mesh(mut self, mesh_handle: &MeshHandle) -> Self { self.component.mesh_handle = Some(mesh_handle.clone()); self } - pub fn material(mut self, material_handle: &MaterialHandle) -> Self { + pub fn material(mut self, material_handle: &PBRMaterialHandle) -> Self { self.component.material_handle = Some(material_handle.clone()); self } @@ -51,8 +52,10 @@ pub struct MeshRenderingComponent { #[readonly] pub mesh_handle: Option, #[readonly] - pub material_handle: Option, - pub(crate) render_queue_key: Option, + pub material_handle: Option, + // [SIMILAR] Precomputes/stores a key for sorting by pipeline/material/mesh outside hot draw loop (per TALK separation of data setup vs draw) + // [API->CLIENT] Key encodes pipeline/material/mesh generational handles used by low-level to minimize state changes + pub(crate) render_queue_key: Option, entity_handle: Option, scene_handle: Option, @@ -65,7 +68,7 @@ impl MeshRenderingComponent { } pub fn new() -> Self { - Self { + Self { mesh_handle: None, material_handle: None, render_queue_key: None, @@ -75,13 +78,15 @@ impl MeshRenderingComponent { } } - pub fn set_material(&mut self, material_handle: &MaterialHandle) { + pub fn set_material(&mut self, material_handle: &PBRMaterialHandle) { self.material_handle = Some(material_handle.clone()); + // [SIMILAR] Mutations post a deferred update; avoids work in the hot path self.post_deferred_update_request(DEFERRED_REQUEST_VARIANT_SET_MATERIAL); } pub fn set_mesh(&mut self, mesh_handle: &MeshHandle) { self.mesh_handle = Some(mesh_handle.clone()); + // [SIMILAR] Mutations post a deferred update; avoids work in the hot path self.post_deferred_update_request(DEFERRED_REQUEST_VARIANT_SET_MESH); } @@ -95,7 +100,7 @@ impl MeshRenderingComponent { self.post_deferred_update_request(DEFERRED_REQUEST_VARIANT_UPDATE_RENDER_QUEUE); } - pub(crate) fn set_material_handle(&mut self, material_handle: Option) { + pub(crate) fn set_material_handle(&mut self, material_handle: Option) { self.material_handle = material_handle; } @@ -103,25 +108,24 @@ impl MeshRenderingComponent { self.mesh_handle = mesh_handle; } - pub(crate) fn update_render_queue_key(&mut self, resource_manager: &ResourceManager) -> Result<()> { + pub(crate) fn update_render_queue_key( + &mut self, + resource_manager: &ResourceManager, + ) -> Result<()> { if self.mesh_handle.is_some() { - // Use default material if no material is set - let material_handle = match self.material_handle { - Some(v) => v, - None => DEFAULT_MATERIAL_HANDLE, - }; - // Compose render queue key and set it - if let Ok(render_queue_key) = compose_render_queue_key(resource_manager, &material_handle, &self.mesh_handle.unwrap()) - { + if let Ok(render_queue_key) = compose_render_queue_key( + resource_manager, + &self.mesh_handle.unwrap(), + &self.material_handle, + ) { + // [SIMILAR] Precompute render binning key when data changes (not per draw) + // [RECOMMENDED] Keep key up-to-date on any mesh/material change self.render_queue_key = Some(render_queue_key); - } - else { + } else { self.render_queue_key = None; } - } - else - { + } else { self.render_queue_key = None; } @@ -130,37 +134,63 @@ impl MeshRenderingComponent { fn post_deferred_update_request(&mut self, request_variant: usize) { if self.deferred_update_manager.is_some() { - let entity_handle = self.entity_handle.expect("Critical: Cannot post deferred update request. No EntityHandle set in Component"); - let scene_handle = self.scene_handle.expect("Critical: Cannot post deferred update request. No SceneHandle set in Component"); - let request = DeferredUpdateComponentRequest::::new(entity_handle, scene_handle, request_variant); - self.deferred_update_manager.as_mut().expect("Critical: No DeferredUpdateManager").post_update_request(request); + let entity_handle = self.entity_handle.expect( + "Critical: Cannot post deferred update request. No EntityHandle set in Component", + ); + let scene_handle = self.scene_handle.expect( + "Critical: Cannot post deferred update request. No SceneHandle set in Component", + ); + let request = DeferredUpdateComponentRequest::::new( + entity_handle, + scene_handle, + request_variant, + ); + self.deferred_update_manager + .as_mut() + .expect("Critical: No DeferredUpdateManager") + .post_update_request(request); } } } impl PillTypeMapKey for MeshRenderingComponent { - type Storage = ComponentStorage; + type Storage = ComponentStorage; } impl Component for MeshRenderingComponent { fn initialize(&mut self, engine: &mut Engine) -> Result<()> { + // [SIMILAR] Uses deferred update system → separates data modification from drawing per TALK // This component is using DeferredUpdateSystem so keep DeferredUpdateManager - let deferred_update_component = engine.get_global_component_mut::().expect("Critical: No DeferredUpdateComponent"); - self.deferred_update_manager = Some(deferred_update_component.borrow_deferred_update_manager()); + let deferred_update_component = engine + .get_global_component_mut::() + .expect("Critical: No DeferredUpdateComponent"); + self.deferred_update_manager = + Some(deferred_update_component.borrow_deferred_update_manager()); // Check if material handle is valid if self.material_handle.is_some() { - engine.get_resource::(&self.material_handle.unwrap()) - .context(format!("Creating {} {} failed", "Component".gobj_style(), get_type_name::().sobj_style()))?; + engine + .get_resource::(&self.material_handle.unwrap()) + .context(format!( + "Creating {} {} failed", + "Component".gobj_style(), + get_type_name::().sobj_style() + ))?; } // Check if mesh handle is valid if self.mesh_handle.is_some() { - engine.get_resource::(&self.mesh_handle.unwrap()) - .context(format!("Creating {} {} failed", "Component".gobj_style(), get_type_name::().sobj_style()))?; + engine + .get_resource::(&self.mesh_handle.unwrap()) + .context(format!( + "Creating {} {} failed", + "Component".gobj_style(), + get_type_name::().sobj_style() + ))?; } // Update mesh rendering queue + // [SIMILAR] Precompute render queue key at init; no per-draw lookups self.update_render_queue_key(&engine.resource_manager)?; Ok(()) @@ -171,35 +201,44 @@ impl Component for MeshRenderingComponent { self.entity_handle = Some(self_entity_handle); } - fn deferred_update(&mut self, engine: &mut Engine, request: usize) -> Result<()> { + fn deferred_update(&mut self, engine: &mut Engine, request: usize) -> Result<()> { match request { - DEFERRED_REQUEST_VARIANT_SET_MATERIAL => - { + DEFERRED_REQUEST_VARIANT_SET_MATERIAL => { // Check if material handle is valid - engine.get_resource::(&self.material_handle.unwrap()) - .context(format!("Setting {} {} failed", "Resource".gobj_style(), "Material".sobj_style()))?; - + engine + .get_resource::(&self.material_handle.unwrap()) + .context(format!( + "Setting {} {} failed", + "Resource".gobj_style(), + "Material".sobj_style() + ))?; + + // [SIMILAR] Update render binning key only when material changes self.update_render_queue_key(&engine.resource_manager)?; - }, - DEFERRED_REQUEST_VARIANT_SET_MESH => - { + } + DEFERRED_REQUEST_VARIANT_SET_MESH => { // Check if mesh handle is valid - engine.get_resource::(&self.mesh_handle.unwrap()) - .context(format!("Setting {} {} failed", "Resource".gobj_style(), "Mesh".sobj_style()))?; - + engine + .get_resource::(&self.mesh_handle.unwrap()) + .context(format!( + "Setting {} {} failed", + "Resource".gobj_style(), + "Mesh".sobj_style() + ))?; + + // [SIMILAR] Update render binning key only when mesh changes self.update_render_queue_key(&engine.resource_manager)?; - }, - DEFERRED_REQUEST_VARIANT_UPDATE_RENDER_QUEUE => - { + } + DEFERRED_REQUEST_VARIANT_UPDATE_RENDER_QUEUE => { // Update mesh rendering queue + // [RECOMMENDED] Keep key consistent when removing mesh/material self.update_render_queue_key(&engine.resource_manager)?; - }, - _ => - { + } + _ => { panic!("Critical: Processing deferred update request with value {} in {} failed. Handling is not implemented", request, get_type_name::().sobj_style()); } } - Ok(()) + Ok(()) } -} \ No newline at end of file +} diff --git a/engine/pill_engine/src/ecs/components/mod.rs b/engine/pill_engine/src/ecs/components/mod.rs index 62123804..63bc9ee2 100644 --- a/engine/pill_engine/src/ecs/components/mod.rs +++ b/engine/pill_engine/src/ecs/components/mod.rs @@ -1,28 +1,21 @@ #![cfg_attr(debug_assertions, allow(dead_code, unused_variables))] -mod component_storage; -mod component; -pub(crate) mod mesh_rendering_component; -pub(crate) mod transform_component; +pub(crate) mod audio_listener_component; +pub(crate) mod audio_manager_component; +pub(crate) mod audio_source_component; pub(crate) mod camera_component; +mod component; +mod component_storage; pub(crate) mod deferred_update_component; +pub(crate) mod egui_manager_component; pub(crate) mod input_component; +pub(crate) mod mesh_rendering_component; +pub(crate) mod render_state_component; pub(crate) mod time_component; -pub(crate) mod audio_listener_component; -pub(crate) mod audio_source_component; -pub(crate) mod audio_manager_component; -pub(crate) mod egui_manager_component; +pub mod transform_component; // --- Use --- -pub use component:: { - Component, - GlobalComponent, - ComponentDestroyer, - ConcreteComponentDestroyer -}; +pub use component::{Component, ComponentDestroyer, ConcreteComponentDestroyer, GlobalComponent}; -pub use component_storage::{ - ComponentStorage, - GlobalComponentStorage, -}; \ No newline at end of file +pub use component_storage::{ComponentStorage, GlobalComponentStorage}; diff --git a/engine/pill_engine/src/ecs/components/render_state_component.rs b/engine/pill_engine/src/ecs/components/render_state_component.rs new file mode 100644 index 00000000..6540d009 --- /dev/null +++ b/engine/pill_engine/src/ecs/components/render_state_component.rs @@ -0,0 +1,49 @@ +use crate::ecs::{GlobalComponent, GlobalComponentStorage}; +use crate::PillTypeMapKey; +use std::sync::{Arc, Mutex}; + +#[derive(Debug, Clone, Copy)] +pub struct PostProcessParams { + pub time_s: f32, // seconds since start + pub focus_point: f32, // meters + pub focus_scale: f32, + pub mb_enabled: bool, + pub mb_debug_velocity: bool, + pub mb_strength: f32, + pub mb_max_samples: u32, + pub mb_min_speed: f32, + pub mb_depth_softness: f32, +} + +// Keeps renderer/bootstrap state; minimal for now +pub struct RenderStateComponent { + pub boot_done: bool, + pub egui_client: Option>, + pub post_process: Arc>, +} + +impl RenderStateComponent { + pub fn new() -> Self { + Self { + boot_done: false, + egui_client: None, + post_process: Arc::new(Mutex::new(PostProcessParams { + time_s: 0.0, + focus_point: 0.0, + focus_scale: 0.0, + mb_enabled: true, + mb_debug_velocity: false, + mb_strength: 1.0, + mb_max_samples: 16, + mb_min_speed: 0.001, + mb_depth_softness: 1.0, + })), + } + } +} + +impl PillTypeMapKey for RenderStateComponent { + type Storage = GlobalComponentStorage; +} + +impl GlobalComponent for RenderStateComponent {} diff --git a/engine/pill_engine/src/ecs/components/transform_component.rs b/engine/pill_engine/src/ecs/components/transform_component.rs index 809639bf..f4e1a4e6 100644 --- a/engine/pill_engine/src/ecs/components/transform_component.rs +++ b/engine/pill_engine/src/ecs/components/transform_component.rs @@ -1,14 +1,15 @@ use crate::{ ecs::{ - Component, ComponentStorage, DeferredUpdateComponent, DeferredUpdateComponentRequest, DeferredUpdateManagerPointer, EntityHandle, SceneHandle - }, engine::Engine -}; -use pill_core::{ - get_type_name, Direction, PillStyle, PillTypeMap, PillTypeMapKey, Vector3f + Component, ComponentStorage, DeferredUpdateComponent, DeferredUpdateComponentRequest, + DeferredUpdateManagerPointer, EntityHandle, SceneHandle, + }, + engine::Engine, }; +use anyhow::{Context, Error, Result}; use cgmath::{Deg, Matrix3, SquareMatrix, Zero}; -use anyhow::{ Result, Context, Error }; -use serde::{ Serialize, Deserialize }; +use glam::{Mat3, Mat4, Quat, Vec3}; +use pill_core::{get_type_name, Direction, PillStyle, PillTypeMap, PillTypeMapKey, Vector3f}; +use serde::{Deserialize, Serialize}; // Coordinate system: // @@ -69,6 +70,8 @@ pub struct TransformComponent { model_matrix: [[f32; 4]; 4], normal_matrix: [[f32; 3]; 3], + #[serde(skip)] + prev_model_matrix: [[f32; 4]; 4], // There may me multiple updates of the position/rotation/scale in the single frame. // Not to calculate matrices multiple times, we will update them only once per frame @@ -89,6 +92,7 @@ impl TransformComponent { scale: Vector3f::new(1.0, 1.0, 1.0), model_matrix: cgmath::Matrix4::identity().into(), normal_matrix: cgmath::Matrix3::identity().into(), + prev_model_matrix: cgmath::Matrix4::identity().into(), matrix_update_required: true, net_dirty: false, } @@ -114,7 +118,7 @@ impl TransformComponent { Direction::WorldRight => self.position.x += delta, Direction::WorldLeft => self.position.x -= delta, Direction::WorldUp => self.position.y += delta, - Direction::WorldDown => self.position.y -= delta + Direction::WorldDown => self.position.y -= delta, } self.matrix_update_required = true; } @@ -125,9 +129,9 @@ impl TransformComponent { } pub fn translate_local(&mut self, delta: Vector3f) { - self.position += self.get_forward_direction() * delta.z + - self.get_right_direction() * delta.x + - self.get_up_direction() * delta.y; + self.position += self.get_forward_direction() * delta.z + + self.get_right_direction() * delta.x + + self.get_up_direction() * delta.y; self.matrix_update_required = true; } @@ -158,10 +162,21 @@ impl TransformComponent { } fn get_rotation_matrix(&self) -> Matrix3 { - let roll = Matrix3::from_angle_z(Deg(self.rotation.z)); - let yaw = Matrix3::from_angle_y(Deg(self.rotation.y)); - let pitch = Matrix3::from_angle_x(Deg(self.rotation.x)); - yaw * pitch * roll + // SIMD via glam: build quaternion then extract 3x3 + let rot = Vec3::new( + self.rotation.x.to_radians(), + self.rotation.y.to_radians(), + self.rotation.z.to_radians(), + ); + let qx = Quat::from_rotation_x(rot.x); + let qy = Quat::from_rotation_y(rot.y); + let qz = Quat::from_rotation_z(rot.z); + let q = qz * qy * qx; // Rz * Ry * Rx + let m = Mat3::from_quat(q); + let c = m.to_cols_array_2d(); + Matrix3::new( + c[0][0], c[0][1], c[0][2], c[1][0], c[1][1], c[1][2], c[2][0], c[2][1], c[2][2], + ) } // --- Rotation --- @@ -183,18 +198,52 @@ impl TransformComponent { self.scale = scale; self.matrix_update_required = true; } - } pub fn update_transform_matrices(transform_component: &mut TransformComponent) { - transform_component.model_matrix = cgmath::Matrix4::model(transform_component.position, transform_component.rotation, transform_component.scale).into(); - transform_component.normal_matrix = cgmath::Matrix3::from_euler_angles(transform_component.rotation).into(); + // SIMD path via glam: quat + SRT + // Capture last frame's model before computing the new one. + transform_component.prev_model_matrix = transform_component.model_matrix; + + let pos = Vec3::new( + transform_component.position.x, + transform_component.position.y, + transform_component.position.z, + ); + let rot = Vec3::new( + transform_component.rotation.x.to_radians(), + transform_component.rotation.y.to_radians(), + transform_component.rotation.z.to_radians(), + ); + let scl = Vec3::new( + transform_component.scale.x, + transform_component.scale.y, + transform_component.scale.z, + ); + + // Convert Euler XYZ -> quat (pitch X, yaw Y, roll Z) + let qx = Quat::from_rotation_x(rot.x); + let qy = Quat::from_rotation_y(rot.y); + let qz = Quat::from_rotation_z(rot.z); + let q = qz * qy * qx; // Match Rz * Ry * Rx ordering + + let m = Mat4::from_scale_rotation_translation(scl, q, pos); + // Convert to cgmath array layout via to_cols_array_2d (column-major 4x4) + transform_component.model_matrix = m.to_cols_array_2d(); + + // Normal matrix: rotation only + let n = Mat3::from_quat(q); + transform_component.normal_matrix = n.to_cols_array_2d(); } pub fn get_model_matrix(transform_component: &TransformComponent) -> [[f32; 4]; 4] { transform_component.model_matrix } +pub fn get_prev_model_matrix(transform_component: &TransformComponent) -> [[f32; 4]; 4] { + transform_component.prev_model_matrix +} + pub fn get_normal_matrix(transform_component: &TransformComponent) -> [[f32; 3]; 3] { transform_component.normal_matrix } @@ -203,14 +252,12 @@ impl PillTypeMapKey for TransformComponent { type Storage = ComponentStorage; } -impl Component for TransformComponent { - -} +impl Component for TransformComponent {} impl Default for TransformComponent { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } pub trait MatrixAngleExt { @@ -218,7 +265,11 @@ pub trait MatrixAngleExt { } pub trait MatrixModelExt { - fn model(position: cgmath::Vector3, rotation: cgmath::Vector3, scale: cgmath::Vector3) -> Self; + fn model( + position: cgmath::Vector3, + rotation: cgmath::Vector3, + scale: cgmath::Vector3, + ) -> Self; } impl MatrixAngleExt for cgmath::Matrix4 { @@ -232,17 +283,21 @@ impl MatrixAngleExt for cgmath::Matrix4 { } impl MatrixModelExt for cgmath::Matrix4 { - fn model(position: cgmath::Vector3, rotation: cgmath::Vector3, scale: cgmath::Vector3) -> Self { - cgmath::Matrix4::from_translation(position) * - cgmath::Matrix4::from_euler_angles(rotation) * - cgmath::Matrix4::from_nonuniform_scale(scale.x, scale.y, scale.z) + fn model( + position: cgmath::Vector3, + rotation: cgmath::Vector3, + scale: cgmath::Vector3, + ) -> Self { + cgmath::Matrix4::from_translation(position) + * cgmath::Matrix4::from_euler_angles(rotation) + * cgmath::Matrix4::from_nonuniform_scale(scale.x, scale.y, scale.z) } } impl MatrixAngleExt for cgmath::Matrix3 { fn from_euler_angles(v: cgmath::Vector3) -> Self { - cgmath::Matrix3::from_angle_z(cgmath::Deg(v.z)) * - cgmath::Matrix3::from_angle_y(cgmath::Deg(v.y)) * - cgmath::Matrix3::from_angle_x(cgmath::Deg(v.x)) + cgmath::Matrix3::from_angle_z(cgmath::Deg(v.z)) + * cgmath::Matrix3::from_angle_y(cgmath::Deg(v.y)) + * cgmath::Matrix3::from_angle_x(cgmath::Deg(v.x)) } -} \ No newline at end of file +} diff --git a/engine/pill_engine/src/ecs/egui_client.rs b/engine/pill_engine/src/ecs/egui_client.rs new file mode 100644 index 00000000..f4101918 --- /dev/null +++ b/engine/pill_engine/src/ecs/egui_client.rs @@ -0,0 +1,35 @@ +use std::sync::{Arc, Mutex}; + +pub struct EguiClient { + events: Mutex>, + ui: Mutex>>, +} + +impl EguiClient { + pub fn new() -> Arc { + Arc::new(Self { + events: Mutex::new(Vec::new()), + ui: Mutex::new(None), + }) + } + + pub fn handle_input(&self, event: &winit::event::WindowEvent) { + let mut q = self.events.lock().unwrap(); + q.push(event.clone()); + } + + pub fn take_events(&self) -> Vec { + let mut q = self.events.lock().unwrap(); + std::mem::take(&mut *q) + } + + pub fn set_ui(&self, ui: Box) { + let mut u = self.ui.lock().unwrap(); + *u = Some(ui); + } + + pub fn take_ui(&self) -> Option> { + let mut u = self.ui.lock().unwrap(); + u.take() + } +} diff --git a/engine/pill_engine/src/ecs/mod.rs b/engine/pill_engine/src/ecs/mod.rs index d1f6f0d4..26f9a0b3 100644 --- a/engine/pill_engine/src/ecs/mod.rs +++ b/engine/pill_engine/src/ecs/mod.rs @@ -1,117 +1,76 @@ #![cfg_attr(debug_assertions, allow(dead_code, unused_variables))] +mod components; mod entity; mod scene; mod scene_manager; -mod components; mod systems; +mod egui_client; // --- Use --- // - Components -pub use components:: { - Component, - GlobalComponent, - ComponentDestroyer, - ConcreteComponentDestroyer, - ComponentStorage, +pub use components::{ + Component, ComponentDestroyer, ComponentStorage, ConcreteComponentDestroyer, GlobalComponent, GlobalComponentStorage, }; pub use components::camera_component::{ - CameraComponent, - CameraAspectRatio, - get_renderer_resource_handle_from_camera_component, + get_renderer_resource_handle_from_camera_component, CameraAspectRatio, CameraComponent, }; -pub use components::audio_manager_component::{ - AudioManagerComponent, - SoundType, -}; +pub use components::audio_manager_component::{AudioManagerComponent, SoundType}; -pub use components::audio_listener_component::{ - AudioListenerComponent, -}; +pub use components::audio_listener_component::AudioListenerComponent; -pub use components::audio_source_component::{ - AudioSourceComponent -}; +pub use components::audio_source_component::AudioSourceComponent; -pub use components::egui_manager_component::{ - EguiManagerComponent, -}; +pub use components::egui_manager_component::EguiManagerComponent; pub use components::deferred_update_component::{ - DeferredUpdateComponent, - DeferredUpdateManager, - DeferredUpdateManagerPointer, - DeferredUpdateRequest, - DeferredUpdateComponentRequest, - DeferredUpdateResourceRequest + DeferredUpdateComponent, DeferredUpdateComponentRequest, DeferredUpdateManager, + DeferredUpdateManagerPointer, DeferredUpdateRequest, DeferredUpdateResourceRequest, }; -pub use components::input_component::{ - InputComponent, - InputEvent, -}; +pub use components::input_component::{InputComponent, InputEvent}; pub use components::transform_component::{ - TransformComponent, - update_transform_matrices, - get_model_matrix, - get_normal_matrix, + get_model_matrix, get_normal_matrix, update_transform_matrices, TransformComponent, }; -pub use components::mesh_rendering_component::{ - MeshRenderingComponent, -}; +pub use components::mesh_rendering_component::MeshRenderingComponent; -pub use components::time_component::{ - TimeComponent, -}; +pub use components::time_component::TimeComponent; + +pub use components::render_state_component::RenderStateComponent; +pub use components::render_state_component::PostProcessParams; // - Systems -pub use systems::{ - SystemManager, - UpdatePhase, - SystemFunction -}; +pub use systems::{SystemFunction, SystemManager, UpdatePhase}; -pub use systems::rendering_system::{ - rendering_system, -}; +// - Egui client (UI input/closure mailbox) +pub use egui_client::EguiClient; -pub use systems::deferred_update_system::{ - deferred_update_system, -}; +pub use systems::rendering_system::rendering_system; -pub use systems::input_system::{ - input_system, -}; +pub use systems::deferred_update_system::deferred_update_system; -pub use systems::time_system::{ - time_system, -}; +pub use systems::input_system::input_system; -pub use systems::audio_system::{ - audio_system, -}; +pub use systems::time_system::time_system; + +pub use systems::audio_system::audio_system; + +pub fn get_prev_model_matrix(transform_component: &TransformComponent) -> [[f32; 4]; 4] { + components::transform_component::get_prev_model_matrix(transform_component) +} // - Other -pub use entity::{ - Entity, - EntityHandle, - EntityBuilder, -}; +pub use entity::{Entity, EntityBuilder, EntityHandle}; -pub use scene::{ - Scene, -}; +pub use scene::Scene; -pub use scene_manager::{ - SceneManager, - SceneHandle, -}; \ No newline at end of file +pub use scene_manager::{SceneHandle, SceneManager}; diff --git a/engine/pill_engine/src/ecs/systems/rendering_system.rs b/engine/pill_engine/src/ecs/systems/rendering_system.rs index 5818d8c0..05f3e6e9 100644 --- a/engine/pill_engine/src/ecs/systems/rendering_system.rs +++ b/engine/pill_engine/src/ecs/systems/rendering_system.rs @@ -1,37 +1,377 @@ +use crate::graphics::RendererTargetDesc; use crate::{ - config::RENDERING_SYSTEM, - ecs::{ scene, update_transform_matrices, CameraAspectRatio, CameraComponent, Component, ComponentStorage, EguiManagerComponent, EntityHandle, MeshRenderingComponent, TransformComponent, UpdatePhase }, - engine::Engine, - graphics::{ compose_render_queue_key, RenderQueueItem, RenderQueueKey }, - resources::{ Material, MaterialHandle, Mesh, MeshHandle, ResourceManager } + config::RENDERING_SYSTEM, + ecs::{ + scene, update_transform_matrices, CameraAspectRatio, CameraComponent, Component, + ComponentStorage, EguiManagerComponent, EntityHandle, MeshRenderingComponent, + TransformComponent, UpdatePhase, + }, + engine::Engine, + graphics::{ + compose_render_queue_key, RenderQuery, RenderQueueFactory, RenderQueueItem, RenderQueueKey, + }, + resources::{Mesh, MeshHandle, Model, PBRMaterial, PBRMaterialHandle, ResourceManager}, }; -use pill_core::{ EngineError, PillSlotMapKey, PillStyle, RendererError, Timer }; +use pill_core::{EngineError, PillSlotMapKey, PillStyle, RendererError, Timer}; -use std::{ ops::Range, time::Instant }; -use anyhow::{ Result, Context, Error }; +use anyhow::{Context, Error, Result}; use boolinator::Boolinator; -use log::{ debug }; +use log::debug; +use std::{ops::Range, time::Instant}; + +use crate::config::{ + DEFAULT_COLOR_TEXTURE_NAME, DEFAULT_MATERIAL_NAME, DEFAULT_NORMAL_TEXTURE_NAME, MAX_MATERIALS, + MAX_MESHES, MAX_MODELS, MAX_SOUNDS, MAX_TEXTURES, +}; +use crate::ecs::components::render_state_component::RenderStateComponent; +use crate::ecs::TimeComponent; +use crate::graphics::{Pass, PassCompose, PassLogo, PassOverlayDepth, PassOverlayUV}; +use crate::resources::{Resource, ResourceLoadType, Texture, TextureHandle, TextureType}; + +// Constants for hot path optimization +const MAX_RENDERABLES_CAPACITY: usize = 100000; // Maximum expected renderables per frame + +// Static preallocated Vec for dirty entities to avoid per-frame allocation +// SAFETY: Single-threaded rendering system - no concurrency issues +static mut DIRTY_ENTITIES: Vec = Vec::new(); pub fn rendering_system(engine: &mut Engine) -> Result<()> { + // One-time bootstrap: register resource types and create default resources + // Require RenderStateComponent; if missing, panic via unwrap + let need_bootstrap = !engine + .get_global_component::() + .unwrap() + .boot_done; + if need_bootstrap { + init_default_resources(engine)?; + + // Preallocate render queue capacity for hot path optimization + engine.render_queue.reserve(MAX_RENDERABLES_CAPACITY); // Reserve space for up to 100k renderables + + // Preallocate static dirty_entities Vec capacity + unsafe { + DIRTY_ENTITIES.reserve(MAX_RENDERABLES_CAPACITY); // Reserve space for up to 100k entities + } + + // Install renderer passes once during bootstrap + { + // Resolve logo texture created in init_default_resources + let tex_logo = engine + .resource_manager + .get_resource_by_name::("pill_logo_horizontal_white")?; + let fmt = engine.renderer.get_surface_format(); + // Convert engine Texture -> renderer texture handle + let tex_logo_rt = tex_logo + .renderer_resource_handle + .expect("renderer handle for logo texture"); + + // Ensure EguiClient exists in global render state + let egui_client = { + let post_process = engine.resource_manager.post_process.clone(); + let rs = engine.get_global_component_mut::()?; + if rs.egui_client.is_none() { + rs.egui_client = Some(crate::ecs::EguiClient::new()); + } + // Share post-process params between Engine render state and ResourceManager for passes. + rs.post_process = post_process; + rs.egui_client.as_ref().unwrap().clone() + }; + + // Logo overlay + let h: f32 = 0.04; + let rect_logo = [0.98 - 3.0 * h, 0.02, 0.98, 0.02 + h]; + + let offscreen_color_texture = + engine.renderer.create_render_target(RendererTargetDesc { + name: "offscreen_color".to_string(), + format: wgpu::TextureFormat::Rgba16Float, // HDR for PBR and bokeh + width: engine.window_size.width, + height: engine.window_size.height, + })?; + let velocity_texture = engine.renderer.create_render_target(RendererTargetDesc { + name: "velocity_buffer".to_string(), + format: wgpu::TextureFormat::Rg16Float, + width: engine.window_size.width, + height: engine.window_size.height, + })?; + + // Create depth and color texture + let depth_texture = engine.renderer.create_depth_texture("depth_texture")?; + + // Depth copy render target for depth-based effects + let depth_copy_rt = engine.renderer.create_render_target(RendererTargetDesc { + name: "depth_copy".to_string(), + format: wgpu::TextureFormat::R16Float, + width: engine.window_size.width, + height: engine.window_size.height, + })?; + + // DOF output render target (HDR to preserve bokeh highlights) + let dof_output_texture = engine.renderer.create_render_target(RendererTargetDesc { + name: "dof_output".to_string(), + format: wgpu::TextureFormat::Rgba16Float, + width: engine.window_size.width, + height: engine.window_size.height, + })?; + let motion_blur_output = engine.renderer.create_render_target(RendererTargetDesc { + name: "motion_blur_output".to_string(), + format: wgpu::TextureFormat::Rgba16Float, + width: engine.window_size.width, + height: engine.window_size.height, + })?; + + // Build passes as an array with elements, then convert to Vec + // Load equirectangular HDR environment map (as 2D) + let env_tex_handle = { + // Add if not already present + match engine.get_resource_handle::("ibl_env_equirect") { + Ok(h) => h, + Err(_) => { + let tex = Texture::new( + "ibl_env_equirect", + TextureType::Linear, + ResourceLoadType::Path("ibl/HDR_111_Parking_Lot_2_Env.hdr".into()), + ); + engine.add_resource::(tex)? + } + } + }; + let env_tex_rt = engine + .get_resource::(&env_tex_handle)? + .renderer_resource_handle + .expect("renderer handle for env texture"); + + // Create IBL output resources up front + let ibl_irradiance_rt = engine.renderer.create_render_target(RendererTargetDesc { + name: "ibl_irradiance_2d".to_string(), + format: wgpu::TextureFormat::Rgba16Float, + width: 64, + height: 32, + })?; + let ibl_brdf_rt = engine.renderer.create_render_target(RendererTargetDesc { + name: "ibl_brdf_lut".to_string(), + format: wgpu::TextureFormat::Rgba16Float, + width: 512, + height: 512, + })?; + + // Build and run diffuse IBL convolution once (init-time) + let mut pass_ibl_diff = crate::graphics::PassIblDiffuseEquirect::new( + "ibl_diffuse_equirect", + env_tex_rt, + ibl_irradiance_rt, + wgpu::TextureFormat::Rgba16Float, + ); + // Manually init to precompute before scene pass init + { + // SAFETY: renderer exposes init path; we can call with engine resources + let self_ptr: *mut dyn crate::graphics::PillRenderer = &mut *engine.renderer; + let rm_ptr: *mut crate::resources::ResourceManager = &mut engine.resource_manager; + unsafe { + pass_ibl_diff.init(&mut *self_ptr, &mut *rm_ptr)?; + } + } + // Provide BRDF target to specular pass + + // Create prefilter target (mip chain) in RenderSystem and pass to pass + let prefilter_handle = { + let device = engine.renderer.get_device(); + let tex = device.create_texture(&wgpu::TextureDescriptor { + label: Some("ibl_prefilter_2d"), + size: wgpu::Extent3d { + width: 256, + height: 128, + depth_or_array_layers: 1, + }, + mip_level_count: 5, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba16Float, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + let view = tex.create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::Repeat, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Linear, + lod_min_clamp: 0.0, + lod_max_clamp: 5.0, + ..Default::default() + }); + let handle = engine.resource_manager.gpu_mut().textures.insert( + crate::renderer::resources::RendererTexture { + texture: tex, + texture_view: view, + sampler, + format: wgpu::TextureFormat::Rgba16Float, + }, + ); + // Sanity: log handle id and format + { + use pill_core::PillSlotMapKey; + let fmt = engine + .resource_manager + .gpu() + .textures + .get(handle) + .map(|t| t.format) + .unwrap_or(wgpu::TextureFormat::Rgba8UnormSrgb); + log::info!( + "rendering_system: created prefilter handle=({},{}) format={:?}", + handle.index(), + handle.generation(), + fmt + ); + } + handle + }; + + // Specular IBL prefilter + BRDF LUT + let mut pass_ibl_spec = crate::graphics::PassIblSpecularEquirect::new( + "ibl_specular_equirect", + env_tex_rt, + prefilter_handle, + ibl_brdf_rt, + 5, + ); + { + let self_ptr: *mut dyn crate::graphics::PillRenderer = &mut *engine.renderer; + let rm_ptr: *mut crate::resources::ResourceManager = &mut engine.resource_manager; + unsafe { + pass_ibl_spec.init(&mut *self_ptr, &mut *rm_ptr)?; + } + } + let ibl_prefilter_rt = pass_ibl_spec.prefilter_texture_handle(); + + let passes: Vec> = vec![ + // Keep the pass in list so it's a no-op afterwards (done flag) + Box::new(pass_ibl_diff), + Box::new(pass_ibl_spec), + // Skybox (draws into offscreen color); must come before scene + // TODO: move after all geometry was drawn and draw at end of depth + Box::new(crate::graphics::PassSkyboxEquirect::new( + "skybox_equirect", + offscreen_color_texture, + wgpu::TextureFormat::Rgba16Float, // HDR format + env_tex_rt, + )), + Box::new(crate::graphics::pass_scene::PassScene::new( + "scene", + offscreen_color_texture, + velocity_texture, + depth_texture, + wgpu::TextureFormat::Rgba16Float, // HDR format + wgpu::TextureFormat::Rg16Float, // velocity format + true, // load offscreen color (skybox already cleared/drew background) + Some(ibl_irradiance_rt), + Some(ibl_prefilter_rt), + Some(ibl_brdf_rt), + )), + // Linearize scene depth into a color RT for later passes + Box::new(crate::graphics::PassLinearizeDepth::new( + "copy_depth", + depth_texture, + depth_copy_rt, + wgpu::TextureFormat::R16Float, + )), + Box::new(crate::graphics::PassDofBokeh::new( + "dof_bokeh", + offscreen_color_texture, + depth_copy_rt, + dof_output_texture, + wgpu::TextureFormat::Rgba16Float, + )), + Box::new(crate::graphics::PassMotionBlur::new( + "motion_blur", + dof_output_texture, + velocity_texture, + depth_copy_rt, + motion_blur_output, + wgpu::TextureFormat::Rgba16Float, + )), + // Compose reads from motion blur output texture + Box::new(crate::graphics::PassCompose::new( + "compose", + motion_blur_output, + fmt, + )), + Box::new(crate::graphics::PassOverlayUV::new( + "overlay_uv", + [0.75, 0.75, 0.95, 0.95], + fmt, + )), + Box::new(crate::graphics::PassOverlayVelocity::new( + "overlay_velocity", + [0.75, 0.25, 0.95, 0.45], + [2.0, 0.0, 16.0, 2.0], // scale, mode, spacing_px, thickness_px + fmt, + velocity_texture, + )), + Box::new(crate::graphics::PassOverlayDepth::new( + "overlay_depth", + [0.75, 0.50, 0.95, 0.70], + [1.0, 1.0, 1.0, 1.0], + fmt, + depth_texture, + )), + Box::new(PassLogo::new( + "overlay_logo", + rect_logo, + [1.0, 1.0, 1.0, 1.0], + tex_logo_rt, + fmt, + )), + Box::new(crate::graphics::PassEgui::new( + "egui", + engine.window.clone(), + egui_client, + )), + ]; + engine.renderer.set_passes(passes)?; + } + + if let Ok(rs) = engine.get_global_component_mut::() { + rs.boot_done = true; + } + + // Skip the rest of the frame; camera may not exist yet during bootstrap + return Ok(()); + } + let mut timer = Timer::new(); timer.begin_context("rendering_system update"); timer.record("Get active camera"); + // Update shared post-process params (time) for render passes + if let Ok(tc) = engine.get_global_component::() { + if let Ok(mut pp) = engine.resource_manager.post_process.lock() { + pp.time_s = tc.time; + } + } + let active_scene_handle = engine.scene_manager.get_active_scene_handle()?; let mut active_camera_entity_handle_result: Option = None; - + { let active_scene = engine.scene_manager.get_active_scene_mut()?; // - Find active camera and update its aspect ratio if needed // Find first enabled camera and use it as active - for (entity_handle, camera_component) in active_scene.get_one_component_iterator_mut::()? { + for (entity_handle, camera_component) in + active_scene.get_one_component_iterator_mut::()? + { if camera_component.enabled { // Update active camera aspect ratio if it is set to automatic if let CameraAspectRatio::Automatic(_) = camera_component.aspect { - let aspect_ratio = engine.window_size.width as f32 / engine.window_size.height as f32; + let aspect_ratio = + engine.window_size.width as f32 / engine.window_size.height as f32; camera_component.aspect = CameraAspectRatio::Automatic(aspect_ratio); } active_camera_entity_handle_result = Some(entity_handle); @@ -40,97 +380,257 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { } } - - let active_camera_entity_handle = active_camera_entity_handle_result.ok_or(Error::new(EngineError::NoActiveCamera))?.clone(); + let active_camera_entity_handle = active_camera_entity_handle_result + .ok_or(Error::new(EngineError::NoActiveCamera))? + .clone(); // - Prepare rendering data timer.record("Clear render queue"); // Clear the render queue + // [SIMILAR] Build and sort a render queue ahead of draw; separates data prep from draw per TALK engine.render_queue.clear(); - engine.render_queue.reserve(200000); // Reserve space for 1000 items timer.record("Prepare render queue"); - let mut matrix_calculation_duration: f32 = 0.0; - let mut add_to_render_queue_duration: f32 = 0.0; - - // Iterate mesh rendering components - for (entity_handle, transform_component, mesh_rendering_component) in - engine.scene_manager.get_two_component_iterator_mut::(active_scene_handle)? + // Prepass: deferred material updates (CPU -> GPU) for dirty PBR materials { - // Update transform matrices if required - - // let update_transform_matrices_start_time = Instant::now(); - if transform_component.matrix_update_required { - update_transform_matrices(transform_component); - transform_component.matrix_update_required = false; + // Collect updates immutably to avoid borrow conflicts + let mut updates: Vec<( + PBRMaterialHandle, + crate::graphics::RendererMaterialHandle, + crate::graphics::MaterialDesc, + )> = Vec::new(); + if let Ok(storage) = engine + .resource_manager + .get_resource_storage::() + { + for (h, opt_mat) in storage.data.iter() { + if let Some(mat) = opt_mat { + if mat.is_dirty { + if let Some(rm_handle) = mat.renderer_resource_handle { + let map_tex = |th: &Option| -> Option { + th.as_ref().and_then(|hh| { + engine + .resource_manager + .get_resource::(hh) + .ok() + .and_then(|t| t.renderer_resource_handle) + }) + }; + let desc = crate::graphics::MaterialDesc { + label: "upd", + albedo: [mat.albedo.x, mat.albedo.y, mat.albedo.z], + metallic: mat.metallic, + roughness: mat.roughness, + emissive: [mat.emissive.x, mat.emissive.y, mat.emissive.z], + albedo_tex: map_tex(&mat.albedo_texture), + normal_tex: map_tex(&mat.normal_texture), + metallic_roughness_tex: map_tex(&mat.metallic_roughness_texture), + emissive_tex: map_tex(&mat.emissive_texture), + }; + updates.push((h, rm_handle, desc)); + } + } + } + } } - // matrix_calculation_duration += update_transform_matrices_start_time.elapsed().as_secs_f32() * 1000.0; - - // Add valid mesh rendering components to render queue - let add_to_render_queue_start_time = Instant::now(); - if let Some(render_queue_key) = mesh_rendering_component.render_queue_key { - let render_queue_item = RenderQueueItem { - key: render_queue_key, - entity_index: entity_handle.data().index as u32, - }; - engine.render_queue.push(render_queue_item); - } else { - debug!("Invalid render queue key"); - continue; + // Apply updates and clear dirty flags + for (ph, rmh, desc) in updates { + let _ = engine.renderer.update_material(rmh, desc); + if let Ok(m) = engine.resource_manager.get_resource_mut::(&ph) { + m.is_dirty = false; + } } - add_to_render_queue_duration += add_to_render_queue_start_time.elapsed().as_secs_f32() * 1000.0; } - timer.record(&format!("Matrix calculation {} ms", matrix_calculation_duration)); - timer.record(&format!("Add to render queue {} ms", add_to_render_queue_duration)); + // Use static preallocated dirty_entities Vec + unsafe { + DIRTY_ENTITIES.clear(); // Clear and reuse preallocated Vec - timer.record("Sort render queue"); + // Phase 1: Sweep components; route transforms needing matrix update to a batch, push clean directly + // [SIMILAR] Batch transform updates; avoid per-draw matrix work + for (entity_handle, transform_component, mesh_rendering_component) in + engine + .scene_manager + .get_two_component_iterator_mut::( + active_scene_handle, + )? + { + if transform_component.matrix_update_required { + // defer update to batch + DIRTY_ENTITIES.push(entity_handle); + continue; + } - // Sort render queue - engine.render_queue.sort(); + // Push clean (non-dirty) items directly into the render queue + // [SIMILAR] Use precomputed render_queue_key (pipeline/material/mesh sorting key) + if let Some(render_queue_key) = mesh_rendering_component.render_queue_key { + let render_queue_item = RenderQueueItem { + key: render_queue_key, + entity_index: entity_handle.data().index as u32, + }; + engine.render_queue.push(render_queue_item); + } + } + + // Phase 2: Batch update transforms with matrix_update_required + // [RECOMMENDED] Consolidate transform matrix updates in one batch outside of pass + if !DIRTY_ENTITIES.is_empty() { + timer.begin_context(&format!("Batch update {} transforms", DIRTY_ENTITIES.len())); + for entity_handle in DIRTY_ENTITIES.iter() { + // Pull the transform component mutably and update matrices + if let Ok(transform_component) = engine + .scene_manager + .get_entity_component::(*entity_handle, active_scene_handle) + { + if transform_component.matrix_update_required { + update_transform_matrices(transform_component); + transform_component.matrix_update_required = false; + } + } + } + timer.end_context()?; + } + + // Phase 3: Add updated (previously dirty) items to the render queue + for entity_handle in DIRTY_ENTITIES.iter() { + if let Ok(mesh_rendering_component) = engine + .scene_manager + .get_entity_component::(*entity_handle, active_scene_handle) + { + if let Some(render_queue_key) = mesh_rendering_component.render_queue_key { + let render_queue_item = RenderQueueItem { + key: render_queue_key, + entity_index: entity_handle.data().index as u32, + }; + engine.render_queue.push(render_queue_item); + } + } + } + } timer.record("Get component storages"); - let egui_ui = EguiManagerComponent::get_ui(engine);// egui_manager_component.get_ui(engine); + let egui_ui: Box = EguiManagerComponent::get_ui(engine); // egui_manager_component.get_ui(engine) + if let Ok(rs) = engine.get_global_component_mut::() { + if let Some(ref client) = rs.egui_client { + client.set_ui(egui_ui); + } + } - let active_scene = engine.scene_manager.get_active_scene_mut()?; - // Get storages - let camera_component_storage = active_scene.get_component_storage::() - .context(format!("{}: Cannot get active {}", "rendering_system".sobj_style(), "Camera".gobj_style()))?; - let transform_component_storage = active_scene.get_component_storage::() - .context(format!("{}: Cannot get {}", "rendering_system".sobj_style(), "TransformComponents".sobj_style())).unwrap(); + // Storages fetched inside renderer via immutable Engine timer.begin_context("Render"); - // Render - match engine.renderer.render( - active_camera_entity_handle, - &engine.render_queue, - camera_component_storage, - transform_component_storage, - egui_ui, - &mut timer - ) { + // Build WorldView with raw pointers to avoid borrow conflicts + let world_view = { + let active_scene = engine.scene_manager.get_active_scene()?; + let camera_components = + active_scene.get_component_storage::()? as *const _; + let transform_components = + active_scene.get_component_storage::()? as *const _; + let render_queue_ptr = &engine.render_queue as *const _; + crate::graphics::WorldView { + active_camera: active_camera_entity_handle, + render_queue_ptr, + camera_components_ptr: camera_components, + transform_components_ptr: transform_components, + } + }; + let factory = crate::graphics::WorldViewFactory { world: world_view }; + match crate::graphics::render_with_factory(engine.renderer.as_mut(), &factory, &mut timer) { Ok(_) => { timer.end_context()?; // End "Render" context - engine.system_manager.update_system_timer(RENDERING_SYSTEM.name, RENDERING_SYSTEM.update_phase, timer)?; + engine.system_manager.update_system_timer( + RENDERING_SYSTEM.name, + RENDERING_SYSTEM.update_phase, + timer, + )?; Ok(()) - } + } Err(e) => { match e.downcast_ref::() { Some(RendererError::SurfaceLost) => { // Recreate lost surface timer.end_context()?; // End "Render" context - engine.system_manager.update_system_timer(RENDERING_SYSTEM.name, RENDERING_SYSTEM.update_phase, timer)?; + engine.system_manager.update_system_timer( + RENDERING_SYSTEM.name, + RENDERING_SYSTEM.update_phase, + timer, + )?; Ok(engine.renderer.resize(engine.window_size)) - }, + } Some(RendererError::SurfaceOutOfMemory) => { panic!("Critical: Renderer error, system out of memory"); - }, + } _ => Err(e), } } } -} \ No newline at end of file +} + +fn init_default_resources(engine: &mut Engine) -> Result<(), Error> { + let max_texture_count = engine + .config + .get_int("MAX_TEXTURES") + .unwrap_or(MAX_TEXTURES as i64) as usize; + let max_mesh_count = engine + .config + .get_int("MAX_MESHES") + .unwrap_or(MAX_MESHES as i64) as usize; + let max_material_count = engine + .config + .get_int("MAX_MATERIALS") + .unwrap_or(MAX_MATERIALS as i64) as usize; + let max_model_count = engine + .config + .get_int("MAX_MODELS") + .unwrap_or(MAX_MODELS as i64) as usize; + // TODO: Move to SoundSystem init + let max_sound_count = engine + .config + .get_int("MAX_SOUNDS") + .unwrap_or(MAX_SOUNDS as i64) as usize; + + engine.register_resource_type::(max_texture_count)?; + engine.register_resource_type::(max_mesh_count)?; + engine.register_resource_type::(max_material_count)?; + engine.register_resource_type::(max_model_count)?; + engine.register_resource_type::(max_sound_count)?; + + let default_color = Box::new(*include_bytes!("../../../res/textures/default_color.png")); + let default_normal = Box::new(*include_bytes!("../../../res/textures/default_normal.png")); + let mut color = Texture::new( + DEFAULT_COLOR_TEXTURE_NAME, + TextureType::Gamma, + ResourceLoadType::Bytes(default_color), + ); + color.initialize(engine)?; + engine.resource_manager.add_resource(color)?; + let mut normal = Texture::new( + DEFAULT_NORMAL_TEXTURE_NAME, + TextureType::Linear, + ResourceLoadType::Bytes(default_normal), + ); + normal.initialize(engine)?; + engine.resource_manager.add_resource(normal)?; + + // Pill logo (overlay) texture + let pill_logo = Box::new(*include_bytes!( + "../../../res/textures/pill_logo_horizontal_white.png" + )); + let mut tex_logo = Texture::new( + "pill_logo_horizontal_white", + TextureType::Gamma, + ResourceLoadType::Bytes(pill_logo), + ); + tex_logo.initialize(engine)?; + engine.resource_manager.add_resource(tex_logo)?; + + let mut mat = PBRMaterial::new(DEFAULT_MATERIAL_NAME); + mat.initialize(engine)?; + engine.resource_manager.add_resource(mat)?; + + Ok(()) +} diff --git a/engine/pill_engine/src/ecs/systems/system_manager.rs b/engine/pill_engine/src/ecs/systems/system_manager.rs index 276b76e2..57014463 100644 --- a/engine/pill_engine/src/ecs/systems/system_manager.rs +++ b/engine/pill_engine/src/ecs/systems/system_manager.rs @@ -2,11 +2,11 @@ use crate::engine::Engine; use pill_core::{EngineError, Timer}; -use core::fmt; -use std::{collections::HashMap, fmt::Display}; -use anyhow::{Result, Context, Error}; +use anyhow::{Context, Error, Result}; use boolinator::Boolinator; +use core::fmt; use indexmap::IndexMap; +use std::{collections::HashMap, fmt::Display}; pub type SystemFunction = fn(engine: &mut Engine) -> Result<()>; @@ -37,39 +37,54 @@ pub struct SystemManager { impl SystemManager { pub fn new() -> Self { - let mut update_phases = IndexMap::>::new(); + let mut update_phases = IndexMap::>::new(); // Register phases update_phases.insert(UpdatePhase::PreGame, IndexMap::::new()); update_phases.insert(UpdatePhase::Game, IndexMap::::new()); update_phases.insert(UpdatePhase::PostGame, IndexMap::::new()); - Self { - update_phases - } + Self { update_phases } } pub fn get_system(&mut self, name: &str, update_phase: UpdatePhase) -> Result<&mut System> { // Find collection of systems for given update phase - let system_collection = self.update_phases.get_mut(&update_phase).ok_or(Error::new(EngineError::SystemUpdatePhaseNotFound(format!("{}", update_phase))))?; + let system_collection = self.update_phases.get_mut(&update_phase).ok_or(Error::new( + EngineError::SystemUpdatePhaseNotFound(format!("{}", update_phase)), + ))?; // Get system by name - system_collection.get_mut(name).ok_or(Error::new(EngineError::SystemNotFound(name.to_string(), format!("{}", update_phase)))) + system_collection + .get_mut(name) + .ok_or(Error::new(EngineError::SystemNotFound( + name.to_string(), + format!("{}", update_phase), + ))) } - pub fn add_system(&mut self, name: &str, system_function: SystemFunction, update_phase: UpdatePhase) -> Result<()> { + pub fn add_system( + &mut self, + name: &str, + system_function: SystemFunction, + update_phase: UpdatePhase, + ) -> Result<()> { // Find collection of systems for given update phase - let system_collection = self.update_phases.get_mut(&update_phase).ok_or(Error::new(EngineError::SystemUpdatePhaseNotFound(format!("{}", update_phase))))?; + let system_collection = self.update_phases.get_mut(&update_phase).ok_or(Error::new( + EngineError::SystemUpdatePhaseNotFound(format!("{}", update_phase)), + ))?; // Check if system with that name already exists if system_collection.contains_key(name) { - return Err(Error::new(EngineError::SystemAlreadyExists(name.to_string(), format!("{}", update_phase)))) + return Err(Error::new(EngineError::SystemAlreadyExists( + name.to_string(), + format!("{}", update_phase), + ))); } // Create system object let system_object = System { name: name.to_string(), - update_phase, + update_phase, system_function, enabled: true, timer: Some(Timer::new()), @@ -81,13 +96,18 @@ impl SystemManager { Ok(()) } - pub fn remove_system(&mut self, name: &str, update_phase: UpdatePhase) -> Result<()> { + pub fn remove_system(&mut self, name: &str, update_phase: UpdatePhase) -> Result<()> { // Find collection of systems for given update phase - let system_collection = self.update_phases.get_mut(&update_phase).ok_or(Error::new(EngineError::SystemUpdatePhaseNotFound(format!("{}", update_phase))))?; + let system_collection = self.update_phases.get_mut(&update_phase).ok_or(Error::new( + EngineError::SystemUpdatePhaseNotFound(format!("{}", update_phase)), + ))?; // Check if system with that name exists if !system_collection.contains_key(name) { - return Err(Error::new(EngineError::SystemNotFound(name.to_string(), format!("{}", update_phase)))) + return Err(Error::new(EngineError::SystemNotFound( + name.to_string(), + format!("{}", update_phase), + ))); } // Remove system @@ -96,12 +116,25 @@ impl SystemManager { Ok(()) } - pub fn toggle_system(&mut self, name: &str, update_phase: UpdatePhase, enabled: bool) -> Result<()> { + pub fn toggle_system( + &mut self, + name: &str, + update_phase: UpdatePhase, + enabled: bool, + ) -> Result<()> { // Find collection of systems for given update phase - let system_collection = self.update_phases.get_mut(&update_phase).ok_or(Error::new(EngineError::SystemUpdatePhaseNotFound(format!("{}", update_phase))))?; + let system_collection = self.update_phases.get_mut(&update_phase).ok_or(Error::new( + EngineError::SystemUpdatePhaseNotFound(format!("{}", update_phase)), + ))?; // Check if system with that name exists - let system = system_collection.get_mut(name).ok_or(Error::new(EngineError::SystemNotFound(name.to_string(), format!("{}", update_phase))))?; + let system = + system_collection + .get_mut(name) + .ok_or(Error::new(EngineError::SystemNotFound( + name.to_string(), + format!("{}", update_phase), + )))?; // Set system state system.enabled = enabled; @@ -113,7 +146,11 @@ impl SystemManager { // It will pass the ownership of the timer to the requsting scope. // This has to be returned back using update_system_timer function, otherwise engine will panic. // NOTE: Before system function is called, engine already starts "System update" context in the timer - pub fn get_system_timer(&mut self, name: &str, update_phase: UpdatePhase) -> Result> { + pub fn get_system_timer( + &mut self, + name: &str, + update_phase: UpdatePhase, + ) -> Result> { // Get system by name let system: &mut System = self.get_system(name, update_phase)?; @@ -121,7 +158,12 @@ impl SystemManager { Ok(system.timer.take()) } - pub fn update_system_timer(&mut self, name: &str, update_phase: UpdatePhase, timer: Timer) -> Result<()> { + pub fn update_system_timer( + &mut self, + name: &str, + update_phase: UpdatePhase, + timer: Timer, + ) -> Result<()> { // Get system by name let system = self.get_system(name, update_phase)?; @@ -130,4 +172,28 @@ impl SystemManager { Ok(()) } -} \ No newline at end of file + + // Peek system timer without taking ownership (cloned snapshot) + pub fn peek_system_timer( + &self, + name: &str, + update_phase: UpdatePhase, + ) -> Result> { + // Find collection of systems for given update phase + let system_collection = self.update_phases.get(&update_phase).ok_or(Error::new( + EngineError::SystemUpdatePhaseNotFound(format!("{}", update_phase)), + ))?; + + // Get system by name + let timer_opt = system_collection + .get(name) + .ok_or(Error::new(EngineError::SystemNotFound( + name.to_string(), + format!("{}", update_phase), + )))? + .timer + .clone(); + + Ok(timer_opt) + } +} diff --git a/engine/pill_engine/src/engine.rs b/engine/pill_engine/src/engine.rs index a7435dd4..d04fb723 100644 --- a/engine/pill_engine/src/engine.rs +++ b/engine/pill_engine/src/engine.rs @@ -1,736 +1,1043 @@ -use crate::{ - resources::*, - ecs::*, - graphics::*, - config::*, -}; - -use pill_core::{ - EngineError, - PillSlotMapKey, - PillStyle, - PillTypeMap, - get_type_name, - get_value_type_name, - get_enum_variant_type_name, - get_game_error_message, - Vector2f, - Timer -}; - -use std::{ any::type_name, any::Any, any::TypeId, collections::VecDeque, cell::RefCell, ops::RangeBounds }; -use anyhow::{Context, Result, Error}; -use boolinator::Boolinator; -use log::{debug, info, error}; -use winit::{ dpi::PhysicalPosition, event::KeyEvent,}; - -// ------------------------------------------------------------------------------- - -pub type Game = Box; -pub type KeyboardKey = winit::keyboard::KeyCode; -pub type MouseButton = winit::event::MouseButton; - -/// Engine <-> Game interface -/// -/// Entry point of the game project. Mandatory to implement. -pub trait PillGame { - fn start(&self, engine: &mut Engine) -> Result<()>; -} - -/// Heart of Pill Engine -pub struct Engine { - pub(crate) config: config::Config, - pub(crate) game: Option, - pub(crate) renderer: Renderer, - pub(crate) scene_manager: SceneManager, - pub(crate) system_manager: SystemManager, - pub(crate) resource_manager: ResourceManager, - pub(crate) global_components: PillTypeMap, - pub(crate) input_queue: VecDeque, - pub(crate) render_queue: Vec, - pub(crate) window_size: winit::dpi::PhysicalSize, - pub(crate) game_resources_directory_path: std::path::PathBuf, - pub(crate) frame_delta_time: f32, // In milliseconds -} - -// ---- INTERNAL API ----------------------------------------------------------------- -struct EguiUI<'a> { - frame_delta_time: &'a f32, // Use a reference to frame_delta_time -} - -impl<'a> EguiUI<'a> { - fn render(&self, ui: &egui::Context) { - egui::Window::new("PillEngine") - .default_open(true) - .resizable(true) - .anchor(egui::Align2::LEFT_TOP, [0.0, 0.0]) - .show(ui, |ui| { - if ui.add(egui::Button::new("Click me")).clicked() { - println!("PRESSED"); - println!("{}", self.frame_delta_time); - } - }); - } -} -/// Pill Engine internal API -#[cfg(feature = "internal")] -impl Engine { - pub fn new( - game: Box, - game_resources_directory_path: std::path::PathBuf, - renderer: Box, - config: config::Config - ) -> Self { - let max_entity_count = config.get_int("MAX_ENTITIES").unwrap_or(MAX_ENTITIES as i64) as usize; - - Self { - config, - game: Some(game), - renderer, - scene_manager: SceneManager::new(max_entity_count), - system_manager: SystemManager::new(), - resource_manager: ResourceManager::new(), - global_components: PillTypeMap::new(), - input_queue: VecDeque::new(), - render_queue: Vec::::with_capacity(max_entity_count), - window_size: winit::dpi::PhysicalSize::::default(), - game_resources_directory_path, - frame_delta_time: 0.0.into(), - } - } - - fn create_default_resources(&mut self) -> Result<()> { - // limits --------------------------------------------------------------- - let max_texture_count = self.config.get_int("MAX_TEXTURES").unwrap_or(MAX_TEXTURES as i64) as usize; - let max_mesh_count = self.config.get_int("MAX_MESHES").unwrap_or(MAX_MESHES as i64) as usize; - let max_material_count = self.config.get_int("MAX_MATERIALS").unwrap_or(MAX_MATERIALS as i64) as usize; - let max_sound_count = self.config.get_int("MAX_SOUNDS").unwrap_or(MAX_SOUNDS as i64) as usize; - - // register resource types -------------------------------------------- - self.register_resource_type::(max_texture_count)?; - self.register_resource_type::(max_mesh_count)?; - self.register_resource_type::(max_material_count)?; - self.register_resource_type::(max_sound_count)?; - - // master shader & defaults ------------------------------------------- - let master_vert = include_bytes!("../res/shaders/master.vert.glsl"); - let master_frag = include_bytes!("../res/shaders/master.frag.glsl"); - self.renderer.set_master_pipeline(master_vert, master_frag)?; - - let default_color = Box::new(*include_bytes!("../res/textures/default_color.png")); - let default_normal = Box::new(*include_bytes!("../res/textures/default_normal.png")); - - let mut color = Texture::new( - DEFAULT_COLOR_TEXTURE_NAME, - TextureType::Color, - ResourceLoadType::Bytes(default_color), - ); - color.initialize(self)?; - self.resource_manager.add_resource(color)?; - - let mut normal = Texture::new( - DEFAULT_NORMAL_TEXTURE_NAME, - TextureType::Normal, - ResourceLoadType::Bytes(default_normal), - ); - normal.initialize(self)?; - self.resource_manager.add_resource(normal)?; - - let mut mat = Material::new(DEFAULT_MATERIAL_NAME); - mat.initialize(self)?; - self.resource_manager.add_resource(mat)?; - - Ok(()) - } - - fn start_game(&mut self) -> Result<()> { - info!("Starting {}", "Game".mobj_style()); - - let game = self.game.take().ok_or(EngineError::Other("Cannot get game".to_string()))?; - let stop_on_game_errors = self.config.get_bool("PANIC_ON_GAME_ERRORS").unwrap_or(PANIC_ON_GAME_ERRORS); - let result = game.start(self); - match stop_on_game_errors { - true => result.context(format!("{} error", "Game".mobj_style()))?, - false => { - if let Some(message) = get_game_error_message(result) { - error!("{}", message); - } - }, - } - self.game = Some(game); - - Ok(()) - } - - /// Initializes Pill Engine - /// - /// Creates default global components, adds default systems, creates default resources, initializes game - pub fn initialize(&mut self, window_size: winit::dpi::PhysicalSize) -> Result<()> { - info!("Initializing {}", "Engine".mobj_style()); - - // Set window size - self.window_size = window_size; - - // Register global components - self.add_global_component(InputComponent::new())?; - self.add_global_component(TimeComponent::new())?; - self.add_global_component(DeferredUpdateComponent::new())?; - self.add_global_component(EguiManagerComponent::new())?; - - let max_ambient_sink_count = self.config.get_int("MAX_CONCURRENT_2D_SOUNDS").unwrap_or(MAX_CONCURRENT_2D_SOUNDS as i64) as usize; - let max_spatial_sink_count = self.config.get_int("MAX_CONCURRENT_3D_SOUNDS").unwrap_or(MAX_CONCURRENT_3D_SOUNDS as i64) as usize; - self.add_global_component(AudioManagerComponent::new(max_ambient_sink_count, max_spatial_sink_count))?; - - // Add built-in systems - self.system_manager.add_system(INPUT_SYSTEM.name, INPUT_SYSTEM.system_function, INPUT_SYSTEM.update_phase)?; - self.system_manager.add_system(TIME_SYSTEM.name, TIME_SYSTEM.system_function, TIME_SYSTEM.update_phase)?; - self.system_manager.add_system(AUDIO_SYSTEM.name, AUDIO_SYSTEM.system_function, AUDIO_SYSTEM.update_phase)?; - self.system_manager.add_system(DEFERRED_UPDATE_SYSTEM.name, DEFERRED_UPDATE_SYSTEM.system_function, DEFERRED_UPDATE_SYSTEM.update_phase)?; - self.system_manager.add_system(RENDERING_SYSTEM.name, RENDERING_SYSTEM.system_function, RENDERING_SYSTEM.update_phase)?; - - // Create default resources - self.create_default_resources().context("Failed to create default resources")?; - - // Start game - self.start_game()?; - - - Ok(()) - } - - /// Main engine update function - /// - /// Runs all systems in order: PreGame -> Game -> PostGame - pub fn update(&mut self, delta_time: std::time::Duration) { - let stop_on_game_errors = self.config.get_bool("PANIC_ON_GAME_ERRORS").unwrap_or(PANIC_ON_GAME_ERRORS); - - // Run systems - for update_phase_index in 0..self.system_manager.update_phases.len() { - for system_index in 0..self.system_manager.update_phases[update_phase_index].len() { - - let (system_name, update_phase, system_function); - { - let system = &mut self.system_manager.update_phases[update_phase_index][system_index]; - if !system.enabled { continue; } - system_name = system.name.to_string(); - update_phase = system.update_phase.clone(); - system_function = system.system_function; - } - - // Create new time and asign it to system so it can be accessed inside the system function - // For rendering system we can't clean its timer here, - // because it has to render its own timer data in the UI - // (and since the frame in which it renders is not yet finished when it renders UI, it has to use previous frame timer data) - if system_name != RENDERING_SYSTEM.name { - - let mut timer = Timer::new(); - timer.begin_context(&format!("{} system update", system_name)); - self.system_manager.update_system_timer(system_name.as_str(), update_phase.clone(), timer).unwrap(); - } - - { - // Run system update and handle errors based on configuration - let result = (system_function)(self) - .context(EngineError::SystemUpdateFailed(system_name.clone(), format!("{:?}", update_phase.clone()))); - - if update_phase == UpdatePhase::Game && stop_on_game_errors { - result.unwrap(); // Panic on error if configured - } else if let Err(err) = result { - if let Some(message) = get_game_error_message(Err(err)) { - error!("{}", message); - } - } - } - - // Update system timer with the final timer state - let mut timer = match self.system_manager.get_system_timer(system_name.as_str(), update_phase.clone()) { - Ok(Some(timer)) => timer, - Ok(None) => { - panic!("{}", Error::new(EngineError::NonReturnedSystemTimer(system_name.clone()))); - } - Err(e) => { - panic!("{}", Error::new(EngineError::Other(e.to_string()))); - } - }; - - timer.end_context().context(format!("Failed to end timer context for {}", system_name.clone())).unwrap(); // End system update context - self.system_manager.update_system_timer(system_name.as_str(), update_phase.clone(), timer).unwrap(); - } - } - - // Update FPS counter - let new_frame_time = delta_time.as_secs_f32() * 1000.0; - let fps = 1000.0 / new_frame_time; - self.frame_delta_time = new_frame_time.into(); - println!("Frame finished (Time: {:.3}ms, FPS {:.0})", new_frame_time, fps); - } - - pub fn shutdown(&mut self) { - info!("Shutting down {}", "Engine".mobj_style()); - } - - pub fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize) { - debug!("{} resized to {}x{}", "Window".mobj_style(), new_window_size.width, new_window_size.height); - self.window_size = new_window_size; - self.renderer.resize(new_window_size); - } - - pub fn pass_keyboard_key_input(&mut self, keyboard_input: &KeyEvent) { - let state: winit::event::ElementState = keyboard_input.state; - match keyboard_input.physical_key { - winit::keyboard::PhysicalKey::Code(key_code) => { - let input_event = InputEvent::KeyboardKey { key: key_code, state: state }; - self.input_queue.push_back(input_event); - debug!("Got new keyboard key input: {:?} {:?}", key_code, state); - } - winit::keyboard::PhysicalKey::Unidentified(_) => { - debug!("Unidentified key input: {:?}", keyboard_input.physical_key); - } - } - } - - pub fn pass_mouse_key_input(&mut self, key: &MouseButton, state: &winit::event::ElementState) { - let input_event = InputEvent::MouseButton { key: *key, state: *state }; - self.input_queue.push_back(input_event); - debug!("Got new mouse key input"); - } - - pub fn pass_mouse_wheel_input(&mut self, delta: &winit::event::MouseScrollDelta) { - let input_event = InputEvent::MouseWheel { delta: *delta }; - self.input_queue.push_back(input_event); - debug!("Got new mouse wheel input"); - } - - pub fn pass_mouse_delta_input(&mut self, delta: &(f64, f64)) { - let input_event = InputEvent::MouseDelta { delta: Vector2f::new(delta.0 as f32, delta.1 as f32) }; - self.input_queue.push_back(input_event); - debug!("Got new mouse motion input"); - } - - pub fn pass_mouse_position_input(&mut self, position: &PhysicalPosition) { - let input_event = InputEvent::MousePosition { position: Vector2f::new(position.x as f32, position.y as f32) }; - self.input_queue.push_back(input_event); - debug!("Got new mouse position input"); - } - - pub fn pass_input_to_egui(&mut self, event: &winit::event::WindowEvent) { - self.renderer.pass_input_to_egui(event).unwrap(); - } - - pub fn get_input_queue(&self) -> &VecDeque { - &self.input_queue - } -} - -// --- API ------------------------------------------------------------------ - -/// Pill Engine game API -impl Engine { - - // --- System API --- - - /// Adds game-defined system to the game update phase - pub fn add_system(&mut self, name: &str, system_function: fn(engine: &mut Engine) -> Result<()>) -> Result<()> { - debug!("Adding {} {} to {} {}", "System".gobj_style(), name.name_style(), "UpdatePhase".sobj_style(), "Game".name_style()); - - self.system_manager.add_system(name, system_function, UpdatePhase::Game).context(format!("Adding {} failed", "System".gobj_style())) - } - - /// Removes game-defined system - pub fn remove_system(&mut self, name: &str) -> Result<()> { - debug!("Removing {} {} from {} {}", "System".gobj_style(), name.name_style(), "UpdatePhase".sobj_style(), "Game".name_style()); - - self.system_manager.remove_system(name, UpdatePhase::Game).context(format!("Removing {} failed", "System".gobj_style())) - } - - /// Toggles game-defined system - pub fn toggle_system(&mut self, name: &str, update_phase: UpdatePhase, enabled: bool) -> Result<()> { - debug!("Toggling {} {} from {} {} to {} state", "System".gobj_style(), name.name_style(), "UpdatePhase".sobj_style(), update_phase.to_string().name_style(), if enabled { "Enabled" } else { "Disabled" }); - - self.system_manager.toggle_system(name, update_phase, enabled).context(format!("Toggling {} failed", "System".gobj_style())) - } - - /// Returns system timer. It has to be returned back using update_system_timer function, otherwise engine will panic. - pub fn get_system_timer(&mut self, name: &str) -> Timer { - debug!("Getting {} {} timer from {} {}", "System".gobj_style(), name.name_style(), "UpdatePhase".sobj_style(), "Game".name_style()); - - self.system_manager.get_system_timer(name, UpdatePhase::Game).unwrap().unwrap() - } - - // --- Entity API --- - - /// Returns EntityBuilder, allowing for handy entity creation - pub fn build_entity(&mut self, scene_handle: SceneHandle) -> EntityBuilder { - let entity_handle = self.create_entity(scene_handle).unwrap(); - EntityBuilder { - engine: self, - entity_handle, - scene_handle, - } - } - - // Creates new entity to scene specified with scene handle - pub fn create_entity(&mut self, scene_handle: SceneHandle) -> Result { - debug!("Creating {} in {} {}", "Entity".gobj_style(), "Scene".gobj_style(), self.scene_manager.get_scene(scene_handle).unwrap().name.name_style()); - - self.scene_manager.create_entity(scene_handle).context(format!("Creating {} failed", "Entity".gobj_style())) - } - - // Removes entity specified with entity handle from scene specified with scene handle - pub fn remove_entity(&mut self, entity_handle: EntityHandle, scene_handle: SceneHandle) -> Result<()> { - debug!("Removing {} from {} {}", "Entity".gobj_style(), "Scene".gobj_style(), self.scene_manager.get_scene(scene_handle).unwrap().name.name_style()); - - let component_destroyers = self.scene_manager.remove_entity(scene_handle, entity_handle).context(format!("Creating {} failed", "Entity".gobj_style()))?; - - // Destroy components using destroyers - for mut component_destroyer in component_destroyers { - component_destroyer.destroy(self, scene_handle, entity_handle)?; - } - - Ok(()) - } - - // --- Component API --- - - /// Registers new component type in scene specified with scene handle - pub fn register_component(&mut self, scene_handle: SceneHandle) -> Result<()> - where T: Component> - { - debug!("Registering {} {} in {} {}", "Component".gobj_style(), get_type_name::().sobj_style(), "Scene".sobj_style(), self.scene_manager.get_scene(scene_handle).unwrap().name.name_style()); - - self.scene_manager.register_component::(scene_handle).context(format!("Registering {} failed", "Component".gobj_style())) - } - - /// Adds new component to the entity specified with scene and entity handle - pub fn add_component_to_entity(&mut self, scene_handle: SceneHandle, entity_handle: EntityHandle, mut component: T) -> Result<()> - where T : Component> - { - debug!("Adding {} {} to {} {} in {} {}", "Component".gobj_style(), get_type_name::().sobj_style(), "Entity".gobj_style(), entity_handle.data().index, "Scene".gobj_style(), self.scene_manager.get_scene(scene_handle).unwrap().name.name_style()); - - // Check if already added - let target_scene = self.scene_manager.get_scene(scene_handle)?; - - if target_scene.entity_has_component::(entity_handle)? { - return Err(Error::new(EngineError::ComponentAlreadyExists(get_type_name::()))) - } - - // Initialize component - component.initialize(self).context(format!("Adding {} {} failed", "Component".gobj_style(), get_type_name::().sobj_style()))?; - - // Add component - self.scene_manager.add_component_to_entity::(scene_handle, entity_handle, component).context(format!("Adding {} to {} failed", "Component".gobj_style(), "Entity".gobj_style()))?; - let component = self.scene_manager.get_entity_component::(entity_handle, scene_handle)?; - - // Pass handles to entity and scene to this component so it can store it if needed - component.pass_handles(scene_handle, entity_handle); - - Ok(()) - } - - /// Removes component from the entity specified with scene and entity handle - pub fn remove_component_from_entity(&mut self, scene_handle: SceneHandle, entity_handle: EntityHandle) -> Result<()> - where T : Component> - { - debug!("Removing {} {} from {} {} in {} {}", "Component".gobj_style(), get_type_name::().sobj_style(), "Entity".gobj_style(), entity_handle.data().index, "Scene".gobj_style(), self.scene_manager.get_scene(scene_handle).unwrap().name.name_style()); - - let mut component = self.scene_manager.remove_component_from_entity::(scene_handle, entity_handle).context("Removing component from entity failed").unwrap(); - - // Destroy component - component.destroy(self, scene_handle, entity_handle)?; - - Ok(()) - } - - // --- Global Component API --- - - /// Adds global component to engine - pub fn add_global_component(&mut self, mut component: T) -> Result<()> - where T: GlobalComponent> - { - // Check if component of this type is not already added - if self.global_components.contains_key::() { - return Err(Error::new(EngineError::GlobalComponentAlreadyExists(get_type_name::()))); - } - - // Initialize component - component.initialize(self)?; - - // Add component - self.global_components.insert::(GlobalComponentStorage::::new(component)); - - Ok(()) - } - - /// Returns global component - pub fn get_global_component(&self) -> Result<&T> - where T: GlobalComponent> - { - // Get component - let component = self.global_components.get::().ok_or(Error::new(EngineError::GlobalComponentNotFound(get_type_name::())))?.data.as_ref().unwrap(); - - Ok(component) - } - - /// Returns global mutable component - pub fn get_global_component_mut(&mut self) -> Result<&mut T> - where T: GlobalComponent> - { - // Get component - let component = self.global_components.get_mut::().ok_or(Error::new(EngineError::GlobalComponentNotFound(get_type_name::())))?.data.as_mut().unwrap(); - - Ok(component) - } - - /// Removes global component from the engine - pub fn remove_global_component(&mut self) -> Result<()> - where T: GlobalComponent> - { - // Check if the type of the component is the same as of the ones, which cannot be removed - if ENGINE_GLOBAL_COMPONENTS.contains(&TypeId::of::()) { - return Err(Error::new(EngineError::GlobalComponentCannotBeRemoved(get_type_name::()))); - } - - // Remove and destroy component - let global_component_storage = self.global_components.remove::().ok_or(EngineError::GlobalComponentNotFound(get_type_name::()))?; - let mut global_component = global_component_storage.data.unwrap(); - global_component.destroy(self)?; - - Ok(()) - } - - // --- Iterator API --- - - /// Returns iterator for specified component - /// - /// Additionally returns entity handle to matching entities - pub fn iterate_one_component(&self) -> Result> - where A: Component> - { - // Get scene handle and iterator - let scene_handle = self.scene_manager.get_active_scene_handle()?; - self.scene_manager.get_one_component_iterator::(scene_handle) - } - - /// Returns iterator for specified component mutable - /// - /// Additionally returns entity handle to matching entities - pub fn iterate_one_component_mut(&mut self) -> Result> - where A: Component> - { - // Get scene handle and iterator - let scene_handle = self.scene_manager.get_active_scene_handle()?; - self.scene_manager.get_one_component_iterator_mut::(scene_handle) - } - - /// Returns iterator for specified component pair - /// - /// Iterator fetches specified components only for those entities which have them all - /// Additionally returns entity handle to matching entities - pub fn iterate_two_components(&self) -> Result> - where - A: Component>, - B: Component> - { - // Get scene handle and iterator - let scene_handle = self.scene_manager.get_active_scene_handle()?; - self.scene_manager.get_two_component_iterator::(scene_handle) - } - - /// Returns iterator for specified component pair mutable - /// - /// Iterator fetches specified components only for those entities which have them all - /// Additionally returns entity handle to matching entities - pub fn iterate_two_components_mut(&mut self) -> Result> - where - A: Component>, - B: Component> - { - // Get scene handle and iterator - let scene_handle = self.scene_manager.get_active_scene_handle()?; - self.scene_manager.get_two_component_iterator_mut::(scene_handle) - } - - /// Returns iterator for specified component triple - /// - /// Iterator fetches specified components only for those entities which have them all - /// Additionally returns entity handle to matching entities - pub fn iterate_three_components(&self) -> Result> - where - A: Component>, - B: Component>, - C: Component> - { - // Get scene handle and iterator - let scene_handle = self.scene_manager.get_active_scene_handle()?; - self.scene_manager.get_three_component_iterator::(scene_handle) - } - - /// Returns iterator for specified component triple mutable - /// - /// Iterator fetches specified components only for those entities which have them all - /// Additionally returns entity handle to matching entities - pub fn iterate_three_components_mut(&mut self) -> Result> - where - A: Component>, - B: Component>, - C: Component> - { - // Get scene handle and iterator - let scene_handle = self.scene_manager.get_active_scene_handle()?; - self.scene_manager.get_three_component_iterator_mut::(scene_handle) - } - - // --- Scene API --- - - // Creates scene - pub fn create_scene(&mut self, name: &str) -> Result { - info!("Creating scene: {}", name); - self.scene_manager.create_scene(name).context(format!("Creating new {} failed", "Scene".gobj_style())) - } - - /// Returns handle to the scene specified by its name - pub fn get_scene_handle(&self, name: &str) -> Result { - self.scene_manager.get_scene_handle(name).context(format!("Getting {} failed", "SceneHandle".sobj_style())) - } - - pub fn set_active_scene(&mut self, scene_handle: SceneHandle) -> Result<()> { - self.scene_manager.set_active_scene(scene_handle).context(format!("Setting active {} failed", "Scene".gobj_style())) - } - - /// Returns handle to the active scene - pub fn get_active_scene_handle(&self) -> Result { - self.scene_manager.get_active_scene_handle().context(format!("Getting {} of active {} failed", "SceneHandle".sobj_style(), "Scene".gobj_style())) - } - - // Removes scene deleting all data in it - pub fn remove_scene(&mut self, scene_handle: SceneHandle) -> Result<()> { - // Get scene - let scene = self.scene_manager.get_scene(scene_handle)?; - - // Get entity handles - let mut entity_handles = Vec::::new(); - for (entity_handle, _) in scene.entities.iter() { - entity_handles.push(entity_handle.clone()); - } - - // Remove entities - for entity_handle in entity_handles { - self.remove_entity(entity_handle, scene_handle)?; - } - - // Remove scene - self.scene_manager.remove_scene(scene_handle).context(format!("Removing {} with usage of {} failed", "Scene".sobj_style(), "SceneHandle".gobj_style()))?; - - Ok(()) - } - - // --- Resource API --- - - // Registers new resource type in the engine - pub fn register_resource_type(&mut self, max_resource_count: usize) -> Result<()> - where T: Resource> - { - self.resource_manager.register_resource_type::(max_resource_count) - } - - // Adds resource to the engine - pub fn add_resource(&mut self, mut resource: T) -> Result - where T: Resource> - { - debug!("Adding {} {} {}", "Resource".gobj_style(), get_type_name::().sobj_style(), resource.get_name().name_style()); - - // Check if resource has proper name - let resource_name = resource.get_name(); - if resource_name.starts_with(DEFAULT_RESOURCE_PREFIX) { - return Err(Error::new(EngineError::WrongResourceName(resource_name.clone()))) - } - - // Initialize resource - resource.initialize(self).context(format!("Adding {} {} failed", "Resource".gobj_style(), get_type_name::().sobj_style()))?; - - // Add resource and get it back - let add_result = self.resource_manager.add_resource(resource)?; - let resource_handle = add_result.0; - let resource = add_result.1; - - // Pass handle to this resource so it can store it if needed - resource.pass_handle(resource_handle); - - Ok(resource_handle) - } - - // Returns resource associated with resource handle - pub fn get_resource<'a, T>(&'a self, resource_handle: &'a T::Handle) -> Result<&'a T> - where T: Resource> - { - Ok(self.resource_manager.get_resource::(resource_handle)?) - } - - /// Returns resource specified by its name - pub fn get_resource_by_name(&self, name: &str) -> Result<&T> - where T: Resource> - { - Ok(self.resource_manager.get_resource_by_name::(name)?) - } - - /// Returns handle to resource specified by the name of this resource - pub fn get_resource_handle(&self, name: &str) -> Result - where T: Resource> - { - Ok(self.resource_manager.get_resource_handle::(name)?) - } - - // Returns mutable resource associated with resource handle - pub fn get_resource_mut<'a, T>(&'a mut self, resource_handle: &'a T::Handle) -> Result<&'a mut T> - where T: Resource> - { - Ok(self.resource_manager.get_resource_mut::(resource_handle)?) - } - - /// Returns mutable resource specified by its name - pub fn get_resource_by_name_mut(&mut self, name: &str) -> Result<&mut T> - where T: Resource> - { - Ok(self.resource_manager.get_resource_by_name_mut::(name)?) - } - - // Removes resource associated with resource handle from the engine - pub fn remove_resource(&mut self, resource_handle: &T::Handle) -> Result<()> - where T: Resource> - { - let error_message = format!("Removing {} {} failed", "Resource".gobj_style(), get_type_name::().sobj_style()); - - // Check if resource is not default - let resource_name = self.resource_manager.get_resource::(resource_handle).context(error_message.to_string())?.get_name(); - if resource_name.starts_with(DEFAULT_RESOURCE_PREFIX) { - return Err(Error::new(EngineError::RemoveDefaultResource(resource_name.clone()))).context(error_message.to_string()) - } - - // Remove and destroy resource - let mut remove_result = self.resource_manager.remove_resource::(resource_handle).context(error_message.to_string())?; - remove_result.1.destroy(self, *resource_handle)?; - - Ok(()) - } - - // Removes resource specified with its name from the engine - pub fn remove_resource_by_name(&mut self, name: &str) -> Result<()> - where T: Resource> - { - let error_message = format!("Removing {} {} {} failed", "Resource".gobj_style(), get_type_name::().sobj_style(), name.to_string().name_style()); - - // Check if resource exists - self.resource_manager.get_resource_by_name::(name).context(error_message.to_string())?; - - // Check if resource is not default - if name.starts_with(DEFAULT_RESOURCE_PREFIX) { - return Err(Error::new(EngineError::RemoveDefaultResource(name.to_string()))).context(error_message.to_string()) - } - - // Remove resource - let mut remove_result = self.resource_manager.remove_resource_by_name::(name).context(error_message.to_string())?; - remove_result.1.destroy(self, remove_result.0)?; - - Ok(()) - } -} \ No newline at end of file +use crate::{config::*, ecs::*, graphics::*, resources::*}; + +use pill_core::{ + get_enum_variant_type_name, get_game_error_message, get_type_name, get_value_type_name, + EngineError, PillSlotMapKey, PillStyle, PillTypeMap, Timer, Vector2f, +}; + +use anyhow::{Context, Error, Result}; +use boolinator::Boolinator; +use log::{debug, error, info}; +use std::{ + any::type_name, any::Any, any::TypeId, cell::RefCell, collections::VecDeque, ops::RangeBounds, +}; +use winit::{dpi::PhysicalPosition, event::KeyEvent}; + +// ------------------------------------------------------------------------------- + +pub type Game = Box; +pub type KeyboardKey = winit::keyboard::KeyCode; +pub type MouseButton = winit::event::MouseButton; + +/// Engine <-> Game interface +/// +/// Entry point of the game project. Mandatory to implement. +pub trait PillGame { + fn start(&self, engine: &mut Engine) -> Result<()>; +} + +/// Heart of Pill Engine +pub struct Engine { + pub(crate) config: config::Config, + pub(crate) game: Option, + pub(crate) renderer: Renderer, + pub(crate) scene_manager: SceneManager, + pub(crate) system_manager: SystemManager, + pub(crate) resource_manager: ResourceManager, + pub(crate) global_components: PillTypeMap, + pub(crate) input_queue: VecDeque, + pub(crate) render_queue: Vec, + pub(crate) window_size: winit::dpi::PhysicalSize, + pub(crate) window: std::sync::Arc, + pub(crate) game_resources_directory_path: std::path::PathBuf, + pub(crate) frame_delta_time: f32, // In milliseconds +} + +// ---- INTERNAL API ----------------------------------------------------------------- +struct EguiUI<'a> { + frame_delta_time: &'a f32, // Use a reference to frame_delta_time +} + +impl<'a> EguiUI<'a> { + fn render(&self, ui: &egui::Context) { + egui::Window::new("PillEngine") + .default_open(true) + .resizable(true) + .anchor(egui::Align2::LEFT_TOP, [0.0, 0.0]) + .show(ui, |ui| { + if ui.add(egui::Button::new("Click me")).clicked() { + println!("PRESSED"); + println!("{}", self.frame_delta_time); + } + }); + } +} +/// Pill Engine internal API +#[cfg(feature = "internal")] +impl Engine { + pub fn new( + game: Box, + game_resources_directory_path: std::path::PathBuf, + renderer: Box, + resource_manager: ResourceManager, + config: config::Config, + window: std::sync::Arc, + ) -> Self { + let max_entity_count = config + .get_int("MAX_ENTITIES") + .unwrap_or(MAX_ENTITIES as i64) as usize; + + Self { + config, + game: Some(game), + renderer, + scene_manager: SceneManager::new(max_entity_count), + system_manager: SystemManager::new(), + resource_manager, + global_components: PillTypeMap::new(), + input_queue: VecDeque::new(), + render_queue: Vec::::with_capacity(max_entity_count), + window_size: winit::dpi::PhysicalSize::::default(), + window, + game_resources_directory_path, + frame_delta_time: 0.0.into(), + } + } + + fn start_game(&mut self) -> Result<()> { + info!("Starting {}", "Game".mobj_style()); + + let game = self + .game + .take() + .ok_or(EngineError::Other("Cannot get game".to_string()))?; + let stop_on_game_errors = self + .config + .get_bool("PANIC_ON_GAME_ERRORS") + .unwrap_or(PANIC_ON_GAME_ERRORS); + let result = game.start(self); + match stop_on_game_errors { + true => result.context(format!("{} error", "Game".mobj_style()))?, + false => { + if let Some(message) = get_game_error_message(result) { + error!("{}", message); + } + } + } + self.game = Some(game); + + Ok(()) + } + + /// Initializes Pill Engine + /// + /// Creates default global components, adds default systems, creates default resources, initializes game + pub fn initialize(&mut self, window_size: winit::dpi::PhysicalSize) -> Result<()> { + info!("Initializing {}", "Engine".mobj_style()); + + // Set window size + self.window_size = window_size; + + // Register global components + self.add_global_component(InputComponent::new())?; + self.add_global_component(TimeComponent::new())?; + self.add_global_component(DeferredUpdateComponent::new())?; + self.add_global_component(EguiManagerComponent::new())?; + self.add_global_component(RenderStateComponent::new())?; + + let max_ambient_sink_count = + self.config + .get_int("MAX_CONCURRENT_2D_SOUNDS") + .unwrap_or(MAX_CONCURRENT_2D_SOUNDS as i64) as usize; + let max_spatial_sink_count = + self.config + .get_int("MAX_CONCURRENT_3D_SOUNDS") + .unwrap_or(MAX_CONCURRENT_3D_SOUNDS as i64) as usize; + self.add_global_component(AudioManagerComponent::new( + max_ambient_sink_count, + max_spatial_sink_count, + ))?; + + // Add built-in systems + self.system_manager.add_system( + INPUT_SYSTEM.name, + INPUT_SYSTEM.system_function, + INPUT_SYSTEM.update_phase, + )?; + self.system_manager.add_system( + TIME_SYSTEM.name, + TIME_SYSTEM.system_function, + TIME_SYSTEM.update_phase, + )?; + self.system_manager.add_system( + AUDIO_SYSTEM.name, + AUDIO_SYSTEM.system_function, + AUDIO_SYSTEM.update_phase, + )?; + self.system_manager.add_system( + DEFERRED_UPDATE_SYSTEM.name, + DEFERRED_UPDATE_SYSTEM.system_function, + DEFERRED_UPDATE_SYSTEM.update_phase, + )?; + self.system_manager.add_system( + RENDERING_SYSTEM.name, + RENDERING_SYSTEM.system_function, + RENDERING_SYSTEM.update_phase, + )?; + + let _ = self.renderer.init(); + self.init_renderer(); + + // Start game + self.start_game()?; + + Ok(()) + } + + fn init_renderer(&mut self) { + // KISS: Run only rendering system once for bootstrap (no active scene yet) + // TODO: Loop over PostGame systems to give them a change to init before game starts + { + let update_phase = UpdatePhase::PostGame; + let (system_name, system_function) = { + let sys = self + .system_manager + .get_system(RENDERING_SYSTEM.name, update_phase.clone()) + .unwrap(); + (sys.name.clone(), sys.system_function) + }; + let mut timer = Timer::new(); + timer.begin_context(&format!("{} system bootstrap", system_name)); + self.system_manager + .update_system_timer(system_name.as_str(), update_phase.clone(), timer) + .unwrap(); + (system_function)(self).unwrap(); + let mut timer = self + .system_manager + .get_system_timer(system_name.as_str(), update_phase.clone()) + .unwrap() + .unwrap(); + timer.end_context().unwrap(); + self.system_manager + .update_system_timer(system_name.as_str(), update_phase.clone(), timer) + .unwrap(); + } + } + + /// Main engine update function + /// + /// Runs all systems in order: PreGame -> Game -> PostGame + pub fn update(&mut self, delta_time: std::time::Duration) { + let stop_on_game_errors = self + .config + .get_bool("PANIC_ON_GAME_ERRORS") + .unwrap_or(PANIC_ON_GAME_ERRORS); + + // Run systems + for update_phase_index in 0..self.system_manager.update_phases.len() { + for system_index in 0..self.system_manager.update_phases[update_phase_index].len() { + let (system_name, update_phase, system_function); + { + let system = + &mut self.system_manager.update_phases[update_phase_index][system_index]; + if !system.enabled { + continue; + } + system_name = system.name.to_string(); + update_phase = system.update_phase.clone(); + system_function = system.system_function; + } + + // Create new time and asign it to system so it can be accessed inside the system function + // For rendering system we can't clean its timer here, + // because it has to render its own timer data in the UI + // (and since the frame in which it renders is not yet finished when it renders UI, it has to use previous frame timer data) + if system_name != RENDERING_SYSTEM.name { + let mut timer = Timer::new(); + timer.begin_context(&format!("{} system update", system_name)); + self.system_manager + .update_system_timer(system_name.as_str(), update_phase.clone(), timer) + .unwrap(); + } + + { + // Run system update and handle errors based on configuration + let result = (system_function)(self).context(EngineError::SystemUpdateFailed( + system_name.clone(), + format!("{:?}", update_phase.clone()), + )); + + if update_phase == UpdatePhase::Game && stop_on_game_errors { + result.unwrap(); // Panic on error if configured + } else if let Err(err) = result { + if let Some(message) = get_game_error_message(Err(err)) { + error!("{}", message); + } + } + } + + // Update system timer with the final timer state + let mut timer = match self + .system_manager + .get_system_timer(system_name.as_str(), update_phase.clone()) + { + Ok(Some(timer)) => timer, + Ok(None) => { + panic!( + "{}", + Error::new(EngineError::NonReturnedSystemTimer(system_name.clone())) + ); + } + Err(e) => { + panic!("{}", Error::new(EngineError::Other(e.to_string()))); + } + }; + + timer + .end_context() + .context(format!( + "Failed to end timer context for {}", + system_name.clone() + )) + .unwrap(); // End system update context + self.system_manager + .update_system_timer(system_name.as_str(), update_phase.clone(), timer) + .unwrap(); + } + } + + // Update FPS counter + let new_frame_time = delta_time.as_secs_f32() * 1000.0; + self.frame_delta_time = new_frame_time.into(); + } + + pub fn shutdown(&mut self) { + info!("Shutting down {}", "Engine".mobj_style()); + } + + pub fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize) { + debug!( + "{} resized to {}x{}", + "Window".mobj_style(), + new_window_size.width, + new_window_size.height + ); + self.window_size = new_window_size; + self.renderer.resize(new_window_size); + } + + pub fn pass_keyboard_key_input(&mut self, keyboard_input: &KeyEvent) { + let state: winit::event::ElementState = keyboard_input.state; + match keyboard_input.physical_key { + winit::keyboard::PhysicalKey::Code(key_code) => { + let input_event = InputEvent::KeyboardKey { + key: key_code, + state: state, + }; + self.input_queue.push_back(input_event); + debug!("Got new keyboard key input: {:?} {:?}", key_code, state); + } + winit::keyboard::PhysicalKey::Unidentified(_) => { + debug!("Unidentified key input: {:?}", keyboard_input.physical_key); + } + } + } + + pub fn pass_mouse_key_input(&mut self, key: &MouseButton, state: &winit::event::ElementState) { + let input_event = InputEvent::MouseButton { + key: *key, + state: *state, + }; + self.input_queue.push_back(input_event); + debug!("Got new mouse key input"); + } + + pub fn pass_mouse_wheel_input(&mut self, delta: &winit::event::MouseScrollDelta) { + let input_event = InputEvent::MouseWheel { delta: *delta }; + self.input_queue.push_back(input_event); + debug!("Got new mouse wheel input"); + } + + pub fn pass_mouse_delta_input(&mut self, delta: &(f64, f64)) { + let input_event = InputEvent::MouseDelta { + delta: Vector2f::new(delta.0 as f32, delta.1 as f32), + }; + self.input_queue.push_back(input_event); + debug!("Got new mouse motion input"); + } + + pub fn pass_mouse_position_input(&mut self, position: &PhysicalPosition) { + let input_event = InputEvent::MousePosition { + position: Vector2f::new(position.x as f32, position.y as f32), + }; + self.input_queue.push_back(input_event); + debug!("Got new mouse position input"); + } + + pub fn pass_input_to_ui(&mut self, event: &winit::event::WindowEvent) { + if let Ok(rs) = self.get_global_component_mut::() { + if let Some(ref client) = rs.egui_client { + client.handle_input(event); + } + } + } + + pub fn get_input_queue(&self) -> &VecDeque { + &self.input_queue + } +} + +// --- API ------------------------------------------------------------------ + +/// Pill Engine game API +impl Engine { + // --- System API --- + + /// Adds game-defined system to the game update phase + pub fn add_system( + &mut self, + name: &str, + system_function: fn(engine: &mut Engine) -> Result<()>, + ) -> Result<()> { + debug!( + "Adding {} {} to {} {}", + "System".gobj_style(), + name.name_style(), + "UpdatePhase".sobj_style(), + "Game".name_style() + ); + + self.system_manager + .add_system(name, system_function, UpdatePhase::Game) + .context(format!("Adding {} failed", "System".gobj_style())) + } + + /// Removes game-defined system + pub fn remove_system(&mut self, name: &str) -> Result<()> { + debug!( + "Removing {} {} from {} {}", + "System".gobj_style(), + name.name_style(), + "UpdatePhase".sobj_style(), + "Game".name_style() + ); + + self.system_manager + .remove_system(name, UpdatePhase::Game) + .context(format!("Removing {} failed", "System".gobj_style())) + } + + /// Toggles game-defined system + pub fn toggle_system( + &mut self, + name: &str, + update_phase: UpdatePhase, + enabled: bool, + ) -> Result<()> { + debug!( + "Toggling {} {} from {} {} to {} state", + "System".gobj_style(), + name.name_style(), + "UpdatePhase".sobj_style(), + update_phase.to_string().name_style(), + if enabled { "Enabled" } else { "Disabled" } + ); + + self.system_manager + .toggle_system(name, update_phase, enabled) + .context(format!("Toggling {} failed", "System".gobj_style())) + } + + /// Returns system timer. It has to be returned back using update_system_timer function, otherwise engine will panic. + pub fn get_system_timer(&mut self, name: &str) -> Timer { + debug!( + "Getting {} {} timer from {} {}", + "System".gobj_style(), + name.name_style(), + "UpdatePhase".sobj_style(), + "Game".name_style() + ); + + self.system_manager + .get_system_timer(name, UpdatePhase::Game) + .unwrap() + .unwrap() + } + + // --- Entity API --- + + /// Returns EntityBuilder, allowing for handy entity creation + pub fn build_entity(&mut self, scene_handle: SceneHandle) -> EntityBuilder<'_> { + let entity_handle = self.create_entity(scene_handle).unwrap(); + EntityBuilder { + engine: self, + entity_handle, + scene_handle, + } + } + + // Creates new entity to scene specified with scene handle + pub fn create_entity(&mut self, scene_handle: SceneHandle) -> Result { + debug!( + "Creating {} in {} {}", + "Entity".gobj_style(), + "Scene".gobj_style(), + self.scene_manager + .get_scene(scene_handle) + .unwrap() + .name + .name_style() + ); + + self.scene_manager + .create_entity(scene_handle) + .context(format!("Creating {} failed", "Entity".gobj_style())) + } + + // Removes entity specified with entity handle from scene specified with scene handle + pub fn remove_entity( + &mut self, + entity_handle: EntityHandle, + scene_handle: SceneHandle, + ) -> Result<()> { + debug!( + "Removing {} from {} {}", + "Entity".gobj_style(), + "Scene".gobj_style(), + self.scene_manager + .get_scene(scene_handle) + .unwrap() + .name + .name_style() + ); + + let component_destroyers = self + .scene_manager + .remove_entity(scene_handle, entity_handle) + .context(format!("Creating {} failed", "Entity".gobj_style()))?; + + // Destroy components using destroyers + for mut component_destroyer in component_destroyers { + component_destroyer.destroy(self, scene_handle, entity_handle)?; + } + + Ok(()) + } + + // --- Component API --- + + /// Registers new component type in scene specified with scene handle + pub fn register_component(&mut self, scene_handle: SceneHandle) -> Result<()> + where + T: Component>, + { + debug!( + "Registering {} {} in {} {}", + "Component".gobj_style(), + get_type_name::().sobj_style(), + "Scene".sobj_style(), + self.scene_manager + .get_scene(scene_handle) + .unwrap() + .name + .name_style() + ); + + self.scene_manager + .register_component::(scene_handle) + .context(format!("Registering {} failed", "Component".gobj_style())) + } + + /// Adds new component to the entity specified with scene and entity handle + pub fn add_component_to_entity( + &mut self, + scene_handle: SceneHandle, + entity_handle: EntityHandle, + mut component: T, + ) -> Result<()> + where + T: Component>, + { + debug!( + "Adding {} {} to {} {} in {} {}", + "Component".gobj_style(), + get_type_name::().sobj_style(), + "Entity".gobj_style(), + entity_handle.data().index, + "Scene".gobj_style(), + self.scene_manager + .get_scene(scene_handle) + .unwrap() + .name + .name_style() + ); + + // Check if already added + let target_scene = self.scene_manager.get_scene(scene_handle)?; + + if target_scene.entity_has_component::(entity_handle)? { + return Err(Error::new(EngineError::ComponentAlreadyExists( + get_type_name::(), + ))); + } + + // Initialize component + component.initialize(self).context(format!( + "Adding {} {} failed", + "Component".gobj_style(), + get_type_name::().sobj_style() + ))?; + + // Add component + self.scene_manager + .add_component_to_entity::(scene_handle, entity_handle, component) + .context(format!( + "Adding {} to {} failed", + "Component".gobj_style(), + "Entity".gobj_style() + ))?; + let component = self + .scene_manager + .get_entity_component::(entity_handle, scene_handle)?; + + // Pass handles to entity and scene to this component so it can store it if needed + component.pass_handles(scene_handle, entity_handle); + + Ok(()) + } + + /// Removes component from the entity specified with scene and entity handle + pub fn remove_component_from_entity( + &mut self, + scene_handle: SceneHandle, + entity_handle: EntityHandle, + ) -> Result<()> + where + T: Component>, + { + debug!( + "Removing {} {} from {} {} in {} {}", + "Component".gobj_style(), + get_type_name::().sobj_style(), + "Entity".gobj_style(), + entity_handle.data().index, + "Scene".gobj_style(), + self.scene_manager + .get_scene(scene_handle) + .unwrap() + .name + .name_style() + ); + + let mut component = self + .scene_manager + .remove_component_from_entity::(scene_handle, entity_handle) + .context("Removing component from entity failed") + .unwrap(); + + // Destroy component + component.destroy(self, scene_handle, entity_handle)?; + + Ok(()) + } + + // --- Global Component API --- + + /// Adds global component to engine + pub fn add_global_component(&mut self, mut component: T) -> Result<()> + where + T: GlobalComponent>, + { + // Check if component of this type is not already added + if self.global_components.contains_key::() { + return Err(Error::new(EngineError::GlobalComponentAlreadyExists( + get_type_name::(), + ))); + } + + // Initialize component + component.initialize(self)?; + + // Add component + self.global_components + .insert::(GlobalComponentStorage::::new(component)); + + Ok(()) + } + + /// Returns global component + pub fn get_global_component(&self) -> Result<&T> + where + T: GlobalComponent>, + { + // Get component + let component = self + .global_components + .get::() + .ok_or(Error::new(EngineError::GlobalComponentNotFound( + get_type_name::(), + )))? + .data + .as_ref() + .unwrap(); + + Ok(component) + } + + /// Returns global mutable component + pub fn get_global_component_mut(&mut self) -> Result<&mut T> + where + T: GlobalComponent>, + { + // Get component + let component = self + .global_components + .get_mut::() + .ok_or(Error::new(EngineError::GlobalComponentNotFound( + get_type_name::(), + )))? + .data + .as_mut() + .unwrap(); + + Ok(component) + } + + /// Removes global component from the engine + pub fn remove_global_component(&mut self) -> Result<()> + where + T: GlobalComponent>, + { + // Check if the type of the component is the same as of the ones, which cannot be removed + if ENGINE_GLOBAL_COMPONENTS.contains(&TypeId::of::()) { + return Err(Error::new(EngineError::GlobalComponentCannotBeRemoved( + get_type_name::(), + ))); + } + + // Remove and destroy component + let global_component_storage = self + .global_components + .remove::() + .ok_or(EngineError::GlobalComponentNotFound(get_type_name::()))?; + let mut global_component = global_component_storage.data.unwrap(); + global_component.destroy(self)?; + + Ok(()) + } + + // --- Iterator API --- + + /// Returns iterator for specified component + /// + /// Additionally returns entity handle to matching entities + pub fn iterate_one_component(&self) -> Result> + where + A: Component>, + { + // Get scene handle and iterator + let scene_handle = self.scene_manager.get_active_scene_handle()?; + self.scene_manager + .get_one_component_iterator::(scene_handle) + } + + /// Returns iterator for specified component mutable + /// + /// Additionally returns entity handle to matching entities + pub fn iterate_one_component_mut( + &mut self, + ) -> Result> + where + A: Component>, + { + // Get scene handle and iterator + let scene_handle = self.scene_manager.get_active_scene_handle()?; + self.scene_manager + .get_one_component_iterator_mut::(scene_handle) + } + + /// Returns iterator for specified component pair + /// + /// Iterator fetches specified components only for those entities which have them all + /// Additionally returns entity handle to matching entities + pub fn iterate_two_components( + &self, + ) -> Result> + where + A: Component>, + B: Component>, + { + // Get scene handle and iterator + let scene_handle = self.scene_manager.get_active_scene_handle()?; + self.scene_manager + .get_two_component_iterator::(scene_handle) + } + + /// Returns iterator for specified component pair mutable + /// + /// Iterator fetches specified components only for those entities which have them all + /// Additionally returns entity handle to matching entities + pub fn iterate_two_components_mut( + &mut self, + ) -> Result> + where + A: Component>, + B: Component>, + { + // Get scene handle and iterator + let scene_handle = self.scene_manager.get_active_scene_handle()?; + self.scene_manager + .get_two_component_iterator_mut::(scene_handle) + } + + /// Returns iterator for specified component triple + /// + /// Iterator fetches specified components only for those entities which have them all + /// Additionally returns entity handle to matching entities + pub fn iterate_three_components( + &self, + ) -> Result> + where + A: Component>, + B: Component>, + C: Component>, + { + // Get scene handle and iterator + let scene_handle = self.scene_manager.get_active_scene_handle()?; + self.scene_manager + .get_three_component_iterator::(scene_handle) + } + + /// Returns iterator for specified component triple mutable + /// + /// Iterator fetches specified components only for those entities which have them all + /// Additionally returns entity handle to matching entities + pub fn iterate_three_components_mut( + &mut self, + ) -> Result> + where + A: Component>, + B: Component>, + C: Component>, + { + // Get scene handle and iterator + let scene_handle = self.scene_manager.get_active_scene_handle()?; + self.scene_manager + .get_three_component_iterator_mut::(scene_handle) + } + + // --- Scene API --- + + // Creates scene + pub fn create_scene(&mut self, name: &str) -> Result { + info!("Creating scene: {}", name); + self.scene_manager + .create_scene(name) + .context(format!("Creating new {} failed", "Scene".gobj_style())) + } + + /// Returns handle to the scene specified by its name + pub fn get_scene_handle(&self, name: &str) -> Result { + self.scene_manager + .get_scene_handle(name) + .context(format!("Getting {} failed", "SceneHandle".sobj_style())) + } + + pub fn set_active_scene(&mut self, scene_handle: SceneHandle) -> Result<()> { + self.scene_manager + .set_active_scene(scene_handle) + .context(format!("Setting active {} failed", "Scene".gobj_style())) + } + + /// Returns handle to the active scene + pub fn get_active_scene_handle(&self) -> Result { + self.scene_manager + .get_active_scene_handle() + .context(format!( + "Getting {} of active {} failed", + "SceneHandle".sobj_style(), + "Scene".gobj_style() + )) + } + + // Removes scene deleting all data in it + pub fn remove_scene(&mut self, scene_handle: SceneHandle) -> Result<()> { + // Get scene + let scene = self.scene_manager.get_scene(scene_handle)?; + + // Get entity handles + let mut entity_handles = Vec::::new(); + for (entity_handle, _) in scene.entities.iter() { + entity_handles.push(entity_handle.clone()); + } + + // Remove entities + for entity_handle in entity_handles { + self.remove_entity(entity_handle, scene_handle)?; + } + + // Remove scene + self.scene_manager + .remove_scene(scene_handle) + .context(format!( + "Removing {} with usage of {} failed", + "Scene".sobj_style(), + "SceneHandle".gobj_style() + ))?; + + Ok(()) + } + + // --- Resource API --- + + // Registers new resource type in the engine + pub fn register_resource_type(&mut self, max_resource_count: usize) -> Result<()> + where + T: Resource>, + { + self.resource_manager + .register_resource_type::(max_resource_count) + } + + // Adds resource to the engine + pub fn add_resource(&mut self, mut resource: T) -> Result + where + T: Resource>, + { + debug!( + "Adding {} {} {}", + "Resource".gobj_style(), + get_type_name::().sobj_style(), + resource.get_name().name_style() + ); + + // Check if resource has proper name + let resource_name = resource.get_name(); + if resource_name.starts_with(DEFAULT_RESOURCE_PREFIX) { + return Err(Error::new(EngineError::WrongResourceName( + resource_name.clone(), + ))); + } + + // Initialize resource + resource.initialize(self).context(format!( + "Adding {} {} failed", + "Resource".gobj_style(), + get_type_name::().sobj_style() + ))?; + + // Add resource and get it back + let add_result = self.resource_manager.add_resource(resource)?; + let resource_handle = add_result.0; + let resource = add_result.1; + + // Pass handle to this resource so it can store it if needed + resource.pass_handle(resource_handle); + + Ok(resource_handle) + } + + // Returns resource associated with resource handle + pub fn get_resource<'a, T>(&'a self, resource_handle: &'a T::Handle) -> Result<&'a T> + where + T: Resource>, + { + Ok(self.resource_manager.get_resource::(resource_handle)?) + } + + /// Returns resource specified by its name + pub fn get_resource_by_name(&self, name: &str) -> Result<&T> + where + T: Resource>, + { + Ok(self.resource_manager.get_resource_by_name::(name)?) + } + + /// Returns handle to resource specified by the name of this resource + pub fn get_resource_handle(&self, name: &str) -> Result + where + T: Resource>, + { + Ok(self.resource_manager.get_resource_handle::(name)?) + } + + // Returns mutable resource associated with resource handle + pub fn get_resource_mut<'a, T>( + &'a mut self, + resource_handle: &'a T::Handle, + ) -> Result<&'a mut T> + where + T: Resource>, + { + Ok(self + .resource_manager + .get_resource_mut::(resource_handle)?) + } + + /// Returns mutable resource specified by its name + pub fn get_resource_by_name_mut(&mut self, name: &str) -> Result<&mut T> + where + T: Resource>, + { + Ok(self.resource_manager.get_resource_by_name_mut::(name)?) + } + + // Removes resource associated with resource handle from the engine + pub fn remove_resource(&mut self, resource_handle: &T::Handle) -> Result<()> + where + T: Resource>, + { + let error_message = format!( + "Removing {} {} failed", + "Resource".gobj_style(), + get_type_name::().sobj_style() + ); + + // Check if resource is not default + let resource_name = self + .resource_manager + .get_resource::(resource_handle) + .context(error_message.to_string())? + .get_name(); + if resource_name.starts_with(DEFAULT_RESOURCE_PREFIX) { + return Err(Error::new(EngineError::RemoveDefaultResource( + resource_name.clone(), + ))) + .context(error_message.to_string()); + } + + // Remove and destroy resource + let mut remove_result = self + .resource_manager + .remove_resource::(resource_handle) + .context(error_message.to_string())?; + remove_result.1.destroy(self, *resource_handle)?; + + Ok(()) + } + + // Removes resource specified with its name from the engine + pub fn remove_resource_by_name(&mut self, name: &str) -> Result<()> + where + T: Resource>, + { + let error_message = format!( + "Removing {} {} {} failed", + "Resource".gobj_style(), + get_type_name::().sobj_style(), + name.to_string().name_style() + ); + + // Check if resource exists + self.resource_manager + .get_resource_by_name::(name) + .context(error_message.to_string())?; + + // Check if resource is not default + if name.starts_with(DEFAULT_RESOURCE_PREFIX) { + return Err(Error::new(EngineError::RemoveDefaultResource( + name.to_string(), + ))) + .context(error_message.to_string()); + } + + // Remove resource + let mut remove_result = self + .resource_manager + .remove_resource_by_name::(name) + .context(error_message.to_string())?; + remove_result.1.destroy(self, remove_result.0)?; + + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/mod.rs b/engine/pill_engine/src/graphics/mod.rs index d531bb80..23b60393 100644 --- a/engine/pill_engine/src/graphics/mod.rs +++ b/engine/pill_engine/src/graphics/mod.rs @@ -1,25 +1,55 @@ #![cfg_attr(debug_assertions, allow(dead_code, unused_imports))] -mod renderer; mod render_queue; +mod renderer; // --- Use --- +pub use pill_core::{ + RendererBufferTag, RendererCameraTag, RendererMaterialTag, RendererMeshTag, + RendererPipelineTag, RendererPipelineV2Tag, RendererTextureTag, +}; +pub use renderer::{render_with_factory, WorldView, WorldViewFactory}; pub use renderer::{ - Renderer, - PillRenderer, - RendererCameraHandle, - RendererMaterialHandle, - RendererMeshHandle, - RendererTextureHandle, - RendererPipelineHandle, + BufferDesc, MaterialDesc, PillRenderer, PipelineV2, PipelineV2Desc, Renderer, + RendererBufferHandle, RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, + RendererPipelineHandle, RendererPipelineV2Handle, RendererTargetDesc, RendererTextureHandle, + ShaderDesc, }; +pub use renderer::{Pass, WorldQuery}; pub use render_queue::{ - RenderQueueItem, - RenderQueueKeyFields, - RenderQueueKey, - compose_render_queue_key, - decompose_render_queue_key, - RENDER_QUEUE_KEY_ORDER, + compose_render_queue_key, decompose_render_queue_key, RenderQueueItem, RenderQueueKey, + RenderQueueKeyFields, RENDER_QUEUE_KEY_ORDER, }; +pub use render_queue::{RenderQuery, RenderQueueFactory}; + +// Passes owned by engine +pub mod pass_logo; +pub use pass_logo::PassLogo; +pub mod pass_overlay_uv; +pub use pass_overlay_uv::PassOverlayUV; +pub mod pass_overlay_depth; +pub use pass_overlay_depth::PassOverlayDepth; +pub mod pass_overlay_velocity; +pub use pass_overlay_velocity::PassOverlayVelocity; +pub mod pass_linearize_depth; +pub use pass_linearize_depth::PassLinearizeDepth; +pub mod pass_compose; +pub use pass_compose::PassCompose; +pub mod pass_scene; +pub use pass_scene::PassScene; +pub mod pass_dof_bokeh; +pub mod pass_egui; +pub use pass_dof_bokeh::PassDofBokeh; +pub use pass_egui::PassEgui; +pub mod pass_motion_blur; +pub use pass_motion_blur::PassMotionBlur; +pub mod pass_skybox_equirect; +pub use pass_skybox_equirect::PassSkyboxEquirect; +pub mod pass_ibl_diffuse_equirect; +pub use pass_ibl_diffuse_equirect::PassIblDiffuseEquirect; +pub mod pass_ibl_specular_equirect; +pub use pass_ibl_specular_equirect::PassIblSpecularEquirect; + +pub mod projection; diff --git a/engine/pill_engine/src/graphics/pass_compose.rs b/engine/pill_engine/src/graphics/pass_compose.rs new file mode 100644 index 00000000..69c976d0 --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_compose.rs @@ -0,0 +1,203 @@ +use anyhow::Result; +use wgpu::CommandEncoder; + +use crate::graphics::renderer::{ + Pass, PillRenderer as EnginePillRenderer, PipelineV2, RendererTextureHandle, WorldQuery, +}; + +pub struct PassCompose { + label: String, + offscreen_color_texture: RendererTextureHandle, + target_format: wgpu::TextureFormat, + pipeline: Option, + bind_group: Option, +} + +impl PassCompose { + pub fn new( + label: &str, + offscreen_color_texture: RendererTextureHandle, + target_format: wgpu::TextureFormat, + ) -> Self { + Self { + label: label.to_string(), + offscreen_color_texture, + target_format, + pipeline: None, + bind_group: None, + } + } +} + +impl Pass for PassCompose { + fn get_label(&self) -> &str { + &self.label + } + + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { + let device = renderer.get_device(); + let vs = r#" + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var pos = array, 3>( + vec2(-1.0, -3.0), + vec2( 3.0, 1.0), + vec2(-1.0, 1.0) + ); + var tuv = array, 3>( + vec2(0.0, 2.0), + vec2(2.0, 0.0), + vec2(0.0, 0.0) + ); + var o: VSOut; + o.pos = vec4(pos[vi], 0.0, 1.0); + o.uv = tuv[vi]; + return o; + } + "#; + + let fs = r#" + @group(0) @binding(0) var t_src: texture_2d; + @group(0) @binding(1) var s_src: sampler; + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let hdr = textureSample(t_src, s_src, uv).rgb; + // Reinhard tone mapping; output remains in linear space. The sRGB swapchain will encode. + let ldr = hdr / (1.0 + hdr); + return vec4(ldr, 1.0); + } + "#; + + // Build bind group layout + let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("compose_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + + let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("compose_pl"), + bind_group_layouts: &[&bgl], + push_constant_ranges: &[], + }); + + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("compose_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("compose_fs"), + source: wgpu::ShaderSource::Wgsl(fs.into()), + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("compose_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.target_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + // Bind offscreen color texture using ResourceManager GPU pools + let view = resources + .gpu() + .textures + .get(self.offscreen_color_texture) + .expect("offscreen color") + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("compose_bind_group"), + layout: &bgl, // Group 0: Texture and sampler bindings + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + ], + }); + + // Store pipeline handle + self.pipeline = Some(PipelineV2 { + pipeline, + bind_group_layouts: vec![bgl], + }); + self.bind_group = Some(bind_group); + + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + _renderer: &mut dyn EnginePillRenderer, + _resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + view: &wgpu::TextureView, + _world: &WorldQuery, + ) -> Result<()> { + // Create render pass for this pass using the provided frame + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + // Draw fullscreen triangle for composition + pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + pass.set_bind_group(0, &self.bind_group.as_ref().unwrap(), &[]); + pass.draw(0..3, 0..1); + + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_dof_bokeh.rs b/engine/pill_engine/src/graphics/pass_dof_bokeh.rs new file mode 100644 index 00000000..893ac5aa --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_dof_bokeh.rs @@ -0,0 +1,436 @@ +//! Depth of Field (DoF) post-processing pass (single-pass bokeh, disk kernel) + +use crate::graphics::Pass; +use crate::graphics::PillRenderer; +use crate::graphics::RendererTextureHandle; +use crate::renderer::Renderer; +use crate::resources::ResourceManager; +use anyhow::Result; +use bytemuck::{Pod, Zeroable}; +use wgpu::util::DeviceExt; +use wgpu::TextureFormat; + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct DofParams { + focus_distance: f32, + aperture: f32, + focal_length: f32, + sensor_size: f32, + time_s: f32, + focus_point: f32, + focus_scale: f32, +} + +pub struct PassDofBokeh { + pub name: String, + pub input_color: RendererTextureHandle, + pub input_depth: RendererTextureHandle, + pub output: RendererTextureHandle, + pub format: TextureFormat, + // Global DoF params (hardcoded for now) + pub focus_distance: f32, + pub aperture: f32, + pub focal_length: f32, + pub sensor_size: f32, + // Pipeline and bind group + pipeline: Option, + bind_group: Option, + params_buffer: Option, +} + +impl PassDofBokeh { + pub fn new( + name: &str, + input_color: RendererTextureHandle, + input_depth: RendererTextureHandle, + output: RendererTextureHandle, + format: TextureFormat, + ) -> Self { + Self { + name: name.to_string(), + input_color, + input_depth, + output, + format, + focus_distance: 5.0, // meters + aperture: 2.8, // f-stop + focal_length: 50.0, // mm + sensor_size: 36.0, // mm (full-frame) + pipeline: None, + bind_group: None, + params_buffer: None, + } + } + + // Optionally: methods to update params, toggle debug, etc. +} + +impl Pass for PassDofBokeh { + fn get_label(&self) -> &str { + &self.name + } + + fn init( + &mut self, + renderer: &mut dyn PillRenderer, + resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { + let device = renderer.get_device(); + eprintln!("[DoF] Initializing PassDofBokeh: {}", self.name); + // Only this inlined WGSL shader is used for DoF (vertex + fragment) + let vs = r#" + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var pos = array, 3>( + vec2(-1.0, -3.0), + vec2( 3.0, 1.0), + vec2(-1.0, 1.0) + ); + var tuv = array, 3>( + vec2(0.0, 2.0), + vec2(2.0, 0.0), + vec2(0.0, 0.0) + ); + var o: VSOut; + o.pos = vec4(pos[vi], 0.0, 1.0); + o.uv = tuv[vi]; + return o; + } + "#; + + // WGSL fragment shader for step-by-step DoF debug and effect + // Set debug mode here: 0 = pass-through, 1 = depth/CoC debug, 2 = DoF effect + // To change mode, edit the value below and rebuild. + let fs = r#" + struct Params { + focus_distance: f32, + aperture: f32, + focal_length: f32, + sensor_size: f32, + time_s: f32, + focus_point: f32, // meters + focus_scale: f32, + }; + @group(0) @binding(0) var t_color: texture_2d; + // Linear view-space depth in meters (R16Float) from PassLinearizeDepth + @group(0) @binding(1) var t_depth: texture_2d; + @group(0) @binding(2) var params: Params; + @group(0) @binding(3) var s_color: sampler; + + // NOTE: Split view for proof: + // - uv.y > 0.5 : debug visualization + // - uv.y <= 0.5: DoF result + const DEBUG_VIEW: i32 = 0; // 0=None, 1=pass-through, 2=abs(CoC), 3=linearDepthNormalized + + const GOLDEN_ANGLE: f32 = 2.39996323; + const MAX_BLUR_SIZE: f32 = 20.0; + const RAD_SCALE: f32 = 0.5; // Smaller = nicer blur, larger = faster + const FAR: f32 = 100.0; + const NEAR: f32 = 0.1; + // Artistic CoC remap: <1 boosts small CoC (bigger bokeh for far objects) without increasing MAX_BLUR_SIZE. + const COC_CURVE: f32 = 0.5; // sqrt + + fn load_linear_depth(uv: vec2, dims: vec2) -> f32 { + let coord0 = vec2(uv * vec2(dims)); + let maxc = vec2(i32(dims.x) - 1, i32(dims.y) - 1); + let coord = clamp(coord0, vec2(0, 0), maxc); + return textureLoad(t_depth, coord, 0).r; + } + + // Reference: + // - `https://web.archive.org/web/20201215123940/https://blog.tuxedolabs.com/2018/05/04/bokeh-depth-of-field-in-single-pass.html` + // - `https://github.com/bkaradzic/bgfx/tree/master/examples/45-bokeh`. + fn coc(depthLinear: f32, focusPoint: f32, focusScale: f32) -> f32 { + return clamp((1.0 / focusPoint - 1.0 / depthLinear) * focusScale, -1.0, 1.0); + } + + @fragment fn main( + @builtin(position) pos: vec4, + @location(0) uv: vec2 + ) -> @location(0) vec4 { + let dims = textureDimensions(t_depth, 0u); + let color4 = textureSample(t_color, s_color, uv); + let color = color4.rgb; + let center_depth = max(load_linear_depth(uv, dims), 1e-4); + let focusPointFromUi = max(params.focus_point, 1e-4); + let focusPoint = focusPointFromUi; + let coc = coc(center_depth, max(focusPoint, 1e-4), params.focus_scale); + let coc_abs = pow(abs(coc), COC_CURVE); + + // Proof view in bottom half (split by real screen-space Y, not UVs). + let y01 = pos.y / f32(dims.y); + if (y01 < 0.25 && DEBUG_VIEW > 0) { + if (DEBUG_VIEW == 1) { + return color4; + } else if (DEBUG_VIEW == 2) { + return vec4(coc_abs, coc_abs, coc_abs, 1.0); + } else if (DEBUG_VIEW == 3) { + let linear_depth = clamp(center_depth / FAR, 0.0, 1.0); + return vec4(linear_depth, linear_depth, linear_depth, 1.0); + } + } + + // DoF result in top half + let center_size = coc_abs * MAX_BLUR_SIZE; + let pixel_size = vec2(1.0 / f32(dims.x), 1.0 / f32(dims.y)); + + var acc = color; + var tot = 1.0; + var radius = RAD_SCALE; + var ang = 0.0; + loop { + if (radius >= MAX_BLUR_SIZE) { break; } + + let dir = vec2(cos(ang), sin(ang)); + let tc = uv + dir * pixel_size * radius; + let sample_color = textureSample(t_color, s_color, tc).rgb; + let sample_depth = max(load_linear_depth(tc, dims), 1e-4); + + let sample_coc = coc(sample_depth, max(focusPoint, 1e-4), params.focus_scale); + var sample_size = pow(abs(sample_coc), COC_CURVE) * MAX_BLUR_SIZE; + + // Prevent background blur bleeding over in-focus foreground (Voxagon clamp trick) + if (sample_depth > center_depth) { + sample_size = clamp(sample_size, 0.0, center_size * 2.0); + } + + let m = smoothstep(radius - 0.5, radius + 0.5, sample_size); + acc = acc + mix(acc / tot, sample_color, m); + tot = tot + 1.0; + + ang = ang + GOLDEN_ANGLE; + radius = radius + RAD_SCALE / radius; + } + + acc = acc / tot; + return vec4(acc, 1.0); + } + "#; + + // Bind group layout: color, depth, params + let bgl = match device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("dof_bokeh_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: false }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }) { + bgl => bgl, + }; + eprintln!("[DoF] Created bind group layout "); + + let pl = match device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("dof_bokeh_pl"), + bind_group_layouts: &[&bgl], + push_constant_ranges: &[], + }) { + pl => pl, + }; + eprintln!("[DoF] Created pipeline layout "); + + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("dof_bokeh_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("dof_bokeh_fs"), + source: wgpu::ShaderSource::Wgsl(fs.into()), + }); + + let pipeline = match device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("dof_bokeh_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.format, // Back to offscreen format + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }) { + p => p, + }; + eprintln!("[DoF] Created render pipeline "); + + // Prepare uniform buffer for params (updated per-frame) + let params = DofParams { + focus_distance: self.focus_distance, + aperture: self.aperture, + focal_length: self.focal_length, + sensor_size: self.sensor_size, + time_s: 0.0, + focus_point: 5.0, + focus_scale: 3.0, + }; + let params_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("dof_bokeh_params"), + contents: bytemuck::bytes_of(¶ms), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + // Get texture views + let color_view = match resources.gpu().textures.get(self.input_color) { + Some(tex) => { + eprintln!("[DoF] Found input_color texture "); + tex.texture + .create_view(&wgpu::TextureViewDescriptor::default()) + } + None => { + eprintln!("[DoF][ERROR] input_color texture not found!"); + return Err(anyhow::anyhow!("input_color texture not found ")); + } + }; + let depth_view = match resources.gpu().textures.get(self.input_depth) { + Some(tex) => { + eprintln!("[DoF] Found input_depth texture "); + tex.texture + .create_view(&wgpu::TextureViewDescriptor::default()) + } + None => { + eprintln!("[DoF][ERROR] input_depth texture not found!"); + return Err(anyhow::anyhow!("input_depth texture not found ")); + } + }; + + let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); + eprintln!("[DoF] Created sampler "); + let bind_group = match device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("dof_bokeh_bind_group"), + layout: &bgl, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&color_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&depth_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: params_buf.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + ], + }) { + bg => bg, + }; + eprintln!("[DoF] Created bind group "); + + self.pipeline = Some(crate::graphics::PipelineV2 { + pipeline, + bind_group_layouts: vec![bgl], + }); + self.bind_group = Some(bind_group); + self.params_buffer = Some(params_buf); + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut wgpu::CommandEncoder, + renderer: &mut dyn PillRenderer, + resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + _view: &wgpu::TextureView, + _world: &crate::graphics::WorldQuery, + ) -> Result<()> { + // Update params from UI/shared state + if let Ok(pp) = resources.post_process.lock() { + let params = DofParams { + focus_distance: self.focus_distance, + aperture: self.aperture, + focal_length: self.focal_length, + sensor_size: self.sensor_size, + time_s: pp.time_s, + focus_point: pp.focus_point, + focus_scale: pp.focus_scale, + }; + renderer.get_queue().write_buffer( + self.params_buffer.as_ref().unwrap(), + 0, + bytemuck::bytes_of(¶ms), + ); + } + + // Render to output texture (which gets read by Compose pass) + let out_view = match resources.gpu().textures.get(self.output) { + Some(tex) => tex + .texture + .create_view(&wgpu::TextureViewDescriptor::default()), + None => { + eprintln!("[DoF][ERROR] output texture not found!"); + return Err(anyhow::anyhow!("output texture not found ")); + } + }; + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.name), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &out_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + pass.set_bind_group(0, self.bind_group.as_ref().unwrap(), &[]); + pass.draw(0..3, 0..1); + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_egui.rs b/engine/pill_engine/src/graphics/pass_egui.rs new file mode 100644 index 00000000..f09554e1 --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_egui.rs @@ -0,0 +1,93 @@ +use std::sync::Arc; + +use anyhow::Result; +use wgpu::CommandEncoder; + +use crate::graphics::renderer::{Pass, PillRenderer as EnginePillRenderer, WorldQuery}; + +pub struct PassEgui { + label: String, + window: Arc, + client: Arc, + state: Option, +} + +struct PassEguiState { + egui: crate::renderer::egui::EguiRenderer, +} + +impl PassEgui { + pub fn new( + label: &str, + window: Arc, + client: Arc, + ) -> Self { + Self { + label: label.to_string(), + window, + client, + state: None, + } + } +} + +impl Pass for PassEgui { + fn get_label(&self) -> &str { + &self.label + } + + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + _resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { + let device = renderer.get_device(); + let format = renderer.get_surface_format(); + let egui = crate::renderer::egui::EguiRenderer::new( + device, + format, + None, + 1, + self.window.clone(), + ); + self.state = Some(PassEguiState { egui }); + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + renderer: &mut dyn EnginePillRenderer, + _resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + view: &wgpu::TextureView, + _world: &WorldQuery, + ) -> Result<()> { + let state = self.state.as_mut().expect("PassEgui not initialized"); + // Feed queued input events + for ev in self.client.take_events().into_iter() { + state.egui.handle_input(&ev); + } + // Build screen descriptor and UI callback + let size = self.window.inner_size(); + let screen_descriptor = egui_wgpu::ScreenDescriptor { + size_in_pixels: [size.width, size.height], + pixels_per_point: state.egui.window_scale_factor, + }; + let run_ui = self + .client + .take_ui() + .unwrap_or_else(|| Box::new(|_ctx: &egui::Context| {})); + state.egui.draw( + renderer.get_device(), + renderer.get_queue(), + encoder, + view, + screen_descriptor, + run_ui, + )?; + Ok(()) + } +} + + diff --git a/engine/pill_engine/src/graphics/pass_ibl_diffuse_equirect.rs b/engine/pill_engine/src/graphics/pass_ibl_diffuse_equirect.rs new file mode 100644 index 00000000..e4cb9db9 --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_ibl_diffuse_equirect.rs @@ -0,0 +1,279 @@ +use crate::graphics::renderer::{Pass, PillRenderer as EnginePillRenderer, WorldQuery}; +use crate::graphics::{PipelineV2, PipelineV2Desc, RendererTextureHandle, ShaderDesc}; +use anyhow::Result; +use wgpu::CommandEncoder; + +pub struct PassIblDiffuseEquirect { + label: String, + env_texture: RendererTextureHandle, + output_handle: RendererTextureHandle, + output_format: wgpu::TextureFormat, + // GPU + bind_group_env: Option, + pipeline: Option, + done: bool, +} + +impl PassIblDiffuseEquirect { + pub fn new( + label: &str, + env_texture: RendererTextureHandle, + output_handle: RendererTextureHandle, + output_format: wgpu::TextureFormat, + ) -> Self { + Self { + label: label.to_string(), + env_texture, + output_handle, + output_format, + bind_group_env: None, + pipeline: None, + done: false, + } + } +} + +impl Pass for PassIblDiffuseEquirect { + fn get_label(&self) -> &str { + &self.label + } + + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + _resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { + if self.pipeline.is_some() { + return Ok(()); + } + // BGLs are declared via PipelineV2Desc (no manual BGL creation needed here) + + // Build pipeline (fullscreen triangle; writes to color via standard render pass) + let vs = r#" + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var pos = array, 3>( + vec2(-1.0, -3.0), + vec2( 3.0, 1.0), + vec2(-1.0, 1.0) + ); + var tuv = array, 3>( + vec2(0.0, 2.0), + vec2(2.0, 0.0), + vec2(0.0, 0.0) + ); + var o: VSOut; + o.pos = vec4(pos[vi], 0.0, 1.0); + o.uv = tuv[vi]; + return o; + } + "#; + + let fs = r#" + const PI: f32 = 3.14159265359; + @group(0) @binding(0) var TEnv: texture_2d; + @group(0) @binding(1) var SEnv: sampler; + + fn dir_to_equirect_uv(dir: vec3) -> vec2 { + let d = normalize(dir); + let u = 0.5 + atan2(d.x, -d.z) / (2.0 * PI); + let v = 0.5 - asin(clamp(d.y, -1.0, 1.0)) / PI; + return vec2(fract(u), clamp(v, 0.0, 1.0)); + } + + fn uv_to_dir_equirect(uv: vec2) -> vec3 { + let phi = (uv.x * 2.0 * PI) - PI; + let theta = uv.y * PI; + let x = sin(phi) * sin(theta); + let y = cos(theta); + let z = -cos(phi) * sin(theta); + return normalize(vec3(x, y, z)); + } + + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + // Output direction (normal) from output pixel uv + let N = uv_to_dir_equirect(uv); + // Hemisphere integration around N (discrete) + let sample_delta: f32 = 0.025; + var irradiance = vec3(0.0, 0.0, 0.0); + var weight = 0.0; + var phi: f32 = 0.0; + loop { + if (phi >= 2.0 * PI) { break; } + var theta: f32 = 0.0; + loop { + if (theta >= 0.5 * PI) { break; } + let x = cos(phi) * sin(theta); + let y = sin(theta); + let z = sin(phi) * sin(theta); + // tangent-to-world around N + let up = vec3(0.0, 1.0, 0.0); + let right = normalize(cross(up, N)); + let upn = cross(N, right); + let sample_vec = normalize(right * x + upn * y + N * cos(theta)); + let st = dir_to_equirect_uv(sample_vec); + let radiance = textureSample(TEnv, SEnv, st).rgb; + irradiance = irradiance + radiance * cos(theta) * sin(theta); + weight = weight + cos(theta) * sin(theta); + theta = theta + sample_delta; + } + phi = phi + sample_delta; + } + irradiance = irradiance / max(weight, 1e-4); + return vec4(irradiance, 1.0); + } + "#; + + // Pipeline fragment target format must match the output render target format used in draw(). + // See wgpu ColorTargetState docs: + // https://docs.rs/wgpu/0.20.1/wgpu/struct.ColorTargetState.html + log::info!( + "ibl_diff:init output_format={:?} surface_format={:?}", + self.output_format, + renderer.get_surface_format() + ); + let pipeline = renderer.create_pipeline_v2(PipelineV2Desc { + label: Some("ibl_diffuse_equirect"), + vs: ShaderDesc { + source: vs, + entry_func: "main", + }, + ps: ShaderDesc { + source: fs, + entry_func: "main", + }, + vertex_buffers: &[], // fullscreen triangle via vertex_index; no vertex buffers + bind_groups: vec![ + // env + vec![ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + ], + targets: &[Some(wgpu::ColorTargetState { + format: self.output_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + })?; + + self.pipeline = Some(pipeline); + // Bind groups built lazily in draw once resources are guaranteed ready + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + _view: &wgpu::TextureView, + _world: &WorldQuery, + ) -> Result<()> { + if self.done { + return Ok(()); + } + // Log formats to verify pipeline target equals the render pass attachment format. + let surface_fmt = renderer.get_surface_format(); + log::info!( + "ibl_diff:draw start surface_format={:?} irradiance_target_format={:?}", + surface_fmt, + self.output_format + ); + if surface_fmt == self.output_format { + log::warn!( + "ibl_diff: target format equals surface; ensure this pass renders to irradiance RT, not the swapchain" + ); + } + // Lazy build bind groups + if self.bind_group_env.is_none() { + if let Some(pipeline) = &self.pipeline { + let device = renderer.get_device(); + // env + if let Some(env) = resources.gpu().textures.get(self.env_texture) { + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::Repeat, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + let bg = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("ibl_diff_env_bg"), + layout: &pipeline.bind_group_layouts[0], + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&env.texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + ], + }); + self.bind_group_env = Some(bg); + } + } + } + // If still not ready (e.g., env missing), skip this frame + if self.bind_group_env.is_none() { + return Ok(()); + } + // Render once into output + let out_view = resources + .gpu() + .textures + .get(self.output_handle) + .expect("out") + .texture + .create_view(&wgpu::TextureViewDescriptor { + label: Some("ibl_irradiance_rt_view"), + ..Default::default() + }); + log::info!( + "ibl_diff:begin pass target=irradiance_2d format={:?}", + self.output_format + ); + let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &out_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + rp.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + rp.set_bind_group(0, self.bind_group_env.as_ref().unwrap(), &[]); + rp.draw(0..3, 0..1); + drop(rp); + log::info!("ibl_diff:draw completed"); + self.done = true; + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_ibl_specular_equirect.rs b/engine/pill_engine/src/graphics/pass_ibl_specular_equirect.rs new file mode 100644 index 00000000..26422a44 --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_ibl_specular_equirect.rs @@ -0,0 +1,644 @@ +use crate::graphics::renderer::{Pass, PillRenderer as EnginePillRenderer, WorldQuery}; +use crate::graphics::{PipelineV2, PipelineV2Desc, ShaderDesc}; +use anyhow::Result; +use pill_core::PillSlotMapKey; +use wgpu::CommandEncoder; + +pub struct PassIblSpecularEquirect { + label: String, + env_texture: crate::graphics::RendererTextureHandle, + // Outputs + prefilter_handle: crate::graphics::RendererTextureHandle, + brdf_lut_handle: crate::graphics::RendererTextureHandle, + // GPU + env_bind_group: Option, + params_buffer: Option, + params_bind_group: Option, + prefilter_pipeline: Option, + brdf_pipeline: Option, + // Tracking last-known formats to support adaptive re-init after resize/changes + last_prefilter_format: Option, + last_brdf_format: Option, + // Config + mip_count: u32, + done: bool, +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct PrefilterParams { + roughness: f32, + _pad: [f32; 3], +} + +impl PassIblSpecularEquirect { + pub fn new( + label: &str, + env_texture: crate::graphics::RendererTextureHandle, + prefilter_handle: crate::graphics::RendererTextureHandle, + brdf_lut_handle: crate::graphics::RendererTextureHandle, + mip_count: u32, + ) -> Self { + Self { + label: label.to_string(), + env_texture, + prefilter_handle, + brdf_lut_handle, + env_bind_group: None, + params_buffer: None, + params_bind_group: None, + prefilter_pipeline: None, + brdf_pipeline: None, + last_prefilter_format: None, + last_brdf_format: None, + mip_count, + done: false, + } + } + + pub fn prefilter_texture_handle(&self) -> crate::graphics::RendererTextureHandle { + self.prefilter_handle + } + pub fn brdf_lut_handle(&self) -> crate::graphics::RendererTextureHandle { + self.brdf_lut_handle + } +} + +impl Pass for PassIblSpecularEquirect { + fn get_label(&self) -> &str { + &self.label + } + + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + _resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { + // All outputs provided by caller; build/refresh pipelines based on current target formats. + + // Build env bind group layout via pipeline desc for prefilter (set 0 -> env tex+sampler, set 1 -> params) + let vs = r#" + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var pos = array, 3>( + vec2(-1.0, -3.0), + vec2( 3.0, 1.0), + vec2(-1.0, 1.0) + ); + var tuv = array, 3>( + vec2(0.0, 2.0), + vec2(2.0, 0.0), + vec2(0.0, 0.0) + ); + var o: VSOut; + o.pos = vec4(pos[vi], 0.0, 1.0); + o.uv = tuv[vi]; + return o; + } + "#; + let fs_prefilter = r#" + const PI: f32 = 3.14159265359; + @group(0) @binding(0) var TEnv: texture_2d; + @group(0) @binding(1) var SEnv: sampler; + struct Params { roughness: f32, _pad0: vec3, } + @group(1) @binding(0) var UParams: Params; + + fn dir_to_equirect_uv(dir: vec3) -> vec2 { + let d = normalize(dir); + let u = 0.5 + atan2(d.x, -d.z) / (2.0 * PI); + let v = 0.5 - asin(clamp(d.y, -1.0, 1.0)) / PI; + return vec2(fract(u), clamp(v, 0.0, 1.0)); + } + fn uv_to_dir_equirect(uv: vec2) -> vec3 { + let phi = (uv.x * 2.0 * PI) - PI; + let theta = uv.y * PI; + let x = sin(phi) * sin(theta); + let y = cos(theta); + let z = -cos(phi) * sin(theta); + return normalize(vec3(x, y, z)); + } + + // GGX VNDF importance sampling (approx) + fn hammersley(i: u32, n: u32) -> vec2 { + // radical inverse Van der Corput in base 2 (underscores removed for wider WGSL parser support) + var bits = i; + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + let rdi = f32(bits) * 2.3283064365386963e-10; + return vec2(f32(i)/f32(n), rdi); + } + fn importance_sample_ggx(xi: vec2, roughness: f32, N: vec3) -> vec3 { + let a = roughness * roughness; + let phi = 2.0 * PI * xi.x; + let cos_theta = sqrt((1.0 - xi.y) / (1.0 + (a*a - 1.0) * xi.y)); + let sin_theta = sqrt(1.0 - cos_theta * cos_theta); + // tangent space + let Ht = vec3(cos(phi) * sin_theta, sin(phi) * sin_theta, cos_theta); + // build basis + let up = vec3(0.0, 1.0, 0.0); + let T = normalize(cross(up, N)); + let B = cross(N, T); + // to world + return normalize(T * Ht.x + B * Ht.y + N * Ht.z); + } + + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let N = uv_to_dir_equirect(uv); + let V = N; + let R = N; + let rough = clamp(UParams.roughness, 0.001, 1.0); + var prefiltered = vec3(0.0, 0.0, 0.0); + var total = 0.0; + let SAMPLE_COUNT: u32 = 1024u; + for (var i: u32 = 0u; i < SAMPLE_COUNT; i = i + 1u) { + let xi = hammersley(i, SAMPLE_COUNT); + let H = importance_sample_ggx(xi, rough, N); + let L = normalize(reflect(-V, H)); + let NdotL = max(dot(N, L), 0.0); + if (NdotL > 0.0) { + let st = dir_to_equirect_uv(L); + let c = textureSample(TEnv, SEnv, st).rgb; + prefiltered = prefiltered + c * NdotL; + total = total + NdotL; + } + } + prefiltered = prefiltered / max(total, 1e-4); + return vec4(prefiltered, 1.0); + } + "#; + + // Current target formats (may change after resize or handle reuse). + // IMPORTANT: In wgpu, RenderPipeline.fragment.targets[n].format MUST equal the + // RenderPass color_attachments[n] texture view format or validation will fail at set_pipeline(). + // See ColorTargetState docs: + // https://docs.rs/wgpu/0.20.1/wgpu/struct.ColorTargetState.html + let prefilter_format = _resources + .gpu() + .textures + .get(self.prefilter_handle) + .expect("prefilter texture handle") + .format; + let env_h = self.env_texture; + let pre_h = self.prefilter_handle; + let brdf_h = self.brdf_lut_handle; + log::info!( + "ibl_spec:init env=({},{}) prefilter=({},{}) brdf=({},{})", + env_h.index(), + env_h.generation(), + pre_h.index(), + pre_h.generation(), + brdf_h.index(), + brdf_h.generation() + ); + log::info!( + "ibl_spec:init prefilter_format={:?} surface_format={:?}", + prefilter_format, + renderer.get_surface_format() + ); + // Rebuild prefilter pipeline if first time or target format changed. + if self + .last_prefilter_format + .map(|f| f != prefilter_format) + .unwrap_or(false) + || self.prefilter_pipeline.is_none() + { + let pipeline = renderer.create_pipeline_v2(PipelineV2Desc { + label: Some("ibl_specular_prefilter_equirect"), + vs: ShaderDesc { + source: vs, + entry_func: "main", + }, + ps: ShaderDesc { + source: fs_prefilter, + entry_func: "main", + }, + vertex_buffers: &[], + bind_groups: vec![ + vec![ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + vec![wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + ], + targets: &[Some(wgpu::ColorTargetState { + format: prefilter_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + })?; + self.prefilter_pipeline = Some(pipeline); + self.last_prefilter_format = Some(prefilter_format); + log::info!( + "ibl_spec:init (re)built prefilter pipeline for {:?}", + prefilter_format + ); + } + + // Params buffer/bind group + let params_buf = renderer + .get_device() + .create_buffer(&wgpu::BufferDescriptor { + label: Some("ibl_spec_params"), + // WGSL/std140: struct { f32 + vec3 } occupies 32 bytes (vec3 rounds to 16 and struct size rounds up). + // Allocate 32 bytes to satisfy shader expectations even if our Rust struct packs to 16. + size: 32, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let params_bg = renderer + .get_device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("ibl_spec_params_bg"), + layout: &self.prefilter_pipeline.as_ref().unwrap().bind_group_layouts[1], + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: params_buf.as_entire_binding(), + }], + }); + self.params_buffer = Some(params_buf); + self.params_bind_group = Some(params_bg); + + // Env bind group (lazy, created in draw when env is guaranteed accessible) + self.env_bind_group = None; + + // BRDF LUT pipeline (separate simple pipeline directly with device API) + // Fragment target format must match the BRDF LUT render target texture format. + let pl = renderer + .get_device() + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("ibl_brdf_pl"), + bind_group_layouts: &[], + push_constant_ranges: &[], + }); + let vs_mod = renderer + .get_device() + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("ibl_brdf_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_brdf = r#" + const PI: f32 = 3.14159265359; + fn radical_inverse_vdc(bits_in: u32) -> f32 { + var bits = bits_in; + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + return f32(bits) * 2.3283064365386963e-10; + } + fn hammersley(i: u32, n: u32) -> vec2 { + return vec2(f32(i) / f32(n), radical_inverse_vdc(i)); + } + fn geometry_schlick_ggx(n_dot_v: f32, roughness: f32) -> f32 { + let a = roughness * roughness; + let k = (a * a) / 2.0; + let denom = n_dot_v * (1.0 - k) + k; + return n_dot_v / denom; + } + fn geometry_smith(n: vec3, v: vec3, l: vec3, roughness: f32) -> f32 { + let n_dot_v = max(dot(n, v), 0.0); + let n_dot_l = max(dot(n, l), 0.0); + let ggx2 = geometry_schlick_ggx(n_dot_v, roughness); + let ggx1 = geometry_schlick_ggx(n_dot_l, roughness); + return ggx1 * ggx2; + } + fn importance_sample_ggx(xi: vec2, roughness: f32, n: vec3) -> vec3 { + let a = roughness * roughness; + let phi = 2.0 * PI * xi.x; + let cos_theta = sqrt((1.0 - xi.y) / (1.0 + (a*a - 1.0) * xi.y)); + let sin_theta = sqrt(1.0 - cos_theta*cos_theta); + let h_t = vec3(cos(phi) * sin_theta, sin(phi) * sin_theta, cos_theta); + let up = vec3(0.0, 1.0, 0.0); + let t = normalize(cross(up, n)); + let b = cross(n, t); + return normalize(t*h_t.x + b*h_t.y + n*h_t.z); + } + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let n = vec3(0.0, 0.0, 1.0); + let v = vec3(sqrt(1.0 - uv.x*uv.x), 0.0, uv.x); + var a = 0.0; + var b = 0.0; + let roughness = uv.y; + let samples: u32 = 512u; + for (var i: u32 = 0u; i < samples; i = i + 1u) { + let xi = hammersley(i, samples); + let h = importance_sample_ggx(xi, roughness, n); + let l = normalize(2.0 * dot(v, h) * h - v); + let n_dot_l = max(l.z, 0.0); + let n_dot_h = max(h.z, 0.0); + let v_dot_h = max(dot(v, h), 0.0); + if (n_dot_l > 0.0) { + let g = geometry_smith(n, v, l, roughness); + let g_vis = (g * v_dot_h) / max(n_dot_h * max(n.z, 0.0), 1e-4); + let fc = pow(1.0 - v_dot_h, 5.0); + a = a + (1.0 - fc) * g_vis; + b = b + fc * g_vis; + } + } + a = a / f32(samples); + b = b / f32(samples); + return vec4(a, b, 0.0, 1.0); + } + "#; + let fs_mod = renderer + .get_device() + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("ibl_brdf_fs"), + source: wgpu::ShaderSource::Wgsl(fs_brdf.into()), + }); + // Match BRDF pipeline color target to BRDF LUT texture format + let brdf_format = _resources + .gpu() + .textures + .get(self.brdf_lut_handle) + .map(|t| t.format) + .unwrap_or(wgpu::TextureFormat::Rgba16Float); + log::info!( + "ibl_spec:init brdf_lut_format={:?} surface_format={:?}", + brdf_format, + renderer.get_surface_format() + ); + if self + .last_brdf_format + .map(|f| f != brdf_format) + .unwrap_or(false) + || self.brdf_pipeline.is_none() + { + let rp = + renderer + .get_device() + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("ibl_brdf_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: brdf_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + self.brdf_pipeline = Some(rp); + self.last_brdf_format = Some(brdf_format); + log::info!( + "ibl_spec:init (re)built brdf pipeline for {:?}", + brdf_format + ); + } + + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + _view: &wgpu::TextureView, + _world: &WorldQuery, + ) -> Result<()> { + if self.done { + return Ok(()); + } + // Log formats used by this pass to quickly diagnose target/pass mismatches. + log::info!( + "ibl_spec:draw handles env=({},{}) pre=({},{}) brdf=({},{})", + self.env_texture.index(), + self.env_texture.generation(), + self.prefilter_handle.index(), + self.prefilter_handle.generation(), + self.brdf_lut_handle.index(), + self.brdf_lut_handle.generation() + ); + let surface_fmt = renderer.get_surface_format(); + let prefilter_fmt = resources + .gpu() + .textures + .get(self.prefilter_handle) + .expect("prefilter") + .format; + log::info!( + "ibl_spec:draw start surface_format={:?} prefilter_target_format={:?}", + surface_fmt, + prefilter_fmt + ); + // Optional guardrail: warn if someone accidentally attempts to render IBL into the swapchain. + if surface_fmt == prefilter_fmt { + log::warn!( + "ibl_spec: surface format equals prefilter format; ensure this pass renders to the HDR prefilter target, not the swapchain" + ); + } + // Defensive guard: if pipeline was built for a different format, skip to avoid panic. + if let Some(built_for) = self.last_prefilter_format { + if built_for != prefilter_fmt { + log::error!( + "ibl_spec: prefilter format changed (built_for={:?}, current={:?}); skipping pass to avoid wgpu validation error", + built_for, + prefilter_fmt + ); + return Ok(()); + } + } + // Lazy env bind group + if self.env_bind_group.is_none() { + if let Some(env) = resources.gpu().textures.get(self.env_texture) { + let device = renderer.get_device(); + let bg = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("ibl_spec_env_bg"), + layout: &self.prefilter_pipeline.as_ref().unwrap().bind_group_layouts[0], + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&env.texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&env.sampler), + }, + ], + }); + self.env_bind_group = Some(bg); + } else { + return Ok(()); + } + } + + // Prefilter: render each mip level with roughness + { + let queue = renderer.get_queue(); + let params_buf = self.params_buffer.as_ref().unwrap(); + let prefilter_tex = &resources + .gpu() + .textures + .get(self.prefilter_handle) + .expect("prefilter") + .texture; + // Validate target format again before encoding multiple passes + if let (Some(built_for), Some(current)) = + (self.last_prefilter_format, Some(prefilter_fmt)) + { + if built_for != current { + log::error!( + "ibl_spec:refusing to render prefilter; pipeline format={:?} current target={:?}", + built_for, + current + ); + self.done = true; + return Ok(()); + } + } + // If the current prefilter texture doesn't have a mipchain (common for swapchain-like targets), + // avoid rendering higher mips to prevent TextureView creation errors. + let effective_mip_count = if self.last_prefilter_format + == Some(wgpu::TextureFormat::Rgba16Float) + { + self.mip_count + } else { + log::warn!( + "ibl_spec: prefilter target format {:?} likely has no mipchain; limiting to mip 0", + prefilter_fmt + ); + 1 + }; + for mip in 0..effective_mip_count { + let roughness = (mip as f32) / ((self.mip_count - 1) as f32).max(1.0); + let params = PrefilterParams { + roughness, + _pad: [0.0, 0.0, 0.0], + }; + log::info!( + "ibl_spec:prefilter pass mip={} roughness={:.4} target_format={:?}", + mip, + roughness, + prefilter_fmt + ); + queue.write_buffer(params_buf, 0, bytemuck::bytes_of(¶ms)); + let view = prefilter_tex.create_view(&wgpu::TextureViewDescriptor { + label: Some(&format!("prefilter_mip{}", mip)), + format: None, + dimension: Some(wgpu::TextureViewDimension::D2), + aspect: wgpu::TextureAspect::All, + base_mip_level: mip, + mip_level_count: Some(1), + base_array_layer: 0, + array_layer_count: Some(1), + }); + // Begin a render pass whose color attachment view inherits the HDR texture format. + // This MUST match the pipeline fragment target format; otherwise wgpu validation will error at set_pipeline. + // See RenderPassDescriptor docs: + // https://docs.rs/wgpu/0.20.1/wgpu/struct.RenderPassDescriptor.html + let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&format!("{}_mip{}", self.label, mip)), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + rp.set_pipeline(&self.prefilter_pipeline.as_ref().unwrap().pipeline); + rp.set_bind_group(0, self.env_bind_group.as_ref().unwrap(), &[]); + rp.set_bind_group(1, self.params_bind_group.as_ref().unwrap(), &[]); + rp.draw(0..3, 0..1); + drop(rp); + } + } + + // BRDF LUT + { + let brdf_fmt = resources + .gpu() + .textures + .get(self.brdf_lut_handle) + .expect("brdf") + .format; + log::info!("ibl_spec:brdf_lut pass target_format={:?}", brdf_fmt); + if let Some(built_for) = self.last_brdf_format { + if built_for != brdf_fmt { + log::error!( + "ibl_spec: brdf lut format changed (built_for={:?}, current={:?}); skipping BRDF pass", + built_for, + brdf_fmt + ); + self.done = true; + return Ok(()); + } + } + let brdf_view = resources + .gpu() + .textures + .get(self.brdf_lut_handle) + .expect("brdf") + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("ibl_brdf_lut_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &brdf_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + rp.set_pipeline(self.brdf_pipeline.as_ref().unwrap()); + rp.draw(0..3, 0..1); + drop(rp); + } + + self.done = true; + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_linearize_depth.rs b/engine/pill_engine/src/graphics/pass_linearize_depth.rs new file mode 100644 index 00000000..02c1a14e --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_linearize_depth.rs @@ -0,0 +1,290 @@ +use anyhow::Result; +use wgpu::CommandEncoder; + +use crate::graphics::projection::{depth_unpack_consts_from_proj, perspective_rh_no}; +use crate::graphics::renderer::{ + Pass, PillRenderer as EnginePillRenderer, PipelineV2, RendererTextureHandle, WorldQuery, +}; +use bytemuck::{Pod, Zeroable}; +use pill_core::PillSlotMapKey; +use wgpu::util::DeviceExt; + +pub struct PassLinearizeDepth { + label: String, + pipeline: Option, + bind_group_material: Option, + params_buffer: Option, + target_format: wgpu::TextureFormat, + // Inputs/outputs + depth_texture: RendererTextureHandle, + depth_copy_target: RendererTextureHandle, +} + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct DepthUnpackParams { + depth_mul: f32, + depth_add: f32, + _pad: [f32; 2], +} + +impl PassLinearizeDepth { + pub fn new( + label: &str, + depth_texture: RendererTextureHandle, + depth_copy_target: RendererTextureHandle, + target_format: wgpu::TextureFormat, + ) -> Self { + Self { + label: label.to_string(), + pipeline: None, + bind_group_material: None, + params_buffer: None, + target_format, + depth_texture, + depth_copy_target, + } + } +} + +impl Pass for PassLinearizeDepth { + fn get_label(&self) -> &str { + &self.label + } + + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { + let device = renderer.get_device(); + + // Fullscreen triangle (no vertex buffers) + let vs = r#" + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var pos = array, 3>( + vec2(-1.0, -3.0), + vec2( 3.0, 1.0), + vec2(-1.0, 1.0) + ); + var tuv = array, 3>( + vec2(0.0, 2.0), + vec2(2.0, 0.0), + vec2(0.0, 0.0) + ); + var o: VSOut; + o.pos = vec4(pos[vi], 0.0, 1.0); + o.uv = tuv[vi]; + return o; + } + "#; + + let fs = r#" + @group(0) @binding(0) var texDepth: texture_depth_2d; + @group(0) @binding(1) var u_depthUnpack: vec4; // (mul, add, _, _) + + fn linearize_depth(screenDepth: f32) -> f32 { + // See projection.rs reference comment. + // linear = depthMul / (depthAdd - screenDepth) + let depthMul = u_depthUnpack.x; + let depthAdd = u_depthUnpack.y; + return depthMul / (depthAdd - screenDepth); + } + + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + // IMPORTANT: textureLoad requires in-bounds coords; fullscreen-triangle UVs can go slightly out of range, + // so clamp to [0..dims-1] to avoid undefined results (often "two flat values"). + let dims = textureDimensions(texDepth, 0u); + let w = i32(dims.x); + let h = i32(dims.y); + let px = vec2(f32(dims.x), f32(dims.y)); + let uv_clamped = clamp(uv, vec2(0.0), vec2(1.0) - vec2(1.0) / px); + let coord_f = uv_clamped * px; + let cx = clamp(i32(coord_f.x), 0, w - 1); + let cy = clamp(i32(coord_f.y), 0, h - 1); + let raw = textureLoad(texDepth, vec2(cx, cy), 0); + let lin = linearize_depth(raw); + // Linear view-space depth in meters in R (render target is R16Float) + return vec4(lin, 0.0, 0.0, 1.0); + } + "#; + + // Bind group layout: depth texture + depth unpack params + let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("copy_depth_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Depth, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some( + std::num::NonZeroU64::new( + std::mem::size_of::() as u64 + ) + .unwrap(), + ), + }, + count: None, + }, + ], + }); + let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("linearize_depth_pl"), + bind_group_layouts: &[&bgl], + push_constant_ranges: &[], + }); + + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("linearize_depth_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("linearize_depth_fs"), + source: wgpu::ShaderSource::Wgsl(fs.into()), + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("linearize_depth_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.target_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + // Params buffer (updated per-frame from active camera projection) + let params_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("depth_unpack_params"), + contents: bytemuck::bytes_of(&DepthUnpackParams { + depth_mul: 0.0, + depth_add: 1.0, + _pad: [0.0; 2], + }), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + // Material bind group: depth view + params buffer + let depth_view = resources + .gpu() + .textures + .get(self.depth_texture) + .expect("scene depth") + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let bind_group_material = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("copy_depth_bg"), + layout: &bgl, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&depth_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: params_buf.as_entire_binding(), + }, + ], + }); + + self.pipeline = Some(PipelineV2 { + pipeline, + bind_group_layouts: vec![bgl], + }); + self.bind_group_material = Some(bind_group_material); + self.params_buffer = Some(params_buf); + + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + _view: &wgpu::TextureView, + world: &WorldQuery, + ) -> Result<()> { + // Depth unpack consts derived from a [-1..1] depth projection. + let camera_storage = world + .camera_components + .data + .get(world.active_camera.data().index as usize) + .unwrap(); + let cam = camera_storage.as_ref().unwrap(); + let fov_y = cam.fov.to_radians(); + let aspect = cam.aspect.get_value(); + let z_near = cam.range.start; + let z_far = cam.range.end; + let proj2 = perspective_rh_no(fov_y, aspect, z_near, z_far); + let (depth_mul, depth_add) = depth_unpack_consts_from_proj(proj2); + renderer.get_queue().write_buffer( + self.params_buffer.as_ref().unwrap(), + 0, + bytemuck::bytes_of(&DepthUnpackParams { + depth_mul, + depth_add, + _pad: [0.0; 2], + }), + ); + + // Target view: write to the depth copy render target + let out_view = resources + .gpu() + .textures + .get(self.depth_copy_target) + .expect("depth copy target") + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &out_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + pass.set_bind_group(0, &self.bind_group_material.as_ref().unwrap(), &[]); + pass.draw(0..3, 0..1); + + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_logo.rs b/engine/pill_engine/src/graphics/pass_logo.rs new file mode 100644 index 00000000..cfc46446 --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_logo.rs @@ -0,0 +1,295 @@ +use anyhow::Result; +use wgpu::CommandEncoder; + +use crate::graphics::renderer::{ + Pass, PillRenderer as EnginePillRenderer, PipelineV2, RendererTextureHandle, WorldQuery, +}; + +pub struct PassLogo { + label: String, + tex_logo: RendererTextureHandle, + rect: [f32; 4], + tint: [f32; 4], + target_format: wgpu::TextureFormat, + state: Option, +} + +struct PassLogoState { + buffer: wgpu::Buffer, + pipeline: PipelineV2, + tex_logo_view: wgpu::TextureView, + tex_logo_sampler: wgpu::Sampler, + tint_buffer: wgpu::Buffer, + bind_group_rect: wgpu::BindGroup, + bind_group_material: wgpu::BindGroup, +} + +impl PassLogo { + pub fn new( + label: &str, + rect: [f32; 4], + tint: [f32; 4], + tex_logo: RendererTextureHandle, + target_format: wgpu::TextureFormat, + ) -> Self { + Self { + label: label.to_string(), + tex_logo, + rect, + tint, + target_format, + state: None, + } + } +} + +impl Pass for PassLogo { + fn get_label(&self) -> &str { + &self.label + } + + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { + let device = renderer.get_device(); + // Create mapped buffer for overlay rect UBO + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_logo_ubo"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM, + mapped_at_creation: true, + }); + { + let mut view = buffer.slice(..).get_mapped_range_mut(); + let src = bytemuck::bytes_of(&self.rect); + view[..src.len()].copy_from_slice(src); + } + buffer.unmap(); + + let vs = r#" + @group(1) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] + + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var unit = array, 6>( + vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), + vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) + ); + let p = unit[vi]; // [-1,1] NDC space + let s = p*0.5+0.5; // [0,1] screen space + let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] + let ndc = r*2.0-1.0; // [-1,1] NDC space + var out: VSOut; + out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); + out.uv = vec2(s.x, 1.0 - s.y); + return out; + } + "#; + + let fs = r#" + @group(0) @binding(0) var tex: texture_2d; + @group(0) @binding(1) var smp: sampler; + @group(0) @binding(2) var UTint: vec4; + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let c = textureSample(tex, smp, uv); + return c * UTint; + } + "#; + + // Build bind group layouts + let bgl_material = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_logo_material_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }, + ], + }); + let bgl_rect = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_logo_rect_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + }); + let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_logo_pl"), + bind_group_layouts: &[&bgl_material, &bgl_rect], + push_constant_ranges: &[], + }); + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_logo_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_logo_fs"), + source: wgpu::ShaderSource::Wgsl(fs.into()), + }); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("overlay_logo_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.target_format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + let bind_group_rect = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_rect_bind_group"), + layout: &bgl_rect, // Group 1: Rect UBO + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); + + let tex_logo_view = resources + .gpu() + .textures + .get(self.tex_logo) + .expect("texture") + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let tex_logo_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::Repeat, + address_mode_v: wgpu::AddressMode::Repeat, + address_mode_w: wgpu::AddressMode::Repeat, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + let tint_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_logo_tint"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM, + mapped_at_creation: true, + }); + { + let mut view = tint_buffer.slice(..).get_mapped_range_mut(); + let src = bytemuck::bytes_of(&self.tint); + view[..src.len()].copy_from_slice(src); + } + tint_buffer.unmap(); + + let bind_group_material = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_logo_material_bg"), + layout: &bgl_material, // Group 0: Material bindings + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&tex_logo_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&tex_logo_sampler), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &tint_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }, + ], + }); + + self.state = Some(PassLogoState { + buffer, + pipeline: PipelineV2 { + pipeline, + bind_group_layouts: vec![bgl_material, bgl_rect], + }, + tex_logo_view, + tex_logo_sampler, + tint_buffer, + bind_group_rect, + bind_group_material, + }); + + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + _renderer: &mut dyn EnginePillRenderer, + _resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + view: &wgpu::TextureView, + _world: &WorldQuery, + ) -> Result<()> { + // Load over the existing frame and draw quad + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + pass.set_pipeline(&self.state.as_ref().unwrap().pipeline.pipeline); + let state = self.state.as_ref().expect("PassLogo not initialized"); + pass.set_bind_group(0, &state.bind_group_material, &[]); + pass.set_bind_group(1, &state.bind_group_rect, &[]); + pass.draw(0..6, 0..1); + + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_motion_blur.rs b/engine/pill_engine/src/graphics/pass_motion_blur.rs new file mode 100644 index 00000000..2a7af30d --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_motion_blur.rs @@ -0,0 +1,416 @@ +use crate::graphics::Pass; +use crate::graphics::PillRenderer; +use crate::graphics::RendererTextureHandle; +use crate::resources::ResourceManager; +use anyhow::Result; +use bytemuck::{Pod, Zeroable}; +use wgpu::util::DeviceExt; +use wgpu::TextureFormat; + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct MotionBlurParams { + velocity_scale: f32, + min_speed: f32, + depth_softness: f32, + enabled: u32, + max_samples: u32, + debug_velocity: u32, + _pad: [u32; 2], +} + +pub struct PassMotionBlur { + pub name: String, + pub input_color: RendererTextureHandle, + pub input_velocity: RendererTextureHandle, + pub input_depth: RendererTextureHandle, + pub output: RendererTextureHandle, + pub format: TextureFormat, + pipeline: Option, + bind_group: Option, + params_buffer: Option, +} + +impl PassMotionBlur { + pub fn new( + name: &str, + input_color: RendererTextureHandle, + input_velocity: RendererTextureHandle, + input_depth: RendererTextureHandle, + output: RendererTextureHandle, + format: TextureFormat, + ) -> Self { + Self { + name: name.to_string(), + input_color, + input_velocity, + input_depth, + output, + format, + pipeline: None, + bind_group: None, + params_buffer: None, + } + } +} + +impl Pass for PassMotionBlur { + fn get_label(&self) -> &str { + &self.name + } + + fn init( + &mut self, + renderer: &mut dyn PillRenderer, + resources: &mut ResourceManager, + ) -> Result<()> { + let device = renderer.get_device(); + let vs = r#" + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var pos = array, 3>( + vec2(-1.0, -3.0), + vec2( 3.0, 1.0), + vec2(-1.0, 1.0) + ); + var tuv = array, 3>( + vec2(0.0, 2.0), + vec2(2.0, 0.0), + vec2(0.0, 0.0) + ); + var o: VSOut; + o.pos = vec4(pos[vi], 0.0, 1.0); + o.uv = tuv[vi]; + return o; + } + "#; + + let fs = r#" + struct Params { + velocity_scale: f32, + min_speed: f32, + depth_softness: f32, + enabled: u32, + max_samples: u32, + debug_velocity: u32, + _pad: vec2, + }; + + @group(0) @binding(0) var t_color: texture_2d; + @group(0) @binding(1) var t_velocity: texture_2d; + @group(0) @binding(2) var t_depth: texture_2d; // linear depth + @group(0) @binding(3) var params: Params; + @group(0) @binding(4) var s_linear: sampler; + + const MAX_SAMPLES: i32 = 64; + + fn sample_depth(uv: vec2) -> f32 { + let dims = textureDimensions(t_depth, 0u); + let coord = clamp(vec2(uv * vec2(dims)), vec2(0,0), vec2(i32(dims.x)-1, i32(dims.y)-1)); + return textureLoad(t_depth, coord, 0).r; + } + + const ARROW_COLOR: vec3 = vec3(1.0, 1.0, 0.2); + + // xyz = arrow color, w = mask (coverage) + fn compute_arrow_overlay( + uv: vec2, + spacing_px: f32, + thickness_px: f32, + scale: f32, + dims: vec2 + ) -> vec4 { + let spacing = spacing_px / vec2(f32(dims.x), f32(dims.y)); + let cell = floor(uv / spacing); + let cell_center = (cell + vec2(0.5, 0.5)) * spacing; + let v_sample = textureSample(t_velocity, s_linear, cell_center).xy * scale; + let v_len = length(v_sample); + if (v_len <= 1e-4) { + return vec4(ARROW_COLOR, 0.0); + } + + let dir = v_sample / v_len; + let tangent = vec2(-dir.y, dir.x); + let rel = uv - cell_center; + let proj = dot(rel, dir); + let ortho = dot(rel, tangent); + let arrow_len = clamp(v_len * 0.5, spacing.x * 0.25, spacing.x * 1.5); + let half_thick = (thickness_px / vec2(f32(dims.x), f32(dims.y))).x; + let line_mask = smoothstep(half_thick, 0.0, abs(ortho)) * step(-spacing.x, proj) * step(proj, arrow_len); + let head_size = arrow_len * 0.2; + let head_mask = step(arrow_len - head_size, proj) * smoothstep(half_thick * 2.0, 0.0, abs(ortho + (proj - arrow_len) * 0.5)); + let mask = clamp(max(line_mask, head_mask), 0.0, 1.0); + return vec4(ARROW_COLOR, mask); + } + + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let dims = textureDimensions(t_color, 0u); + let texel = 1.0 / vec2(f32(dims.x), f32(dims.y)); + let spacing_px = 24.0; + let thickness_px = 1.25; + + var vel = textureSample(t_velocity, s_linear, uv).xy * params.velocity_scale; + let center_speed = length(vel / texel); + let enabled = params.enabled != 0u; + if (!enabled || center_speed < params.min_speed) { + let base = textureSample(t_color, s_linear, uv).rgb; + if (params.debug_velocity != 0u) { + let dbg = vec3(0.5, 0.5, 0.5) + vec3(vel, 0.0) * 0.5; + let overlay = compute_arrow_overlay(uv, spacing_px, thickness_px, params.velocity_scale, dims); + let out_rgb = mix(dbg, overlay.xyz, overlay.w); + return vec4(out_rgb, 1.0); + } + return vec4(base, 1.0); + } + + let center_depth = sample_depth(uv); + let maxSamples = min(i32(params.max_samples), MAX_SAMPLES); + let mut_samples = max(1, maxSamples); + let nSamples = clamp(i32(center_speed), 1, mut_samples); + + var acc = textureSample(t_color, s_linear, uv).rgb; + var w_acc = 1.0; + // guard for single sample + if (nSamples > 1) { + for (var i: i32 = 1; i < nSamples; i = i + 1) { + let t = (f32(i) / f32(nSamples - 1)) - 0.5; + let offset = vel * t; + let sample_uv = uv + offset; + let sample_color = textureSample(t_color, s_linear, sample_uv).rgb; + let sample_depth = sample_depth(sample_uv); + let dz = abs(sample_depth - center_depth); + let depth_w = 1.0 / (1.0 + dz * params.depth_softness); + acc = acc + sample_color * depth_w; + w_acc = w_acc + depth_w; + } + } + var color_out = acc / w_acc; + if (params.debug_velocity != 0u) { + let overlay = compute_arrow_overlay(uv, spacing_px, thickness_px, params.velocity_scale, dims); + let out_rgb = mix(color_out, overlay.xyz, overlay.w); + return vec4(out_rgb, 1.0); + } + return vec4(color_out, 1.0); + } + "#; + + let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("motion_blur_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: false }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 4, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + + let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("motion_blur_pl"), + bind_group_layouts: &[&bgl], + push_constant_ranges: &[], + }); + + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("motion_blur_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("motion_blur_fs"), + source: wgpu::ShaderSource::Wgsl(fs.into()), + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("motion_blur_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + let params = MotionBlurParams { + velocity_scale: 1.0, + min_speed: 0.001, + depth_softness: 50.0, + enabled: 1, + max_samples: 16, + debug_velocity: 0, + _pad: [0, 0], + }; + let params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("motion_blur_params"), + contents: bytemuck::bytes_of(¶ms), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + let color_view = resources + .gpu() + .textures + .get(self.input_color) + .expect("motion blur input color") + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let velocity_view = resources + .gpu() + .textures + .get(self.input_velocity) + .expect("motion blur velocity") + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let depth_view = resources + .gpu() + .textures + .get(self.input_depth) + .expect("motion blur depth") + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("motion_blur_bg"), + layout: &bgl, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&color_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&velocity_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::TextureView(&depth_view), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: params_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 4, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + ], + }); + + self.pipeline = Some(crate::graphics::PipelineV2 { + pipeline, + bind_group_layouts: vec![bgl], + }); + self.bind_group = Some(bind_group); + self.params_buffer = Some(params_buffer); + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut wgpu::CommandEncoder, + renderer: &mut dyn PillRenderer, + resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + _view: &wgpu::TextureView, + _world: &crate::graphics::WorldQuery, + ) -> Result<()> { + if let Ok(pp) = resources.post_process.lock() { + let params = MotionBlurParams { + velocity_scale: pp.mb_strength, + min_speed: pp.mb_min_speed, + depth_softness: pp.mb_depth_softness, + enabled: if pp.mb_enabled { 1 } else { 0 }, + max_samples: pp.mb_max_samples, + debug_velocity: if pp.mb_debug_velocity { 1 } else { 0 }, + _pad: [0, 0], + }; + renderer.get_queue().write_buffer( + self.params_buffer.as_ref().unwrap(), + 0, + bytemuck::bytes_of(¶ms), + ); + } + + let out_view = resources + .gpu() + .textures + .get(self.output) + .expect("motion blur output") + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.name), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &out_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + pass.set_bind_group(0, self.bind_group.as_ref().unwrap(), &[]); + pass.draw(0..3, 0..1); + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_overlay_depth.rs b/engine/pill_engine/src/graphics/pass_overlay_depth.rs new file mode 100644 index 00000000..1b32ea07 --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_overlay_depth.rs @@ -0,0 +1,286 @@ +use anyhow::Result; +use wgpu::CommandEncoder; + +use crate::graphics::renderer::{ + Pass, PillRenderer as EnginePillRenderer, PipelineV2, RendererTextureHandle, WorldQuery, +}; + +pub struct PassOverlayDepth { + label: String, + pipeline: Option, + bind_group_rect: Option, + bind_group_material: Option, + tint_buffer: Option, + rect: [f32; 4], + tint: [f32; 4], + target_format: wgpu::TextureFormat, + depth_texture: RendererTextureHandle, +} + +impl PassOverlayDepth { + pub fn new( + label: &str, + rect: [f32; 4], + tint: [f32; 4], + target_format: wgpu::TextureFormat, + depth_texture: RendererTextureHandle, + ) -> Self { + Self { + label: label.to_string(), + pipeline: None, + bind_group_rect: None, + bind_group_material: None, + tint_buffer: None, + rect, + tint, + target_format, + depth_texture, + } + } +} + +impl Pass for PassOverlayDepth { + fn get_label(&self) -> &str { + &self.label + } + + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { + let device = renderer.get_device(); + // Create buffer for overlay rect UBO + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_depth_rect_ubo"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM, + mapped_at_creation: true, + }); + { + let mut view = buffer.slice(..).get_mapped_range_mut(); + let src = bytemuck::bytes_of(&self.rect); + view[..src.len()].copy_from_slice(src); + } + buffer.unmap(); + + let vs = r#" + @group(1) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] + + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var unit = array, 6>( + vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), + vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) + ); + let p = unit[vi]; // [-1,1] NDC space + let s = p*0.5+0.5; // [0,1] screen space + let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] + let ndc = r*2.0-1.0; // [-1,1] NDC space + var out: VSOut; + out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); + out.uv = vec2(s.x, 1.0 - s.y); + return out; + } + "#; + + let fs = r#" + @group(0) @binding(0) var tex: texture_depth_2d; + @group(0) @binding(1) var UTint: vec4; + + // Hardcoded toggle (compile-time): false=raw depth (0..1), true=linear view-space depth normalized by FAR. + const SHOW_VIEWSPACE_DEPTH: bool = true; + const NEAR: f32 = 0.1; + const FAR: f32 = 100.0; + + fn linearize_depth(rawDepth: f32) -> f32 { + // See projection.rs reference comment. + let depthMul = (FAR * NEAR) / (FAR - NEAR); + let depthAdd = FAR / (FAR - NEAR); + return depthMul / (depthAdd - rawDepth); + } + + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let dims = textureDimensions(tex, 0u); + let coord = vec2(uv * vec2(dims)); + let d : f32 = textureLoad(tex, coord, 0); + + let linear_depth = linearize_depth(d) / FAR; + let vis = select(d, linear_depth, SHOW_VIEWSPACE_DEPTH); + return vec4(vis, vis, vis, 1.0) * UTint; + } + "#; + + // Build bind group layouts + let bgl_material = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_depth_material_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Depth, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }, + ], + }); + let bgl_rect = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_depth_rect_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + }); + let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_depth_pl"), + bind_group_layouts: &[&bgl_material, &bgl_rect], + push_constant_ranges: &[], + }); + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_depth_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_depth_fs"), + source: wgpu::ShaderSource::Wgsl(fs.into()), + }); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("overlay_depth_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.target_format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + let bind_group_rect = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_depth_rect_bind_group"), + layout: &bgl_rect, // Group 1: Rect UBO + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); + + // Create tint buffer + let tint_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_depth_tint"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM, + mapped_at_creation: true, + }); + { + let mut view = tint_buffer.slice(..).get_mapped_range_mut(); + let src = bytemuck::bytes_of(&self.tint); + view[..src.len()].copy_from_slice(src); + } + tint_buffer.unmap(); + + // Create material bind group using ResourceManager-provided depth texture view + let depth_view = resources + .gpu() + .textures + .get(self.depth_texture) + .expect("depth texture") + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let bind_group_material = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_depth_material_bg"), + layout: &bgl_material, // Group 0: Material bindings + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&depth_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &tint_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }, + ], + }); + + // Store pipeline handle + self.pipeline = Some(PipelineV2 { + pipeline, + bind_group_layouts: vec![bgl_material, bgl_rect], + }); + self.bind_group_rect = Some(bind_group_rect); + self.bind_group_material = Some(bind_group_material); + self.tint_buffer = Some(tint_buffer); + + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + _renderer: &mut dyn EnginePillRenderer, + _resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + view: &wgpu::TextureView, + _world: &WorldQuery, + ) -> Result<()> { + // Create render pass for this pass using the provided frame + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + // Draw depth overlay (bind material at group 0 and rect at group 1) + pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + pass.set_bind_group(0, &self.bind_group_material.as_ref().unwrap(), &[]); + pass.set_bind_group(1, &self.bind_group_rect.as_ref().unwrap(), &[]); + pass.draw(0..6, 0..1); + + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_overlay_uv.rs b/engine/pill_engine/src/graphics/pass_overlay_uv.rs new file mode 100644 index 00000000..aa27faa8 --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_overlay_uv.rs @@ -0,0 +1,185 @@ +use anyhow::Result; +use wgpu::CommandEncoder; + +use crate::graphics::renderer::{ + Pass, PillRenderer as EnginePillRenderer, PipelineV2, RendererTextureHandle, WorldQuery, +}; + +pub struct PassOverlayUV { + label: String, + buffer: Option, + pipeline: Option, + bind_group: Option, + rect: [f32; 4], + target_format: wgpu::TextureFormat, +} + +impl PassOverlayUV { + pub fn new(label: &str, rect: [f32; 4], target_format: wgpu::TextureFormat) -> Self { + Self { + label: label.to_string(), + buffer: None, + pipeline: None, + bind_group: None, + rect, + target_format, + } + } +} + +impl Pass for PassOverlayUV { + fn get_label(&self) -> &str { + &self.label + } + + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + _resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { + let device = renderer.get_device(); + // Create buffer for overlay rect UBO + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_uv_rect_ubo"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM, + mapped_at_creation: true, + }); + { + let mut view = buffer.slice(..).get_mapped_range_mut(); + let src = bytemuck::bytes_of(&self.rect); + view[..src.len()].copy_from_slice(src); + } + buffer.unmap(); + + let vs = r#" + @group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] + + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var unit = array, 6>( + vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), + vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) + ); + let p = unit[vi]; // [-1,1] NDC space + let s = p*0.5+0.5; // [0,1] screen space + let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] + let ndc = r*2.0-1.0; // [-1,1] NDC space + var out: VSOut; + out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); + out.uv = s; + return out; + } + "#; + + let fs = r#" + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + return vec4(uv, 0.0, 0.5); + } + "#; + + // Build bind group layout and pipeline + let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_uv_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + }); + let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_uv_pl"), + bind_group_layouts: &[&bgl], + push_constant_ranges: &[], + }); + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_uv_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_uv_fs"), + source: wgpu::ShaderSource::Wgsl(fs.into()), + }); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("overlay_uv_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.target_format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_uv_rect_bind_group"), + layout: &bgl, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); + + // Store pipeline handle + self.pipeline = Some(PipelineV2 { + pipeline, + bind_group_layouts: vec![bgl], + }); + self.bind_group = Some(bind_group); + self.buffer = Some(buffer); + + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + _renderer: &mut dyn EnginePillRenderer, + _resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + view: &wgpu::TextureView, + _world: &WorldQuery, + ) -> Result<()> { + // Create render pass for this pass using the provided frame + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + // Draw small overlay UV quad in normalized rect via UBO + pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + pass.set_bind_group(0, self.bind_group.as_ref().unwrap(), &[]); + pass.draw(0..6, 0..1); + + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_overlay_velocity.rs b/engine/pill_engine/src/graphics/pass_overlay_velocity.rs new file mode 100644 index 00000000..4ff12bf4 --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_overlay_velocity.rs @@ -0,0 +1,334 @@ +use anyhow::Result; +use wgpu::CommandEncoder; + +use crate::graphics::renderer::{ + Pass, PillRenderer as EnginePillRenderer, PipelineV2, RendererTextureHandle, WorldQuery, +}; + +pub struct PassOverlayVelocity { + label: String, + pipeline: Option, + bind_group_rect: Option, + bind_group_material: Option, + params_buffer: Option, + rect: [f32; 4], + params: [f32; 4], // x: scale, y: mode (0=signed rgb, 1=magnitude), z: spacing_px, w: thickness_px + target_format: wgpu::TextureFormat, + velocity_texture: RendererTextureHandle, +} + +impl PassOverlayVelocity { + pub fn new( + label: &str, + rect: [f32; 4], + params: [f32; 4], + target_format: wgpu::TextureFormat, + velocity_texture: RendererTextureHandle, + ) -> Self { + Self { + label: label.to_string(), + pipeline: None, + bind_group_rect: None, + bind_group_material: None, + params_buffer: None, + rect, + params, + target_format, + velocity_texture, + } + } +} + +impl Pass for PassOverlayVelocity { + fn get_label(&self) -> &str { + &self.label + } + + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { + let device = renderer.get_device(); + // Rect UBO + let rect_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_velocity_rect_ubo"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM, + mapped_at_creation: true, + }); + { + let mut view = rect_buffer.slice(..).get_mapped_range_mut(); + let src = bytemuck::bytes_of(&self.rect); + view[..src.len()].copy_from_slice(src); + } + rect_buffer.unmap(); + + // Params UBO + let params_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_velocity_params"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: true, + }); + { + let mut view = params_buffer.slice(..).get_mapped_range_mut(); + let src = bytemuck::bytes_of(&self.params); + view[..src.len()].copy_from_slice(src); + } + params_buffer.unmap(); + + let vs = r#" + @group(1) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] + + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var unit = array, 6>( + vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), + vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) + ); + let p = unit[vi]; // [-1,1] NDC space + let s = p*0.5+0.5; // [0,1] screen space + let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] + let ndc = r*2.0-1.0; // [-1,1] NDC space + var out: VSOut; + out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); + out.uv = vec2(s.x, 1.0 - s.y); + return out; + } + "#; + + let fs = r#" + @group(0) @binding(0) var tex: texture_2d; + @group(0) @binding(1) var smp: sampler; + @group(0) @binding(2) var UParams: vec4; // x: scale, y: mode, z: spacing_px, w: thickness_px + + const MODE_SIGNED: f32 = 0.0; + const ARROW_COLOR: vec3 = vec3(1.0, 1.0, 0.2); + + // xyz = arrow color, w = mask (coverage) + fn compute_arrow_overlay( + uv: vec2, + spacing_px: f32, + thickness_px: f32, + scale: f32, + dims: vec2 + ) -> vec4 { + let spacing = spacing_px / vec2(f32(dims.x), f32(dims.y)); + let cell = floor(uv / spacing); + let cell_center = (cell + vec2(0.5, 0.5)) * spacing; + let v_sample = textureSample(tex, smp, cell_center).xy * scale; + let v_len = length(v_sample); + if (v_len <= 1e-4) { + return vec4(ARROW_COLOR, 0.0); + } + + let dir = v_sample / v_len; + let tangent = vec2(-dir.y, dir.x); + let rel = uv - cell_center; + let proj = dot(rel, dir); + let ortho = dot(rel, tangent); + let arrow_len = clamp(v_len * 0.5, spacing.x * 0.25, spacing.x * 1.5); + let half_thick = (thickness_px / vec2(f32(dims.x), f32(dims.y))).x; + let line_mask = smoothstep(half_thick, 0.0, abs(ortho)) * step(-spacing.x, proj) * step(proj, arrow_len); + let head_size = arrow_len * 0.2; + let head_mask = step(arrow_len - head_size, proj) * smoothstep(half_thick * 2.0, 0.0, abs(ortho + (proj - arrow_len) * 0.5)); + let mask = clamp(max(line_mask, head_mask), 0.0, 1.0); + return vec4(ARROW_COLOR, mask); + } + + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let dims = textureDimensions(tex, 0u); + let scale = UParams.x; + let mode = UParams.y; + let spacing_px = max(UParams.z, 4.0); + let thickness_px = max(UParams.w, 1.0); + + // Sample velocity at the current pixel (for background visualization). + let coord = vec2(uv * vec2(dims)); + let vel_px = textureLoad(tex, coord, 0).xy; + + // Base visualization (signed or magnitude). + var base_rgb: vec3; + if (mode <= MODE_SIGNED + 0.5) { + base_rgb = vec3(0.5, 0.5, 0.5) + vec3(vel_px, 0.0) * scale; + } else { + let m = length(vel_px) * scale; + base_rgb = vec3(m, m, m); + } + + let overlay = compute_arrow_overlay(uv, spacing_px, thickness_px, scale, dims); + let arrow_rgb = mix(base_rgb, overlay.xyz, overlay.w); + + return vec4(arrow_rgb, 1.0); + } + "#; + + // Bind group layouts + let bgl_material = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_velocity_material_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }, + ], + }); + let bgl_rect = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_velocity_rect_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + }); + + let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_velocity_pl"), + bind_group_layouts: &[&bgl_material, &bgl_rect], + push_constant_ranges: &[], + }); + + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_velocity_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_velocity_fs"), + source: wgpu::ShaderSource::Wgsl(fs.into()), + }); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("overlay_velocity_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.target_format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + let bind_group_rect = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_velocity_rect_bg"), + layout: &bgl_rect, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: rect_buffer.as_entire_binding(), + }], + }); + + let velocity_view = resources + .gpu() + .textures + .get(self.velocity_texture) + .expect("velocity texture") + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); + let bind_group_material = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_velocity_material_bg"), + layout: &bgl_material, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&velocity_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: params_buffer.as_entire_binding(), + }, + ], + }); + + self.pipeline = Some(PipelineV2 { + pipeline, + bind_group_layouts: vec![bgl_material, bgl_rect], + }); + self.bind_group_rect = Some(bind_group_rect); + self.bind_group_material = Some(bind_group_material); + self.params_buffer = Some(params_buffer); + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + _renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + view: &wgpu::TextureView, + _world: &WorldQuery, + ) -> Result<()> { + if let Ok(pp) = resources.post_process.lock() { + if !pp.mb_debug_velocity { + return Ok(()); + } + } + + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + pass.set_bind_group(0, &self.bind_group_material.as_ref().unwrap(), &[]); + pass.set_bind_group(1, &self.bind_group_rect.as_ref().unwrap(), &[]); + pass.draw(0..6, 0..1); + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_scene.rs b/engine/pill_engine/src/graphics/pass_scene.rs new file mode 100644 index 00000000..049a4bd7 --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_scene.rs @@ -0,0 +1,1090 @@ +use crate::ecs::{get_model_matrix, get_prev_model_matrix}; +use crate::graphics::decompose_render_queue_key; +use crate::graphics::projection::perspective_rh_zo; +use crate::graphics::renderer::{ + Pass, PillRenderer as EnginePillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc, WorldQuery, +}; +use crate::graphics::{ + BufferDesc, RendererMaterialHandle, RendererMeshHandle, RendererTextureHandle, +}; +use anyhow::Result; +use glam::{EulerRot, Mat4, Quat, Vec3, Vec4}; +use pill_core::PillSlotMapKey; +use wgpu::CommandEncoder; + +// Minimal camera uniform used by this pass (matches WGSL layout: position + viewProjection) +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct CameraUniform { + position: [f32; 4], + view_projection_matrix: [[f32; 4]; 4], +} + +impl CameraUniform { + fn new() -> Self { + Self { + position: [0.0; 4], + view_projection_matrix: Mat4::IDENTITY.to_cols_array_2d(), + } + } + + fn update_data( + &mut self, + camera_component: &crate::ecs::CameraComponent, + transform_component: &crate::ecs::TransformComponent, + ) { + // Position + self.position = [ + transform_component.position.x, + transform_component.position.y, + transform_component.position.z, + 0.0, + ]; + + // View matrix (Yaw-Pitch-Roll: Y then X then Z; forward = +Z) + let yaw = transform_component.rotation.y.to_radians(); + let pitch = transform_component.rotation.x.to_radians(); + let roll = transform_component.rotation.z.to_radians(); + let q = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll); + let eye = Vec3::new( + transform_component.position.x, + transform_component.position.y, + transform_component.position.z, + ); + let dir = q * Vec3::Z; + let view = Mat4::look_to_rh(eye, dir, Vec3::Y); + + // Projection matrix (see projection.rs reference comment) + let fov_y = camera_component.fov.to_radians(); + let aspect = camera_component.aspect.get_value(); + let z_near = camera_component.range.start; + let z_far = camera_component.range.end; + let proj = perspective_rh_zo(fov_y, aspect, z_near, z_far); + + self.view_projection_matrix = (proj * view).to_cols_array_2d(); + } +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct PerDrawStd140 { + mvp: [[f32; 4]; 4], + prev_mvp: [[f32; 4]; 4], + model: [[f32; 4]; 4], +} +unsafe impl bytemuck::Zeroable for PerDrawStd140 {} +unsafe impl bytemuck::Pod for PerDrawStd140 {} + +#[repr(C)] +#[derive(Copy, Clone)] +struct MaterialParamsStd140 { + base_color_factor: [f32; 3], + _pad0: f32, + metallic: f32, + roughness: f32, + _pad1: [f32; 2], + emissive_factor: [f32; 3], + _pad2: f32, +} +unsafe impl bytemuck::Zeroable for MaterialParamsStd140 {} +unsafe impl bytemuck::Pod for MaterialParamsStd140 {} + +// Static configuration for per-draw buffering (derive capacities, avoid magic numbers) +const MAX_EXPECTED_PER_DRAW_INSTANCES: usize = 100_000; +const UNIFORM_OFFSET_ALIGNMENT: usize = 256; +const PER_DRAW_STRIDE_BYTES: usize = ((std::mem::size_of::() + + (UNIFORM_OFFSET_ALIGNMENT - 1)) + / UNIFORM_OFFSET_ALIGNMENT) + * UNIFORM_OFFSET_ALIGNMENT; + +pub const MATERIAL_BIND_GROUP_GLOBALS: usize = 0; +pub const MATERIAL_BIND_GROUP_TEXTURES: usize = 1; +pub const MATERIAL_BIND_GROUP_PARAMS: usize = 2; +pub const MATERIAL_BIND_GROUP_PERDRAW: usize = 3; + +// Preallocated buffer structs for hot path optimization +pub(crate) struct VisiblePreDraw { + pub(crate) material_handle: RendererMaterialHandle, + pub(crate) mesh_handle: RendererMeshHandle, + pub(crate) entity_index: u32, + pub(crate) mvp: [[f32; 4]; 4], + pub(crate) prev_mvp: [[f32; 4]; 4], + pub(crate) model: [[f32; 4]; 4], +} + +// M3: Hello Mesh + per-draw MVP (dynamic offsets) +pub(crate) struct MeshBatch { + pub(crate) mesh_handle: RendererMeshHandle, + pub(crate) instances: Vec, + pub(crate) base_offset_u32: u32, // offset into per-draw ring for first instance +} + +pub(crate) struct GroupCmd { + pub(crate) pipeline: *const wgpu::RenderPipeline, + pub(crate) material_handle: RendererMaterialHandle, + pub(crate) batches: Vec, +} + +pub struct PassScene { + label: String, + offscreen_color_texture: RendererTextureHandle, + velocity_texture: RendererTextureHandle, + depth_texture: RendererTextureHandle, + color_format: wgpu::TextureFormat, + velocity_format: wgpu::TextureFormat, + load_existing_color: bool, + ibl_irradiance_tex: Option, + ibl_prefilter_tex: Option, + ibl_brdf_lut_tex: Option, + // Per-frame dynamic UBO ring (Milestone 5) + per_draw_stride: u64, + per_draw_capacity: u64, + // Working buffers + visible_pre_draw_buffer: Vec, + groups_buffer: Vec, + staging_buffer: Vec, + // Pass-local state initialized in init(), read every draw + state: Option, +} + +struct PassSceneState { + camera_uniform: CameraUniform, + camera_buffer: wgpu::Buffer, + prev_view_projection: Mat4, + globals_bind_group: wgpu::BindGroup, + pipeline: PipelineV2, + ibl_sampler: wgpu::Sampler, + // Per-draw resources + per_draw_buffer: wgpu::Buffer, + per_draw_bind_group: wgpu::BindGroup, +} + +impl PassScene { + pub fn new( + label: &str, + offscreen_color_texture: RendererTextureHandle, + velocity_texture: RendererTextureHandle, + depth_texture: RendererTextureHandle, + color_format: wgpu::TextureFormat, + velocity_format: wgpu::TextureFormat, + load_existing_color: bool, + ibl_irradiance_tex: Option, + ibl_prefilter_tex: Option, + ibl_brdf_lut_tex: Option, + ) -> Self { + Self { + label: label.to_string(), + offscreen_color_texture, + velocity_texture, + depth_texture, + color_format, + velocity_format, + load_existing_color, + ibl_irradiance_tex, + ibl_prefilter_tex, + ibl_brdf_lut_tex, + per_draw_stride: PER_DRAW_STRIDE_BYTES as u64, + per_draw_capacity: 0, + visible_pre_draw_buffer: Vec::with_capacity(MAX_EXPECTED_PER_DRAW_INSTANCES), + groups_buffer: Vec::with_capacity(2000), + staging_buffer: Vec::with_capacity( + MAX_EXPECTED_PER_DRAW_INSTANCES * PER_DRAW_STRIDE_BYTES, + ), + state: None, + } + } +} + +fn get_state(pass: &mut PassScene) -> &mut PassSceneState { + debug_assert!(pass.state.is_some()); + // SAFETY: initialized once in init(), read every draw + unsafe { pass.state.as_mut().unwrap_unchecked() } +} + +impl Pass for PassScene { + fn get_label(&self) -> &str { + &self.label + } + + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { + // [SIMILAR] Prebuilt PSO used; avoid hot-path pipeline creation per TALK + // Shaders: must match bind group layout indices: 0(camera),1(material textures),2(material params),3(per-draw) + let vertex_wgsl = r#" + struct Camera { + position: vec4, + viewProjection: mat4x4, + }; + @group(0) @binding(0) var UCamera: Camera; + // IBL irradiance moved into globals (set 0) + @group(0) @binding(1) var texIrradiance: texture_2d; + @group(0) @binding(2) var smpIrradiance: sampler; + struct PerDraw { mvp: mat4x4, prev_mvp: mat4x4, model: mat4x4, }; + @group(3) @binding(0) var UPerDraw: PerDraw; + struct VSIn { + @location(0) pos: vec3, + @location(1) uv: vec2, + @location(2) normal: vec3, + }; + struct VSOut { + @builtin(position) pos: vec4, + @location(0) uv: vec2, + @location(1) worldPos: vec3, + @location(2) worldNormal: vec3, + @location(3) clipCurr: vec4, + @location(4) clipPrev: vec4, + }; + @vertex fn main(input: VSIn) -> VSOut { + var out: VSOut; + let worldPos4 = UPerDraw.model * vec4(input.pos, 1.0); + // TODO: Use a proper normal matrix (inverse-transpose of model) if non-uniform scaling is used. + let n = normalize((UPerDraw.model * vec4(input.normal, 0.0)).xyz); + let clip_curr = UPerDraw.mvp * vec4(input.pos, 1.0); + let clip_prev = UPerDraw.prev_mvp * vec4(input.pos, 1.0); + out.pos = clip_curr; + out.clipCurr = clip_curr; + out.clipPrev = clip_prev; + out.uv = input.uv; + out.worldPos = worldPos4.xyz; + out.worldNormal = n; + return out; + } + "#; + let fragment_wgsl = r#" + // PBR material textures (set 1) + @group(1) @binding(0) var texBaseColor: texture_2d; + @group(1) @binding(1) var smpBaseColor: sampler; + @group(1) @binding(2) var texNormal: texture_2d; + @group(1) @binding(3) var smpNormal: sampler; + @group(1) @binding(4) var texMetallicRoughness: texture_2d; + @group(1) @binding(5) var smpMetallicRoughness: sampler; + @group(1) @binding(6) var texEmissive: texture_2d; + @group(1) @binding(7) var smpEmissive: sampler; + // PBR params UBO (set 2) + struct MaterialParams { + baseColorFactor: vec3, _pad0: f32, + metallicFactor: f32, roughnessFactor: f32, _pad1: vec2, + emissiveFactor: vec3, _pad2: f32, + } + @group(2) @binding(0) var UMaterial: MaterialParams; + // Per-draw + struct PerDraw { mvp: mat4x4, prev_mvp: mat4x4, model: mat4x4, }; + @group(3) @binding(0) var UPerDraw: PerDraw; + struct Camera { + position: vec4, + viewProjection: mat4x4, + }; + @group(0) @binding(0) var UCamera: Camera; + // IBL resources in globals (set 0) + @group(0) @binding(1) var texIrradiance: texture_2d; + @group(0) @binding(2) var smpIrradiance: sampler; + @group(0) @binding(3) var texPrefilter: texture_2d; + @group(0) @binding(4) var smpPrefilter: sampler; + @group(0) @binding(5) var texBrdfLut: texture_2d; + @group(0) @binding(6) var smpBrdfLut: sampler; + + const PI: f32 = 3.14159265359; + + // Directional lights: vec4(direction.xyz, intensity) + // Direction points toward the surface (light-to-surface). + const LIGHT_DIR0: vec3 = vec3(-0.5, -1.0, -0.2); + const LIGHT_DIR1: vec3 = vec3( 0.5, -0.5, 0.2); + const LIGHT_DIR2: vec3 = vec3( 0.0, 1.0, 0.0); + const LIGHT_COL0: vec4 = vec4( 1.0, 0.5, 0.5, 10.0); // Key + const LIGHT_COL1: vec4 = vec4( 0.5, 0.5, 1.0, 3.0); // Fill + const LIGHT_COL2: vec4 = vec4( 0.1, 0.1, 1.0, 0.2); // Rim + + fn DistributionGGX(N: vec3, H: vec3, roughness: f32) -> f32 { + // Add epsilon to avoid singularities at very low roughness. + let a = max(roughness * roughness, 0.0025); + let a2 = a * a; + let NdotH = max(dot(N, H), 0.0); + let NdotH2 = NdotH * NdotH; + let denom = (NdotH2 * (a2 - 1.0) + 1.0); + return a2 / (PI * denom * denom + 1e-7); + } + + fn GeometrySchlickGGX(NdotV: f32, roughness: f32) -> f32 { + // Heitz's k for direct lighting approximation + let r = roughness + 1.0; + let k = (r * r) / 8.0; + let denom = NdotV * (1.0 - k) + k; + return NdotV / denom; + } + + fn GeometrySmith(N: vec3, V: vec3, L: vec3, roughness: f32) -> f32 { + let NdotV = max(dot(N, V), 0.0); + let NdotL = max(dot(N, L), 0.0); + let ggx2 = GeometrySchlickGGX(NdotV, roughness); + let ggx1 = GeometrySchlickGGX(NdotL, roughness); + return ggx1 * ggx2; + } + + fn fresnelSchlick(cosTheta: f32, F0: vec3) -> vec3 { + return F0 + (vec3(1.0, 1.0, 1.0) - F0) * pow(1.0 - cosTheta, 5.0); + } + fn fresnelSchlickRoughness(cosTheta: f32, F0: vec3, roughness: f32) -> vec3 { + return F0 + (max(vec3(1.0, 1.0, 1.0) * (1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0); + } + + fn dir_to_equirect_uv(dir: vec3) -> vec2 { + let d = normalize(dir); + let u = 0.5 + atan2(d.x, -d.z) / (2.0 * PI); + let v = 0.5 - asin(clamp(d.y, -1.0, 1.0)) / PI; + return vec2(fract(u), clamp(v, 0.0, 1.0)); + } + + // Directional light accumulator (white light scaled by intensity). + fn accumulateDirLight( + N: vec3, V: vec3, F0: vec3, + albedo: vec3, roughness: f32, metallic: f32, + lightDir: vec3, lightColor: vec4 + ) -> vec3 { + // lightDir is direction from light to surface; incoming L is opposite + let L = normalize(-lightDir.xyz); + let H = normalize(V + L); + let radiance = lightColor.w * lightColor.xyz; + + let NDF = DistributionGGX(N, H, roughness); + let G = GeometrySmith(N, V, L, roughness); + let F = fresnelSchlick(max(dot(H, V), 0.0), F0); + + let kS = F; + var kD = vec3(1.0, 1.0, 1.0) - kS; + kD = kD * (1.0 - metallic); + + let numerator = NDF * G * F; + let denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.0001; + let specular = numerator / vec3(denominator, denominator, denominator); + + let NdotL = max(dot(N, L), 0.0); + return (kD * (albedo / PI) + specular) * radiance * NdotL; + } + + struct FSOut { + @location(0) color: vec4, + @location(1) velocity: vec2, + }; + + @fragment fn main( + @location(0) uv: vec2, + @location(1) WorldPos: vec3, + @location(2) NormalIn: vec3, + @location(3) clipCurr: vec4, + @location(4) clipPrev: vec4 + ) -> FSOut { + // Surface parameters + var albedo = textureSample(texBaseColor, smpBaseColor, uv).rgb * UMaterial.baseColorFactor; + let mr = textureSample(texMetallicRoughness, smpMetallicRoughness, uv).gb; + var roughness = clamp(mr.x * UMaterial.roughnessFactor, 0.0, 1.0); + // Robustness: keep roughness in a sane range to preserve highlight and stability. + roughness = clamp(roughness, 0.045, 0.99); + let metallic = clamp(mr.y * UMaterial.metallicFactor, 0.0, 1.0); + // TODO: Support normal mapping (tangent space) and AO texture. + let N = normalize(NormalIn); + let V = normalize(UCamera.position.xyz - WorldPos); + + var F0 = vec3(0.04, 0.04, 0.04); + F0 = mix(F0, albedo, vec3(metallic, metallic, metallic)); + + var Lo = vec3(0.0, 0.0, 0.0); + Lo = Lo + accumulateDirLight(N, V, F0, albedo, roughness, metallic, LIGHT_DIR0, LIGHT_COL0); + Lo = Lo + accumulateDirLight(N, V, F0, albedo, roughness, metallic, LIGHT_DIR1, LIGHT_COL1); + Lo = Lo + accumulateDirLight(N, V, F0, albedo, roughness, metallic, LIGHT_DIR2, LIGHT_COL2); + + var color = Lo; + // Diffuse IBL ambient (equirect irradiance) + let kS = fresnelSchlick(max(dot(N, V), 0.0), F0); + let kD = (vec3(1.0, 1.0, 1.0) - kS) * (1.0 - metallic); + let uvIrr = dir_to_equirect_uv(N); + let irradiance = textureSample(texIrradiance, smpIrradiance, uvIrr).rgb; + let ambient_diffuse = kD * irradiance * albedo; + // Specular IBL + let R = reflect(-V, N); + let MAX_REFLECTION_LOD: f32 = 4.0; + let prefilteredColor = textureSampleLevel( + texPrefilter, smpPrefilter, dir_to_equirect_uv(R), roughness * MAX_REFLECTION_LOD + ).rgb; + let envBRDF = textureSample(texBrdfLut, smpBrdfLut, vec2(max(dot(N, V), 0.0), roughness)).rg; + let F = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); + let specular = prefilteredColor * (F * envBRDF.x + envBRDF.y); + color = color + ambient_diffuse + specular; + // Velocity (screen space) for motion blur + // Preserve sign of w (required for correct perspective divide); only clamp magnitude to avoid INF. + let w_curr = sign(clipCurr.w) * max(abs(clipCurr.w), 1e-4); + let w_prev = sign(clipPrev.w) * max(abs(clipPrev.w), 1e-4); + // Convert NDC (y-up) to texture UV (y-down) to match fullscreen post-process passes. + let ndc_curr = clipCurr.xy / w_curr; + let ndc_prev = clipPrev.xy / w_prev; + let a = vec2(ndc_curr.x * 0.5 + 0.5, -ndc_curr.y * 0.5 + 0.5); + let b = vec2(ndc_prev.x * 0.5 + 0.5, -ndc_prev.y * 0.5 + 0.5); + let vel = a - b; + + var out: FSOut; + out.color = vec4(color, 1.0); + out.velocity = vel; + return out; + } + "#; + + // Describe bind group layouts for pipeline creation + let bind_groups: Vec> = vec![ + // 0: globals (camera + IBL) + vec![ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + // Prefiltered spec texture + sampler + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 4, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + // BRDF LUT texture + sampler + wgpu::BindGroupLayoutEntry { + binding: 5, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 6, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + // 1: material textures (PBR) + vec![ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 4, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 5, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 6, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 7, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + // 2: material params (PBR) + vec![wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + // 3: per-draw dynamic UBO (MVP) + vec![wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: Some( + std::num::NonZeroU64::new(std::mem::size_of::() as u64) + .unwrap(), + ), + }, + count: None, + }], + ]; + + let desc = PipelineV2Desc { + label: Some("scene_pipeline_v2"), + vs: ShaderDesc { + source: vertex_wgsl, + entry_func: "main", + }, + ps: ShaderDesc { + source: fragment_wgsl, + entry_func: "main", + }, + vertex_buffers: &[::data_layout_descriptor()], + bind_groups, + targets: &[ + Some(wgpu::ColorTargetState { + format: self.color_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + }), + Some(wgpu::ColorTargetState { + format: self.velocity_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + }), + ], + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState::default(), + }; + + // Use renderer API to build pipeline + let pipeline = renderer.create_pipeline_v2(desc)?; + + // Camera buffer (group 0 binding 0) + let camera_buffer = { + let device = renderer.get_device(); + device.create_buffer(&wgpu::BufferDescriptor { + label: Some("camera_buffer"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }) + }; + + // Pre-create per-draw dynamic UBO ring and its bind group with large preallocation + // Creation happens in init; growth/recreation only when capacity is insufficient during draw + self.per_draw_capacity = MAX_EXPECTED_PER_DRAW_INSTANCES as u64; + let size = self.per_draw_stride * self.per_draw_capacity; + let per_draw_buffer = renderer.create_buffer(BufferDesc { + label: Some("per_draw_dynamic_ubo_ring"), + byte_size: size, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + })?; + let per_draw_bind_group = { + let layout_ptr: *const wgpu::BindGroupLayout = + &pipeline.bind_group_layouts[MATERIAL_BIND_GROUP_PERDRAW as usize] as *const _; + let device = renderer.get_device(); + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("per_draw_bind_group"), + layout: unsafe { &*layout_ptr }, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &per_draw_buffer, + offset: 0, + size: Some( + std::num::NonZeroU64::new(std::mem::size_of::() as u64) + .unwrap(), + ), + }), + }], + }) + }; + + // IBL resources and globals bind group (group 0: camera + IBL) + let (globals_bind_group, ibl_sampler) = { + let device = renderer.get_device(); + // choose texture view: if missing, use default color + let tex_view_ref: &wgpu::TextureView = if let Some(h) = self.ibl_irradiance_tex { + &resources + .gpu() + .textures + .get(h) + .expect("ibl irradiance") + .texture_view + } else { + let (def_color_h, _) = { + let device_ptr: *const wgpu::Device = renderer.get_device(); + let queue_ptr: *const wgpu::Queue = renderer.get_queue(); + unsafe { + resources + .gpu_mut() + .ensure_default_textures(&*device_ptr, &*queue_ptr) + } + }; + &resources + .gpu() + .textures + .get(def_color_h) + .expect("default color") + .texture_view + }; + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::Repeat, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + // Prefilter + BRDF LUT views (optional) + let prefilter_view_opt: Option<&wgpu::TextureView> = self + .ibl_prefilter_tex + .and_then(|h| resources.gpu().textures.get(h).map(|t| &t.texture_view)); + let brdf_view_opt: Option<&wgpu::TextureView> = self + .ibl_brdf_lut_tex + .and_then(|h| resources.gpu().textures.get(h).map(|t| &t.texture_view)); + + let bg = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("globals_bind_group"), + layout: &pipeline.bind_group_layouts[MATERIAL_BIND_GROUP_GLOBALS as usize], + entries: &[ + // binding 0: camera buffer + wgpu::BindGroupEntry { + binding: 0, + resource: camera_buffer.as_entire_binding(), + }, + // binding 1: IBL texture + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(tex_view_ref), + }, + // binding 2: IBL sampler + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + // binding 3: Prefilter texture (or fallback to irradiance if missing) + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::TextureView( + prefilter_view_opt.unwrap_or(tex_view_ref), + ), + }, + // binding 4: Prefilter sampler (use mipmap-capable sampler) + wgpu::BindGroupEntry { + binding: 4, + resource: wgpu::BindingResource::Sampler(&device.create_sampler( + &wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::Repeat, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Linear, + lod_min_clamp: 0.0, + lod_max_clamp: 16.0, + ..Default::default() + }, + )), + }, + // binding 5: BRDF LUT texture (fallback to irradiance if missing) + wgpu::BindGroupEntry { + binding: 5, + resource: wgpu::BindingResource::TextureView( + brdf_view_opt.unwrap_or(tex_view_ref), + ), + }, + // binding 6: BRDF LUT sampler + wgpu::BindGroupEntry { + binding: 6, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + ], + }); + (bg, sampler) + }; + + self.state = Some(PassSceneState { + camera_uniform: CameraUniform::new(), + camera_buffer, + prev_view_projection: Mat4::IDENTITY, + globals_bind_group, + pipeline, + ibl_sampler, + per_draw_buffer, + per_draw_bind_group, + }); + + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + _view: &wgpu::TextureView, + world: &WorldQuery, + ) -> Result<()> { + let active_camera_entity_handle = world.active_camera; + let camera_component_storage = world.camera_components; + let transform_component_storage = world.transform_components; + + // TODO: Update all isDirty=true materials, for each: + // - Color3f -> Update material uniform + // - Texture2D -> Update material bind groups + + // Read active camera + transform + let camera_storage = camera_component_storage + .data + .get(active_camera_entity_handle.data().index as usize) + .unwrap(); + let active_camera_component = camera_storage.as_ref().unwrap(); + let camera_transform_storage = transform_component_storage + .data + .get(active_camera_entity_handle.data().index as usize) + .unwrap(); + let active_camera_transform_component = camera_transform_storage.as_ref().unwrap(); + + // Update camera uniform and write to GPU buffer (no allocations) + let (prev_view_proj, vp_mat_arr): (Mat4, [[f32; 4]; 4]) = { + let state = get_state(self); + let prev = state.prev_view_projection; + state + .camera_uniform + .update_data(active_camera_component, active_camera_transform_component); + renderer.get_queue().write_buffer( + &state.camera_buffer, + 0, + bytemuck::bytes_of(&state.camera_uniform), + ); + let current_vp = Mat4::from_cols_array_2d(&state.camera_uniform.view_projection_matrix); + state.prev_view_projection = current_vp; + (prev, state.camera_uniform.view_projection_matrix) + }; + let clear_color = active_camera_component.clear_color; + + // View-projection matrix + let vp_mat: Mat4 = Mat4::from_cols_array_2d(&vp_mat_arr); + + // Build visible set + self.visible_pre_draw_buffer.clear(); + for render_queue_item in world.render_queue.iter() { + let key = decompose_render_queue_key(render_queue_item.key); + let mesh_handle = + RendererMeshHandle::from_parts(key.mesh_index as u32, key.mesh_version as u32); + let material_handle = RendererMaterialHandle::from_parts( + key.material_index as u32, + key.material_version as u32, + ); + + // Transform + let entity_index = render_queue_item.entity_index as usize; + let transform = transform_component_storage + .data + .get(entity_index) + .unwrap() + .as_ref() + .unwrap(); + let model_arr = get_model_matrix(transform); + let model: Mat4 = Mat4::from_cols_array_2d(&model_arr); + let prev_model_arr = get_prev_model_matrix(transform); + let prev_model: Mat4 = Mat4::from_cols_array_2d(&prev_model_arr); + + // NOTE: Temporarily skip AABB-based frustum culling to avoid accessing renderer internals. + + let view_proj: Mat4 = vp_mat; + let mvp: [[f32; 4]; 4] = (view_proj * model).to_cols_array_2d(); + let prev_mvp: [[f32; 4]; 4] = (prev_view_proj * prev_model).to_cols_array_2d(); + self.visible_pre_draw_buffer.push(VisiblePreDraw { + material_handle, + mesh_handle, + entity_index: render_queue_item.entity_index, + mvp, + prev_mvp, + model: model.to_cols_array_2d(), + }); + } + + // Sort and group (Pipeline -> Material -> Mesh) + self.visible_pre_draw_buffer.sort_by_key(|v| { + ((v.material_handle.generation() as u64) << 32) | (v.material_handle.index() as u64) + }); + self.groups_buffer.clear(); + // Resolve pipeline pointer for this frame (single pipeline for now) + let pipeline_ptr: *const wgpu::RenderPipeline = { + let state = get_state(self); + &state.pipeline.pipeline as *const _ + }; + for v in &self.visible_pre_draw_buffer { + let need_new_group = self + .groups_buffer + .last() + .map(|g| g.material_handle != v.material_handle) + .unwrap_or(true); + if need_new_group { + self.groups_buffer.push(GroupCmd { + pipeline: pipeline_ptr, + material_handle: v.material_handle, + batches: Vec::new(), + }); + } + let g = self.groups_buffer.last_mut().unwrap(); + if let Some(batch) = g + .batches + .iter_mut() + .find(|b| b.mesh_handle == v.mesh_handle) + { + batch.instances.push(PerDrawStd140 { + mvp: v.mvp, + prev_mvp: v.prev_mvp, + model: v.model, + }); + } else { + g.batches.push(MeshBatch { + mesh_handle: v.mesh_handle, + instances: vec![PerDrawStd140 { + mvp: v.mvp, + prev_mvp: v.prev_mvp, + model: v.model, + }], + base_offset_u32: 0, + }); + } + } + + // Per-draw ring buffer setup + let needed: u64 = self + .groups_buffer + .iter() + .map(|g| { + g.batches + .iter() + .map(|b| b.instances.len() as u64) + .sum::() + }) + .sum(); + if self.per_draw_capacity < needed { + #[cfg(debug_assertions)] + { + log::error!( + "Per-draw capacity exceeded: needed={} capacity={}", + needed, + self.per_draw_capacity + ); + } + // Release: proceed; only first (capacity) entries will be used by draws + } + + self.staging_buffer.clear(); + let mut next_offset_u32: u32 = 0; + for g in self.groups_buffer.iter_mut() { + for b in g.batches.iter_mut() { + b.base_offset_u32 = next_offset_u32; + for pd in &b.instances { + self.staging_buffer + .extend_from_slice(bytemuck::bytes_of(pd)); + let pad = + (self.per_draw_stride as usize) - std::mem::size_of::(); + self.staging_buffer.extend(std::iter::repeat(0u8).take(pad)); + next_offset_u32 = next_offset_u32.wrapping_add(self.per_draw_stride as u32); + } + } + } + { + let state_ref: &PassSceneState = unsafe { self.state.as_ref().unwrap_unchecked() }; + renderer + .get_queue() + .write_buffer(&state_ref.per_draw_buffer, 0, &self.staging_buffer); + } + + // Encode offscreen pass + let color_view = resources + .gpu() + .textures + .get(self.offscreen_color_texture) + .expect("offscreen color") + .texture + .create_view(&wgpu::TextureViewDescriptor { + label: Some("scene_offscreen_color_view"), + ..Default::default() + }); + let velocity_view = resources + .gpu() + .textures + .get(self.velocity_texture) + .expect("velocity texture") + .texture + .create_view(&wgpu::TextureViewDescriptor { + label: Some("scene_velocity_view"), + ..Default::default() + }); + let depth_view = resources + .gpu() + .textures + .get(self.depth_texture) + .expect("depth") + .texture + .create_view(&wgpu::TextureViewDescriptor { + label: Some("scene_depth_view"), + ..Default::default() + }); + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("m6_offscreen_pass"), + color_attachments: &[ + Some(wgpu::RenderPassColorAttachment { + view: &color_view, + resolve_target: None, + ops: if self.load_existing_color { + wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + } + } else { + wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: clear_color.x as f64, + g: clear_color.y as f64, + b: clear_color.z as f64, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + } + }, + }), + Some(wgpu::RenderPassColorAttachment { + view: &velocity_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + }), + ], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &depth_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + // Immutable view of pass state for binding (no aliasing with groups_buffer immutable borrow) + let state_ref: &PassSceneState = unsafe { self.state.as_ref().unwrap_unchecked() }; + for group in &self.groups_buffer { + rpass.set_pipeline(unsafe { &*group.pipeline }); + rpass.set_bind_group( + MATERIAL_BIND_GROUP_GLOBALS as u32, + &state_ref.globals_bind_group, + &[], + ); + { + let mat = resources + .gpu() + .materials + .get(group.material_handle) + .expect("material"); + rpass.set_bind_group( + MATERIAL_BIND_GROUP_TEXTURES as u32, + &mat.texture_bind_group, + &[], + ); + rpass.set_bind_group( + MATERIAL_BIND_GROUP_PARAMS as u32, + &mat.param_bind_group, + &[], + ); + } + for batch in &group.batches { + let mesh = resources.gpu().meshes.get(batch.mesh_handle).expect("mesh"); + rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); + rpass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); + for i in 0..batch.instances.len() { + let offset = batch + .base_offset_u32 + .wrapping_add((i as u32) * (self.per_draw_stride as u32)); + rpass.set_bind_group( + MATERIAL_BIND_GROUP_PERDRAW as u32, + &state_ref.per_draw_bind_group, + &[offset], + ); + rpass.draw_indexed(0..mesh.index_count, 0, 0..1); + } + } + } + + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_skybox_equirect.rs b/engine/pill_engine/src/graphics/pass_skybox_equirect.rs new file mode 100644 index 00000000..d65c0e2d --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_skybox_equirect.rs @@ -0,0 +1,385 @@ +use crate::graphics::projection::perspective_rh_zo; +use crate::graphics::renderer::{ + Pass, PillRenderer as EnginePillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc, WorldQuery, +}; +use crate::graphics::RendererTextureHandle; +use anyhow::Result; +use glam::{Mat4, Quat, Vec3}; +use pill_core::PillSlotMapKey; +use wgpu::CommandEncoder; + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct CameraUniform { + position: [f32; 4], + view_projection_matrix: [[f32; 4]; 4], + inverse_projection_matrix: [[f32; 4]; 4], + inverse_view_matrix: [[f32; 4]; 4], +} + +impl CameraUniform { + fn new() -> Self { + Self { + position: [0.0; 4], + view_projection_matrix: Mat4::IDENTITY.to_cols_array_2d(), + inverse_projection_matrix: Mat4::IDENTITY.to_cols_array_2d(), + inverse_view_matrix: Mat4::IDENTITY.to_cols_array_2d(), + } + } +} + +pub struct PassSkyboxEquirect { + label: String, + offscreen_color_texture: RendererTextureHandle, + color_format: wgpu::TextureFormat, + env_texture: RendererTextureHandle, + + // GPU + camera_buffer: Option, + camera_bind_group: Option, + env_bind_group: Option, + pipeline: Option, + sampler_clamp: Option, +} + +impl PassSkyboxEquirect { + pub fn new( + label: &str, + offscreen_color_texture: RendererTextureHandle, + color_format: wgpu::TextureFormat, + env_texture: RendererTextureHandle, + ) -> Self { + Self { + label: label.to_string(), + offscreen_color_texture, + color_format, + env_texture, + camera_buffer: None, + camera_bind_group: None, + env_bind_group: None, + pipeline: None, + sampler_clamp: None, + } + } +} + +impl Pass for PassSkyboxEquirect { + fn get_label(&self) -> &str { + &self.label + } + + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { + // Log the formats used by the skybox pass. The pipeline target format must match the + // offscreen color texture's format. This offscreen render target typically matches the + // swapchain/surface format for later composition. + log::info!( + "skybox:init offscreen_color_format={:?} surface_format={:?}", + self.color_format, + renderer.get_surface_format() + ); + // Camera buffer + bind group + let camera_bgl = + renderer + .get_device() + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("skybox_camera_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + let camera_buffer = renderer + .get_device() + .create_buffer(&wgpu::BufferDescriptor { + label: Some("skybox_camera_ubo"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let camera_bind_group = + renderer + .get_device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("skybox_camera_bg"), + layout: &camera_bgl, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: camera_buffer.as_entire_binding(), + }], + }); + + // Env texture + sampler (clamp) + let env_bgl = + renderer + .get_device() + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("skybox_env_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + let sampler_clamp = renderer + .get_device() + .create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::Repeat, // wrap horizontally + address_mode_v: wgpu::AddressMode::ClampToEdge, // clamp vertically at poles + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + let env_tex = resources + .gpu() + .textures + .get(self.env_texture) + .expect("env texture"); + let env_bind_group = renderer + .get_device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("skybox_env_bg"), + layout: &env_bgl, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&env_tex.texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler_clamp), + }, + ], + }); + + // Pipeline (fullscreen triangle) + let vs_src = r#" + struct VSOut { + @builtin(position) pos: vec4, + @location(0) uv: vec2, + }; + @vertex + fn main(@builtin(vertex_index) vid: u32) -> VSOut { + var out: VSOut; + var p: vec2; + switch (vid) { + case 0u: { p = vec2(-1.0, -1.0); } + case 1u: { p = vec2(-1.0, 3.0); } + default: { p = vec2( 3.0, -1.0); } + } + let uv = p * 0.5 + vec2(0.5, 0.5); + out.pos = vec4(p, 0.0, 1.0); + out.uv = vec2(uv.x, 1.0 - uv.y); + return out; + } + "#; + let ps_src = r#" + const PI: f32 = 3.141592653589793; + struct Camera { + position: vec4, + viewProjection: mat4x4, + invProjection: mat4x4, + invView: mat4x4, + }; + @group(0) @binding(0) var UCamera: Camera; + @group(1) @binding(0) var TEnv: texture_2d; + @group(1) @binding(1) var SEnv: sampler; + + fn dir_to_equirect_uv(dir: vec3) -> vec2 { + let d = normalize(dir); + // Match common equirectangular convention: forward (-Z) maps to u=0.5 + let u = 0.5 + atan2(d.x, -d.z) / (2.0 * PI); + let v = 0.5 - asin(clamp(d.y, -1.0, 1.0)) / PI; + return vec2(fract(u), clamp(v, 0.0, 1.0)); + } + + fn setCamera(camDir: vec3) -> mat3x3 { + let cw = normalize(camDir); + let cp = vec3(0.0, 1.0, 0.0); + let cu = normalize(cross(cw, cp)); + let cv = cross(cu, cw); + return mat3x3(cu, cv, cw); + } + + @fragment + fn main(@location(0) uv: vec2) -> @location(0) vec4 { + // Raylib-style ray reconstruction using camera basis and aspect-scaled screen coords + let camDir = normalize((UCamera.invView * vec4(0.0, 0.0, -1.0, 0.0)).xyz); + let ca = setCamera(camDir); + var nCoord = vec2(-1.0 + 2.0 * uv.x, 1.0 - 2.0 * uv.y); + let aspect = UCamera.invProjection[0][0] / UCamera.invProjection[1][1]; + nCoord.x *= aspect; + let fl = length(camDir); + let rd = ca * normalize(vec3(nCoord, fl)); + let st = dir_to_equirect_uv(rd); + let col = textureSample(TEnv, SEnv, st); + return vec4(col.rgb, 1.0); + } + "#; + // Build pipeline directly (no vertex buffers) + let device = renderer.get_device(); + let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("skybox_equirect_pl"), + bind_group_layouts: &[&camera_bgl, &env_bgl], + push_constant_ranges: &[], + }); + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("skybox_equirect_v2_vs"), + source: wgpu::ShaderSource::Wgsl(vs_src.into()), + }); + let ps_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("skybox_equirect_v2_ps"), + source: wgpu::ShaderSource::Wgsl(ps_src.into()), + }); + let rp = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("skybox_equirect_v2_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], // no vertex inputs + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &ps_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.color_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + self.camera_buffer = Some(camera_buffer); + self.camera_bind_group = Some(camera_bind_group); + self.env_bind_group = Some(env_bind_group); + self.pipeline = Some(PipelineV2 { + pipeline: rp, + bind_group_layouts: vec![camera_bgl, env_bgl], + }); + self.sampler_clamp = Some(sampler_clamp); + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + _view: &wgpu::TextureView, + world: &WorldQuery, + ) -> Result<()> { + // Update camera UBO + let active_camera_entity_handle = world.active_camera; + let camera_storage = world + .camera_components + .data + .get(active_camera_entity_handle.data().index as usize) + .unwrap(); + let camera = camera_storage.as_ref().unwrap(); + let transform_storage = world + .transform_components + .data + .get(active_camera_entity_handle.data().index as usize) + .unwrap(); + let transform = transform_storage.as_ref().unwrap(); + // Compute view-projection from camera + transform + let eye = Vec3::new( + transform.position.x, + transform.position.y, + transform.position.z, + ); + let rot_rad = Vec3::new( + transform.rotation.y.to_radians(), + transform.rotation.x.to_radians(), + transform.rotation.z.to_radians(), + ); + let q = Quat::from_euler(glam::EulerRot::YXZ, rot_rad.x, rot_rad.y, rot_rad.z); + // Engine forward is -Z; use -Z so that camera forward maps to center of the equirect + let dir = q * -Vec3::Z; + let view = Mat4::look_to_rh(eye, dir, Vec3::Y); + let fov_y = camera.fov.to_radians(); + let aspect = camera.aspect.get_value(); + let z_near = camera.range.start; + let z_far = camera.range.end; + let proj = perspective_rh_zo(fov_y, aspect, z_near, z_far); + let view_proj = proj * view; + let inv_proj = proj.inverse(); + let inv_view = view.inverse(); + let mut cam = CameraUniform::new(); + cam.position = [eye.x, eye.y, eye.z, 1.0]; + cam.view_projection_matrix = view_proj.to_cols_array_2d(); + cam.inverse_projection_matrix = inv_proj.to_cols_array_2d(); + cam.inverse_view_matrix = inv_view.to_cols_array_2d(); + renderer.get_queue().write_buffer( + self.camera_buffer.as_ref().unwrap(), + 0, + bytemuck::bytes_of(&cam), + ); + + // Begin render pass targeting offscreen color + let color_view = resources + .gpu() + .textures + .get(self.offscreen_color_texture) + .expect("offscreen color") + .texture + .create_view(&wgpu::TextureViewDescriptor { + label: Some("skybox_offscreen_color_view"), + ..Default::default() + }); + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &color_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + // Bind and draw fullscreen triangle + rpass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + rpass.set_bind_group(0, self.camera_bind_group.as_ref().unwrap(), &[]); + rpass.set_bind_group(1, self.env_bind_group.as_ref().unwrap(), &[]); + rpass.draw(0..3, 0..1); + // Explicitly end the pass so pass boundaries are clear in logs and ordering. + drop(rpass); + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/projection.rs b/engine/pill_engine/src/graphics/projection.rs new file mode 100644 index 00000000..13071b62 --- /dev/null +++ b/engine/pill_engine/src/graphics/projection.rs @@ -0,0 +1,48 @@ +use glam::Mat4; + +// Reference: Dennis Gustafsson DoF blog + bgfx example 45-bokeh. + +/// Right-handed perspective projection with NDC z in [0..1] (wgpu/Metal/Vulkan/D3D). +pub fn perspective_rh_zo(fov_y_radians: f32, aspect: f32, z_near: f32, z_far: f32) -> Mat4 { + let tan_half_fovy = (0.5 * fov_y_radians).tan(); + let ys = 1.0 / tan_half_fovy; + let xs = ys / aspect; + + let zz = z_far / (z_near - z_far); + let wz = (z_far * z_near) / (z_near - z_far); + + Mat4::from_cols_array(&[ + xs, 0.0, 0.0, 0.0, // + 0.0, ys, 0.0, 0.0, // + 0.0, 0.0, zz, -1.0, // + 0.0, 0.0, wz, 0.0, // + ]) +} + +/// Right-handed perspective projection with NDC z in [-1..1] (OpenGL). +pub fn perspective_rh_no(fov_y_radians: f32, aspect: f32, z_near: f32, z_far: f32) -> Mat4 { + let tan_half_fovy = (0.5 * fov_y_radians).tan(); + let ys = 1.0 / tan_half_fovy; + let xs = ys / aspect; + + let zz = (z_far + z_near) / (z_near - z_far); + let wz = (2.0 * z_far * z_near) / (z_near - z_far); + + Mat4::from_cols_array(&[ + xs, 0.0, 0.0, 0.0, // + 0.0, ys, 0.0, 0.0, // + 0.0, 0.0, zz, -1.0, // + 0.0, 0.0, wz, 0.0, // + ]) +} + +/// Depth-unpack constants derived from the projection matrix (mul=-m[14], add=m[10], with sign correction). +pub fn depth_unpack_consts_from_proj(proj: Mat4) -> (f32, f32) { + let p = proj.to_cols_array(); // column-major float[16] + let depth_mul = -p[3 * 4 + 2]; + let mut depth_add = p[2 * 4 + 2]; + if depth_mul * depth_add < 0.0 { + depth_add = -depth_add; + } + (depth_mul, depth_add) +} diff --git a/engine/pill_engine/src/graphics/render_queue.rs b/engine/pill_engine/src/graphics/render_queue.rs index 95247a13..2c68dc60 100644 --- a/engine/pill_engine/src/graphics/render_queue.rs +++ b/engine/pill_engine/src/graphics/render_queue.rs @@ -1,29 +1,22 @@ -use crate::{ - resources::{ - ResourceManager, - MaterialHandle, - TextureHandle, - Material, - MeshHandle, - Mesh, - }, +use crate::{ config::*, + resources::{Mesh, MeshHandle, PBRMaterial, PBRMaterialHandle, ResourceManager, TextureHandle}, }; -use pill_core::PillSlotMapKey; +use pill_core::Handle; +use anyhow::{Context, Error, Result}; +use core::fmt::{self, Debug}; +use lazy_static::lazy_static; use std::{ cmp::Ordering, - fmt::{Binary, Display}, - ops::{Add, Not, Shl, Sub, Range}, convert::{TryFrom, TryInto}, - path::{Path, PathBuf} + fmt::{Binary, Display}, + ops::{Add, Not, Range, Shl, Sub}, + path::{Path, PathBuf}, }; -use core::fmt::{Debug, self}; -use anyhow::{Result, Context, Error}; -use lazy_static::lazy_static; -// --- Render queue +// --- Render queue pub struct RenderQueue { pub items: Vec, @@ -54,7 +47,7 @@ impl PartialEq for RenderQueueItem { } } -impl Eq for RenderQueueItem { } +impl Eq for RenderQueueItem {} impl Display for RenderQueueItem { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -70,22 +63,34 @@ impl Debug for RenderQueueItem { // --- Render queue field --- -pub struct RenderQueueField { +pub struct RenderQueueField { pub mask_range: core::ops::Range, pub mask_shift: T, pub mask: T, - pub max: T, + pub max: T, } pub trait Pow { fn pow(self, exp: Self) -> Self; } -impl RenderQueueField +impl RenderQueueField where - T: Copy + Default + Pow + Binary + Debug + From + From + Ord + Shl + Sub + Add + Not, + T: Copy + + Default + + Pow + + Binary + + Debug + + From + + From + + Ord + + Shl + + Sub + + Add + + Not, { - pub fn new(mask_range: core::ops::Range) -> Self { // Compile-time evaluable function + pub fn new(mask_range: core::ops::Range) -> Self { + // Compile-time evaluable function let one: T = T::from(1 as u8); let two: T = T::from(2 as u8); let mask_range_length = mask_range.end - mask_range.start + one; //if mask_range.start == zero { mask_range.end + one } else { mask_range.end - mask_range.start }; @@ -104,16 +109,37 @@ where } // Creates pill engine render queue composed from order, material index, material version, mesh index, mesh version -pub fn compose_render_queue_key(resource_manager: &ResourceManager, material_handle: &MaterialHandle, mesh_handle: &MeshHandle) -> Result { - let material = resource_manager.get_resource::(material_handle)?; +pub fn compose_render_queue_key( + resource_manager: &ResourceManager, + mesh_handle: &MeshHandle, + material_handle: &Option, +) -> Result { let mesh = resource_manager.get_resource::(mesh_handle)?; - let render_queue_key: RenderQueueKey = - ((RENDER_QUEUE_KEY_ORDER.max - material.rendering_order as RenderQueueKey) << RENDER_QUEUE_KEY_ORDER.mask_shift) | // Order has to be inverted for proper sorting - ((material.renderer_resource_handle.unwrap().data().index as RenderQueueKey) << RENDER_QUEUE_KEY_MATERIAL_INDEX.mask_shift) | - ((material.renderer_resource_handle.unwrap().data().version.get() as RenderQueueKey) << RENDER_QUEUE_KEY_MATERIAL_VERSION.mask_shift) | - ((mesh.renderer_resource_handle.unwrap().data().index as RenderQueueKey) << RENDER_QUEUE_KEY_MESH_INDEX.mask_shift ) | - ((mesh.renderer_resource_handle.unwrap().data().version.get() as RenderQueueKey) << RENDER_QUEUE_KEY_MESH_VERSION.mask_shift); + let h_mesh = mesh.renderer_resource_handle.unwrap(); + // Material (optional) + let (mat_index, mat_gen) = if let Some(mh) = material_handle { + if let Ok(mat) = resource_manager.get_resource::(mh) { + if let Some(rh) = mat.renderer_resource_handle { + ( + rh.index() as RenderQueueKey, + rh.generation() as RenderQueueKey, + ) + } else { + (0, 0) + } + } else { + (0, 0) + } + } else { + (0, 0) + }; + + let render_queue_key: RenderQueueKey = ((h_mesh.index() as RenderQueueKey) + << RENDER_QUEUE_KEY_MESH_INDEX.mask_shift) + | ((h_mesh.generation() as RenderQueueKey) << RENDER_QUEUE_KEY_MESH_VERSION.mask_shift) + | (mat_index << RENDER_QUEUE_KEY_MATERIAL_INDEX.mask_shift) + | (mat_gen << RENDER_QUEUE_KEY_MATERIAL_VERSION.mask_shift); Ok(render_queue_key) } @@ -127,14 +153,18 @@ pub struct RenderQueueKeyFields { } // Decomposes pill engine render queue key into separate fields -pub fn decompose_render_queue_key(render_queue_key: RenderQueueKey) -> RenderQueueKeyFields { - +pub fn decompose_render_queue_key(render_queue_key: RenderQueueKey) -> RenderQueueKeyFields { // [TODO] What if render queue key is not valid - let order: u8 = ((render_queue_key & RENDER_QUEUE_KEY_ORDER.mask as RenderQueueKey) >> RENDER_QUEUE_KEY_ORDER.mask_shift as RenderQueueKey) as u8; - let material_index: u8 = ((render_queue_key & RENDER_QUEUE_KEY_MATERIAL_INDEX.mask) >> RENDER_QUEUE_KEY_MATERIAL_INDEX.mask_shift) as u8; - let material_version: u8 = ((render_queue_key & RENDER_QUEUE_KEY_MATERIAL_VERSION.mask) >> RENDER_QUEUE_KEY_MATERIAL_VERSION.mask_shift) as u8; - let mesh_index: u8 = ((render_queue_key & RENDER_QUEUE_KEY_MESH_INDEX.mask) >> RENDER_QUEUE_KEY_MESH_INDEX.mask_shift) as u8; - let mesh_version: u8 = ((render_queue_key & RENDER_QUEUE_KEY_MESH_VERSION.mask) >> RENDER_QUEUE_KEY_MESH_VERSION.mask_shift) as u8; + let order: u8 = ((render_queue_key & RENDER_QUEUE_KEY_ORDER.mask as RenderQueueKey) + >> RENDER_QUEUE_KEY_ORDER.mask_shift as RenderQueueKey) as u8; + let material_index: u8 = ((render_queue_key & RENDER_QUEUE_KEY_MATERIAL_INDEX.mask) + >> RENDER_QUEUE_KEY_MATERIAL_INDEX.mask_shift) as u8; + let material_version: u8 = ((render_queue_key & RENDER_QUEUE_KEY_MATERIAL_VERSION.mask) + >> RENDER_QUEUE_KEY_MATERIAL_VERSION.mask_shift) as u8; + let mesh_index: u8 = ((render_queue_key & RENDER_QUEUE_KEY_MESH_INDEX.mask) + >> RENDER_QUEUE_KEY_MESH_INDEX.mask_shift) as u8; + let mesh_version: u8 = ((render_queue_key & RENDER_QUEUE_KEY_MESH_VERSION.mask) + >> RENDER_QUEUE_KEY_MESH_VERSION.mask_shift) as u8; RenderQueueKeyFields { order, @@ -151,16 +181,18 @@ pub type RenderQueueKey = crate::config::RenderQueueKeyType; impl Pow for RenderQueueKey { fn pow(self, exp: Self) -> Self { - RenderQueueKey::pow(self, exp.try_into().unwrap()) + RenderQueueKey::pow(self, exp.try_into().unwrap()) } } fn get_render_queue_key_item_range(render_queue_item_index: u8) -> Range { let mut start: RenderQueueKey = 0; let mut end: RenderQueueKey = 0; - for i in 0..render_queue_item_index + 1 - { - start += i.ne(&0).then(|| RENDER_QUEUE_KEY_ITEMS_LENGTH[i as usize - 1]).unwrap_or(0); + for i in 0..render_queue_item_index + 1 { + start += i + .ne(&0) + .then(|| RENDER_QUEUE_KEY_ITEMS_LENGTH[i as usize - 1]) + .unwrap_or(0); end += RENDER_QUEUE_KEY_ITEMS_LENGTH[i as usize]; } start..(end - 1) @@ -172,4 +204,25 @@ lazy_static! { // This will be initialized in runtime instead of compile-time (t pub static ref RENDER_QUEUE_KEY_MATERIAL_VERSION: RenderQueueField = RenderQueueField::::new(get_render_queue_key_item_range(RENDER_QUEUE_KEY_MATERIAL_VERSION_IDX)); pub static ref RENDER_QUEUE_KEY_MESH_INDEX: RenderQueueField = RenderQueueField::::new(get_render_queue_key_item_range(RENDER_QUEUE_KEY_MESH_INDEX_IDX)); pub static ref RENDER_QUEUE_KEY_MESH_VERSION: RenderQueueField = RenderQueueField::::new(get_render_queue_key_item_range(RENDER_QUEUE_KEY_MESH_VERSION_IDX)); -} \ No newline at end of file +} + +// --- Borrow-only render query bundle and alias --- + +use crate::ecs::{CameraComponent, ComponentStorage, EntityHandle, TransformComponent}; + +/// Borrow-only bundle of references used by the renderer hot path. +/// Zero-cost: constructed by the caller and passed by value; no allocations/copies. +pub struct RenderQueueRefs<'a> { + pub active_camera: EntityHandle, + pub render_queue: &'a Vec, + pub camera_components: &'a ComponentStorage, + pub transform_components: &'a ComponentStorage, +} + +/// Back-compat alias to match existing plan/task wording. +pub type RenderQuery<'a> = RenderQueueRefs<'a>; + +/// Zero-cost factory trait producing borrowed render query refs. +pub trait RenderQueueFactory { + fn get<'a>(&'a self) -> RenderQuery<'a>; +} diff --git a/engine/pill_engine/src/graphics/renderer.rs b/engine/pill_engine/src/graphics/renderer.rs index e5e02054..9e888bcf 100644 --- a/engine/pill_engine/src/graphics/renderer.rs +++ b/engine/pill_engine/src/graphics/renderer.rs @@ -1,92 +1,211 @@ -use crate::{ - engine::Engine, - ecs::{ - EntityHandle, - ComponentStorage, - TransformComponent, - CameraComponent, - }, - resources::{ - MaterialHandle, - MeshData, - MeshHandle, - TextureHandle, - TextureType, - MaterialTextureMap, - MaterialParameterMap - }, - graphics::{ - RenderQueueItem, - }, +use crate::{ + ecs::{CameraComponent, ComponentStorage, EntityHandle, TransformComponent}, + engine::Engine, + graphics::{RenderQuery, RenderQueueFactory, RenderQueueItem}, + resources::{MeshData, MeshHandle, TextureHandle, TextureType}, }; -use pill_core::{PillSlotMapKey, Timer}; use pill_core::PillStyle; +use pill_core::{ + Handle, RendererBufferTag, RendererCameraTag, RendererMaterialTag, RendererMeshTag, + RendererPipelineTag, RendererPipelineV2Tag, RendererTextureTag, Timer, +}; +use anyhow::{Context, Error, Result}; use std::{path::PathBuf, sync::Arc}; use thiserror::Error; -use anyhow::{Result, Context, Error}; +// --- Renderer resource handles (typed generational handles) --- +pub type RendererMeshHandle = Handle; + +pub type RendererPipelineHandle = Handle; + +pub type RendererMaterialHandle = Handle; + +pub type RendererCameraHandle = Handle; + +pub type RendererTextureHandle = Handle; + +pub type RendererBufferHandle = Handle; -// --- Renderer resource handles --- +pub type RendererPipelineV2Handle = Handle; -pill_core::define_new_pill_slotmap_key! { - pub struct RendererMaterialHandle; +// --- Descriptors --- + +// Minimal buffer creation helper mirroring the planned ResourceManager API +#[derive(Clone, Copy)] +pub struct BufferDesc<'a> { + pub label: Option<&'a str>, + pub byte_size: u64, + pub usage: wgpu::BufferUsages, } -pill_core::define_new_pill_slotmap_key! { - pub struct RendererMeshHandle; +#[derive(Clone, Copy, Debug)] +pub struct ShaderDesc<'a> { + pub source: &'a str, + pub entry_func: &'a str, } -pill_core::define_new_pill_slotmap_key! { - pub struct RendererPipelineHandle; +#[derive(Clone, Debug)] +pub struct PipelineV2Desc<'a> { + pub label: Option<&'a str>, + pub vs: ShaderDesc<'a>, + pub ps: ShaderDesc<'a>, + // Vertex buffer layouts; empty slice means no vertex buffers (e.g., fullscreen triangle via vertex_index) + pub vertex_buffers: &'a [wgpu::VertexBufferLayout<'a>], + pub bind_groups: Vec>, + pub targets: &'a [Option], + pub depth_stencil: Option, + pub multisample: wgpu::MultisampleState, } -pill_core::define_new_pill_slotmap_key! { - pub struct RendererCameraHandle; +pub struct MaterialDesc<'a> { + pub label: &'a str, + // Factors + pub albedo: [f32; 3], + pub metallic: f32, + pub roughness: f32, + pub emissive: [f32; 3], + // Textures (optional; renderer will fallback to defaults if None) + pub albedo_tex: Option, + pub normal_tex: Option, + pub metallic_roughness_tex: Option, + pub emissive_tex: Option, } -pill_core::define_new_pill_slotmap_key! { - pub struct RendererTextureHandle; +pub struct RendererTargetDesc { + pub name: String, + pub format: wgpu::TextureFormat, + pub width: u32, + pub height: u32, } // --- Renderer trait definition --- -pub trait PillRenderer { - fn new(window: Arc, config: config::Config) -> Self where Self: Sized; +// RendererTexture is renderer-internal; engine code uses handles only. + +pub struct PipelineV2 { + pub pipeline: wgpu::RenderPipeline, + pub bind_group_layouts: Vec, +} +// --- Shared pass API (engine-visible) ------------------------------------------------- +pub trait Pass { + fn get_label(&self) -> &str; + fn init( + &mut self, + renderer: &mut dyn PillRenderer, + resources: &mut crate::resources::ResourceManager, + ) -> Result<()>; + fn draw( + &mut self, + encoder: &mut wgpu::CommandEncoder, + renderer: &mut dyn PillRenderer, + resources: &mut crate::resources::ResourceManager, + frame: &wgpu::SurfaceTexture, + view: &wgpu::TextureView, + world: &WorldQuery, + ) -> Result<()>; +} + +// Alias engine render query for passes +pub type WorldQuery<'a> = RenderQuery<'a>; + +pub trait PillRenderer { + fn new( + resources: &mut crate::resources::ResourceManager, + window: Arc, + config: config::Config, + ) -> Self + where + Self: Sized; + + fn init(&mut self) -> Result<()>; fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize); - fn set_master_pipeline(&mut self, vertex_shader_bytes: &[u8], fragment_shader_bytes: &[u8],) -> Result<()>; - + + // Creates a 256B-aligned uniform buffer (COPY_DST) and returns its handle + fn create_buffer(&mut self, desc: BufferDesc) -> Result; + fn create_pipeline_v2(&mut self, desc: PipelineV2Desc) -> Result; + fn create_material(&mut self, desc: MaterialDesc) -> Result; + fn update_material( + &mut self, + renderer_material_handle: RendererMaterialHandle, + desc: MaterialDesc, + ) -> Result; + fn create_render_target(&mut self, desc: RendererTargetDesc) -> Result; + fn create_depth_texture(&mut self, label: &str) -> Result; fn create_mesh(&mut self, name: &str, mesh_data: &MeshData) -> Result; - fn create_texture(&mut self, name: &str, image_data: &image::DynamicImage, texture_type: TextureType) -> Result; - fn create_material(&mut self, name: &str, textures: &MaterialTextureMap, parameters: &MaterialParameterMap) -> Result; + fn create_texture( + &mut self, + name: &str, + image_data: &image::DynamicImage, + texture_type: TextureType, + ) -> Result; fn create_camera(&mut self) -> Result; - fn update_material_textures(&mut self, renderer_material_handle: RendererMaterialHandle, textures: &MaterialTextureMap) -> Result<()>; - fn update_material_parameters(&mut self, renderer_material_handle: RendererMaterialHandle, parameters: &MaterialParameterMap) -> Result<()>; - fn destroy_mesh(&mut self, renderer_mesh_handle: RendererMeshHandle) -> Result<()>; fn destroy_texture(&mut self, renderer_texture_handle: RendererTextureHandle) -> Result<()>; - fn destroy_material(&mut self, renderer_material_handle: RendererMaterialHandle) -> Result<()>; fn destroy_camera(&mut self, renderer_camera_handle: RendererCameraHandle) -> Result<()>; - fn pass_input_to_egui(&mut self, event: &winit::event::WindowEvent) -> Result<()>; - - fn render(&mut self, + fn render( + &mut self, active_camera_entity_handle: EntityHandle, - render_queue: &Vec::, + render_queue: &Vec, camera_component_storage: &ComponentStorage, transform_component_storage: &ComponentStorage, - egui_ui: Box, - timer: &mut Timer + timer: &mut Timer, ) -> Result<()>; + // --- Engine pass management and helpers --- + fn set_passes(&mut self, passes: Vec>) -> Result<()>; + fn get_surface_format(&self) -> wgpu::TextureFormat; + fn get_device(&self) -> &wgpu::Device; + fn get_queue(&self) -> &wgpu::Queue; } pub type Renderer = Box; +// --- Zero-cost factory helper -------------------------------------------------- + +/// Renders using a zero-cost factory that provides borrowed references. +/// Keeps the PillRenderer trait object-safe while allowing call sites to be generic and inlined. +#[inline(always)] +pub fn render_with_factory(renderer: &mut R, factory: &F, timer: &mut Timer) -> Result<()> +where + R: PillRenderer + ?Sized, + F: RenderQueueFactory, +{ + let q = factory.get(); + renderer.render( + q.active_camera, + q.render_queue, + q.camera_components, + q.transform_components, + timer, + ) +} +// WorldView raw-pointer based view to avoid borrow conflicts at call site +pub struct WorldView { + pub active_camera: crate::ecs::EntityHandle, + pub render_queue_ptr: *const Vec, + pub camera_components_ptr: *const ComponentStorage, + pub transform_components_ptr: *const ComponentStorage, +} +pub struct WorldViewFactory { + pub world: WorldView, +} - +impl RenderQueueFactory for WorldViewFactory { + fn get<'b>(&'b self) -> RenderQuery<'b> { + unsafe { + RenderQuery { + active_camera: self.world.active_camera, + render_queue: &*self.world.render_queue_ptr, + camera_components: &*self.world.camera_components_ptr, + transform_components: &*self.world.transform_components_ptr, + } + } + } +} diff --git a/engine/pill_engine/src/lib.rs b/engine/pill_engine/src/lib.rs index 9e480363..c40d313d 100644 --- a/engine/pill_engine/src/lib.rs +++ b/engine/pill_engine/src/lib.rs @@ -1,15 +1,16 @@ #![cfg_attr(debug_assertions, allow(dead_code, unused_imports))] +mod config; +mod ecs; mod engine; -mod resources; mod graphics; -mod ecs; -mod config; +mod renderer; +mod resources; // --- Macros --- +pub use ecs::{Component, ComponentStorage, GlobalComponent, GlobalComponentStorage}; pub use pill_core::PillTypeMapKey; -pub use ecs::{Component, GlobalComponent, ComponentStorage, GlobalComponentStorage}; #[macro_export] macro_rules! define_component { @@ -54,127 +55,55 @@ macro_rules! define_global_component { #[cfg(feature = "game")] pub mod game { pub use crate::{ - engine::{ - Engine, - PillGame, - KeyboardKey, - MouseButton, - }, ecs::{ - SceneHandle, - MeshRenderingComponent, - TransformComponent, - InputComponent, - CameraComponent, - CameraAspectRatio, - EntityHandle, - AudioSourceComponent, - AudioListenerComponent, - TimeComponent, - UpdatePhase, - AudioManagerComponent, - EguiManagerComponent, - Component, - ComponentStorage, - GlobalComponent, - GlobalComponentStorage, - SoundType, + AudioListenerComponent, AudioManagerComponent, AudioSourceComponent, CameraAspectRatio, + CameraComponent, Component, ComponentStorage, EguiManagerComponent, EntityHandle, + GlobalComponent, GlobalComponentStorage, InputComponent, MeshRenderingComponent, + SceneHandle, SoundType, TimeComponent, TransformComponent, UpdatePhase, }, + engine::{Engine, KeyboardKey, MouseButton, PillGame}, resources::{ - Resource, - ResourceStorage, - Texture, - TextureHandle, - TextureType, - Material, - MaterialHandle, - Mesh, - MeshHandle, - ResourceLoadType, - Sound + Mesh, MeshHandle, Model, ModelHandle, PBRMaterial, PBRMaterialHandle, Resource, + ResourceLoadType, ResourceStorage, Sound, Texture, TextureHandle, TextureType, }, }; - + extern crate pill_core; - pub use pill_core::{ - PillTypeMapKey, - Vector2f, - Vector3f, - Color, - Vector2i, - Vector3i, - Vector3fExt, - create_game, - define_new_pill_slotmap_key, + pub use pill_core::{ + create_game, define_new_pill_slotmap_key, Color, PillTypeMapKey, Vector2f, Vector2i, + Vector3f, Vector3fExt, Vector3i, }; - + extern crate anyhow; - pub use anyhow::{ Context, Result, Error }; + pub use anyhow::{Context, Error, Result}; } #[cfg(feature = "internal")] pub mod internal { pub use crate::{ - engine::{ - Engine, - PillGame, - }, config::*, - graphics::{ - PillRenderer, - RenderQueueKey, - RenderQueueItem, - RenderQueueKeyFields, - decompose_render_queue_key, - - RendererCameraHandle, - RendererMaterialHandle, - RendererMeshHandle, - RendererPipelineHandle, - RendererTextureHandle, - RENDER_QUEUE_KEY_ORDER - }, ecs::{ - Scene, - ComponentStorage, - MeshRenderingComponent, - TransformComponent, - CameraComponent, - EntityHandle, - InputComponent, - TimeComponent, - CameraAspectRatio, - AudioSourceComponent, - AudioListenerComponent, - AudioManagerComponent, - EguiManagerComponent, - get_renderer_resource_handle_from_camera_component, - update_transform_matrices, - get_model_matrix, - get_normal_matrix, + get_model_matrix, get_normal_matrix, + get_renderer_resource_handle_from_camera_component, update_transform_matrices, + AudioListenerComponent, AudioManagerComponent, AudioSourceComponent, CameraAspectRatio, + CameraComponent, ComponentStorage, EguiManagerComponent, EntityHandle, InputComponent, + MeshRenderingComponent, Scene, TimeComponent, TransformComponent, + }, + engine::{Engine, PillGame}, + graphics::{ + decompose_render_queue_key, BufferDesc, MaterialDesc, Pass, PillRenderer, PipelineV2, + PipelineV2Desc, RenderQueueItem, RenderQueueKey, RenderQueueKeyFields, Renderer, + RendererBufferHandle, RendererBufferTag, RendererCameraHandle, RendererCameraTag, + RendererMaterialHandle, RendererMaterialTag, RendererMeshHandle, RendererMeshTag, + RendererPipelineHandle, RendererPipelineTag, RendererPipelineV2Handle, + RendererPipelineV2Tag, RendererTargetDesc, RendererTextureHandle, RendererTextureTag, + ShaderDesc, WorldQuery, RENDER_QUEUE_KEY_ORDER, }, resources::{ - Texture, - TextureHandle, - TextureType, - - Material, - MaterialHandle, - - Mesh, - MeshHandle, - MeshData, - MeshVertex, - - ResourceLoadType, - ResourceManager, - - MaterialTexture, - MaterialTextureMap, - MaterialParameter, - MaterialParameterMap, - get_renderer_texture_handle_from_material_texture, + Mesh, MeshData, MeshHandle, MeshVertex, PBRMaterial, PBRMaterialHandle, + ResourceLoadType, ResourceManager, Texture, TextureHandle, TextureType, }, }; + // Expose the concrete wgpu renderer type for standalone host + pub use crate::renderer::Renderer as WgpuRenderer; } - diff --git a/engine/pill_engine/src/renderer/DESIGN.md b/engine/pill_engine/src/renderer/DESIGN.md new file mode 100644 index 00000000..afcc893f --- /dev/null +++ b/engine/pill_engine/src/renderer/DESIGN.md @@ -0,0 +1,241 @@ +# Pill Renderer vNext (wgpu) – Minimal MVP, API-Correct + +### Goals + +- Lean MVP aligned with wgpu/Vulkan semantics (no GL-era patterns). +- Master pipeline from `WindowComponent`, renders nothing by default. +- Mesh visible by Milestone 2 via a single pass; complexity grows incrementally. +- Separate resource mutation from drawing; small, stable public API. + +### Hardware/API alignment from TALK_0.md (applied to MVP) + +- Separate data modification from drawing: precreate PSOs/bind groups; upload dynamic data once per pass. +- Use bind groups (no per-draw shader resource setup); convention: set 0 (globals), set 1 (material), set 2 (optional). +- Dynamic offsets over per-draw map/unmap; per-frame ring buffer; obey min uniform offset alignment (256 bytes on wgpu). +- Avoid base-instance tricks and push-constant reliance; stick to uniform/SSBO with dynamic offsets. +- Pack meshes later; MVP uses per-mesh vertex/index buffers; introduce packing after correctness (Milestone 8 optional). + +### Surface/window init via WindowComponent (wgpu-correct) + +- Choose `SurfaceConfiguration` from capabilities: + - Format: pick first sRGB-capable if present (prefer `Rgba8UnormSrgb`), else first supported. + - Usage: `TextureUsages::RENDER_ATTACHMENT` (+ `TEXTURE_BINDING` for offscreen targets later). + - Present mode: `Fifo` for vsync=true; for vsync=false try `Mailbox` then `Immediate`, else fall back to `Fifo`. + - Alpha: default `CompositeAlphaMode::Auto`. +- Handle `SurfaceError` properly: `Lost|Outdated` → reconfigure; `Timeout` → skip frame; `OutOfMemory` → exit gracefully. +- Depth: create a depth texture per size change (`Depth24Plus` or `Depth32Float`), usage `RENDER_ATTACHMENT`. + +### Core minimal public API (stable names; MVP may leave some unimplemented until their milestone) + +```rust +pub struct Renderer; +impl Renderer { + pub fn new_from_window_component(window: &WindowComponent) -> anyhow::Result; + pub fn master(&mut self) -> MasterPipeline; + + // Views (Milestone 3) + pub fn get_view_from_camera(&self, cam: &CameraView) -> ViewId; + pub fn get_view_from_entities<'a, I>(&self, cam: &CameraView, entities: I) -> ViewId + where I: IntoIterator>; + + // Resource access (Milestone 2) + pub fn resources(&mut self) -> ResourceManager; +} + +pub struct MasterPipeline; +impl MasterPipeline { + pub fn add_pass(&mut self, pass: RenderPassDesc) -> PassId; + pub fn remove_pass(&mut self, id: PassId); + pub fn set_order(&mut self, new_order: &[PassId]); +} + +pub struct RenderPassDesc { + pub name: &'static str, + pub target: TargetDesc, // swapchain or offscreen texture + pub clear: ClearDesc, + pub subpipeline: Subpipeline, +} + +pub enum TargetDesc { Swapchain, Texture { tex: Handle, level: u32, layer: u32 } } + +pub struct Viewport { pub x: u32, pub y: u32, pub w: u32, pub h: u32 } + +pub struct ClearDesc { pub color: Option<[f32;4]>, pub depth: Option, pub stencil: Option } + +pub struct Subpipeline { + pub pipeline: Handle, // PSO (wgpu::RenderPipeline) + pub globals: Handle, // set 0 + pub draws: DrawRecipe, // how to record draws +} + +pub enum DrawRecipe { + FromView { view: ViewId, material_set: Handle, shader_set: Option> }, + Inline(Box), +} + +pub struct DrawListBuilder; +impl DrawListBuilder { + pub fn set_pipeline(&mut self, pso: Handle); + pub fn set_bind_groups_with_offsets( + &mut self, + g0: Handle, + g1: Option>, g2: Option>, + offs0: Option, offs1: Option, offs2: Option + ); + pub fn set_mesh(&mut self, mesh: Handle, base_vertex: u32, base_index: u32); + pub fn push_per_draw(&mut self, data: &T) -> u32; // returns 256B-aligned dynamic offset + pub fn draw_indexed(&mut self, index_count: u32, first_index: u32); +} + +pub struct ResourceManager; +impl ResourceManager { + pub fn create_texture(&mut self, desc: TextureDesc) -> Handle; + pub fn create_buffer(&mut self, desc: BufferDesc) -> Handle; + pub fn create_bind_group(&mut self, desc: BindGroupDesc) -> Handle; + pub fn create_pipeline(&mut self, desc: GraphicsPipelineDesc) -> Handle; + pub fn upload_mesh(&mut self, mesh: CpuMesh) -> Handle; +} + +// Milestone 3 +pub struct CameraView { pub view: glam::Mat4, pub proj: glam::Mat4, pub frustum: Frustum } +#[repr(transparent)] pub struct ViewId(u32); + +pub struct EntityRenderProxy<'a> { + pub world_from_object: glam::Mat4, + pub mesh: Handle, + pub material: Handle, + pub bounds_ws: Aabb, + pub user_data: &'a dyn std::any::Any, +} +``` + +### WGSL shader correctness (M2 baseline) + +- Use WGSL (portable across native/web). Uniforms must respect WGSL alignment; per-draw UBO struct padded to 256 bytes. +- Example (unlit color) minimal pair: +```wgsl +// globals @group(0) @binding(0) +struct Globals { view_proj: mat4x4 }; +@group(0) @binding(0) var G: Globals; + +struct VSIn { @location(0) pos: vec3; }; +struct VSOut { @builtin(position) pos: vec4; }; + +@vertex fn vs_main(input: VSIn) -> VSOut { + var out: VSOut; + out.pos = G.view_proj * vec4(input.pos, 1.0); + return out; +} + +@fragment fn fs_main() -> @location(0) vec4 { return vec4(1.0, 0.7, 0.3, 1.0); } +``` + + +### Milestones (incremental, debuggable) with API-correctness notes + +1) [Done] Bootstrap: Black Window + +- Expected result: Black frame; vsync honored; resizes OK. +- Client API: `Renderer::new_from_window_component(&wc)?;` run loop; no passes. +- Implementables: + - winit + wgpu init; capabilities-driven `SurfaceConfiguration`. + - Depth texture created but unused yet; recreate on resize. + - Surface error handling (Lost/Outdated/Timeout/OutOfMemory). +- wgpu correctness: + - Use `surface.get_capabilities(&adapter)` for format/present mode. + - Set `usage: RENDER_ATTACHMENT` and correct `alpha_mode`. + +2) [Done] Hello Mesh: Single Pass, Inline Draw + +- Expected result: Pill mesh or fallback triangle; fixed MVP (identity or simple ortho) hardcoded; depth test on. +- Client API: Create mesh + pipeline; `Inline` pass records one draw. +- Implementables: + - `ResourceManager`: create vertex/index buffers with `VERTEX|INDEX|COPY_DST` usages. + - Minimal pipeline: WGSL VS/FS, color target = surface format, depth = `Depth24Plus`. + - `DrawListBuilder` encodes: set pipeline, set vertex/index buffers, set bind groups, draw. +- wgpu correctness: + - Index format `Uint32` or `Uint16` consistent with buffer. + - Pipeline layout: explicit `BindGroupLayout` for globals even if empty; avoid implicit layout. + - Clear ops via `RenderPassColorAttachment` and depth attachment. + +3) [Done] Camera + FromView Culling + +- Expected result: Camera matrices applied; frustum culling removes offscreen entities. +- Client API: Build `CameraView`; `get_view_from_entities`; pass uses `FromView`. +- Implementables: + - CPU frustum planes; AABB test; build draw list from filtered entities. + - Per-entity MVP uniform placed in per-frame uniform buffer; 256B alignment. +- wgpu correctness: + - Uniform buffer alignment (offset multiple of 256); `BindGroupLayoutEntry` for uniform with `has_dynamic_offset: true`. + - Call `set_bind_group(group_index, &bg, &[offset])` matching the layout. + +4) [Done] Materials + Textures (set 1) + +- Expected result: Textured mesh with material BG; per-entity tint supported from UBO. +- Client API: `create_texture`, `create_bind_group(material_desc)`; assign material handle. +- Implementables: + - Texture creation with `TEXTURE_BINDING | COPY_DST`; view with correct `TextureViewDescriptor`. + - Sampler (filtering); material BG layout: `@group(1) @binding(0) texture_2d`, `@group(1) @binding(1) sampler`. + - FS samples sRGB texture; output to sRGB surface. +- wgpu correctness: + - Use sRGB surface format and sRGB textures for correct gamma; or convert in shader. + +5) [Done] Per-frame Ring Buffer + Dynamic Offsets + +- Expected result: No per-draw map/unmap; dynamic uniform offsets. +- Client API: `push_per_draw` returns aligned offset; pass offsets via `set_bind_groups_with_offsets`. +- Implementables: + - CPU staging buffer (MAP_WRITE) per frame; `queue.write_buffer` into GPU-visible uniform buffer. + - Layout entries with `has_dynamic_offset=true` only for dynamic bindings; offsets are multiples of 256. +- wgpu correctness: + - Avoid mapping GPU-only buffers; prefer `write_buffer` for simplicity. + +6) [Done] Multi-pass + Offscreen Targets + Quad Composition + +- Expected result: Offscreen render (T0) → FSQ/quads composite to swapchain; quads can place T0 anywhere in the framebuffer. +- Client API: Add two passes: `Texture` then `Swapchain`; post uses `Inline` to draw a quad (or fullscreen triangle) sampling T0 with UVs that map to desired target rect. +- Implementables: + - Offscreen target texture with `RENDER_ATTACHMENT | TEXTURE_BINDING`; correct view format; sampler. + - Composition shader samples T0; quad vertex data/uniforms carry position/size; no viewport/scissor used. +- wgpu correctness: + - Presentable texture cannot be sampled; offscreen must be separate texture. + +7) Resource Pools + Hot/Cold Split + +- Expected result: Opaque `Handle` pools; safe reuse; minimal hot metadata. +- Client API: Unchanged; `ResourceManager` returns handles. +- Implementables: + - Freelist + generation; hot (wgpu handles + light metadata) vs cold (descs, debug names). +- wgpu correctness: + - Keep GPU handles alive until all pass encoders referencing them are submitted. + +8) [Basic] Draw List Optimization + Done: sort by PSo/material/mesh + Remaining: optional client sort keys, per-pipeline bind group layout identity safety, and a minor mesh buffer rebind micro-optimization. +- Expected result: Fewer state changes; sorted by PSO/material. +- Client API: Optional sort keys; otherwise internal. +- Implementables: + - Build sort keys; stable sort; encode draws. +- wgpu correctness: + - Ensure binds set when changing PSO/material; match pipeline layouts. + +### Client usage snapshots + +- M1: just create renderer from `WindowComponent`. +- M2: create mesh + pipeline; one Inline pass to draw. +- M3: create `CameraView`, build `ViewId`, use `FromView`. +- M4–M6: add material BG + textures; add offscreen + post; use viewport rects. + +### Files to add later + +- `engine/pill_renderer/DESIGN.md` (this document) +- `engine/pill_renderer/src/renderer.rs` +- `engine/pill_renderer/src/pass.rs` +- `engine/pill_renderer/src/resources/*` +- `engine/pill_renderer/src/view.rs` + +### References + +- wgpu: `https://docs.rs/wgpu/latest/wgpu/` +- winit: `https://docs.rs/winit/latest/winit/` +- WGSL: `https://www.w3.org/TR/WGSL/` \ No newline at end of file diff --git a/engine/pill_engine/src/renderer/DESIGN_DEPTH_PREPASS.md b/engine/pill_engine/src/renderer/DESIGN_DEPTH_PREPASS.md new file mode 100644 index 00000000..35a646b8 --- /dev/null +++ b/engine/pill_engine/src/renderer/DESIGN_DEPTH_PREPASS.md @@ -0,0 +1,41 @@ +# Align Renderer With TALK: 2-Pass Occlusion + CPU Visibility + +### Scope + +- Add a depth-only prepass and build a HiZ depth pyramid each frame. +- Use last frame’s HiZ to perform conservative CPU occlusion culling after frustum culling. +- Keep overlay/composition unchanged; integrate via feature flag and metrics. + +### Files To Touch + +- `Pill-Engine/engine/pill_renderer/src/resources/renderer_pipeline.rs` (add depth-only pipeline + layout) +- `Pill-Engine/engine/pill_renderer/src/renderer.rs` (insert prepass; build HiZ; visibility stage; drive main pass survivors) +- `Pill-Engine/engine/pill_renderer/src/visibility/` (new: `hiz.rs`, `occlusion.rs` helpers) + +### Design + +- Depth prepass: render opaque geometry with a depth-only pipeline (no color, depth write enabled). +- HiZ pyramid: allocate mip-chain texture for depth; generate with compute (preferred) or render downsampling. Store previous frame’s HiZ for CPU queries. +- CPU occlusion: project entity world AABB to screen-space bounds; query HiZ at appropriate mip level using conservative test; mark occluded entities as culled. +- Frame cadence: use frame N-1 HiZ to cull frame N (avoids GPU→CPU sync); update HiZ after main pass for frame N+1. + +### Integration Points + +- Before main pass in `renderer.rs`: +1) run depth prepass +2) dispatch HiZ generation +3) frustum cull → occlusion cull → build `visible` list +4) draw main pass using `visible` +- Add `Renderer` toggles: `enable_depth_prepass`, `enable_hiz_culling` (config/env). +- Add counters: total, frustum_kept, occluded, drawn; expose in logs. + +### Minimal API/Resource Changes + +- Add depth-only `RenderPipeline` to `RendererPipeline` (e.g., `depth_prepass_pipeline`). +- Add persistent `hiz_texture` with mips; `hiz_bind_group` + compute pipeline for downsampling. +- Store per-mesh AABB (or compute at load); per-entity world AABB is derived via transform. + +### Risks/Notes + +- One-frame-late occlusion may pop on camera cuts; mitigate via camera-move tolerance or disable on large camera deltas. +- Keep culling conservative (AABB expanded slightly) to avoid over-culling. \ No newline at end of file diff --git a/engine/pill_engine/src/renderer/PLAN_v0.md b/engine/pill_engine/src/renderer/PLAN_v0.md new file mode 100644 index 00000000..27766f20 --- /dev/null +++ b/engine/pill_engine/src/renderer/PLAN_v0.md @@ -0,0 +1,294 @@ +# Pill Renderer vNext (wgpu) API Design + +### Goals + +- Modern, low-overhead rendering on wgpu (no GL-era patterns). +- Master pipeline initializes the window from a global `WindowComponent` and renders nothing by default. +- Users can define subpipelines/passes in game code, add them at runtime, and control targets/rects. +- Views: `get_view(camera)` and `get_view(entities)` produce culled renderables for passes. +- Stable API with prebuilt pipelines, bind groups, and generational handles. + +### Principles distilled from TALK_0.md + +- **Separate data modification from drawing**: precreate pipelines, bind groups; upload dynamic data per-pass in batches. +- **Generational handles + hot/cold SoA** for resources (textures, buffers, pipelines, materials). +- **Three bind group slots** convention (globals, material, shader-specific), dynamic offsets for temp data. +- **Minimize API calls per draw**: packed meshes, prebound resources, small draw metadata. + +### High-level architecture + +``` ++---------------------------+ +---------------------------+ +| game.rs (user code) | | pill_renderer (wgpu) | +| - builds passes | uses | - Renderer | +| - defines views +------->| - MasterPipeline | +| - registers renderables | | - PassBuilder/PassGraph | ++---------------------------+ | - ResourceManager (pools) | + | - ViewBuilder + Culling | + | - FrameTempAllocator | + +---------------------------+ +``` + +### Surface/window init via WindowComponent + +- `WindowComponent` fields consumed at init: `width`, `height`, `vsync`, `fullscreen/windowed`, `title`. +- Map to winit + wgpu `SurfaceConfiguration`: + - `vsync=true` → `PresentMode::Fifo`. + - `vsync=false` → prefer `Mailbox` else `Immediate` if supported; fall back to `Fifo`. + - Auto-pick `TextureFormat` via `surface.get_capabilities().formats[0]` unless overridden. + +### Core types (public API) + +```rust +// Handles (32-bit index + 32-bit gen) +#[repr(transparent)] pub struct Handle(u64); + +// Renderer creation +pub struct Renderer; +impl Renderer { + pub fn new_from_window_component(window: &WindowComponent) -> anyhow::Result; + pub fn master(&mut self) -> MasterPipeline; + + // Views + pub fn get_view_from_camera(&self, cam: &CameraView) -> ViewId; + pub fn get_view_from_entities<'a, I>(&self, cam: &CameraView, entities: I) -> ViewId + where I: IntoIterator>; +} + +// Master pipeline is empty; users add passes at runtime +pub struct MasterPipeline; +impl MasterPipeline { + pub fn add_pass(&mut self, pass: RenderPassDesc) -> PassId; + pub fn remove_pass(&mut self, id: PassId); + pub fn set_order(&mut self, new_order: &[PassId]); +} + +// Pass description (immutable after build) +pub struct RenderPassDesc { + pub name: &'static str, + pub target: TargetDesc, // swapchain or texture + pub viewport: Option,// rect in window + pub clear: ClearDesc, // per-attachment clears + pub subpipeline: Subpipeline, // PSO/shaders, draw list recipe +} + +pub enum TargetDesc { Swapchain, Texture { tex: Handle, level: u32, layer: u32 } } + +pub struct Viewport { pub x: u32, pub y: u32, pub w: u32, pub h: u32 } + +pub struct ClearDesc { pub color: Option<[f32;4]>, pub depth: Option, pub stencil: Option } + +// Subpipeline: bind groups, pipeline, and a draw callback/recipe +pub struct Subpipeline { + pub pipeline: Handle, // PSO + pub globals: Handle, // set 0 + pub draws: DrawRecipe, // how to build draw list per frame +} + +pub enum DrawRecipe { + // Renderer builds draws from a View (culled scene) + FromView { view: ViewId, material_set: Handle /* set 1 */, shader_set: Option> /* set 2 */ }, + // User supplies a closure to record draws + Inline(Box), +} + +// View building and culling +pub struct CameraView { pub view: glam::Mat4, pub proj: glam::Mat4, pub frustum: Frustum } +#[repr(transparent)] pub struct ViewId(u32); + +pub struct EntityRenderProxy<'a> { + pub world_from_object: glam::Mat4, + pub mesh: Handle, + pub material: Handle, + pub bounds_ws: Aabb, // for culling + pub user_data: &'a dyn std::any::Any, // optional +} + +// Draw list +pub struct DrawListBuilder; // state-sorted writes; per-frame temp data +impl DrawListBuilder { + pub fn set_pipeline(&mut self, pso: Handle); + pub fn set_bind_groups(&mut self, g0: Handle, g1: Option>, g2: Option>); + pub fn set_mesh(&mut self, mesh: Handle, base_v: u32, base_i: u32); + pub fn push_per_draw(&mut self, data: &T) -> u32; // returns dynamic offset + pub fn draw_indexed(&mut self, index_count: u32, first_index: u32); +} + +// Resource creation (pre-pass) +pub struct ResourceManager; +impl ResourceManager { + pub fn create_texture(&mut self, desc: TextureDesc) -> Handle; + pub fn create_buffer(&mut self, desc: BufferDesc) -> Handle; + pub fn create_bind_group(&mut self, desc: BindGroupDesc) -> Handle; + pub fn create_pipeline(&mut self, desc: GraphicsPipelineDesc) -> Handle; + pub fn upload_mesh(&mut self, mesh: CpuMesh) -> Handle; // packed mesh heap +} +``` + +### Data ownership and performance + +- All resources live in **typed generational pools** with hot/cold split: + - Hot: GPU handles, minimal render-time metadata (format, size, bind group ids). + - Cold: debug name, creation descriptors, CPU-side helpers. +- No per-draw map/unmap. Dynamic per-draw data is uploaded through a per-frame **ring buffer** (COPY_DST) and bound via **dynamic offsets**. +- Bind group convention: + - set 0: pass globals (camera, sun, shadow maps). + - set 1: material (textures + material UBO/SSBO). + - set 2: shader-specific (optional). + +### Pass graph and execution + +``` +Frame N: ++-------------+ +-------------+ +-------------+ +| Pass A | --> | Pass B | --> | Pass C | +| target=T0 | | target=T1 | | target=Swap | ++-------------+ +-------------+ +-------------+ + writes T0 reads T0 reads T1 +``` + +- Default: linear order; optional read-after-write deps declared in `RenderPassDesc` for future graphing. +- Each pass gets a viewport; swapchain always presents full frame, viewport limits draw area. + +### How users create renderables from scene entities + +- Provide two adapters: + - `renderer.get_view_from_camera(cam)` → returns `ViewId` with built frustum. + - `renderer.get_view_from_entities(cam, iter)` → users pass an iterator of `EntityRenderProxy`; renderer does frustum culling and produces a `DrawList` internally for `DrawRecipe::FromView` passes. +- Users can also bypass views and push draws manually via `DrawRecipe::Inline` for full-screen quads, gizmos, etc. + +### Client usage (game.rs excerpts) + +```rust +fn start(&self, engine: &mut Engine) -> Result<()> { + // Window setup via component + let wc = WindowComponent { width: 1280, height: 720, vsync: true, windowed: true, title: "Pill".into() }; + + // Create renderer + let mut renderer = pill_renderer::Renderer::new_from_window_component(&wc)?; + + // Create resources + let mut rm = renderer.resources(); + let mesh = rm.upload_mesh(load_obj("models/pill.obj")?); + let globals_layout = layouts::globals(); // provided by engine or shader gen + let material_bg = rm.create_bind_group(material_desc()); + let pipeline = rm.create_pipeline(pipeline_desc(globals_layout, material_layout(), swapchain_format(&renderer))); + + // Build camera + let cam = CameraView::from_transform_fov(transform, fov_y, aspect, near, far); + + // Define a view from entities + let view = renderer.get_view_from_entities(&cam, engine.iter_renderables()?); + + // Build an opaque pass targeting the swapchain + let pass_id = renderer.master().add_pass(RenderPassDesc { + name: "Opaque", + target: TargetDesc::Swapchain, + viewport: Some(Viewport { x: 0, y: 0, w: wc.width, h: wc.height }), + clear: ClearDesc { color: Some([0.02,0.02,0.03,1.0]), depth: Some(1.0), stencil: None }, + subpipeline: Subpipeline { + pipeline, + globals: build_globals_bg(&mut rm, &cam)?, + draws: DrawRecipe::FromView { view, material_set: material_bg, shader_set: None }, + }, + }); + + // Fullscreen quad post-process pass (manual draws) + let fsq_pso = rm.create_pipeline(fsq_pipeline_desc(swapchain_format(&renderer))); + renderer.master().add_pass(RenderPassDesc { + name: "Post", + target: TargetDesc::Swapchain, + viewport: None, + clear: ClearDesc { color: None, depth: None, stencil: None }, + subpipeline: Subpipeline { + pipeline: fsq_pso, + globals: default_globals(), + draws: DrawRecipe::Inline(Box::new(|dl| { + dl.set_pipeline(fsq_pso); + dl.set_bind_groups(globals_bg, None, None); + dl.set_mesh(fullscreen_triangle_mesh(), 0, 0); + dl.draw_indexed(3, 0); + })), + }, + }); + + Ok(()) +} +``` + +### WGPU details and constraints + +- Dynamic offsets require matching `BindGroupLayoutEntry` with `has_dynamic_offset: true`. +- Per-frame ring buffer: allocate once `MAP_WRITE | COPY_SRC` staging on CPU, `queue.write_buffer` into GPU-visible dynamic UBO/SSBO chunks. +- Swapchain rects: presentation is full-surface; use viewport/scissor to restrict drawing area. +- Pipeline creation uses `depth24plus` or `depth32float` depth formats; precreate all PSOs at startup. + +### Files to add (later implementation) + +- `engine/pill_renderer/DESIGN.md` (this document) +- `engine/pill_renderer/src/renderer.rs` (Renderer + MasterPipeline) +- `engine/pill_renderer/src/pass.rs` (RenderPassDesc, Subpipeline, DrawListBuilder) +- `engine/pill_renderer/src/resources/*` (pools, handles, upload) +- `engine/pill_renderer/src/view.rs` (CameraView, culling) + + +### Milestones (incremental, debuggable) + +1) Bootstrap: Black Window +- Expected result: Resizable window presents a black frame based on `WindowComponent` (vsync, size). No passes. +- Expected client API: + - `let mut renderer = Renderer::new_from_window_component(&wc)?;` then run engine loop; no passes added. +- Implementables: + - winit + wgpu init; `SurfaceConfiguration` using present mode from vsync; acquire→clear→present. + - `MasterPipeline` exists but is empty; resize handled. + +2) Hello Mesh: Single Pass, Inline Draw +- Expected result: Pill mesh on screen; fallback to built-in triangle if asset missing. Fixed MVP in shader. +- Expected client API: + - Create mesh + pipeline; `master.add_pass(RenderPassDesc { draws: Inline(|dl| { set_pipeline; set_mesh; draw_indexed; }) })`. +- Implementables: + - Minimal `ResourceManager` for buffers, shader module, pipeline; depth buffer; unlit shader. + - `DrawListBuilder`: `set_pipeline`, `set_mesh`, `draw_indexed`. + +3) Camera + FromView Culling +- Expected result: Camera matrices applied; cull out-of-frustum entities. +- Expected client API: + - Build `CameraView`; `renderer.get_view_from_entities(&cam, iter)`; pass uses `DrawRecipe::FromView { view, material_set, .. }`. +- Implementables: + - Frustum planes; `EntityRenderProxy`; per-entity MVP uniform; basic sort by PSO/material. + +4) Materials + Textures (set 1) +- Expected result: Textured opaque geometry via material bind group (set 1); optional per-entity tint/scalar. +- Expected client API: + - `create_texture`, `create_bind_group(material_desc)`; assign to entities. +- Implementables: + - Texture upload (RGBA8 sRGB), sampler; material BG layout; shader samples `albedo`. + +5) Per-frame Ring Buffer + Dynamic Offsets +- Expected result: Per-draw uniforms via dynamic offsets; no per-draw map/unmap. +- Expected client API: + - `let off = dl.push_per_draw(&PerDraw); set_bind_groups(globals, Some(material), None);` offsets applied. +- Implementables: + - CPU staging + `queue.write_buffer`; layouts with `has_dynamic_offset: true`; per-frame ring. + +6) Multi-pass + Offscreen Targets + Viewport Rects +- Expected result: Render to offscreen texture, post-process FSQ to swapchain; passes honor viewport/scissor rects. +- Expected client API: + - Two passes: `TargetDesc::Texture` then `TargetDesc::Swapchain`; post pass via `Inline`. +- Implementables: + - Texture render targets; FSQ pipeline; `fullscreen_triangle_mesh`. + +7) Resource Pools + Hot/Cold Split +- Expected result: Opaque `Handle` with generational reuse; minimal hot metadata. +- Expected client API: + - Same API; resource creation returns `Handle`. +- Implementables: + - Typed pools, freelist + generation; hot/cold structs. + +8) Draw List Optimization +- Expected result: Fewer PSO/material switches; sorted draws. +- Expected client API: + - Optional user sort key; default internal order. +- Implementables: + - Build sort keys; reorder before encoding; expose state-change counters. + diff --git a/engine/pill_engine/src/renderer/PLAN_v1.md b/engine/pill_engine/src/renderer/PLAN_v1.md new file mode 100644 index 00000000..c740677c --- /dev/null +++ b/engine/pill_engine/src/renderer/PLAN_v1.md @@ -0,0 +1,240 @@ +# Pill Renderer vNext (wgpu) – Minimal MVP, API-Correct + +### Goals + +- Lean MVP aligned with wgpu/Vulkan semantics (no GL-era patterns). +- Master pipeline from `WindowComponent`, renders nothing by default. +- Mesh visible by Milestone 2 via a single pass; complexity grows incrementally. +- Separate resource mutation from drawing; small, stable public API. + +### Hardware/API alignment from TALK_0.md (applied to MVP) + +- Separate data modification from drawing: precreate PSOs/bind groups; upload dynamic data once per pass. +- Use bind groups (no per-draw shader resource setup); convention: set 0 (globals), set 1 (material), set 2 (optional). +- Dynamic offsets over per-draw map/unmap; per-frame ring buffer; obey min uniform offset alignment (256 bytes on wgpu). +- Avoid base-instance tricks and push-constant reliance; stick to uniform/SSBO with dynamic offsets. +- Pack meshes later; MVP uses per-mesh vertex/index buffers; introduce packing after correctness (Milestone 8 optional). + +### Surface/window init via WindowComponent (wgpu-correct) + +- Choose `SurfaceConfiguration` from capabilities: + - Format: pick first sRGB-capable if present (prefer `Rgba8UnormSrgb`), else first supported. + - Usage: `TextureUsages::RENDER_ATTACHMENT` (+ `TEXTURE_BINDING` for offscreen targets later). + - Present mode: `Fifo` for vsync=true; for vsync=false try `Mailbox` then `Immediate`, else fall back to `Fifo`. + - Alpha: default `CompositeAlphaMode::Auto`. +- Handle `SurfaceError` properly: `Lost|Outdated` → reconfigure; `Timeout` → skip frame; `OutOfMemory` → exit gracefully. +- Depth: create a depth texture per size change (`Depth24Plus` or `Depth32Float`), usage `RENDER_ATTACHMENT`. + +### Core minimal public API (stable names; MVP may leave some unimplemented until their milestone) + +```rust +pub struct Renderer; +impl Renderer { + pub fn new_from_window_component(window: &WindowComponent) -> anyhow::Result; + pub fn master(&mut self) -> MasterPipeline; + + // Views (Milestone 3) + pub fn get_view_from_camera(&self, cam: &CameraView) -> ViewId; + pub fn get_view_from_entities<'a, I>(&self, cam: &CameraView, entities: I) -> ViewId + where I: IntoIterator>; + + // Resource access (Milestone 2) + pub fn resources(&mut self) -> ResourceManager; +} + +pub struct MasterPipeline; +impl MasterPipeline { + pub fn add_pass(&mut self, pass: RenderPassDesc) -> PassId; + pub fn remove_pass(&mut self, id: PassId); + pub fn set_order(&mut self, new_order: &[PassId]); +} + +pub struct RenderPassDesc { + pub name: &'static str, + pub target: TargetDesc, // swapchain or offscreen texture + pub clear: ClearDesc, + pub subpipeline: Subpipeline, +} + +pub enum TargetDesc { Swapchain, Texture { tex: Handle, level: u32, layer: u32 } } + +pub struct Viewport { pub x: u32, pub y: u32, pub w: u32, pub h: u32 } + +pub struct ClearDesc { pub color: Option<[f32;4]>, pub depth: Option, pub stencil: Option } + +pub struct Subpipeline { + pub pipeline: Handle, // PSO (wgpu::RenderPipeline) + pub globals: Handle, // set 0 + pub draws: DrawRecipe, // how to record draws +} + +pub enum DrawRecipe { + FromView { view: ViewId, material_set: Handle, shader_set: Option> }, + Inline(Box), +} + +pub struct DrawListBuilder; +impl DrawListBuilder { + pub fn set_pipeline(&mut self, pso: Handle); + pub fn set_bind_groups_with_offsets( + &mut self, + g0: Handle, + g1: Option>, g2: Option>, + offs0: Option, offs1: Option, offs2: Option + ); + pub fn set_mesh(&mut self, mesh: Handle, base_vertex: u32, base_index: u32); + pub fn push_per_draw(&mut self, data: &T) -> u32; // returns 256B-aligned dynamic offset + pub fn draw_indexed(&mut self, index_count: u32, first_index: u32); +} + +pub struct ResourceManager; +impl ResourceManager { + pub fn create_texture(&mut self, desc: TextureDesc) -> Handle; + pub fn create_buffer(&mut self, desc: BufferDesc) -> Handle; + pub fn create_bind_group(&mut self, desc: BindGroupDesc) -> Handle; + pub fn create_pipeline(&mut self, desc: GraphicsPipelineDesc) -> Handle; + pub fn upload_mesh(&mut self, mesh: CpuMesh) -> Handle; +} + +// Milestone 3 +pub struct CameraView { pub view: glam::Mat4, pub proj: glam::Mat4, pub frustum: Frustum } +#[repr(transparent)] pub struct ViewId(u32); + +pub struct EntityRenderProxy<'a> { + pub world_from_object: glam::Mat4, + pub mesh: Handle, + pub material: Handle, + pub bounds_ws: Aabb, + pub user_data: &'a dyn std::any::Any, +} +``` + +### WGSL shader correctness (M2 baseline) + +- Use WGSL (portable across native/web). Uniforms must respect WGSL alignment; per-draw UBO struct padded to 256 bytes. +- Example (unlit color) minimal pair: +```wgsl +// globals @group(0) @binding(0) +struct Globals { view_proj: mat4x4 }; +@group(0) @binding(0) var G: Globals; + +struct VSIn { @location(0) pos: vec3; }; +struct VSOut { @builtin(position) pos: vec4; }; + +@vertex fn vs_main(input: VSIn) -> VSOut { + var out: VSOut; + out.pos = G.view_proj * vec4(input.pos, 1.0); + return out; +} + +@fragment fn fs_main() -> @location(0) vec4 { return vec4(1.0, 0.7, 0.3, 1.0); } +``` + + +### Milestones (incremental, debuggable) with API-correctness notes + +1) Bootstrap: Black Window + +- Expected result: Black frame; vsync honored; resizes OK. +- Client API: `Renderer::new_from_window_component(&wc)?;` run loop; no passes. +- Implementables: + - winit + wgpu init; capabilities-driven `SurfaceConfiguration`. + - Depth texture created but unused yet; recreate on resize. + - Surface error handling (Lost/Outdated/Timeout/OutOfMemory). +- wgpu correctness: + - Use `surface.get_capabilities(&adapter)` for format/present mode. + - Set `usage: RENDER_ATTACHMENT` and correct `alpha_mode`. + +2) Hello Mesh: Single Pass, Inline Draw + +- Expected result: Pill mesh or fallback triangle; fixed MVP (identity or simple ortho) hardcoded; depth test on. +- Client API: Create mesh + pipeline; `Inline` pass records one draw. +- Implementables: + - `ResourceManager`: create vertex/index buffers with `VERTEX|INDEX|COPY_DST` usages. + - Minimal pipeline: WGSL VS/FS, color target = surface format, depth = `Depth24Plus`. + - `DrawListBuilder` encodes: set pipeline, set vertex/index buffers, set bind groups, draw. +- wgpu correctness: + - Index format `Uint32` or `Uint16` consistent with buffer. + - Pipeline layout: explicit `BindGroupLayout` for globals even if empty; avoid implicit layout. + - Clear ops via `RenderPassColorAttachment` and depth attachment. + +3) Camera + FromView Culling + +- Expected result: Camera matrices applied; frustum culling removes offscreen entities. +- Client API: Build `CameraView`; `get_view_from_entities`; pass uses `FromView`. +- Implementables: + - CPU frustum planes; AABB test; build draw list from filtered entities. + - Per-entity MVP uniform placed in per-frame uniform buffer; 256B alignment. +- wgpu correctness: + - Uniform buffer alignment (offset multiple of 256); `BindGroupLayoutEntry` for uniform with `has_dynamic_offset: true`. + - Call `set_bind_group(group_index, &bg, &[offset])` matching the layout. + +4) Materials + Textures (set 1) + +- Expected result: Textured mesh with material BG; per-entity tint supported from UBO. +- Client API: `create_texture`, `create_bind_group(material_desc)`; assign material handle. +- Implementables: + - Texture creation with `TEXTURE_BINDING | COPY_DST`; view with correct `TextureViewDescriptor`. + - Sampler (filtering); material BG layout: `@group(1) @binding(0) texture_2d`, `@group(1) @binding(1) sampler`. + - FS samples sRGB texture; output to sRGB surface. +- wgpu correctness: + - Use sRGB surface format and sRGB textures for correct gamma; or convert in shader. + +5) Per-frame Ring Buffer + Dynamic Offsets + +- Expected result: No per-draw map/unmap; dynamic uniform offsets. +- Client API: `push_per_draw` returns aligned offset; pass offsets via `set_bind_groups_with_offsets`. +- Implementables: + - CPU staging buffer (MAP_WRITE) per frame; `queue.write_buffer` into GPU-visible uniform buffer. + - Layout entries with `has_dynamic_offset=true` only for dynamic bindings; offsets are multiples of 256. +- wgpu correctness: + - Avoid mapping GPU-only buffers; prefer `write_buffer` for simplicity. + +6) Multi-pass + Offscreen Targets + Quad Composition + +- Expected result: Offscreen render (T0) → FSQ/quads composite to swapchain; quads can place T0 anywhere in the framebuffer. +- Client API: Add two passes: `Texture` then `Swapchain`; post uses `Inline` to draw a quad (or fullscreen triangle) sampling T0 with UVs that map to desired target rect. +- Implementables: + - Offscreen target texture with `RENDER_ATTACHMENT | TEXTURE_BINDING`; correct view format; sampler. + - Composition shader samples T0; quad vertex data/uniforms carry position/size; no viewport/scissor used. +- wgpu correctness: + - Presentable texture cannot be sampled; offscreen must be separate texture. + +7) Resource Pools + Hot/Cold Split + +- Expected result: Opaque `Handle` pools; safe reuse; minimal hot metadata. +- Client API: Unchanged; `ResourceManager` returns handles. +- Implementables: + - Freelist + generation; hot (wgpu handles + light metadata) vs cold (descs, debug names). +- wgpu correctness: + - Keep GPU handles alive until all pass encoders referencing them are submitted. + +8) Draw List Optimization + +- Expected result: Fewer state changes; sorted by PSO/material. +- Client API: Optional sort keys; otherwise internal. +- Implementables: + - Build sort keys; stable sort; encode draws. +- wgpu correctness: + - Ensure binds set when changing PSO/material; match pipeline layouts. + +### Client usage snapshots + +- M1: just create renderer from `WindowComponent`. +- M2: create mesh + pipeline; one Inline pass to draw. +- M3: create `CameraView`, build `ViewId`, use `FromView`. +- M4–M6: add material BG + textures; add offscreen + post; use viewport rects. + +### Files to add later + +- `engine/pill_renderer/DESIGN.md` (this document) +- `engine/pill_renderer/src/renderer.rs` +- `engine/pill_renderer/src/pass.rs` +- `engine/pill_renderer/src/resources/*` +- `engine/pill_renderer/src/view.rs` + +### References + +- wgpu: `https://docs.rs/wgpu/latest/wgpu/` +- winit: `https://docs.rs/winit/latest/winit/` +- WGSL: `https://www.w3.org/TR/WGSL/` \ No newline at end of file diff --git a/engine/pill_engine/src/renderer/TALK_0.md b/engine/pill_engine/src/renderer/TALK_0.md new file mode 100644 index 00000000..11a88864 --- /dev/null +++ b/engine/pill_engine/src/renderer/TALK_0.md @@ -0,0 +1,1357 @@ +Hi, I am Sebastian Aaltonen. I have over 20 years of experience in graphics +programming. In the past have been working at Ubisoft and Unity building their cross +platform rendering technologies. + +I joined HypeHype one year ago with a mission to rewrite their mobile rendering +technology. Today I am going to be talking about the first milestone of that project: +rewriting the low level graphics API and the platform backends. + +HypeHype is a mobile game development platform. You create games directly on the +touchscreen and upload them to our cloud server. + +Gamers use a Tik Tok-style feed to browse the games. The games are instant +loading. This is a big technical challenge. Both the game binary size and the loading +code have to be highly optimized. To make the initial binary smaller, we store data in +highly compressed form and also lean on streaming. + +HypeHype has up to 8 player multiplayer. Multiplayer features and the player count +will increase in the future, once our cloud game server infrastructure is deployed. + +We have a full blown game editor inside the mobile app. Visual scripting system is +used for writing the game logic. Players can spectate creators creating games, and +multiple creators can collaboratively create games together at real time. It’s a bit like +Google Docs, but for game creation. Test play is instant and all the spectators join +multiplayer test session as players. This improves the iteration time drastically. + +We of course have a full set of social features, including chat, leaderboards, replays +and similar. + +HypeHype is mainly targeting mobile devices and tablets. But we also have a web +client and native PC and Mac applications. + +I have a console development background at Ubisoft, so I like to compare mobile +devices to the past console generations to understand them better. + +Xbox 360 and PS3 are nowadays equal to low-mid tier mobile devices in GPU +performance. This is excellent news, since these consoles offered the greatest visual +jump we have seen between console generations so far: We got HD output +resolution, and were able to implement proper HDR lighting pipelines, physically +based material models and image post processing for the first time. All of that is +possible today on mainstream mobile devices. And we can scale that down to bottom +tier devices at 30 fps with upscaling. + +When you look at the high end, you see latest 1000$+ phones reaching Xbox One +and PS4 levels of performance already. However these phones run at higher native +resolution and are thermally constrained, thus in reality, we can’t quite reach that +generation of visual fidelity yet on mobile devices in real games. And we don’t even +want to, since that would make the devices hot and drain their battery in couple of +hours. + +HypeHype games have been limited to simple visuals: Stylized untextured objects, +simple gamma space lighting and tiny scenes with short view distance. This has been +fine for simple hyper-casual games. + +This is however a big limitation to the platform, so we started building a new renderer +from the scratch one year ago. The visual fidelity target for the new renderer is to +match the best looking Xbox 360 and PS3 games. We will be bringing full PBR +pipeline with modern lighting, shadowing and post processing techniques. We will be +targeting larger game worlds and longer draw distances to allow more game genres +to be built properly on the platform. + +This is all nice of course, but we have to be really careful about the performance cost +of all these new improvements. We still want to be running HypeHype games at +locked 60 fps on mid tier mobile phones without throttling the devices. This is a big +concern for us and is the main reason why we are focusing heavily on performance in +our new rendering architecture. + +If you compare current mainstream phones with Xbox 360, you notice a lot of +similarities. + +Both designs have slow shared main memory. Bandwidth is the main limiting factor. +Both designs also employ techniques to reduce the memory bandwidth usage. The +most important one being on-chip storage for render targets. On Xbox 360 you had a +10MB EDRAM buffer for your whole render target. On mobile phones, you have a +smaller on-chip tile memory. Both technologies solve similar problems. Overdraw +doesn’t require extra memory bandwidth and Z-buffering and blending happens fully +on chip. On mobile phones, you also have framebuffer fetch, allowing you to load +back the previous pixel from the same render target location without a memory round +trip. The newer Xbox One console also was equipped with read-write ESRAM +allowing similar optimizations. + +Since the main memory is slow, you want to avoid resolving render targets as much +as possible. You want to minimize the amount of render passes. Doing multiple things +at once is the key to good performance. Modern mobile phones also have framebuffer +compression, to reduce the render target resolve and sampling bandwidth cost. This +is a good addition, but doesn’t fully solve the problem. ASTC texture compression +also helps. It offers better quality and smaller footprint than DXT5 back in the day. + +Mobile phones also have double rate fp16 math. This helps, since you don’t want to +lean on memory lookups on bandwidth starved devices. And there’s now better lower +precision HDR framebuffer formats available. + +But some old limitations still remain: Mobile GPUs are still designed around uniform +buffers. SSBO loads from dynamic addresses are still slow. If you are able to +scalarize your memory access patterns you hitting the performance sweet spot. This +limits the algorithms we are able to implement efficiently. Many mobile phones also +write vertex varyings to main memory, which costs significant amount of precious +bandwidth. Optimizing the size of the varyings is key for good performance on these +devices. + +I was talking about GPU-driven renderer already 8 years ago at SIGGRAPH, we +presented the core ideas such as the clustered rendering and the 2-pass occlusion +culling which have become a de-facto standard nowadays. + +Recently Nanite by Epic made GPU-driven rendering available for mainstream. They +combine V-buffer, material classification, analytic derivatives and software rasterizer +to make GPU-driven rendering robust enough for generic engine. + +However, there’s still a lot of unsolved performance problems with GPU-driven +rendering on mainstream mobile GPUs. + +Mobile GPUs are not optimized yet for SSBO loads. AMD and Nvidia optimized their +data paths couple of generations ago when they added ray-tracing. Ray-tracing +access patterns are dynamic and you can’t lean on tiny on-chip buffers for vertex +attributes anymore. We still need to wait for mobile GPUs with similar optimizations to +become mainstream. + +V-buffer requires you to run the vertex shader 3 times per pixel, and this includes +fetching all the vertex attributes of these 3 vertices. You also need to fetch all the +instance data and material data from dynamic location. This is over 20 non-uniform +memory loads in the pixel shader. Mobile chips aren’t simply designed for this kinds +of memory heavy workloads. + +No current mobile GPU supports framebuffer compression for compute shader writes. +Compute shader is the most efficient way to implement full screen material passes in +deferred V-buffer shading. If you do that on mobile, you waste a lot of bandwidth. + +64 bit atomics are commonly used in software rasterizers. You pack the Z value in +high bits and payload in low bits and let the atomic resolve the closest surface. +There’s no 64 bit atomic support in mobile GPUs. SampleGrad is also slow. ⅛ rate or +even slower. Which makes deferred texturing with analytic gradients quite costly. And +there support for wave intrinsics is spotty and you even have emulated groupshared +memory on some low end devices. + +As a result, traditional CPU-based rendering is still the best be for mainstream mobile +phones today. We could do 10,000 draw calls on Xbox 360 back in the day at 60 fps. +To reach that goal on mobile devices today, we need to write very well optimized +rendering code. + +Let’s talk about our roadmap. + +We split the renderer rewrite into two stages. First we rewrote the low level gfx API +and all the platform specific backend code. In order to run both the old and the new +backends in tandem, we introduce a minimal wrapper with ifdefs, so that we can keep +shipping the old rendering code and switch between the new and the old to compare +them. We have already deleted 200 files of old rendering code and recently started +tearing down the wrapper and replacing it with direct calls to the new platform API. + +This presentation will focus on the low level platform API and the backends. I will be +later talking about our new high level rendering code. Our design allows us to refactor +these pieces completely independently of each other. I will come back to that topic +later in the presentation. + +The first thing we need to decide is the platform abstraction level. What code is +platform specific and what code is platform independent. + +Game engines generally limit platform specific code to the lowest levels of the stack. +This minimizes the amount of code that is platform specific and reduces the +implementation and maintenance cost. However some engine and renderer specifics +tend to leak to the lowest level platform code. + +If you look at mobile apps, the platforms specific code tends to reach a bit higher +levels in the stack. For example the popular Google Flutter app framework is +developed by multiple platform specific teams. They usually ship new features first on +mobiles and later to desktops. Android and iOS don’t have full feature parity either. +Their high level rendering code is different on desktop and mobile platforms, including +Mac and iOS, even though they both use the same Metal API. + +Many mobile apps bring the code separation even higher. There’s often completely +separate iOS and Android team with their dedicated code bases. Most of the business +logic in these apps tends to be running in a cloud server, which is of course shared +and maintained by a third team. + +HypeHype is a real time game engine, so we of course need to have all of our world +state locally. Games must run identically on all devices and cross play must work +across all devices. The old HypeHype gfx code base had duplicated shaders for +Metal and some duplicated higher level code as well. This bloated the test matrix, +added maintenance cost and made adding new features slow. This was the first thing +I wanted to solve. The goal was to bring the platform API to even lower level +compared to existing game engines. + +We want as small amount of platform specific code as possible. This leads to a +design where we tightly wrap the existing low level gfx APIs. + +The design work started by cross referencing Vulkan, Metal and WebGPU docs. I was +already familiar with all of these APIs, which made the work easier and less error +prone. + +When writing a wrapper, you first want to find the common set of features. These are +often straightforward to wrap. The difficulties arise when there’s differences in the API +design. Care must be taken in order to abstract these differences in a way that is +performance optimal. We chose to use Metal 2.0 because it’s closer to Vulkan and +WebGPU and it provides placement heaps, argument buffers and manual fences, +allowing us to extract a bit more performance out of Apple devices too. We also +support MoltenVK to make cross platform development easier, but we don’t ship it +since our Metal 2.0 backend is roughly 40% faster for CPU. + +In order to make the API more compact, we trim all deprecated stuff that nobody uses +anymore. These things were failed experiments that never lived up to our +expectations regarding performance. Vertex buffers are an interesting topic. At +Ubisoft, we deprecated vertex buffers in our GPU-driven renderer already 8 years + +ago. But at HypeHype we still support vertex buffers, since some mobile GPU shader +compilers generate better code for them. Also we are still using WebGL2 in our web +client, since WebGPU coverage is not yet good enough. I will likely be removing +vertex buffers from the API in a few years. + +Single set of shaders is crucial for tech artist productivity. We use modern open +source tools such as SPIRV-Cross to cross compile our shaders to all target +platforms. + +Let’s talk about the design goals of this new platform API. + +First, we want it to be a standalone library. Designed and maintained independently +from the HypeHype engine. It needs to have an stable API that doesn’t change often. + +I have seen a lot of graphics platform abstractions during my career and the problem +in most abstractions is that user land concepts creep into the hardware API. Having a +mesh and material in the platform code is the most common issue. This is problematic +since mesh and material both have change pressure. Meshlets and bindless textures +are the future. We don’t want to commit to a certain way of presenting them. Mesh +can be simply represented as an index buffer binding + N vertex buffer bindings, and +material can be represented as a bind group containing multiple texture descriptors +and a buffer for value data. + +Automatic the uniform handling might feel like a good idea in the beginning, but +eventually you want to add stuff like geometry instancing and now you need to +refactor your backend code to change the data layout. Or even worse, add a new fast +path to complicate the API. And eventually you add a new fast path for GPU-driven +rendering too, bloating the API further. In our design, the user land code is +responsible for setting up all the data! + +Zero extra API overhead is another crucial design core pillar for us. The platform +interface should not add significant cost. It should be as easy to use as DX11, but +always as efficient as hand written optimized DX12. A wrong solution is to copy the +DX11 API as is. This way you end up emulating the DX11 driver in your code base, +and rest assured that Nvidia and AMD does this better than your team. Thus your +modern bankends are slower than DX11. The reason for this is that you have too fine +grained inputs, too fine grained render state, lots of shadow state and data copies. +PSO and render state tracking and caching is a big performance drain, and slow +software command buffer design usually adds to the cost. + +So, we have very strict performance standards for our API, but at the same time we +want it to be as easy to use as DX11. How can we achieve this? + +We need a good process for designing the API. + +The traditional way would be to spend months researching the API documentations +and writing a big technical design document describing the new API in detail, splitting +in into tasks and estimating the implementation time for each task for each backend. + +The issue with this approach is that you lock in the design too early and it’s hard to +change later. Small nitty gritty details matter a lot in platform specific graphics code. +You can’t really understand the performance impact of all the corner cases without +writing any code. Now you will notice those issues when there’s a lot of production +ready code written. It’s too hard to justify a full rewrite of plans and code at this point. + +Agile test driven development has the opposite problem. You are focusing on what +you need in the next sprints. You implement small independent pieces of code that +have full test coverage. The assumption is that once you put these pieces together, +you have good architecture. But you didn’t even do any architecture design. More +pieces equals more interfaces, equals more communication overhead. It is difficult to + +reach optimal performance with this kind of programming practice. And it’s even more +difficult to throw away lots of production ready code with full test coverage and lots of +story points spent once you notice that the architecture needs a big overhaul to meet +the performance goals. + +Our solution for this problem is to use a highly iterative design process. + +I start by writing mock user land code. I use my prior expertise and start writing my +dream graphics code, assuming I have the perfect API. That API doesn’t exist yet, but +I keep writing mock code until I am happy with it. I write code for creating all the +resources I need for rendering, textures, shaders, buffers and so and then I write a +small draw loop using these resources. The draw loop is called multiple times, with +some resources mutated to implement animation. It’s important to design both +dynamic and static data paths early. + +Once I am happy with the first iteration of the user land code, I write a mock platform +API for it. This is just a hollow API at this point. There’s no backend implementation. +But it allows me so start using the compiler to do syntax checking and autocomplete. +Now I can really start experimenting with the API to see how good it feels to use. I will +of course refactor all the time when I find even slightest need for it. I add missing +mock use cases and go through the Vulkan, Metal and WebGPU API docs to ensure +that I have not missed anything important. + +Then I will do a performance check for all the user land code. I have a good +understanding how all the platform APIs work, so I think what kind of implementation + +each API call would require in Vulkan and Metal and WebGPU backends. If the +implementation is trivial then it’s fine. If the implementation requires extra data copies, +hash map lookups, memory allocations or other expensive operations, then I scrap +that design and rewrite that part of the API to be more efficient. As you remember, our +goal is to be as fast as hand optimized DX12 in every single case. We can’t do that if +our API doesn’t map perfectly to the underlying hardware API. + +Once we are happy with the performance of our mock code, we start implementing +the backends. We of course notice nitty gritty details that we missed during this +process and immediately refactor the mock code and the mock API when this +happens. We don’t write heavy test suite just yet, as that would slow down our +iteration time. Instead we are leaning on Vulkan and Metal validation layers to provide +us thousands of test cases for free. We hook the validation layer error callback to our +automated tests to ensure our code keeps functioning when we refactor it. + +The last topic about API design I wanted to discuss today is doing things at right +frequency and granularity. + +A big problem in rendering code tends to be that expensive operations are done at +too high frequency. This also tends to add tracking cost to the hot draw loop. + +Games have a lot of temporal coherency. You load the game world and you slowly +mutate it every frame. Most of the data stays the same. Also the camera is most of +the time moving slowly. Human brain needs temporal coherency between the frames +to see smooth movement. This is great for us! We want to exploit it! + +Let’s take a look at all the stuff happening: Loading the game world and all the shader +PSOs happens in the beginning. If you have a larger level, you also load textures, +meshes and materials when you move around. Most of the objects are spawned in +the beginning, but sections of the level might be spawned during streaming, enemies, +loot, projectiles, etc are generally spawned throughout the game, but not that many +every frame. The only really high frequency operation is culling all the objects and +drawing the visible objects. The culling and draw loops are the most time sensitive +loops in your whole code base. + +I have highlighted problem cases with red. People tend to do processing related to +these inside their hot draw loop. + +Modifying material bindings is not common. How often do you replace the normal +map of an already loaded material? How often do you change the shader that’s used +to render an object? How often you change the render state that is used to render an +object? Pretty much never, except in some special effects. Animating object color and +object transform are more common operations. A small subset of objects animate +every frame. We only want to pay for these things when they happen. Not for every +draw call. + +Our solution for this problem is to fully separate all data modification from drawing. All +the data is ready before the draw loop. + +Pipeline state objects (PSO) should be built at application startup or at level load time. +Building PSOs at runtime causes stuttering. In our philosophy shader variants are +authored by coders and tech artists and hand optimized. There’s only a limited +amount of them. This is similar to that id-Software does and provides very good +performance. + +We store the PSO handle directly to each object’s visual component. We don’t need +hash map lookups to obtain it every frame. + +We precreate all the bind groups (descriptor sets). Material descriptor sets contain all +their textures and buffer for value data. We store the material bind group handle to +objects visual component. This avoids a hashmap lookup and makes it possible to +efficiently change the material bindings with a single Vulkan, Metal and WebGPU +command. + +Separating persistent and dynamic data is important. Persistent data is uploaded at +startup and delta updated when changed. I had a talk about this topic two years ago + +at REAC 2011. You can refer to that presentation if you want more information about +that topic. + +Dynamic data should be batch uploaded once per pass instead of using map/unmap +per draw call. Global data should be separated from per-draw data to minimize the +wasted bandwidth cost. + +Resource synchronization is a big CPU cost in many engines. Our current solution is +simple: When a render pass begins, we transition the render target to writable layout. +At the end of the render pass, we transition it back to sampled texture layout. This +way all the textures (both static and dynamic) are always in sampler readable layout. +We never need to do per-draw call resource tracking at all. This saves significant +amount of CPU cycles. + +And now we are ready to talk about the implementation details. + +A renderer needs textures, buffers, shaders and several other resource objects. We +need a good way to store these objects and ensure they are safe to use. + +The modern C++ practice would be to use smart pointers, reference counting and +RAII (resource acquisition is initialization). + +Frankly, these are too slow for us. Reference counted smart pointers tie the life time +of a reference and the backing memory together. This results in a lot of small memory +allocations. Memory allocations are expensive in current highly multithreaded +systems. Allocations are also randomly scattered around the system memory, making +data access patterns worse and increasing the cache misses. Copying reference +counted smart pointers requires two atomics (add, sub), since we are in a +multithreaded system. Ownership can be shared between threads. + +There’s also safety issues. Ref counting makes object lifetime vague. Hard to reason +about. It might die in any thread. RAII objects such as listeners cause destructors to +have side effects. Example: Object ref count runs in another thread, destructor of +listener de-registers it from an array. Another thread is just iterating that array. + +CRASH! To avoid this crash, you have to protect part of the destructor with a mutex. +This means that every time you delete an object, you need a mutex lock. This is very +expensive. HypeHype is loading and unloading games rapidly in the feed. We can’t +afford slow loading and tear down code! + +Our solution for this problem (and most other problems too) is to use arrays! + +There’s one big allocation containing all objects of the same type. Array index is a +surprisingly nice data handle. If I have an array of textures, I can simply ask texture at +index 4. Index is POD data. It’s trivial to copy around. I can pass it to worker threads +safely too. As long as that thread doesn’t have access to the array, they can do +nothing dangerous with the indices. This allows us to write culling and draw stream +generation tasks with no safety concerns. These threads simply take array indices +from one place and combine them to form draw calls. No access to the data arrays is +needed at all. + +But there’s a critical flaw: An array index doesn’t guarantee object lifetime. We of +course reuse slots in our data arrays. Data could have died and slot reused... + +To solve the problem, we need to replace our array indices with generational handles. +Let’s discuss what this means. + +A pool is similar to our data array. It is an typed array of objects, but now it also has +an additional generation counter array. Generation counter tells how many times the +slot has been reused. Counter is increased when the current data in that slot is freed. + +We also have a freelist in the pool. A freelist is simply a linear array containing the +indices of each free slots. It has stack semantics. When you allocate a new object, +you pop a free index from the top. When you delete an object you push the index of +the freed slot on top of the free list. These are both fast O(1) operations. When the +freelist runs out, we double the size of the pool. This is safe as nobody is allowed to +have direct pointer references to the data in the array. All references are done using +handles. + +The handle is just a POD struct. It contains the array index just like in the previous +slide, but now we also have the generation counter next to it. This is in total 32 bit (for +example 16+16 bit split) or 64 bits (32 + 32 bit), depending how many simultaneously +active resources you require and how short are the lifetimes of the objects. In +HypeHype we use 32 bit (16+16) handles for all graphics resources, this is enough for + +65536 resources of each particular type. + +Pool offers a getter API, which takes the handle as a parameter. It reads pool’s +generation counter array at handle index and compares it to the handle’s generation +counter. If they match, you get the data. If they don’t you get a null pointer. + +This results in weak reference semantics. It’s completely safe to use stale handles. +You just get a null pointer back. A null check is a predictable branch, and is almost +free on modern CPUs. The branch prediction fails once when the handle is deleted. +At that point you clean up yourself too. Weak references result in coding practice that +doesn’t required callbacks. Callbacks require buffering or mutexes to avoid race +conditions in multithreaded systems. + +One of our main goals was to make the API as easy to use as DX11. This requires us +to bundle auxiliary data in our data structs in the pools. In Vulkan the VkTexture +handle doesn’t know anything about itself, which is annoying if you try to write your +rendering code in pure Vulkan. We want our texture struct to know it’s size, format, +data pointer for writing, allocator for deleting it and so forth. + +This auxiliary data is required for low frequency tasks such as modifying the resource +and deleting the resource. Since our design principle is to separate resource +modifications from drawing, we are accessing this data only when the resource is +modified or deleted. This means that putting the auxiliary data in the same struct as +the data required in the hot draw loop is not cache efficient. The draw loop will load +data to L1$ that is not used. I hate trade-offs between performance and usability. + +Our solution for this problem is to use SoA layout inside the pools. We identify which +data is required every frame in the hot draw loops and put that data in one struct and +the remaining low frequency auxiliary data in another struct. The pool now has two +data arrays instead of one. We can use the same array index in the handle to access +either of the data arrays (or both). This way we only need to load the hot data to +caches in the performance critical draw loop. The auxiliary data struct is only loaded +at low frequency, solving our performance issue with L1$ cache utilization. + +Now we have a good way to store and refer to graphics resources. The next topic is +creating the resources. + +Creating graphics resources in Vulkan and DX12 is cumbersome. You need to fill big +structs that contain other big structs. Some of these structs also contain pointers to +arrays of structs too. This makes it possible to shoot yourself in the foot with +temporary object life times. + +The most common existing solution for this problem is using builder pattern for +resource descriptors: The builder object contains good default state for the descriptor. +It offers an API to mutate itself to set all the fields you want to change. Once you are +ready, you call build function to get the final descriptor struct. This is easy to use, but +the codegen, especially in debug mode is far from perfect. At HypeHype we use +debug mode a lot during development, so we want it to be fast too. + +Our solution for this problem is to use C++20 designated struct initializers in +combination with C++11 struct aggregate initialization. These two features in +combination allow us to set default values to each struct in a trivial way. Look at the +code example box below. If you want to override one of these defaults, you use the +designated struct initializer syntax to override the values of named fields. The syntax + +is super clean and codegen is perfect. + +To solve the array data cleanly, we have to write our own span class. C++20 built-in +span class doesn’t support initializer lists, because initializer lists have very short life +time. They die immediately after the statement. It was too dangerous to allow putting +initializer list inside a span in the generic case. However we use this only in a special +case, and we have a solution for it: C++ const && function parameter only accepts +temporary unnamed objects. C++ guarantees that temporary objects in function +parameter list live long enough to finish the function call. This gives us enough +guarantees to safely store initializer lists inside spans in our resource descriptor +structs. + +And this is how it looks in practice. + +Let’s start with the left side: First we are creating a vertex buffer and a texture. The +syntax here is nice and we are only declaring fields that differ from the struct default +values. + +If you look at the bottom left, you see us declaring a material. This is a bind group. +The bind group has an array of textures: albedo, normal and properties. We are using +initializer list here to provide the array. This makes the syntax super clean. And it’s +worth noting that this array doesn’t require any heap allocations. The initializer list and +the whole descriptor struct lives in the stack. It is never copied. We just pass a +reference to it in the resource creation function call. This is as fast as raw DX12 or +raw Vulkan. + +One the right side you see us initializing a more complex resource. This looks a bit +like json. We have named fields, arrays and fields and arrays inside each other with +proper indentation. This is much more easier to write and read compared to raw DX12 +and Vulkan. Yet still we pay no runtime cost. There’s still no memory allocations or +data copies. Everything is pure stack data. + +Now that we have a good way to create and store resources, we need to allocate +GPU memory for them. + +I prefer to use temporary memory whenever possible. Temporary memory doesn’t +fragment your memory pools and allocating it is as simple as adding a number to a +counter. + +We use 128MB memory heaps in our bump allocator. The heaps are stored in a ring. +If the bump allocator reaches the tail, we allocate a new heap block. Once we reach +stable state, there’s no heap allocations happening at all. We create a platform +specific buffer handle for each GPU heap we create. This buffer handle maps the +whole heap. This way we don’t need to create platform specific buffer objects at +runtime. Our buffer struct simply contains an heap index and an offset. It’s super +efficient to construct them at runtime and pass to user. + +As an extra optimization, we provide the user land a concrete bump allocator object. +This has a function to allocate N bytes. This function inlines perfectly to the caller. It +simply increments a counter and then tests whether the counter is over the heap +block boundary. This check is a predictable branch. When the block runs out, we call +a virtual function in th gfx API to obtain the new temp allocator block. This happens + +only once for 128MB of data, making it highly efficient. + +Since WebGPU doesn’t yet have 100% coverage, we had to add WebGL2 support +during the project. We use the same temp allocator abstraction for WebGL2. User +land code doesn’t need to know whether the returned pointer is a CPU pointer or a +GPU pointer. In WebGL2 we use 8MB CPU side temp buffers and we copy these +buffers using a single glBufferSubData at beginning of each render pass. This +amortizes the cost of data updates, and is a big performance win over calling +map/unmap per draw call. + +We do persistent allocations only when needed, since persistent allocation is always +much slower than temporary. + +I implemented a two-level segregated fit algorithm. This is O(1) hard real time +allocator. It uses a two level bitfield and two lzcnt instructions to find the bin. Bin size +classes follow floating point distribution. This guarantees that overhead percentage is +always small, independent of the size class. Delete operation is similar to allocate. +But in addition you are checking neighbor pointers on both sides and merging empty +memory regions. This is also O(1). + +We use the same allocator for both Vulkan and Metal 2.0 (placement heaps). I open +sourced the offset allocator. It can be used for sub-allocating GPU heaps or buffers, +and generally anything that requires a contiguous range of elements (and doesn’t +require CPU memory backing for embedded metadata). + +One of the biggest differences our design has towards other renderers is user land +bind groups (descriptor sets in Vulkan terminology). + +The traditional way is to have separate bindings for each texture and buffer. Before +drawing you set all bindings separately. The gfx backend has to combine these +bindings in shader specific layout and create respective bind groups (WebGPU), +descriptor sets (Vulkan), argument buffers (Metal) or descriptor tables (DX12). These +bind group objects are GPU objects and are expensive to create. IHVs recommend +you to precreate all GPU objects to avoid stalls and memory fragmentation issues. + +The common workaround is to cache bind groups in a hash map in the backend. All +bindings are hashed and a lookup is made. If the bind group exists, then it is reused +instead of created. The problem with this approach is that hashing is expensive and +hash map lookups randomize your memory access pattern. If you are rendering from +multiple threads, you might even need to protect your bind group hash with a mutex, +making it even more expensive. + +Our solution is to bring bind groups directly to user land: User creates immutable bind +groups ahead of time. For example a material bind group contains 5 textures and one +uniform buffer (filled with value data). You get a handle, which you use to bind the + +material. + +Our draw call API exposes three bind group slots to the user land. Vulkan on Android +and WebGPU mandate minimum of four bind group slots. Three first groups are +exposed directly to use land code, matching the GLSL set=X semantics. This is easy +for gfx programmers and tech artists to understand. + +HypeHype higher level rendering code uses an convention to split data to bind groups +by binding frequency. The first group has render pass global bindings (sun light, +camera matrices, shadow maps, etc), the second slot has material bindings, third slot +has shader specific bindings and the last slot is special. + +We use the last slot in Vulkan and WebGPU for dynamic offset bound buffers. This is +important for bump allocated temporary data, such as uniform buffers. Metal API +doesn’t have similar offset update API for argument buffer buffer bindings. Instead we +use Metal setBuffer API to set these dynamic buffers separately, and use setOffset +API to change their offset. This provides an abstraction that uses the most efficient +code paths on all platform APIs. + +Push constants are emulated on some mobile GPUs. It’s faster to bump allocate your +uniforms and just change the offset. + +I already said that software command buffers are slow, yet we have one :) + +This software command buffer is entirely different to the ones most people are familiar +with. We don’t have any data in the software command buffer. We only have +metadata pointing to already uploaded data. The metadata is also grouped, making it +much smaller than individual bindings and individual state. This allows us to represent +a draw call with only 64 bytes of data, which is just a single CPU cache line. + +Our initial design was to use an array of draw structs. The draw struct contains +handles to the shader (this is a resolved PSO variant including all render state), 3 +user land bind groups, dynamic buffers (for temp allocated offset bound data), index +and vertex buffers and some offsets. Offsets are needed because sub-allocating +resources is usually a big performance win. + +This 64 byte struct is already pretty good, but I wanted to improve it further. I analyzed +the data and noticed that all fields are 32 bits. Optimized rendering uses sorted order +to minimize the costly PSO and render state transitions. When rendering binned +content we notice that most fields don’t change between draw calls. On average only +18 bytes change between the draws. We want to take advantage of this. + +The idea is to store only the fields that change. This leads to a draw stream design. + +We store a 32 bit bitmask in front of each draw call. This bit mask tells which fields in +the draw struct have changed. + +It’s the responsibility of the user land code to write data according to the stream data +API contract. For this we have user land draw stream writer class. It contains a single +draw struct describing the current state and a dirty mask. The draw stream writer +provides an function for setting each field in the struct. These functions check whether +the data value was changed. If yes, then set the corresponding dirty bit and write that +field to the stream. After writing all fields the user calls draw, which simply writes the +dirty bitmask in front of the data values. + +The backend is simple: For each draw call it reads the dirty bitmask. Then it reads +one uint32 from the stream for each bit and calls the corresponding platform API call +to set that binding/state/value. The advantage of this design is that the backend +doesn’t need any state filtering. We have already done that in the user land code. +This is handy on platforms where secondary command buffers are not available or +are slow (some Qualcomm GPUs disable optimizations with secondary command +buffers). We can still generate draw stream using multiple worker threads and offload + +the state filtering cost there. The render thread is as fast as possible, which is a big +win since the platform API calls are slow on mobile devices. We also save roughly 3x +bandwidth versus full blown 64 byte structs. + +Let’s talk about draw call performance. + +This slide represents a quite traditional DX11 and OpenGL style draw loop. For each +draw call we call map/unmap and write uniforms separately. We also bind vertex +buffer and index buffer and we bind our textures and buffers. Here I am simply +binding set 2 (material) and set 3 following the conventions we have. + +In total this is 6 to 7 API calls per draw call. 6 calls when the material doesn’t change +and 7 otherwise. If we bin by material, then we can assume that the number is closer +to 6 than 7. + +This is using the temp allocator to bump allocate uniforms (and other dynamic data). +Now we don’t need to call map/unmap per draw call. This reduces the API call count +to 4-5 per draw call. + +Map/unmap are surprisingly expensive calls. Our old GLES backend was uploading +uniforms per draw call. The biggest difference in our new GLES3 backend (WebGL2) +was the lack of map/unmap per draw and this change alone got around 3x CPU +performance gain for us. + +We didn’t implement per draw map/unmap to our new Vulkan backend (Vulkan +supports persistent mapping), so I can’t unfortunately show you Vulkan numbers +here. + +The next optimization with big impact was packing meshes. We allocate big 128MB +heap blocks and have one platform buffer handle for each. This makes it easy for us +to sub-allocate meshes and simply change the base vertex and base index in each +draw call to change the mesh. + +This way we get rid of two API calls: set vertex buffer and set index buffer. We are +down to 2-3 API calls per draw, which is very nice! + +This optimization improved the CPU throughput on all devices. We saw biggest gains +on desktop GPUs (close to 2x), but mobile GPUs also showed notable gains (30%- +40%). + +The last optimization I want to discuss is base instance. + +Base instance drawing uses identical data layout as instancing uses. You use tightly +packed array of draw data. On mobile uniform buffers have 16KB binding size +limitation. The idea is to change the binding offset once per 16KB, amortizing the cost +of rebinding the temp allocator buffer with a different offset. This cuts our API call +count by 1 and we now have optimal amount of API calls: just the draw itself and the +possible material bind group change. The draw call has base instance parameter, +which we change to point to different slot in the uniform buffer data array. + +So why not use instancing instead? Base instance results in better shader codegen +on many platforms. The reason is that instance ID is dynamic offset. GPUs pack +multiple instances in the same vertex wave, meaning that all data indexed by instance +ID must use vector registers and vector loads. This is a lot of extra register bloat for +loading 4x4 matrices and similar. Base instance on the other hand is a static per-draw +offset. Every lane loads from the same location. This means that compilers can scalar +code paths and/or use fast constant buffer hardware. + +In practice however, we run into various issues. While the base instance codegen is +perfect on PC, on mobile GPUs it’s a mixed bag. Some drivers simply don’t optimize + +this properly. Also this feature has poor coverage. DX12 doesn’t support base +instance at all and WebGL and WebGPU also have no support. So I wouldn’t +recommend this optimization, unless you are shipping only on desktop. Not worth it +for mobiles. + +Let’s take a look at the performance numbers. + +This is using a single render thread. Ten thousand actual draw calls without any +instancing tricks. Each draw call using an unique mesh and unique material. With +bind groups and packed meshes it’s fast to change the material and the mesh. + +I didn’t have time to implement GPU-persistent scene data yet in HypeHype. These +numbers are with per-draw bump allocated uniforms, as described in the previous +slides. + +We are targeting 10k draw calls because that’s what we managed to push 15 years +ago with Xbox 360 at 60 fps. And the results are impressive. Even the low end 99$ +Android phones are close to hitting 60 fps in this stress test. In a real kit bashed UGC +game scene we will have lots of repeated meshes and materials, allowing batching +and reduction in gfx API call counts. We also intend to multithread the rendering. + +On AMD’s modern integrated GPUs (found also in the Steam Deck and ROG Ally +handheld) our renderer can push 10k draws in less than one millisecond. When +multithreading is used, our renderer could push up to 1 million draw calls at 60 fps on +modern AMD and Nvidia GPUs. + +--- + +## SIGGRAPH 2023 Presentation Slides + +### Thin low level gfx API wrapper +- Cross reference Vulkan, Metal and WebGPU docs +- Find the common set of features and differences +- Design performance optimal way to abstract the differences +- Metal 2.0: Placement heaps, argument buffers, fences + - MoltenVK for debugging. Our Metal 2.0 backend is ~40% faster (CPU) + +### Trim deprecated stuff +- Transform feedback, strips, fans +- Geometry shaders, HW tessellation +- Vertex buffers? + - Some mobile devices still benefit (shader codegen) and WebGL2! + +### Single set of shaders +- GLSL and use SPIRV-Cross to cross compile [4] + +--- + +## Hype → NEW Architecture + +``` ++---------------------------------+ +| Business logic | ++---------------------------------+ +| Data model | ++---------------------------------+ +| High level rendering | ++---------------------------------+ +| Shaders | ++---------------------------------+ +| Low level rendering | ++---------------------------------+ +| GFX API calls | ++---------------------------------+ +``` + +--- + +## Platform API Design Goals + +**Standalone library** +- Independently designed and maintained. Stable API +**Avoid higher level concepts creeping into the API** +- No mesh & material: Can be represented as IB+VBs and bind groups +- No automatic data setup or forced data layout +- No fixed draw algorithm: Traditional, instancing. Future = GPU-driven +- **User land code responsible for setting up all the data!** +**"Zero" extra API overhead** +- Design core pillar: As easy as DX11, as fast as optimized DX12 +- Wrong solution: Implement DX11 driver in your code base + - Fine-grained inputs, render state, shadow state, copies + - PSO + render state caching, bind group caching (hash tables) + - Software command buffers + +**Architecture Diagram:** +``` ++---------------------------------+ +| High level rendering code | +| | +| Changes frequently | ++---------------------------------+ + | + | ++---------------------------------+ +| Platform | +| API | ++---------------------------------+ <-- Standalone library + | + | ++---------------------------------+ +| Backends | +| | +| Vulkan. OpenGL ES 3.0 | +| M WebGL. 2.0 | +| WebGPU | ++---------------------------------+ +``` + +--- + +## Our Solution: Separate Data Modification from Drawing + +**PSOs** +- Build all pipelines (all render state combinations) at application startup. Doable since our PSO count is low +- Store the PSO handle to each objects visual component + +**Bind groups (descriptor sets)** +- Create a bind group per material at level load: Contains all texture and buffer bindings +- Store the material bind group handle to each objects visual component +- Changing the material = a single Vulkan, Metal, WebGPU command + +**Data upload** +- **Persistent data:** Upload once at startup. Delta update when data changes. [5] +- **Dynamic Data:** + - Batch upload whole pass: No per-draw map & unmap + - Separate by frequency: Per pass | per draw + +**Resource synchronization** +- Render pass: RT texture transitioned to write and then read +- No state tracking per draw call + +--- + +## Fast & Safe Object Storage & Lifetime Tracking + +**Modern practices: Smart pointers, ref counting and RAII [6]?** +- Too slow: Memory alloc per object. Scatters data around the memory (cache misses). Copy pointer is 2x atomics +- Safety issues: Ref count runs out → RAII side effect → invalidates iterator (another thread). Mutex is expensive! + +**Our solution: Arrays!** +- One big allocation for all objects of the same type +- Array index is a nice data handle + - POD. Trivial to copy and pass around +- Safe to pass to worker threads + - Can't dereference an array index. Needs access to the array +- **PROBLEM:** Old handles referring an array slot that has been reused? + +--- + +## Generational Pools and Handles + +**Pool** +- Typed array of objects + - Every array slot has a generation counter + - Counter is increased when the slot is freed +- Freelist for slot reuse + - An array (stack) of unused pool indices + - Delete object = push index + - Create object = pop index. Resize if needed (no ptrs → safe) + +**Handle** +- POD struct: Array index + generation counter (32/64 bits) +- `pool.get(handle)`: Compare generations. Not match? → return null +- Typed `Handle`. Pool has the same handle type. T is forward declared + +**Weak reference semantics** +- Null check (predictable branch) is almost free on modern CPUs +- Much better than callbacks in multithreaded systems. No races / mutexes! + +**Diagram:** +``` +Pool: ++-----+-----+-----+-----+-----+-----+ +| 0 | 1 | 2 | 3 | 4 | 5 | <-- Indices ++-----+-----+-----+-----+-----+-----+ +|data |data |data |data |data |data | <-- Data ++-----+-----+-----+-----+-----+-----+ +| 1 | 1 | 1 | 5 | 2 | 1 | <-- Generation Counter ++-----+-----+-----+-----+-----+-----+ + | + | + V +Handle: ++-------+-----+ +| index | gen | ++-------+-----+ +| 4 | 2 | ++-------+-----+ + match! +``` + +--- + +## Hot vs Cold Data + +**Easy to use API needs auxiliary data** +- Texture can't be just a VkTexture or MTL::Texture +- Additional data: size, format, data ptr, allocator... +- Needed for low frequency tasks: + - Update, readback, sync, create dependent resources, free memory + +**Rendering needs only the hot data** +- Auxiliary data bloats the struct → critical draw loop L1$ suffers +- Hate compromising performance and usability :( + +**Our solution: Split hot at cold data inside the pool** +- Pool has two types and two arrays: Hot and cold +- Both can be accessed with the same handle (using the same array index) +- Split hot and cold data (investigate). Compromise avoided! + +**SoA Layout Chart:** +``` + 0 1 2 3 4 5 + +-----+-----+-----+-----+-----+-----+ +Hot | hot | hot | hot | hot | hot | hot | (Red row) + +-----+-----+-----+-----+-----+-----+ +Cold| cold| cold| cold| cold| cold| cold| (Blue row) + +-----+-----+-----+-----+-----+-----+ +Gen | 1 | 1 | 1 | 5 | 2 | 1 | (Green row) + +-----+-----+-----+-----+-----+-----+ +``` + +**Lookup Mechanism:** +``` ++-------+-------+ +| index | gen | ++-------+-------+ +| 4 | 2 | (The '2' in 'gen' is highlighted) ++-------+-------+ +``` +Next to this lookup box: "match!" + +--- + +## Fast & Clean C++20 API for Resource Construction + +**Problem Statement:** +- Vulkan and DirectX use big structs to initialize complex resources + - Structs contain other structs and non-owning pointer references to arrays of structs + - Code bloat. No default values. Lifetime of temporary objects causes bugs + +**Existing Solutions:** +- Builder pattern: Debug perf is horrible. Release codegen not optimal either + +**Proposed Solution:** +- Our solution: Use C++20 designated struct initializers [8] + - The best C99 feature finally in C++. Waited 11 years! + - Default values: + - Provided by C++11 struct aggregate initialization + - Extremely clean syntax. Best readability + - Array data? + - Custom span that supports initializer lists + - Safety: const && parameter forces temporaries + +**Struct Definition Example:** +```cpp +struct BufferDesc +{ + const char* debugName = nullptr; + uint32 byteSize = 0; + USAGE usage = USAGE_UNIFORM; + MEMORY memory = MEMORY::CPU; + f::Span initialData; +}; +``` + +--- + +## Resource Construction Examples + +**Buffer Creation:** +```cpp +Handle vertexBuffer = rm->createBuffer({ + .debugName = "cube", + .byteSize = vertexSize * vertexAmo, + .usage = BufferDesc::USAGE_VERTEX, + .memory = MEMORY::GPU_CPU +}); +``` + +**Texture Creation:** +```cpp +Handle texture = rm->createTexture({ + .debugName = "lion.png", + .dimensions = Vector3I(256, 256, 1), + .format = FORMAT::RGBA8_SRGB, + .initialData = Span((uint8*)data, dataSize) +}); +``` + +**BindGroup Creation:** +```cpp +Handle material = m_rm->createBindGroup({ + .debugName = "Car Paint", + .layout = materialBindingsLayout, + .textures = { albedo, normal, properties }, + .buffers = {{.buffer = uniforms, .byteOffset = 64}} +}); +``` + +**Shader/Render Pipeline Creation:** +```cpp +m_shader = rm->createShader({ + .debugName = "mesh_simple", + .VS { .byteCode = shaderVS, .entryFunc = "main" }, + .PS { .byteCode = shaderPS, .entryFunc = "main" }, + .bindGroups = { + { m_globalsBindingsLayout }, // Globals bind group (0) + { materialBindingsLayout }, // Material bind group (1) + }, + .dynamicBuffers = dynamicBindings.getLayout(), + .graphicsState = { + .depthTest = COMPARE::GREATER_OR_EQUAL, // inverse Z + .vertexBufferBindings { + { + // Position vertex buffer (0) + .byteStride = 12, .attributes = { + { .byteOffset = 0, .format = FORMAT::RGB32_FLOAT } + } + }, + { + // 2nd vertex buffer: tangent, normal, color, texcoord + .byteStride = 24, .attributes = { + { .byteOffset = 0, .format = FORMAT::RGBA16_FLOAT }, + { .byteOffset = 8, .format = FORMAT::RGBA16_FLOAT }, + { .byteOffset = 16, .format = FORMAT::RGBA8_UNORM }, + { .byteOffset = 20, .format = FORMAT::RG16_FLOAT } + } + } + } + }, + .renderPassLayout = m_renderPassLayout +}); +``` + +--- + +## Software Command Buffer, but 10x+ Faster... + +**Initial design: Array of draw structs** +- Only contains the "metadata" +- Simple and fast + - 64 bytes = 1 cache line per draw +- Actual data inside buffers (inside groups) + - Write temp data from N threads directly into GPU memory + +**Can we do even better?** +- All fields are 32 bit integers +- Most data doesn't change between draw calls when rendering binned content +- On average 4.5 fields change (~18 bytes) + +**Draw Struct Definition:** +```cpp +struct Draw +{ + Handle shader; // PSO with all render state + Handle bindGroups[3]; + Handle dynamicBuffers; + Handle indexBuffer; + Handle vertexBuffers[3]; + uint32 indexOffset = 0; + uint32 vertexOffset = 0; + uint32 instanceOffset = 0; + uint32 instanceCount = 1; + uint32 dynamicBufferOffsets[2] = {0}; + uint32 triangleCount = 0; +}; +``` + +--- + +## Draw Stream: Data Interface for Draw Calls + +**Core Concept: Store only modified fields using dirty bitmask** + +The Draw Stream optimization reduces bandwidth by streaming only the fields of a `Draw` struct that have changed between draw calls, indicated by a dirty mask. + +### Draw Stream Layout + +**Stream Structure:** +- Each draw call starts with a 32-bit dirty mask indicating which fields changed +- Only modified fields are written to the stream +- Average of ~18 bytes change between draws (vs 64 bytes full struct) + +**Example Stream Encoding:** +``` +Draw 0: [SBBBD IVVVI VIIDT] [shader] [bind group] [index buffer] [vertex buffer] [tri count] +Draw 1: [SBBBD IVVVI VIIDT] [dynbuf offset] +Draw 2: [SBBBD IVVVI VIIDT] [dynbuf offset] +Draw 3: [SBBBD IVVVI VIIDT] [index offset] [vertex offset] [dynbuf offset] +... +``` + +### User Land: Draw Stream Writer Class +- Contains a draw struct (current state) + dirty mask +- Setter for each field: if changed → set dirty bit + write field to stream +- Draw: write dirty mask in front of the draw (stored offset) + +**Benefits:** +- No change tracking in the backend +- ~3x reduced bandwidth vs full 64-byte structs +- Enables efficient multi-threaded draw stream generation + +### Backend: Stream Decoding + +**Processing Flow:** +- For each draw: Read the dirty field bitmask +- For each set bit: Read field and emit a gfx API call +- **Advantages**: No change tracking in the backend. ~3x reduced BW + +### Draw Struct Definition +```cpp +struct Draw +{ + Handle shader; + Handle bindGroups[3]; + Handle dynamicBuffers; + Handle indexBuffer; + Handle vertexBuffers[3]; + uint32 indexOffset = 0; + uint32 vertexOffset = 0; + uint32 instanceOffset = 0; + uint32 instanceCount = 1; + uint32 dynamicBufferOffsets[2] = {0}; + uint32 triangleCount = 0; +}; +``` + +--- + +## Draw Call Performance: Baseline (Worst Case) + +**6-7 API calls per draw** + +### Draw Call Sequence +For each Draw (Draw 0, Draw 1, Draw 2, Draw 3...): +1. `(Bind set 2)` - Material binding +2. `Map + unmap` - Uniform buffer mapping/unmapping +3. `Bind set 3` - Dynamic data binding with offsets + - Draw 0: `Bind set 3` + - Draw 1: `Bind set 3 (96)` + - Draw 2: `Bind set 3 (192)` + - Draw 3: `Bind set 3 (384)` +4. `Bind VB + Bind IB` - Vertex Buffer + Index Buffer binding +5. `Draw` - Actual draw call + +### Per-Draw Data +```cpp +struct PerDrawData { + mat4x3 MVP; // Model-View-Projection matrix + mat3x3 invT; // Inverse Transpose matrix + float3 color; // Object color +}; +``` + +**Note:** Material (set 2) doesn't change every draw + +--- + +## Draw Call Performance: Bump Alloc + Offset Bind + +**4-5 API calls per draw** + +### Optimized Draw Call Sequence +For each Draw (Draw 0, Draw 1, Draw 2, Draw 3...): +1. `(Bind set 2)` - Material binding +2. `Bind set 3 (offset)` - Dynamic data binding with bump allocator offsets + - Draw 0: `Bind set 3 (0)` + - Draw 1: `Bind set 3 (96)` + - Draw 2: `Bind set 3 (192)` + - Draw 3: `Bind set 3 (384)` +3. `Bind VB + Bind IB` - Vertex Buffer + Index Buffer binding +4. `Draw` - Actual draw call + +### Temp Allocator Data Layout +```cpp +struct PerDrawData { + mat4x3 MVP; // Model-View-Projection matrix + mat3x3 invT; // Inverse Transpose matrix + float3 color; // Object color + PADDING; // Memory alignment padding +}; +``` + +**Allocation Pattern:** +- **Draw 0**: Offset 0 - MVP, invT, color, padding +- **Draw 1**: Offset 96 - MVP, invT, color, padding +- **Draw 2**: Offset 192 - MVP, invT, color, padding +- **Draw 3**: Offset 384 - MVP, invT, color, padding + +### Performance Results +- **~3x versus old WebGL / GLES backend** +- **No Vulkan results :(** + +--- + +## Draw Call Performance: Pack Meshes + +**2-3 API calls per draw** + +### Optimized Draw Call Sequence +For each Draw (Draw 0, Draw 1, Draw 2, Draw 3...): +1. `(Bind set 2)` - Material binding +2. `Bind set 3 (offset)` - Dynamic data binding with bump allocator offsets + - Draw 0: `Bind set 3 (0)` + - Draw 1: `Bind set 3 (96)` + - Draw 2: `Bind set 3 (192)` + - Draw 3: `Bind set 3 (384)` +3. `Draw(baseVert, baseIdx)` - Draw call with base vertex and base index + +### Mesh Buffer Layout +``` ++------------------+ +| Mesh 1 | ← Draw 0 (baseVert=0, baseIdx=0) +| vertices + indices| ++------------------+ +| Mesh 2 | ← Draw 1 (baseVert=N, baseIdx=M) +| vertices + indices| ++------------------+ +| Mesh 3 | ← Draw 2 (baseVert=P, baseIdx=Q) +| vertices + indices| ++------------------+ +| Mesh 4 | ← Draw 3 (baseVert=R, baseIdx=S) +| vertices + indices| ++------------------+ +| ... | ++------------------+ +``` + +### Performance Results +- **Nvidia: 2.17x** +- **AMD: 1.75x** +- **Adreno: 1.29x** +- **Mali: 1.40x** + +--- + +## Draw Call Performance: BaseInstance (FAILED) + +**1-2 API calls per draw (amortized)** + +### Instancing Data Layout (No Padding) +- 16KB UBO binding limit +- Change offset every 16KB +- Amortized over >100 draws + +### Better Shader Codegen vs. Instancing +- `gl_InstanceID = dynamic offset` (Instancing) +- `gl_BaseInstance = static offset` (BaseInstance) +- **Benefits:** Enables scalar loads / fast UBO path. Saves vector registers / loads + +### BaseInstance Draw Call Sequence +**Temp Allocator `drawData[]` Layout:** +``` +16KB UBO Region: ++------------------+ +| Instance 0 | ← Draw 0 (baseInstance=0) +| mat4x3 MVP | +| mat3x3 invT | +| float3 color | ++------------------+ +| Instance 1 | ← Draw 1 (baseInstance=1) +| mat4x3 MVP | +| mat3x3 invT | +| float3 color | ++------------------+ +| Instance 2 | ← Draw 2 (baseInstance=2) +| mat4x3 MVP | +| mat3x3 invT | +| float3 color | ++------------------+ +| ... | ++------------------+ +``` + +For each Draw (Draw 0, Draw 1, Draw 2, Draw 3...): +1. `(Bind set 2)` - Material binding +2. `Bind set 3 (offset)` - Dynamic data binding (amortized over 16KB) +3. `Draw(baseVertex, baseIndex, baseInstance=X)` - Instanced draw call + +### Why BaseInstance FAILED +- **Not supported in web / DX12 :(** +- **Mobile shader codegen issues:** Often slower for the GPU :( + +--- + +## Performance Numbers + +**10,000 Draw Calls (Single CPU Thread)** + +### Benchmark Conditions +- 10,000 unique materials +- 10,000 unique meshes +- Single CPU thread +- Actual draw calls (no instancing) +- No GPU persistent scene data +- ~90% time spent in driver + +### Performance Results +| Hardware Configuration | Time (ms) | Notes | +|------------------------|-----------|-------| +| **AMD RDNA2 iGPU + 6800HS 4.7GHz** | **0.85ms** | High-end desktop integrated | +| **Apple iPhone 6s + 1.85GHz** | **11.27ms** | 7 years old | +| **PowerVR GE8320 + A53 2.3GHz** | **20.93ms** | €99 device | +| **ARM Mali G57 MP1 + A75 1.6GHz** | **15.01ms** | €99 device | +| **QC Adreno 610 + Kryo 260 2GHz** | **13.69ms** | €149 device | + +--- + +## References + +### [1] Various hardware performance numbers (Wikipedia) +- **ARM Mali:** https://en.wikipedia.org/wiki/Mali (processor) +- **Qualcomm Adreno:** https://en.wikipedia.org/wiki/Adreno +- **PowerVR:** https://en.wikipedia.org/wiki/PowerVR +- **Nintendo Switch:** https://en.wikipedia.org/wiki/Nintendo_Switch +- **Microsoft Xbox 360:** https://en.wikipedia.org/wiki/Xbox_360 +- **Microsoft Xbox One:** https://en.wikipedia.org/wiki/Xbox_One +- **Sony PS3:** https://en.wikipedia.org/wiki/PlayStation_3 +- **Sony PS4:** https://en.wikipedia.org/wiki/PlayStation_4 + +### [2] Haar, Aaltonen: GPU-Driven Rendering Pipelines, SIGGRAPH 2015: Advances in Real-Time Rendering in Games +https://advances.realtimerendering.com/s2015/aaltonenhaar_siggraph2015_combined_final_footer_220dpi.pdf + +### [3] Karis, Stubbe, Wihlidal: Nanite, A Deep Dive, SIGGRAPH 2021: Advances in Real-Time Rendering in Games +https://advances.realtimerendering.com/s2021/Karis_Nanite_SIGGRAPH_Advances_2021_final.pdf + +### [4] SPIRV-Cross +https://github.com/KhronosGroup/SPIRV-Cross + +### [5] Tatarchuk, Cooper, Aaltonen: Unity Rendering Architecture, Rendering Engine Architecture Conference (REAC), 2023 +https://enginearchitecture.realtimerendering.com/downloads/reac2021 unity rendering engine architecture.pdf + +### [6] RAII, Wikipedia +https://en.wikipedia.org/wiki/Resource acquisition is initialization + +### [7] SoA layout, Wikipedia +https://en.wikipedia.org/wiki/AoS and SoA + +### [8] C++20 designated initializers, cppreference.com +https://en.cppreference.com/w/cpp/language/aggregate_initialization + +### [9] Masmano, Ripoll, Crespo, Real: TLSF: A new memory allocator for real-time systems +http://www.gii.upv.es/tlsf/files/ecrts04 tlsf.pdf + +### [10] Aaltonen: OffsetAllocator, GitHub +https://github.com/sebbbi/OffsetAllocator + + diff --git a/engine/pill_renderer/src/egui.rs b/engine/pill_engine/src/renderer/egui.rs similarity index 80% rename from engine/pill_renderer/src/egui.rs rename to engine/pill_engine/src/renderer/egui.rs index 58728a21..0cd77486 100644 --- a/engine/pill_renderer/src/egui.rs +++ b/engine/pill_engine/src/renderer/egui.rs @@ -2,15 +2,15 @@ use std::sync::Arc; use egui::epaint::Shadow; use egui::{Context, Visuals}; -use egui_wgpu::ScreenDescriptor; use egui_wgpu::Renderer; +use egui_wgpu::ScreenDescriptor; +use anyhow::{Error, Result}; use egui_winit::State; use pill_core::Timer; use wgpu::{CommandEncoder, Device, Queue, TextureFormat, TextureView}; use winit::event::WindowEvent; use winit::window::Window; -use anyhow::{Error, Result}; pub struct EguiRenderer { pub context: Context, @@ -32,7 +32,7 @@ impl EguiRenderer { let egui_context = egui::Context::default(); let id = egui_context.viewport_id(); const BORDER_RADIUS: f32 = 2.0; - + let visuals = egui::Visuals { window_rounding: egui::Rounding::same(BORDER_RADIUS), window_shadow: egui::Shadow::NONE, @@ -54,7 +54,7 @@ impl EguiRenderer { state: egui_state, renderer: egui_renderer, window_scale_factor, - window + window, } } @@ -70,30 +70,26 @@ impl EguiRenderer { window_surface_view: &TextureView, screen_descriptor: ScreenDescriptor, run_ui: impl FnOnce(&Context), - timer: &mut Timer, ) -> Result<()> { - timer.record("Prepare window and input"); - let window = &self.window; let raw_input = self.state.take_egui_input(&window); let full_output = self.context.run(raw_input, |_| { run_ui(&self.context); }); - timer.record("Handle platform output"); + self.state + .handle_platform_output(&window, full_output.platform_output); - self.state.handle_platform_output(&window, full_output.platform_output); - - timer.record("Tesselate and update textures"); - - let tris = self.context.tessellate(full_output.shapes, full_output.pixels_per_point); + let tris = self + .context + .tessellate(full_output.shapes, full_output.pixels_per_point); for (id, image_delta) in &full_output.textures_delta.set { - self.renderer.update_texture(&device, &queue, *id, &image_delta); + self.renderer + .update_texture(&device, &queue, *id, &image_delta); } - timer.record("Update buffers and record render pass"); - - self.renderer.update_buffers(&device, &queue, encoder, &tris, &screen_descriptor); + self.renderer + .update_buffers(&device, &queue, encoder, &tris, &screen_descriptor); let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { color_attachments: &[Some(wgpu::RenderPassColorAttachment { @@ -110,9 +106,8 @@ impl EguiRenderer { occlusion_query_set: None, }); - timer.record("Render"); - - self.renderer.render(&mut render_pass, &tris, &screen_descriptor); + self.renderer + .render(&mut render_pass, &tris, &screen_descriptor); drop(render_pass); for x in &full_output.textures_delta.free { @@ -122,4 +117,3 @@ impl EguiRenderer { Ok(()) } } - diff --git a/engine/pill_renderer/src/instance.rs b/engine/pill_engine/src/renderer/instance.rs similarity index 100% rename from engine/pill_renderer/src/instance.rs rename to engine/pill_engine/src/renderer/instance.rs diff --git a/engine/pill_engine/src/renderer/lib.rs b/engine/pill_engine/src/renderer/lib.rs new file mode 100644 index 00000000..5baff22a --- /dev/null +++ b/engine/pill_engine/src/renderer/lib.rs @@ -0,0 +1,14 @@ +#![cfg_attr(debug_assertions, allow(dead_code, unused_imports))] + +pub mod egui; +pub mod instance; +pub mod renderer; +pub mod resource_manager; +pub mod resource_snapshot; +pub mod resources; + +// --- Use --- + +pub use renderer::*; + +pub use instance::Instance; diff --git a/engine/pill_engine/src/renderer/mod.rs b/engine/pill_engine/src/renderer/mod.rs new file mode 100644 index 00000000..2671d713 --- /dev/null +++ b/engine/pill_engine/src/renderer/mod.rs @@ -0,0 +1,7 @@ +#![cfg_attr(debug_assertions, allow(dead_code, unused_imports))] + +pub mod egui; +pub mod resources; +pub mod wgpu; + +pub use wgpu::Renderer; diff --git a/engine/pill_engine/src/renderer/renderer_resource_storage.rs b/engine/pill_engine/src/renderer/renderer_resource_storage.rs new file mode 100644 index 00000000..2927c26a --- /dev/null +++ b/engine/pill_engine/src/renderer/renderer_resource_storage.rs @@ -0,0 +1,75 @@ +use super::resources::{ + RendererCamera, RendererMaterial, RendererMesh, RendererPipeline, RendererTexture, +}; + +use pill_core::PillSlotMap; + +use crate::graphics::{ + RendererBufferHandle, RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, + RendererPipelineHandle, RendererPipelineV2Handle, RendererTextureHandle, +}; + +pub const MAX_PIPELINES: usize = 10; +pub const MAX_PIPELINES_V2: usize = 10; +pub const MAX_TEXTURES: usize = 10; +pub const MAX_MATERIALS: usize = 10; +pub const MAX_MESHES: usize = 10; +pub const MAX_CAMERAS: usize = 10; +pub const MAX_BUFFERS: usize = 64; + +pub struct RendererResourceStorage { + pub(crate) pipelines: PillSlotMap, + pub(crate) pipelines_v2: PillSlotMap, + pub(crate) materials: PillSlotMap, + pub(crate) textures: PillSlotMap, + pub(crate) meshes: PillSlotMap, + pub(crate) cameras: PillSlotMap, + pub(crate) buffers: PillSlotMap, +} + +impl RendererResourceStorage { + pub fn new(config: &crate::config::Config) -> Self { + let max_pipeline_count = config + .get_int("MAX_PIPELINES") + .unwrap_or(MAX_PIPELINES as i64) as usize; + let max_pipeline_v2_count = config + .get_int("MAX_PIPELINES_V2") + .unwrap_or(MAX_PIPELINES_V2 as i64) as usize; + let max_texture_count = config + .get_int("MAX_TEXTURES") + .unwrap_or(MAX_TEXTURES as i64) as usize; + let max_material_count = config + .get_int("MAX_MATERIALS") + .unwrap_or(MAX_MATERIALS as i64) as usize; + let max_mesh_count = config.get_int("MAX_MESHS").unwrap_or(MAX_MESHES as i64) as usize; + let max_camera_count = config.get_int("MAX_CAMERAS").unwrap_or(MAX_CAMERAS as i64) as usize; + let max_buffer_count = config.get_int("MAX_BUFFERS").unwrap_or(MAX_BUFFERS as i64) as usize; + + RendererResourceStorage { + pipelines: + PillSlotMap::::with_capacity_and_key( + max_pipeline_count, + ), + pipelines_v2: + PillSlotMap::::with_capacity_and_key( + max_pipeline_v2_count, + ), + textures: PillSlotMap::::with_capacity_and_key( + max_texture_count, + ), + materials: + PillSlotMap::::with_capacity_and_key( + max_material_count, + ), + meshes: PillSlotMap::::with_capacity_and_key( + max_mesh_count, + ), + cameras: PillSlotMap::::with_capacity_and_key( + max_camera_count, + ), + buffers: PillSlotMap::::with_capacity_and_key( + max_buffer_count, + ), + } + } +} diff --git a/engine/pill_renderer/src/resources/mod.rs b/engine/pill_engine/src/renderer/resources/mod.rs similarity index 54% rename from engine/pill_renderer/src/resources/mod.rs rename to engine/pill_engine/src/renderer/resources/mod.rs index 6e136fd0..0d36b0c8 100644 --- a/engine/pill_renderer/src/resources/mod.rs +++ b/engine/pill_engine/src/renderer/resources/mod.rs @@ -1,21 +1,18 @@ -mod renderer_texture; -mod renderer_mesh; mod renderer_camera; mod renderer_material; +mod renderer_mesh; mod renderer_pipeline; +mod renderer_texture; // --- Use --- -pub use renderer_mesh::{ - RendererMesh, - Vertex -}; +pub use renderer_mesh::{RendererMesh, Vertex}; pub use renderer_texture::RendererTexture; +pub use renderer_camera::CameraUniform; pub use renderer_camera::RendererCamera; -pub use renderer_material::RendererMaterial; - +pub use renderer_material::RendererMaterialTextures; +pub use renderer_material::{RendererMaterial, RendererMaterialParamsStd140}; pub use renderer_pipeline::RendererPipeline; - diff --git a/engine/pill_renderer/src/resources/renderer_camera.rs b/engine/pill_engine/src/renderer/resources/renderer_camera.rs similarity index 51% rename from engine/pill_renderer/src/resources/renderer_camera.rs rename to engine/pill_engine/src/renderer/resources/renderer_camera.rs index 17646f66..a82f4c82 100644 --- a/engine/pill_renderer/src/resources/renderer_camera.rs +++ b/engine/pill_engine/src/renderer/resources/renderer_camera.rs @@ -1,12 +1,9 @@ use cgmath::{EuclideanSpace, SquareMatrix, Zero}; -use pill_engine::internal::{ - TransformComponent, - CameraComponent -}; +use crate::ecs::{CameraComponent, TransformComponent}; -use anyhow::{ Result }; -use wgpu::util::DeviceExt; +use anyhow::Result; use std::f32::consts::FRAC_PI_2; +use wgpu::util::DeviceExt; #[rustfmt::skip] pub const OPENGL_TO_WGPU_MATRIX: cgmath::Matrix4 = cgmath::Matrix4::new( @@ -20,8 +17,8 @@ pub const OPENGL_TO_WGPU_MATRIX: cgmath::Matrix4 = cgmath::Matrix4::new( #[repr(C)] #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -pub(crate) struct CameraUniform { - pub(crate) position: [f32; 4], // Camera position +pub struct CameraUniform { + pub(crate) position: [f32; 4], // Camera position pub(crate) view_projection_matrix: [[f32; 4]; 4], // Perspective manipulation } @@ -33,42 +30,49 @@ impl CameraUniform { } } - pub fn update_data(&mut self, camera_component: &CameraComponent, transform_component: &TransformComponent) { + pub fn update_data( + &mut self, + camera_component: &CameraComponent, + transform_component: &TransformComponent, + ) { // Update position - self.position = cgmath::Vector4:: { - x: transform_component.position.x, - y: transform_component.position.y, - z: transform_component.position.z, - w: 0.0 - }.into(); + self.position = cgmath::Vector4:: { + x: transform_component.position.x, + y: transform_component.position.y, + z: transform_component.position.z, + w: 0.0, + } + .into(); // Update view-projection - self.view_projection_matrix = (CameraUniform::calculate_projection_matrix(camera_component) * CameraUniform::calculate_view_matrix(transform_component)).into(); + self.view_projection_matrix = + (CameraUniform::calculate_projection_matrix(camera_component) + * CameraUniform::calculate_view_matrix(transform_component)) + .into(); } - fn calculate_view_matrix(transform_component: &TransformComponent) -> cgmath::Matrix4:: { + fn calculate_view_matrix(transform_component: &TransformComponent) -> cgmath::Matrix4 { let position = cgmath::Point3::from_vec(transform_component.position); - let roll_matrix = cgmath::Matrix3::from_angle_z(cgmath::Deg(transform_component.rotation.z)); - let yaw_matrix = cgmath::Matrix3::from_angle_y(cgmath::Deg(transform_component.rotation.y)); - let pitch_matrix = cgmath::Matrix3::from_angle_x(cgmath::Deg(transform_component.rotation.x)); + let roll_matrix = + cgmath::Matrix3::from_angle_z(cgmath::Deg(transform_component.rotation.z)); + let yaw_matrix = cgmath::Matrix3::from_angle_y(cgmath::Deg(transform_component.rotation.y)); + let pitch_matrix = + cgmath::Matrix3::from_angle_x(cgmath::Deg(transform_component.rotation.x)); let rotation_matrix = yaw_matrix * pitch_matrix * roll_matrix; - let direction = rotation_matrix * cgmath::Vector3::::unit_z(); + let direction = rotation_matrix * cgmath::Vector3::::unit_z(); - cgmath::Matrix4::look_to_rh( - position, - direction, - cgmath::Vector3::unit_y() - ) + cgmath::Matrix4::look_to_rh(position, direction, cgmath::Vector3::unit_y()) } - fn calculate_projection_matrix(camera_component: &CameraComponent) -> cgmath::Matrix4:: { - OPENGL_TO_WGPU_MATRIX * cgmath::perspective( - cgmath::Deg(camera_component.fov), - camera_component.aspect.get_value(), - camera_component.range.start, - camera_component.range.end - ) + fn calculate_projection_matrix(camera_component: &CameraComponent) -> cgmath::Matrix4 { + OPENGL_TO_WGPU_MATRIX + * cgmath::perspective( + cgmath::Deg(camera_component.fov), + camera_component.aspect.get_value(), + camera_component.range.start, + camera_component.range.end, + ) } } @@ -82,8 +86,10 @@ pub struct RendererCamera { } impl RendererCamera { - pub fn new(device: &wgpu::Device, camera_bind_group_layout: &wgpu::BindGroupLayout) -> Result { - + pub fn new( + device: &wgpu::Device, + camera_bind_group_layout: &wgpu::BindGroupLayout, + ) -> Result { let uniform = CameraUniform::new(); let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { @@ -110,8 +116,14 @@ impl RendererCamera { Ok(camera) } - pub fn update(&mut self, queue: &wgpu::Queue, camera_component: &CameraComponent, transform_component: &TransformComponent) { - self.uniform.update_data(camera_component, transform_component); + pub fn update( + &mut self, + queue: &wgpu::Queue, + camera_component: &CameraComponent, + transform_component: &TransformComponent, + ) { + self.uniform + .update_data(camera_component, transform_component); queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(&[self.uniform])); } } diff --git a/engine/pill_engine/src/renderer/resources/renderer_material.rs b/engine/pill_engine/src/renderer/resources/renderer_material.rs new file mode 100644 index 00000000..4cfffae4 --- /dev/null +++ b/engine/pill_engine/src/renderer/resources/renderer_material.rs @@ -0,0 +1,33 @@ +use crate::renderer::resources::RendererTexture; + +// COLD: construction-only helper; no allocations, just references bundled for factory use. +pub struct RendererMaterialTextures<'a> { + pub albedo: &'a RendererTexture, + pub normal: &'a RendererTexture, + pub metallic_roughness: &'a RendererTexture, + pub emissive: &'a RendererTexture, +} + +#[repr(C)] +#[derive(Copy, Clone)] +// HOT(ish): per-material UBO bound during draws; keep compact (<= 64B ideal). +// Layout is 3x vec3+pads → 48 bytes total, 16-byte aligned to match WGSL/std140 rules. +pub struct RendererMaterialParamsStd140 { + pub albedo: [f32; 3], + pub _pad0: f32, + pub metallic: f32, + pub roughness: f32, + pub _pad1: [f32; 2], + pub emissive: [f32; 3], + pub _pad2: f32, +} +unsafe impl bytemuck::Zeroable for RendererMaterialParamsStd140 {} +unsafe impl bytemuck::Pod for RendererMaterialParamsStd140 {} + +// COLD: GPU handles (bind groups/buffer) created infrequently and reused across many draws. +pub struct RendererMaterial { + pub name: String, + pub texture_bind_group: wgpu::BindGroup, + pub param_buffer: wgpu::Buffer, + pub param_bind_group: wgpu::BindGroup, +} diff --git a/engine/pill_engine/src/renderer/resources/renderer_mesh.rs b/engine/pill_engine/src/renderer/resources/renderer_mesh.rs new file mode 100644 index 00000000..af82c929 --- /dev/null +++ b/engine/pill_engine/src/renderer/resources/renderer_mesh.rs @@ -0,0 +1,65 @@ +use crate::resources::{MeshData, MeshVertex}; + +use anyhow::*; +use wgpu::util::DeviceExt; + +// --- Vertex --- + +pub trait Vertex { + // Defines how a data is layed out in memory (To specify how RenderPipeline needs to map the buffer in the shader) + fn data_layout_descriptor<'a>() -> wgpu::VertexBufferLayout<'a>; +} + +// --- Mesh --- + +pub struct RendererMesh { + pub vertex_buffer: wgpu::Buffer, + pub index_buffer: wgpu::Buffer, + pub index_count: u32, + pub aabb_min: [f32; 3], + pub aabb_max: [f32; 3], +} + +impl RendererMesh { + pub fn new(device: &wgpu::Device, name: &str, mesh_data: &MeshData) -> Result { + let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{:?}_vertex_buffer", name)), + contents: bytemuck::cast_slice(&mesh_data.vertices), + usage: wgpu::BufferUsages::VERTEX, + }); + + let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{:?}_vertex_buffer", name)), + contents: bytemuck::cast_slice(&mesh_data.indices), + usage: wgpu::BufferUsages::INDEX, + }); + + let renderer_mesh = Self { + vertex_buffer, + index_buffer, + index_count: mesh_data.indices.len() as u32, + aabb_min: mesh_data.aabb_min, + aabb_max: mesh_data.aabb_max, + }; + + Ok(renderer_mesh) + } +} + +impl Vertex for RendererMesh { + fn data_layout_descriptor<'a>() -> wgpu::VertexBufferLayout<'a> { + // Use a static attribute array to satisfy lifetime requirements + const ATTRS: [wgpu::VertexAttribute; 5] = wgpu::vertex_attr_array![ + 0 => Float32x3, // position + 1 => Float32x2, // uv + 2 => Float32x3, // normal + 3 => Float32x3, // tangent + 4 => Float32x3, // bitangent + ]; + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &ATTRS, + } + } +} diff --git a/engine/pill_engine/src/renderer/resources/renderer_pipeline.rs b/engine/pill_engine/src/renderer/resources/renderer_pipeline.rs new file mode 100644 index 00000000..39be9c9b --- /dev/null +++ b/engine/pill_engine/src/renderer/resources/renderer_pipeline.rs @@ -0,0 +1,229 @@ +use anyhow::Result; + +// --- Pipeline --- + +pub struct RendererPipeline { + pub render_pipeline: wgpu::RenderPipeline, + pub material_texture_bind_group_layout: wgpu::BindGroupLayout, + pub material_parameter_bind_group_layout: wgpu::BindGroupLayout, + pub camera_bind_group_layout: wgpu::BindGroupLayout, + pub per_draw_bind_group_layout: wgpu::BindGroupLayout, +} + +impl RendererPipeline { + pub fn new( + device: &wgpu::Device, + vertex_shader: wgpu::ShaderModule, + fragment_shader: wgpu::ShaderModule, + color_format: wgpu::TextureFormat, + depth_format: Option, + vertex_layouts: &[wgpu::VertexBufferLayout], + ) -> Result { + // Define material bind group layout (Describes a set of resources and how they can be accessed by a shader) + // [SIMILAR] Predefined bind group layouts matching shader sets; created once, reused per TALK + let material_texture_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("material_texture_bind_group_layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + // Entry for the sampled texture at binding 0 + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, // Visible only to fragment shader + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + // Entry for the sampler at binding 1 + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, // Visible only to fragment shader + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + // Normal map + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + // MetallicRoughness texture + sampler + wgpu::BindGroupLayoutEntry { + binding: 4, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 5, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + // Emissive texture + sampler + wgpu::BindGroupLayoutEntry { + binding: 6, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 7, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + + let material_parameter_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("material_parameter_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, // Specifies if this buffer will be changing size or not + min_binding_size: None, + }, + count: None, + }], + }); + + // Define camera bind group layout + // [SIMILAR] Camera UBO in a dedicated layout; updated before pass, not per-draw + let camera_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("camera_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, // Specifies if this buffer will be changing size or not + min_binding_size: None, + }, + count: None, + }], + }); + + // Define per-draw bind group layout (dynamic uniform buffer) + // [SIMILAR] Dynamic offset UBO for per-draw data; avoids map/unmap per draw per TALK + let per_draw_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("per_draw_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: true, + // Per-draw UBO: MVP only (mat4 = 64 bytes) + min_binding_size: Some(std::num::NonZeroU64::new(64).unwrap()), + }, + count: None, + }], + }); + + // Create pipeline layout descriptor + // [SIMILAR] Fixed PSO layout per TALK: globals(camera), material textures, material params, dynamic per-draw + let pipeline_layout_descriptor = wgpu::PipelineLayoutDescriptor { + label: Some("render_pipeline_layout"), + bind_group_layouts: &[ + &camera_bind_group_layout, + &material_texture_bind_group_layout, + &material_parameter_bind_group_layout, + &per_draw_bind_group_layout, + ], + push_constant_ranges: &[], + }; + + // Create pipeline layout + let layout = device.create_pipeline_layout(&pipeline_layout_descriptor); + + // Create color target states that specifies what what color outputs wgpu should set up + let color_target_states = &[Some(wgpu::ColorTargetState { + format: color_format, + blend: Some(wgpu::BlendState { + alpha: wgpu::BlendComponent::REPLACE, + color: wgpu::BlendComponent::REPLACE, + }), + write_mask: wgpu::ColorWrites::ALL, + })]; + + let render_pipeline_descriptor = wgpu::RenderPipelineDescriptor { + label: Some("render_pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &vertex_shader, + entry_point: "main", + buffers: vertex_layouts, // Specifies structure of vertices that will be passed to the vertex shader + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fragment_shader, + entry_point: "main", + targets: color_target_states, + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + // Specifies how to interpret vertices when converting them into triangles + topology: wgpu::PrimitiveTopology::TriangleList, // Each three vertices will correspond to one triangle + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, // Specifies how to determine whether a given triangle is facing forward or not (FrontFace::Ccw means that a triangle is facing forward if the vertices are arranged in a counter clockwise direction) + cull_mode: Some(wgpu::Face::Back), // Triangles that are not considered facing forward are culled (not included in the render) as specified by CullMode::Back + polygon_mode: wgpu::PolygonMode::Fill, // Setting this to anything other than Fill requires Features::NON_FILL_POLYGON_MODE + conservative: false, // Requires Features::CONSERVATIVE_RASTERIZATION + unclipped_depth: true, // Requires Features::DEPTH_CLAMPING + }, + depth_stencil: depth_format.map(|format| wgpu::DepthStencilState { + format, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, // Specifies when to discard a new pixel. Using LESS means pixels will be drawn front to back + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + count: 1, // Determines how many samples pipeline will use (Multisampling) + mask: !0, // Specifies which samples should be active + alpha_to_coverage_enabled: false, + }, + multiview: None, + //cache: None, + }; + + let render_pipeline = device.create_render_pipeline(&render_pipeline_descriptor); + + let pipeline = Self { + render_pipeline, + material_texture_bind_group_layout, + material_parameter_bind_group_layout, + camera_bind_group_layout, + per_draw_bind_group_layout, + }; + + Ok(pipeline) + } +} diff --git a/engine/pill_engine/src/renderer/resources/renderer_texture.rs b/engine/pill_engine/src/renderer/resources/renderer_texture.rs new file mode 100644 index 00000000..e40c1304 --- /dev/null +++ b/engine/pill_engine/src/renderer/resources/renderer_texture.rs @@ -0,0 +1,206 @@ +use crate::resources::TextureType; +use anyhow::Result; +use image::{DynamicImage, GenericImageView}; +use wgpu::{Device, Queue, SurfaceConfiguration, TextureFormat}; + +pill_core::define_new_pill_slotmap_key! { + pub struct RendererTextureHandle; +} + +pub struct RendererTexture { + pub texture: wgpu::Texture, + pub texture_view: wgpu::TextureView, + pub sampler: wgpu::Sampler, + pub format: wgpu::TextureFormat, +} + +impl RendererTexture { + pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; + + pub fn new_texture( + device: &wgpu::Device, + queue: &wgpu::Queue, + name: Option<&str>, + image_data: &image::DynamicImage, + texture_type: TextureType, + ) -> Result { + let dimensions = image_data.dimensions(); + let rgba = image_data.to_rgba8(); + let size = wgpu::Extent3d { + width: dimensions.0, + height: dimensions.1, + depth_or_array_layers: 1, + }; + + let format = match texture_type { + TextureType::Gamma => wgpu::TextureFormat::Rgba8UnormSrgb, + TextureType::Linear => wgpu::TextureFormat::Rgba8Unorm, + }; + + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: name, + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + + // Write with proper row alignment to avoid stretching/scrambling + // WebGPU requires bytes_per_row to be COPY_BYTES_PER_ROW_ALIGNMENT (256) aligned. + let bpp: u32 = 4; + let unpadded_bytes_per_row = (dimensions.0 * bpp) as usize; + let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; + let padded_bytes_per_row = ((unpadded_bytes_per_row + align - 1) / align) * align; + if padded_bytes_per_row == unpadded_bytes_per_row { + // Fast path: already aligned + queue.write_texture( + wgpu::ImageCopyTexture { + texture: &texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &rgba, + wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(unpadded_bytes_per_row as u32), + rows_per_image: Some(dimensions.1), + }, + size, + ); + } else { + // Pad each row up to alignment + let height = dimensions.1 as usize; + let mut padded = vec![0u8; padded_bytes_per_row * height]; + let src = rgba.as_raw(); + for y in 0..height { + let src_offset = y * unpadded_bytes_per_row; + let dst_offset = y * padded_bytes_per_row; + padded[dst_offset..dst_offset + unpadded_bytes_per_row] + .copy_from_slice(&src[src_offset..src_offset + unpadded_bytes_per_row]); + } + queue.write_texture( + wgpu::ImageCopyTexture { + texture: &texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &padded, + wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(padded_bytes_per_row as u32), + rows_per_image: Some(dimensions.1), + }, + size, + ); + } + + let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::Repeat, + address_mode_v: wgpu::AddressMode::Repeat, + address_mode_w: wgpu::AddressMode::Repeat, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + Ok(Self { + texture, + texture_view, + sampler, + format, + }) + } + + pub fn new_depth_texture( + device: &wgpu::Device, + surface_config: &SurfaceConfiguration, + name: &str, + ) -> Result { + let size = wgpu::Extent3d { + width: surface_config.width, + height: surface_config.height, + depth_or_array_layers: 1, + }; + + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some(name), + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: Self::DEPTH_FORMAT, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + compare: Some(wgpu::CompareFunction::LessEqual), + ..Default::default() + }); + + Ok(Self { + texture, + texture_view, + sampler, + format: Self::DEPTH_FORMAT, + }) + } + + pub fn new_render_target( + device: &wgpu::Device, + surface_config: &SurfaceConfiguration, + format: TextureFormat, + name: &str, + ) -> Result { + let size = wgpu::Extent3d { + width: surface_config.width, + height: surface_config.height, + depth_or_array_layers: 1, + }; + + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some(name), + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + Ok(Self { + texture, + texture_view, + sampler, + format, + }) + } +} diff --git a/engine/pill_engine/src/renderer/wgpu.rs b/engine/pill_engine/src/renderer/wgpu.rs new file mode 100644 index 00000000..726752c0 --- /dev/null +++ b/engine/pill_engine/src/renderer/wgpu.rs @@ -0,0 +1,1010 @@ +// https://github.com/ejb004/egui-wgpu-demo/blob/master/src/lib.rs +// https://github.com/kaphula/winit-egui-wgpu-template/blob/master/src/main.rs +// https://github.com/emilk/egui/discussions/3067 + +use crate::renderer::resources::{ + RendererCamera, RendererMaterial, RendererMaterialParamsStd140, RendererMaterialTextures, + RendererMesh, RendererPipeline, RendererTexture, Vertex, +}; + +use std::sync::Arc; + +use crate::ecs::{CameraComponent, ComponentStorage, EntityHandle, TransformComponent}; +use crate::graphics::{ + BufferDesc, MaterialDesc, Pass, PillRenderer, PipelineV2, PipelineV2Desc, RenderQueueItem, + RendererBufferHandle, RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, + RendererPipelineHandle, RendererPipelineV2Handle, RendererTargetDesc, RendererTextureHandle, + ShaderDesc, WorldQuery, +}; +use crate::resources::{MeshData, TextureType}; + +use pill_core::{Handle, PillSlotMapKey, PillStyle, RendererError, Timer}; + +use std::num::NonZeroU32; + +use cgmath::{Deg, InnerSpace}; +use glam::{Mat4, Vec3, Vec4}; + +use anyhow::{Error, Result}; +use log::{error, info}; + +use image::GenericImageView; +use wgpu::util::DeviceExt; + +pub const MAX_INSTANCE_BATCH_SIZE: usize = 10000; // Maximum number of instances that can be drawn in a single draw call +pub const INITIAL_INSTANCE_VECTOR_CAPACITY: usize = 10000; + +pub struct DeviceContext { + pub(crate) config: config::Config, + // Window + pub(crate) window_ref: Arc, + pub(crate) window_size: winit::dpi::PhysicalSize, + // GPU API + pub(crate) device: wgpu::Device, + pub(crate) queue: wgpu::Queue, + pub(crate) surface: wgpu::Surface<'static>, + // Framebuffer variables + pub(crate) surface_configuration: wgpu::SurfaceConfiguration, +} + +pub struct State { + passes: Vec>, +} + +pub struct Renderer { + pub state: State, + pub ctx: DeviceContext, + resource_manager: *mut crate::resources::ResourceManager, +} + +impl PillRenderer for Renderer { + fn new( + resources: &mut crate::resources::ResourceManager, + window: Arc, + config: config::Config, + ) -> Self { + info!("Initializing {}", "Renderer".mobj_style()); + let ctx: DeviceContext = pollster::block_on(async move { + let window_size = window.inner_size(); + let window_ref = window.clone(); + + let backends = wgpu::util::backend_bits_from_env().unwrap_or_default(); + let dx12_shader_compiler = + wgpu::util::dx12_shader_compiler_from_env().unwrap_or_default(); + let gles_minor_version = wgpu::util::gles_minor_version_from_env().unwrap_or_default(); + + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends, + flags: wgpu::InstanceFlags::from_build_config().with_env(), + dx12_shader_compiler, + gles_minor_version, + }); + let surface = instance.create_surface(window).unwrap(); + + // Specify adapter options (Options passed here are not guaranteed to work for all devices) + let request_adapter_options = wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::default(), + compatible_surface: Some(&surface), + force_fallback_adapter: false, + }; + + // Create adapter + let adapter = instance + .request_adapter(&request_adapter_options) + .await + .unwrap(); + let adapter_info = adapter.get_info(); + info!( + "Using GPU: {} ({:?})", + adapter_info.name, adapter_info.backend + ); + + let features = wgpu::Features::DEPTH_CLIP_CONTROL; + + // Create device descriptor + let device_descriptor = wgpu::DeviceDescriptor { + label: None, + required_features: features, // Allows to specify what extra features of GPU that needs to be included (e.g. depth clamping, push constants, texture compression, etc) + required_limits: wgpu::Limits::default(), // Allows to specify the limit of certain types of resources that will be used (e.g. max samplers, uniform buffers, etc) + //memory_hints: wgpu::MemoryHints::MemoryUsage, + }; + + // Create device and queue + let (device, queue) = adapter + .request_device(&device_descriptor, None) + .await + .unwrap(); + + // Specify surface configuration + let preferred_format = wgpu::TextureFormat::Rgba8UnormSrgb; + + // Get supported present modes and choose the best one + let surface_caps = surface.get_capabilities(&adapter); + let present_mode = if surface_caps + .present_modes + .contains(&wgpu::PresentMode::Mailbox) + { + wgpu::PresentMode::Mailbox + } else if surface_caps + .present_modes + .contains(&wgpu::PresentMode::Immediate) + { + wgpu::PresentMode::Immediate + } else { + wgpu::PresentMode::Fifo + }; + + // Choose the best supported format + let format = if surface_caps.formats.contains(&preferred_format) { + preferred_format + } else if surface_caps + .formats + .contains(&wgpu::TextureFormat::Bgra8UnormSrgb) + { + wgpu::TextureFormat::Bgra8UnormSrgb + } else if surface_caps + .formats + .contains(&wgpu::TextureFormat::Bgra8Unorm) + { + wgpu::TextureFormat::Bgra8Unorm + } else { + surface_caps.formats[0] // Use first available format + }; + + // macOS (Retina) note: use physical size (inner_size) and prefer premultiplied alpha if supported + let alpha_mode = if surface_caps + .alpha_modes + .contains(&wgpu::CompositeAlphaMode::PreMultiplied) + { + wgpu::CompositeAlphaMode::PreMultiplied + } else { + wgpu::CompositeAlphaMode::Auto + }; + + let surface_configuration = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, // Defines how the swap_chain's underlying textures will be used + format: format, // Defines how the swap_chain's textures will be stored on the gpu + width: window_size.width, + height: window_size.height, + desired_maximum_frame_latency: 2, + present_mode: present_mode, // Defines how to sync the surface with the display + alpha_mode, + view_formats: vec![format], + }; + + // Configure surface + surface.configure(&device, &surface_configuration); + + DeviceContext { + config, + window_ref, + window_size, + device, + queue, + surface, + surface_configuration, + } + }); + + let state: State = { + // Configure collections + // let mut resource_manager = RendererResourceManager::new(); + + // Use Rgba16Float for HDR color buffers; it's the common, well-supported, + // performant choice. Reserve Rgba32Float for niche cases needing extreme + // precision and accept the 2× bandwidth/memory hit. If alpha isn't needed, + // R11b10Float/R11G11B10_FLOAT is a fast alternative. + // Tone-map to the sRGB swapchain in the composite pass. + // let color_format = wgpu::TextureFormat::Rgba16Float; + // let depth_format = wgpu::TextureFormat::Depth32Float; + + State { passes: vec![] } + }; + Self { + state, + ctx, + resource_manager: resources as *mut _, + } + } + + fn init(&mut self) -> Result<()> { + // Ensure default textures are created before any pass init that may need them + let (device_ptr, queue_ptr): (*const wgpu::Device, *const wgpu::Queue) = + (&self.ctx.device, &self.ctx.queue); + unsafe { + self.rm_mut() + .gpu_mut() + .ensure_default_textures(&*device_ptr, &*queue_ptr); + } + Ok(()) + } + + fn create_buffer(&mut self, desc: BufferDesc) -> Result { + let aligned_size = ((desc.byte_size + 64) / 64) * 64; // 64B for Metal UBOs + let buffer = self.ctx.device.create_buffer(&wgpu::BufferDescriptor { + label: desc.label, + size: aligned_size, + usage: desc.usage, + mapped_at_creation: false, + }); + Ok(buffer) + } + + fn create_pipeline_v2(&mut self, desc: PipelineV2Desc) -> Result { + /* + // Example shader pipeline descriptor for factory method + n_shader = rm->createShader({ + .debugName = "mesh_simple", + .VS (.byteCode = shaderVS, .entryFunc="main"), + .PS (.byteCode = shaderPS, . entryFunc="main"), + .CS (.byteCode = shaderCS, . entryFunc="main"), + .bindGroups = { + { m_globalsBindingsLayout }, // Globals bind group (0) + { materialBindingsLayout }, // Material bind group (1) + }, + .dynamicBuffers = dynamicBindings-getLayout(), + .graphicsState = { + .depthTest = COMPARE::GREATER_OR_EQUAL, // inverse Z + .vertexBufferBindings = { + // Position vertex buffer (8) + .byteStride = 12, .attributes = { + { .byteOffset = 0, .format = FORMAT::RGB32_FLOAT } + } + }, + { + // 2nd vertex buffer: tangent, normal, color, texcoord + .byteStride = 24, .attributes = { + { .byteOffset = 0, .format = FORMAT::RGBA16_FLOAT }, + { .byteOffset = 8, .format = FORMAT::RGBA16_FLOAT }, + { .byteOffset = 16, .format = FORMAT::RGBA8_UNORM }, + { .byteOffset = 20, .format = FORMAT::RG16_FLOAT } + } + } + }, + .renderPassLayout = m_renderPassLayout + }); + */ + + let vs_label_owned = format!("{}{}", desc.label.unwrap_or("program"), "_vs"); + let vs_label = vs_label_owned.as_str(); + let vs = self + .ctx + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some(vs_label), + source: wgpu::ShaderSource::Wgsl(desc.vs.source.into()), + }); + + let ps_label_owned = format!("{}{}", desc.label.unwrap_or("program"), "_ps"); + let ps_label = ps_label_owned.as_str(); + let ps = self + .ctx + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some(ps_label), + source: wgpu::ShaderSource::Wgsl(desc.ps.source.into()), + }); + + // Create multiple bind group layouts + let mut bind_group_layouts = Vec::new(); + for (i, bindings) in desc.bind_groups.iter().enumerate() { + let layout = + self.ctx + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some( + format!("{}{}{}", desc.label.unwrap_or("program"), "_bgl_", i).as_str(), + ), + entries: bindings, + }); + bind_group_layouts.push(layout); + } + + // Create pipeline layout with all bind groups + let pipeline_layout = + self.ctx + .device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_pl"), + bind_group_layouts: &bind_group_layouts.iter().collect::>(), + push_constant_ranges: &[], + }); + + let pl_label_owned = format!("{}{}", desc.label.unwrap_or("program"), "_pipeline"); + let pl_label = pl_label_owned.as_str(); + let pipeline = self + .ctx + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some(pl_label), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &vs, + entry_point: desc.vs.entry_func, + buffers: if desc.vertex_buffers.is_empty() { + &[] + } else { + desc.vertex_buffers + }, + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &ps, + entry_point: desc.ps.entry_func, + targets: &desc.targets, + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + cull_mode: Some(wgpu::Face::Back), + ..wgpu::PrimitiveState::default() + }, + depth_stencil: desc.depth_stencil, + multisample: desc.multisample, + multiview: None, + }); + + Ok(PipelineV2 { + pipeline, + bind_group_layouts, + }) + } + + fn create_material(&mut self, desc: MaterialDesc) -> Result { + // Create bind group layouts matching PassScene + let textures_bgl = + self.ctx + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("material_textures_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 4, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 5, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 6, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 7, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + let params_bgl = + self.ctx + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("material_params_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + + // Build params from desc + let params = RendererMaterialParamsStd140 { + albedo: desc.albedo, + _pad0: 0.0, + metallic: desc.metallic, + roughness: desc.roughness, + _pad1: [0.0, 0.0], + emissive: desc.emissive, + _pad2: 0.0, + }; + + // Build params buffer + bind group + let param_buffer = self + .ctx + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{}_material_params", desc.label)), + contents: bytemuck::bytes_of(¶ms), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + let param_bind_group = self + .ctx + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + layout: ¶ms_bgl, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: param_buffer.as_entire_binding(), + }], + label: Some(&format!("{}_material_params_bg", desc.label)), + }); + // Resolve textures (use defaults if None) + let (device_ptr, queue_ptr): (*const wgpu::Device, *const wgpu::Queue) = + (&self.ctx.device, &self.ctx.queue); + let (def_color_h, def_normal_h) = unsafe { + self.rm_mut() + .gpu_mut() + .ensure_default_textures(&*device_ptr, &*queue_ptr) + }; + let def_color = self.rm().gpu().textures.get(def_color_h).unwrap(); + let def_normal = self.rm().gpu().textures.get(def_normal_h).unwrap(); + let albedo_tex = desc + .albedo_tex + .and_then(|h| self.rm().gpu().textures.get(h)) + .unwrap_or(def_color); + let normal_tex = desc + .normal_tex + .and_then(|h| self.rm().gpu().textures.get(h)) + .unwrap_or(def_normal); + let mr_tex = desc + .metallic_roughness_tex + .and_then(|h| self.rm().gpu().textures.get(h)) + .unwrap_or(def_color); + let emissive_tex = desc + .emissive_tex + .and_then(|h| self.rm().gpu().textures.get(h)) + .unwrap_or(def_color); + + // Build texture bind group + let texture_bind_group = self + .ctx + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + layout: &textures_bgl, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&albedo_tex.texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&albedo_tex.sampler), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::TextureView(&normal_tex.texture_view), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::Sampler(&normal_tex.sampler), + }, + wgpu::BindGroupEntry { + binding: 4, + resource: wgpu::BindingResource::TextureView(&mr_tex.texture_view), + }, + wgpu::BindGroupEntry { + binding: 5, + resource: wgpu::BindingResource::Sampler(&mr_tex.sampler), + }, + wgpu::BindGroupEntry { + binding: 6, + resource: wgpu::BindingResource::TextureView(&emissive_tex.texture_view), + }, + wgpu::BindGroupEntry { + binding: 7, + resource: wgpu::BindingResource::Sampler(&emissive_tex.sampler), + }, + ], + label: Some(&format!("{}_material_textures_bg", desc.label)), + }); + let mat = RendererMaterial { + name: desc.label.to_string(), + texture_bind_group, + param_buffer, + param_bind_group, + }; + let handle = self.rm_mut().gpu_mut().materials.insert(mat); + Ok(handle) + } + + fn update_material( + &mut self, + renderer_material_handle: RendererMaterialHandle, + desc: MaterialDesc, + ) -> Result { + // Prepare params struct + let params = RendererMaterialParamsStd140 { + albedo: desc.albedo, + _pad0: 0.0, + metallic: desc.metallic, + roughness: desc.roughness, + _pad1: [0.0, 0.0], + emissive: desc.emissive, + _pad2: 0.0, + }; + + // Resolve textures without holding a mutable borrow to materials + let (device_ptr, queue_ptr): (*const wgpu::Device, *const wgpu::Queue) = + (&self.ctx.device, &self.ctx.queue); + let (def_color_h, def_normal_h) = unsafe { + self.rm_mut() + .gpu_mut() + .ensure_default_textures(&*device_ptr, &*queue_ptr) + }; + let def_color = self.rm().gpu().textures.get(def_color_h).unwrap(); + let def_normal = self.rm().gpu().textures.get(def_normal_h).unwrap(); + let albedo_tex = desc + .albedo_tex + .and_then(|h| self.rm().gpu().textures.get(h)) + .unwrap_or(def_color); + let normal_tex = desc + .normal_tex + .and_then(|h| self.rm().gpu().textures.get(h)) + .unwrap_or(def_normal); + let mr_tex = desc + .metallic_roughness_tex + .and_then(|h| self.rm().gpu().textures.get(h)) + .unwrap_or(def_color); + let emissive_tex = desc + .emissive_tex + .and_then(|h| self.rm().gpu().textures.get(h)) + .unwrap_or(def_color); + + // Create a compatible layout and new texture bind group + let textures_bgl = + self.ctx + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("material_textures_bgl_update"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 4, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 5, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 6, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 7, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + let new_texture_bg = self + .ctx + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + layout: &textures_bgl, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&albedo_tex.texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&albedo_tex.sampler), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::TextureView(&normal_tex.texture_view), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::Sampler(&normal_tex.sampler), + }, + wgpu::BindGroupEntry { + binding: 4, + resource: wgpu::BindingResource::TextureView(&mr_tex.texture_view), + }, + wgpu::BindGroupEntry { + binding: 5, + resource: wgpu::BindingResource::Sampler(&mr_tex.sampler), + }, + wgpu::BindGroupEntry { + binding: 6, + resource: wgpu::BindingResource::TextureView(&emissive_tex.texture_view), + }, + wgpu::BindGroupEntry { + binding: 7, + resource: wgpu::BindingResource::Sampler(&emissive_tex.sampler), + }, + ], + label: Some(&format!("{}_material_textures_bg", desc.label)), + }); + + // Mutate material once + let queue_ptr: *const wgpu::Queue = &self.ctx.queue; + if let Some(mat) = self + .rm_mut() + .gpu_mut() + .materials + .get_mut(renderer_material_handle) + { + unsafe { + (*queue_ptr).write_buffer(&mat.param_buffer, 0, bytemuck::bytes_of(¶ms)); + } + mat.texture_bind_group = new_texture_bg; + } + Ok(renderer_material_handle) + } + + fn create_render_target(&mut self, desc: RendererTargetDesc) -> Result { + let size = wgpu::Extent3d { + width: desc.width, + height: desc.height, + depth_or_array_layers: 1, + }; + let texture = self.ctx.device.create_texture(&wgpu::TextureDescriptor { + label: Some(desc.name.as_str()), + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: desc.format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = self.ctx.device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + lod_min_clamp: 0.0, + lod_max_clamp: 100.0, + ..Default::default() + }); + let handle = self.rm_mut().gpu_mut().textures.insert(RendererTexture { + texture, + texture_view, + sampler, + format: desc.format, + }); + Ok(handle) + } + + fn create_mesh(&mut self, name: &str, mesh_data: &MeshData) -> Result { + let mesh = RendererMesh::new(&self.ctx.device, name, mesh_data)?; + let handle = self.rm_mut().gpu_mut().meshes.insert(mesh); + + Ok(handle) + } + + fn create_texture( + &mut self, + name: &str, + image_data: &image::DynamicImage, + texture_type: TextureType, + ) -> Result { + let texture = RendererTexture::new_texture( + &self.ctx.device, + &self.ctx.queue, + Some(name), + image_data, + texture_type, + )?; + let handle = self.rm_mut().gpu_mut().textures.insert(texture); + + Ok(handle) + } + + fn create_depth_texture(&mut self, label: &str) -> Result { + let tex = RendererTexture::new_depth_texture( + &self.ctx.device, + &self.ctx.surface_configuration, + label, + )?; + Ok(self.rm_mut().gpu_mut().textures.insert(tex)) + } + + fn create_camera(&mut self) -> Result { + let camera_bind_group_layout = + self.ctx + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("camera_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + let camera = RendererCamera::new(&self.ctx.device, &camera_bind_group_layout)?; + let handle = self.rm_mut().gpu_mut().cameras.insert(camera); + + Ok(handle) + } + + fn destroy_mesh(&mut self, renderer_mesh_handle: RendererMeshHandle) -> Result<()> { + self.rm_mut() + .gpu_mut() + .meshes + .remove(renderer_mesh_handle) + .unwrap(); + + Ok(()) + } + + fn destroy_texture(&mut self, renderer_texture_handle: RendererTextureHandle) -> Result<()> { + self.rm_mut() + .gpu_mut() + .textures + .remove(renderer_texture_handle) + .unwrap(); + + Ok(()) + } + + fn destroy_camera(&mut self, renderer_camera_handle: RendererCameraHandle) -> Result<()> { + self.rm_mut() + .gpu_mut() + .cameras + .remove(renderer_camera_handle) + .unwrap(); + + Ok(()) + } + + fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize) { + info!("Resizing {} resources", "Renderer".mobj_style()); + if new_window_size.width > 0 && new_window_size.height > 0 { + self.ctx.window_size = new_window_size; + self.ctx.surface_configuration.width = new_window_size.width; + self.ctx.surface_configuration.height = new_window_size.height; + self.ctx + .surface + .configure(&self.ctx.device, &self.ctx.surface_configuration); + + // Reinitialize existing passes in place (engine may have set custom passes) + let mut passes = std::mem::take(&mut self.state.passes); + let self_ptr: *mut Renderer = self as *mut _; + let rm_ptr: *mut crate::resources::ResourceManager = self.resource_manager; + for pass in passes.iter_mut() { + // Avoid double mutable borrow of self by using raw pointer for resources + let _ = unsafe { pass.init(&mut *self_ptr, &mut *rm_ptr) }; + } + self.state.passes = passes; + // Old offscreen texture/view/sampler are dropped when replaced; wgpu defers actual GPU resource + // destruction until safe. See optional early reclamation via device.poll: + // https://docs.rs/wgpu/latest/wgpu/struct.Device.html#method.poll + self.ctx.device.poll(wgpu::Maintain::Wait); + } + } + + fn render( + &mut self, + active_camera_entity_handle: EntityHandle, + render_queue: &Vec, + camera_component_storage: &ComponentStorage, + transform_component_storage: &ComponentStorage, + timer: &mut Timer, + ) -> Result<()> { + timer.record("Frame: acquire"); + + // Get frame or return mapped error if failed + // ALLOCATION: Surface texture allocation (GPU memory) - ~4MB for 1920x1080 RGBA8 + let frame = self.ctx.surface.get_current_texture().unwrap(); + + // ALLOCATION: TextureView creation (GPU memory) - ~64 bytes metadata + let view: wgpu::TextureView = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + timer.record("Frame: setup view"); + + // Scene draw is now a Pass; remaining work is handled in user passes encoder below + { + // User passes with separate encoder + timer.begin_context("User passes"); + // ALLOCATION: CommandEncoder creation (CPU memory) - ~1KB command buffer + let mut encoder = + self.ctx + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("user_passes"), + }); + + // Loop over state.passes, for each + // Construct world query bundle for passes + let world_q = WorldQuery { + active_camera: active_camera_entity_handle, + render_queue, + camera_components: camera_component_storage, + transform_components: transform_component_storage, + }; + let mut passes = std::mem::take(&mut self.state.passes); + let self_ptr: *mut Renderer = self as *mut _; + let rm_ptr: *mut crate::resources::ResourceManager = self.resource_manager; + for pass in passes.iter_mut() { + let label_owned = pass.get_label().to_string(); + timer.begin_context(&label_owned); + // Avoid double mutable borrow of self by using raw pointer for resources + let _ = unsafe { + pass.draw( + &mut encoder, + &mut *self_ptr, + &mut *rm_ptr, + &frame, + &view, + &world_q, + ) + }; + timer.record(&label_owned); + } + self.state.passes = passes; + + self.ctx.queue.submit([encoder.finish()]); + timer.record("User passes submit"); + timer.end_context()?; // End "User passes" + } + + // Present frame + frame.present(); + timer.record("Present"); + + // Allow wgpu to process pending work and retire resources referenced by submitted encoders + // without blocking the CPU. See Device::poll docs. + self.ctx.device.poll(wgpu::Maintain::Poll); + + Ok(()) + } + + // --- Engine pass management and helpers --- + fn set_passes(&mut self, mut passes: Vec>) -> Result<()> { + let self_ptr: *mut Renderer = self as *mut _; + let rm_ptr: *mut crate::resources::ResourceManager = self.resource_manager; + for pass in passes.iter_mut() { + // Avoid double mutable borrow of self by using raw pointer for resources + let _ = unsafe { pass.init(&mut *self_ptr, &mut *rm_ptr) }; + } + self.state.passes = passes; + Ok(()) + } + + fn get_surface_format(&self) -> wgpu::TextureFormat { + self.ctx.surface_configuration.format + } + + fn get_device(&self) -> &wgpu::Device { + &self.ctx.device + } + + fn get_queue(&self) -> &wgpu::Queue { + &self.ctx.queue + } +} + +impl Renderer { + #[inline] + fn rm(&self) -> &crate::resources::ResourceManager { + assert!( + !self.resource_manager.is_null(), + "Renderer ResourceManager not attached" + ); + unsafe { &*self.resource_manager } + } + + #[inline] + fn rm_mut(&mut self) -> &mut crate::resources::ResourceManager { + assert!( + !self.resource_manager.is_null(), + "Renderer ResourceManager not attached" + ); + unsafe { &mut *self.resource_manager } + } + + pub fn get_surface_format(&self) -> wgpu::TextureFormat { + self.ctx.surface_configuration.format + } + + pub fn get_device(&self) -> &wgpu::Device { + &self.ctx.device + } + + pub fn get_surface(&self) -> &wgpu::Surface<'_> { + &self.ctx.surface + } +} diff --git a/engine/pill_engine/src/resources/gpu_resources.rs b/engine/pill_engine/src/resources/gpu_resources.rs new file mode 100644 index 00000000..bc44f316 --- /dev/null +++ b/engine/pill_engine/src/resources/gpu_resources.rs @@ -0,0 +1,121 @@ +use std::collections::HashMap; + +use crate::graphics::{ + RendererBufferHandle, RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, + RendererPipelineHandle, RendererTextureHandle, +}; +use crate::renderer::resources::{ + RendererCamera, RendererMaterial, RendererMesh, RendererPipeline, RendererTexture, +}; +use crate::resources::TextureType; + +use pill_core::{ + Handle, RendererBufferTag, RendererCameraTag, RendererMaterialTag, RendererMeshTag, + RendererPipelineTag, RendererTextureTag, ResourcePool, +}; + +use image::{DynamicImage, Rgba, RgbaImage}; + +#[derive(Hash, PartialEq, Eq, Clone, Copy)] +pub struct PipelineKey(pub u64); + +pub struct PipelineCache { + key_to_handle: HashMap, +} + +impl PipelineCache { + pub fn new() -> Self { + Self { + key_to_handle: HashMap::new(), + } + } + + pub fn lookup(&self, key: &PipelineKey) -> Option { + self.key_to_handle.get(key).copied() + } + + pub fn insert(&mut self, key: PipelineKey, h: RendererPipelineHandle) { + self.key_to_handle.insert(key, h); + } +} + +pub struct GpuResources { + pub buffers: ResourcePool, + pub pipelines: ResourcePool, + pub textures: ResourcePool, + pub materials: ResourcePool, + pub meshes: ResourcePool, + pub cameras: ResourcePool, + pub pipeline_cache: PipelineCache, + // cached defaults + pub default_color_tex: Option, + pub default_normal_tex: Option, +} + +impl GpuResources { + pub fn new() -> Self { + Self { + buffers: ResourcePool::new(), + pipelines: ResourcePool::new(), + textures: ResourcePool::new(), + materials: ResourcePool::new(), + meshes: ResourcePool::new(), + cameras: ResourcePool::new(), + pipeline_cache: PipelineCache::new(), + default_color_tex: None, + default_normal_tex: None, + } + } + + #[inline] + pub fn buffer(&self, h: RendererBufferHandle) -> &wgpu::Buffer { + self.buffers.get(h).expect("Invalid buffer handle") + } + + #[inline] + pub fn pipeline(&self, h: RendererPipelineHandle) -> &RendererPipeline { + self.pipelines.get(h).expect("Invalid pipeline handle") + } + + pub fn ensure_default_textures( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + ) -> (RendererTextureHandle, RendererTextureHandle) { + if self.default_color_tex.is_none() { + let mut img = RgbaImage::new(1, 1); + img.put_pixel(0, 0, Rgba([255, 255, 255, 255])); + let dyn_img = DynamicImage::ImageRgba8(img); + let tex = RendererTexture::new_texture( + device, + queue, + Some("default_color"), + &dyn_img, + TextureType::Gamma, + ) + .expect("create default color tex"); + let h = self.textures.insert(tex); + self.default_color_tex = Some(h); + } + if self.default_normal_tex.is_none() { + let mut img = RgbaImage::new(1, 1); + // normal map flat (128,128,255) + img.put_pixel(0, 0, Rgba([128, 128, 255, 255])); + let dyn_img = DynamicImage::ImageRgba8(img); + let tex = RendererTexture::new_texture( + device, + queue, + Some("default_normal"), + &dyn_img, + TextureType::Linear, + ) + .expect("create default normal tex"); + let h = self.textures.insert(tex); + self.default_normal_tex = Some(h); + } + ( + self.default_color_tex.unwrap(), + self.default_normal_tex.unwrap(), + ) + } +} diff --git a/engine/pill_engine/src/resources/material.rs b/engine/pill_engine/src/resources/material.rs index a60000cf..db710eb6 100644 --- a/engine/pill_engine/src/resources/material.rs +++ b/engine/pill_engine/src/resources/material.rs @@ -1,494 +1,150 @@ use crate::{ - engine::Engine, - graphics::{ RendererTextureHandle, RendererMaterialHandle, RENDER_QUEUE_KEY_ORDER }, - resources::{ TextureHandle, TextureType, Texture, ResourceStorage, Resource }, - ecs::{ DeferredUpdateManagerPointer, DeferredUpdateResourceRequest, MeshRenderingComponent, DeferredUpdateComponent }, config::*, + engine::Engine, + graphics::{ + MaterialDesc, RendererMaterialHandle, RendererTextureHandle, RENDER_QUEUE_KEY_ORDER, + }, + resources::{Resource, ResourceStorage, Texture, TextureHandle, TextureType}, }; -use pill_core::{ Color, EngineError, PillSlotMapKey, PillTypeMapKey, PillStyle, enum_variant_eq, get_enum_variant_type_name, get_type_name }; - -use anyhow::{ Result, Context, Error }; -use boolinator::*; -use std::{ - path::{ Path, PathBuf }, - collections::HashMap, - ops::{Range, RangeInclusive} -}; - - -const DEFERRED_REQUEST_VARIANT_RENDERING_ORDER: usize = 0; -const DEFERRED_REQUEST_VARIANT_PARAMETER: usize = 1; - -const DEFERRED_REQUEST_VARIANT_TEXTURE_START: usize = 2; -const DEFERRED_REQUEST_VARIANT_TEXTURE_END: usize = 10; - -// --- Material parameters --- - -#[derive(Debug)] -pub enum MaterialParameter { - Scalar(Option), - Bool(Option), - Color(Option), -} - -impl MaterialParameter { - pub fn is_some(&self) -> bool { - match self { - MaterialParameter::Scalar(v) => v.is_some(), - MaterialParameter::Bool(v) => v.is_some(), - MaterialParameter::Color(v) => v.is_some(), - } - } -} - -pub struct MaterialParameterMap { - pub data: HashMap, - pub(crate) mapping: Vec, // Maps index to slot name -} - -impl MaterialParameterMap { - pub fn new() -> Self { - Self { - data: HashMap::::new(), - mapping: Vec::::new(), - } - } - - pub fn get_scalar(&self, parameter_name: &str) -> Result { - let error = EngineError::MaterialParameterSlotNotFound(parameter_name.to_string(), "Scalar".to_string()); - match self.data.get(parameter_name).context(error.clone())? { - MaterialParameter::Scalar(v) => match v { - Some(vv) => Ok(vv.clone()), - None => panic!(), - }, - _ => Err(Error::new(error)) - } - } - - pub fn get_bool(&self, parameter_name: &str) -> Result { - let error = EngineError::MaterialParameterSlotNotFound(parameter_name.to_string(), "Bool".to_string()); - match self.data.get(parameter_name).context(error.clone())? { - MaterialParameter::Bool(v) => match v { - Some(vv) => Ok(vv.clone()), - None => panic!(), - }, - _ => Err(Error::new(error)) - } - } - - pub fn get_color(&self, parameter_name: &str) -> Result { - let error = EngineError::MaterialParameterSlotNotFound(parameter_name.to_string(), "Color".to_string()); - match self.data.get(parameter_name).context(error.clone())? { - MaterialParameter::Color(v) => match v { - Some(vv) => Ok(vv.clone()), - None => panic!(), - }, - _ => Err(Error::new(error)) - } - } - - pub fn set_parameter(&mut self, parameter_name: &str, value: MaterialParameter) -> Result<()> { - let error = Error::new(EngineError::MaterialParameterSlotNotFound(parameter_name.to_string(), pill_core::get_enum_variant_type_name(&value).to_string())); - let parameter = self.data.get_mut(parameter_name).context(error)?; - - if pill_core::enum_variant_eq::(¶meter, &value) { - *parameter = value; - } - - Ok(()) - } -} - -// --- Material textures --- +use pill_core::{get_type_name, Color, EngineError, PillSlotMapKey, PillStyle, PillTypeMapKey}; -pub struct MaterialTexture { - pub texture_type: TextureType, - pub texture_handle: Option, - pub(crate) renderer_texture_handle: Option, -} - -impl MaterialTexture { - pub fn new(texture_type: TextureType) -> Self { - Self { - texture_type, - texture_handle: None, - renderer_texture_handle: None, - } - } -} - -// This needed so that renderer can get renderer texture handle from material texture while it is still hidden in game API -pub fn get_renderer_texture_handle_from_material_texture(material_texture: &MaterialTexture) -> &Option { - &material_texture.renderer_texture_handle -} - -pub struct MaterialTextureMap { - pub data: HashMap, - pub(crate) mapping: Vec, // Maps index to slot name -} +use anyhow::{Context, Error, Result}; -impl MaterialTextureMap { - pub fn new() -> Self { - Self { - data: HashMap::::new(), - mapping: Vec::::new(), - } - } +// --- Material (PBR POD) --- - pub fn get(&self, name: &str) -> Option<&MaterialTexture> { - self.data.get(name) - } -} - -// --- Builder --- - -pub struct MaterialBuilder { - material: Material, -} - -impl MaterialBuilder { - pub fn new(name: &str) -> Self { - Self { - material: Material::new(name), - } - } - - pub fn texture(mut self, slot_name: &str, texture_handle: TextureHandle) -> Result { - self.material.set_texture(slot_name, texture_handle)?; - Ok(self) - } - - pub fn scalar(mut self, slot_name: &str, value: f32) -> Result { - self.material.set_scalar(slot_name, value)?; - Ok(self) - } - - pub fn bool(mut self, slot_name: &str, value: bool) -> Result { - self.material.set_bool(slot_name, value)?; - Ok(self) - } - - pub fn color(mut self, slot_name: &str, value: Color) -> Result { - self.material.set_color(slot_name, value)?; - Ok(self) - } - - pub fn rendering_order(mut self, order: u8) -> Result { - self.material.set_rendering_order(order)?; - Ok(self) - } - - pub fn build(self) -> Material { - self.material - } -} - -// --- Material --- - -pill_core::define_new_pill_slotmap_key! { - pub struct MaterialHandle; +pill_core::define_new_pill_slotmap_key! { + pub struct PBRMaterialHandle; } #[readonly::make] -pub struct Material { +pub struct PBRMaterial { #[readonly] pub name: String, #[readonly] - textures: MaterialTextureMap, - #[readonly] - parameters: MaterialParameterMap, - #[readonly] - pub rendering_order: u8, - pub renderer_resource_handle: Option, - - handle: Option, - deferred_update_manager: Option, + // PBR factors (no alpha) + pub albedo: Color, // vec3 + pub metallic: f32, + pub roughness: f32, + pub emissive: Color, // vec3 + + // PBR textures + pub albedo_texture: Option, // Color + pub normal_texture: Option, // Normal + pub metallic_roughness_texture: Option, // Color (G=roughness, B=metallic) + pub emissive_texture: Option, // Color + + // Dirty flag to trigger GPU updates + pub is_dirty: bool, + pub(crate) renderer_resource_handle: Option, } -impl Material { - pub fn builder(name: &str) -> MaterialBuilder { - MaterialBuilder::new(name) - } - - pub fn new(name: &str) -> Self { - let mut textures = MaterialTextureMap::new(); - textures.data.insert(MASTER_SHADER_COLOR_TEXTURE_SLOT.to_string(), MaterialTexture::new(TextureType::Color)); - textures.mapping.push(MASTER_SHADER_COLOR_TEXTURE_SLOT.to_string()); - textures.data.insert(MASTER_SHADER_NORMAL_TEXTURE_SLOT.to_string(), MaterialTexture::new(TextureType::Normal)); - textures.mapping.push(MASTER_SHADER_NORMAL_TEXTURE_SLOT.to_string()); - - let mut parameters = MaterialParameterMap::new(); - parameters.data.insert(MASTER_SHADER_TINT_PARAMETER_SLOT.to_string(), MaterialParameter::Color(None)); - textures.mapping.push(MASTER_SHADER_TINT_PARAMETER_SLOT.to_string()); - parameters.data.insert(MASTER_SHADER_SPECULARITY_PARAMETER_SLOT.to_string(), MaterialParameter::Scalar(None)); - textures.mapping.push(MASTER_SHADER_SPECULARITY_PARAMETER_SLOT.to_string()); - +impl PBRMaterial { + pub fn new(name: &str) -> Self { Self { - name: name.to_string(), - textures, - parameters, - rendering_order: RENDER_QUEUE_KEY_ORDER.max as u8, - renderer_resource_handle: None, - handle: None, - deferred_update_manager: None, + name: name.to_string(), + albedo: Color::new(1.0, 1.0, 1.0), + metallic: 0.0, + roughness: 0.5, + emissive: Color::new(0.0, 0.0, 0.0), + albedo_texture: None, + normal_texture: None, + metallic_roughness_texture: None, + emissive_texture: None, + is_dirty: true, + renderer_resource_handle: None, } } - pub fn set_texture(&mut self, slot_name: &str, texture_handle: TextureHandle) -> Result<()> { - // Get texture slot - let texture_slot = self.textures.data.get_mut(slot_name) - .ok_or( Error::new(EngineError::MaterialTextureSlotNotFound(slot_name.to_string())))?; - - // Get texture slot index - let texture_slot_index = self.textures.mapping.iter().position(|v| v == slot_name).expect("Critical: No mapping"); - - // Set new handle but not renderer resource handle (it will be set by deferred update system) - let _ = texture_slot.texture_handle.insert(texture_handle.clone()); - - // Post deferred update request (only if renderer resource handle is set (it means that material is initialized)) - if self.renderer_resource_handle.is_some() { - self.post_deferred_update_request(DEFERRED_REQUEST_VARIANT_TEXTURE_START + texture_slot_index); - } - - Ok(()) + // Factor setters (mark dirty) + pub fn set_base_color_factor(&mut self, value: Color) { + self.albedo = Color::new( + value.x.clamp(0.0, 1.0), + value.y.clamp(0.0, 1.0), + value.z.clamp(0.0, 1.0), + ); + self.is_dirty = true; } - pub fn remove_texture(&mut self, slot_name: &str) -> Result<()> { - // Get texture slot - let texture_slot = self.textures.data.get_mut(slot_name) - .ok_or( Error::new(EngineError::MaterialTextureSlotNotFound(slot_name.to_string())))?; - - // Get texture slot index - let texture_slot_index = self.textures.mapping.iter().position(|v| v == slot_name).expect("Critical: No mapping"); - - // Set new handle and renderer resource handle - texture_slot.texture_handle = None; - texture_slot.renderer_texture_handle = None; - - // Post deferred update request (only if renderer resource handle is set (it means that material is initialized)) - if self.renderer_resource_handle.is_some() { - self.post_deferred_update_request(DEFERRED_REQUEST_VARIANT_TEXTURE_START + texture_slot_index); - } - - Ok(()) - } - - pub fn set_rendering_order(&mut self, order: u8) -> Result<()> { - if order < RENDER_QUEUE_KEY_ORDER.max as u8 { - // Set new order - self.rendering_order = order; - - // Post deferred update request (only if renderer resource handle is set (it means that material is initialized)) - if self.renderer_resource_handle.is_some() { - self.post_deferred_update_request(DEFERRED_REQUEST_VARIANT_RENDERING_ORDER); - } - } - else { - return Err(Error::new(EngineError::WrongRenderingOrder(order.to_string(), format!("{}-{}", 0, RENDER_QUEUE_KEY_ORDER.max.to_string())))); - } - - Ok(()) + pub fn set_metallic_factor(&mut self, value: f32) { + self.metallic = value.clamp(0.0, 1.0); + self.is_dirty = true; } - pub fn get_scalar(&self, parameter_name: &str) -> Result { - self.parameters.get_scalar(parameter_name) + pub fn set_roughness_factor(&mut self, value: f32) { + self.roughness = value.clamp(0.0, 1.0); + self.is_dirty = true; } - pub fn get_bool(&self, parameter_name: &str) -> Result { - self.parameters.get_bool(parameter_name) + pub fn set_emissive_factor(&mut self, value: Color) { + self.emissive = Color::new( + value.x.clamp(0.0, 1.0), + value.y.clamp(0.0, 1.0), + value.z.clamp(0.0, 1.0), + ); + self.is_dirty = true; } - pub fn get_color(&self, parameter_name: &str) -> Result { - self.parameters.get_color(parameter_name) + // Texture setters (mark dirty) + pub fn set_albedo_texture(&mut self, handle: TextureHandle) { + self.albedo_texture = Some(handle); + self.is_dirty = true; } - pub fn set_scalar(&mut self, parameter_name: &str, value: f32) -> Result<()> { - self.set_parameter(parameter_name, MaterialParameter::Scalar(Some(value))) + pub fn set_normal_texture(&mut self, handle: TextureHandle) { + self.normal_texture = Some(handle); + self.is_dirty = true; } - pub fn set_bool(&mut self, parameter_name: &str, value: bool) -> Result<()> { - self.set_parameter(parameter_name, MaterialParameter::Bool(Some(value))) - } - - pub fn set_color(&mut self, parameter_name: &str, value: Color) -> Result<()> { - // Clamp color channel values between 0.0 and 1.0 - let valid_color = Color::new(value.x.clamp(0.0, 1.0), value.y.clamp(0.0, 1.0), value.z.clamp(0.0, 1.0)); - self.set_parameter(parameter_name, MaterialParameter::Color(Some(valid_color))) - } - - fn set_parameter(&mut self, parameter_name: &str, value: MaterialParameter) -> Result<()> { - // Set parameter - self.parameters.set_parameter(parameter_name, value)?; - - // Post deferred update request (only if renderer resource handle is set (it means that material is initialized)) - if self.renderer_resource_handle.is_some() { - self.post_deferred_update_request(DEFERRED_REQUEST_VARIANT_PARAMETER); - } - - Ok(()) + pub fn set_metallic_roughness_texture(&mut self, handle: TextureHandle) { + self.metallic_roughness_texture = Some(handle); + self.is_dirty = true; } - pub(crate) fn get_textures(&mut self) -> &mut MaterialTextureMap { - &mut self.textures - } - - fn post_deferred_update_request(&mut self, request_variant: usize) { - let handle = self.handle.expect("Critical: Cannot post deferred update request. No Handle set in Resource"); - let request = DeferredUpdateResourceRequest::::new(handle, request_variant); - self.deferred_update_manager.as_mut().expect("Critical: No DeferredUpdateManager").post_update_request(request); + pub fn set_emissive_texture(&mut self, handle: TextureHandle) { + self.emissive_texture = Some(handle); + self.is_dirty = true; } } -impl PillTypeMapKey for Material { - type Storage = ResourceStorage; +impl PillTypeMapKey for PBRMaterial { + type Storage = ResourceStorage; } -impl Resource for Material { - type Handle = MaterialHandle; +impl Resource for PBRMaterial { + type Handle = PBRMaterialHandle; fn get_name(&self) -> String { self.name.clone() } fn initialize(&mut self, engine: &mut Engine) -> Result<()> { - let error_message = format!("Initializing {} {} failed", "Resource".gobj_style(), get_type_name::().sobj_style()); - - // This resource is using DeferredUpdateSystem so keep DeferredUpdateManager - let deferred_update_component = engine.get_global_component_mut::().expect("Critical: No DeferredUpdateComponent"); - self.deferred_update_manager = Some(deferred_update_component.borrow_deferred_update_manager()); - - // Check if assigned textures are of correct type - for texture_slot in self.textures.data.iter_mut() { - if let Some(texture_handle) = texture_slot.1.texture_handle { - // Get texture to be set - let texture = engine.get_resource::(&texture_handle) - .context(error_message.clone()).context(format!("Invalid {} for {} in slot {}", "Handle".sobj_style(), "Texture".sobj_style(), texture_slot.0.name_style()))?; - - // Check if slots are of same type - if !enum_variant_eq(&texture.texture_type,&texture_slot.1.texture_type) { - return Err(Error::new(EngineError::WrongTextureType( - get_enum_variant_type_name(&texture.texture_type), - texture_slot.0.to_string(), - get_enum_variant_type_name(&texture_slot.1.texture_type) - ))); - } - - // Set renderer resource handle - texture_slot.1.renderer_texture_handle = texture.renderer_resource_handle; - } - } - - // Set default parameters if not already set - let parameter_values = vec![ - (MASTER_SHADER_TINT_PARAMETER_SLOT, MaterialParameter::Color(Some(Color::new(1.0, 1.0, 1.0)))), - (MASTER_SHADER_SPECULARITY_PARAMETER_SLOT, MaterialParameter::Scalar(Some(0.0))) - ]; - for parameter_value in parameter_values { - let parameter = self.parameters.data.get_mut(parameter_value.0); - match parameter { - Some(v) => { - if pill_core::enum_variant_eq::(¶meter_value.1, v) { - if !v.is_some() { - *v = parameter_value.1; - } - } - }, - None => panic!("Critical: Wrong parameters setup"), - } - } - - // Create new renderer material resource - let renderer_resource_handle = engine.renderer.create_material(&self.name, &self.textures, &self.parameters).context(error_message)?; - self.renderer_resource_handle = Some(renderer_resource_handle); - - Ok(()) - } - - fn pass_handle(&mut self, self_handle: H) { - self.handle = Some(MaterialHandle::from(self_handle.data())); - } - - fn deferred_update(&mut self, engine: &mut Engine, request: usize) -> Result<()> { - match request { - DEFERRED_REQUEST_VARIANT_RENDERING_ORDER => - { - // Find mesh rendering components that use this material and update them - for (scene_handle, scene) in engine.scene_manager.scenes.iter_mut() { - for (entity_handle, mesh_rendering_component) in scene.get_one_component_iterator_mut::()? { - if let Some(material_handle) = mesh_rendering_component.material_handle { - // If mesh rendering component has handle to this material - if material_handle.data() == self.handle.unwrap().data() { - mesh_rendering_component.update_render_queue_key(&engine.resource_manager).unwrap(); - } - } - } - } - }, - DEFERRED_REQUEST_VARIANT_PARAMETER => - { - // Update renderer counterpart - engine.renderer.update_material_parameters(self.renderer_resource_handle.unwrap(), &self.parameters)?; - }, - DEFERRED_REQUEST_VARIANT_TEXTURE_START..=DEFERRED_REQUEST_VARIANT_TEXTURE_END => - { - // Check if assigned texture is of correct type - let texture_slot_name = self.textures.mapping.get(request - DEFERRED_REQUEST_VARIANT_TEXTURE_START).unwrap(); - let texture_slot = self.textures.data.get_mut(texture_slot_name).unwrap(); - if let Some(texture_handle) = texture_slot.texture_handle { - // Get texture to be set - let texture = engine.get_resource::(&texture_handle) - .context(format!("Cannot set {}. Invalid {} for {} in slot {}. ", "Texture".sobj_style(), "Handle".sobj_style(), "Texture".sobj_style(), texture_slot_name.name_style()))?; - - // Check if slots are of same type - if !enum_variant_eq(&texture.texture_type,&texture_slot.texture_type) { - return Err(Error::new(EngineError::WrongTextureType( - get_enum_variant_type_name(&texture.texture_type), - texture_slot_name.to_string(), - get_enum_variant_type_name(&texture_slot.texture_type) - ))); - } - - // Set renderer resource handle - let _ = texture_slot.renderer_texture_handle.insert(texture.renderer_resource_handle.unwrap().clone()); - } - - // Update renderer counterpart - engine.renderer.update_material_textures(self.renderer_resource_handle.unwrap(), &self.textures)?; - }, - _ => - { - panic!("Critical: Processing deferred update request with value {} in {} failed. Handling is not implemented", request, get_type_name::().sobj_style()); + // Build initial descriptor + let desc = { + // Resolve renderer texture handles if available + let to_renderer = |th: &Option| -> Option { + th.as_ref().and_then(|h| { + engine + .resource_manager + .get_resource::(h) + .ok() + .and_then(|t| t.renderer_resource_handle) + }) + }; + MaterialDesc { + label: &self.name, + albedo: [self.albedo.x, self.albedo.y, self.albedo.z], + metallic: self.metallic, + roughness: self.roughness, + emissive: [self.emissive.x, self.emissive.y, self.emissive.z], + albedo_tex: to_renderer(&self.albedo_texture), + normal_tex: to_renderer(&self.normal_texture), + metallic_roughness_tex: to_renderer(&self.metallic_roughness_texture), + emissive_tex: to_renderer(&self.emissive_texture), } - } - + }; + let h = engine.renderer.create_material(desc)?; + self.renderer_resource_handle = Some(h); Ok(()) } - - fn destroy(&mut self, engine: &mut Engine, self_handle: H) -> Result<()> { - // Destroy renderer resource - if let Some(v) = self.renderer_resource_handle { - engine.renderer.destroy_material(v).unwrap(); - } - - // Find mesh rendering components that use this material and update them - - - - for (scene_handle, scene) in engine.scene_manager.scenes.iter_mut() { - let x = &engine.resource_manager; - - // for (entity_handle, mesh_rendering_component) in engine.iterate_one_component::()? { - // if let Some(material_handle) = mesh_rendering_component.material_handle { - // // If mesh rendering component has handle to this material - // if material_handle.data() == self_handle.data() { - // mesh_rendering_component.set_material_handle(Option::::None); - // mesh_rendering_component.update_render_queue_key(&engine.resource_manager).unwrap(); - // } - // } - // } - } - - Ok(()) - } -} \ No newline at end of file +} diff --git a/engine/pill_engine/src/resources/mesh.rs b/engine/pill_engine/src/resources/mesh.rs index 616229fc..7463675f 100644 --- a/engine/pill_engine/src/resources/mesh.rs +++ b/engine/pill_engine/src/resources/mesh.rs @@ -1,19 +1,21 @@ use crate::{ - engine::Engine, - graphics::{ RendererMeshHandle }, - resources::{ ResourceStorage, Resource }, - ecs::{ DeferredUpdateManagerPointer, MeshRenderingComponent }, config::*, + ecs::{DeferredUpdateManagerPointer, MeshRenderingComponent}, + engine::Engine, + graphics::RendererMeshHandle, + resources::{Resource, ResourceStorage}, }; -use pill_core::{ EngineError, PillSlotMapKey, PillTypeMap, PillTypeMapKey, Vector3f, PillStyle, get_type_name }; +use pill_core::{ + get_type_name, EngineError, PillSlotMapKey, PillStyle, PillTypeMap, PillTypeMapKey, Vector3f, +}; -use std::path::{ Path, PathBuf }; +use anyhow::{Context, Error, Result}; use boolinator::Boolinator; use cgmath::InnerSpace; +use gltf::import as gltf_import; +use std::path::{Path, PathBuf}; use tobj::LoadOptions; -use anyhow::{Result, Context, Error}; - pill_core::define_new_pill_slotmap_key! { pub struct MeshHandle; @@ -40,7 +42,17 @@ impl Mesh { path, renderer_resource_handle: None, mesh_data: None, - flip_uv_y: false + flip_uv_y: false, + } + } + + pub fn from_mesh_data(name: &str, data: MeshData) -> Self { + Self { + name: name.to_string(), + path: PathBuf::new(), + renderer_resource_handle: None, + mesh_data: Some(data), + flip_uv_y: false, } } @@ -48,6 +60,10 @@ impl Mesh { self.flip_uv_y = flip; self } + + pub fn get_aabb(&self) -> Option<([f32; 3], [f32; 3])> { + self.mesh_data.as_ref().map(|d| (d.aabb_min, d.aabb_max)) + } } impl PillTypeMapKey for Mesh { @@ -62,26 +78,71 @@ impl Resource for Mesh { } fn initialize(&mut self, engine: &mut Engine) -> Result<()> { - let error_message = format!("Initializing {} {} failed", "Resource".gobj_style(), get_type_name::().sobj_style()); - - // Check if path to asset is correct - let resource_file_path = engine.game_resources_directory_path.join(&self.path); - pill_core::validate_asset_path(&resource_file_path, &["obj"]).context(error_message.clone())?; - - // Create mesh data - let mesh_data = MeshData::new(&resource_file_path, self.flip_uv_y).context(error_message.clone()) - .context(format!("Failed to create mesh data from {} file", resource_file_path.file_name().unwrap().to_string_lossy()))?; - self.mesh_data = Some(mesh_data); + let error_message = format!( + "Initializing {} {} failed", + "Resource".gobj_style(), + get_type_name::().sobj_style() + ); + + // If mesh data is not pre-populated, load from path + if self.mesh_data.is_none() { + // Resolve absolute path + let resource_file_path = engine.game_resources_directory_path.join(&self.path); + let ext = resource_file_path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .to_ascii_lowercase(); + + // Route by extension + match ext.as_str() { + "obj" => { + // Validate and load OBJ + pill_core::validate_asset_path(&resource_file_path, &["obj"]) + .context(error_message.clone())?; + let mesh_data = MeshData::new(&resource_file_path, self.flip_uv_y) + .context(error_message.clone()) + .context(format!( + "Failed to create mesh data from {} file", + resource_file_path.file_name().unwrap().to_string_lossy() + ))?; + self.mesh_data = Some(mesh_data); + } + "gltf" | "glb" => { + // Validate and load glTF/GLB + pill_core::validate_asset_path(&resource_file_path, &["gltf", "glb"]) + .context(error_message.clone())?; + let mesh_data = load_meshdata_from_gltf(&resource_file_path) + .context(error_message.clone()) + .context(format!( + "Failed to create mesh data from {} file", + resource_file_path.file_name().unwrap().to_string_lossy() + ))?; + self.mesh_data = Some(mesh_data); + } + _ => { + return Err(Error::new(EngineError::InvalidModelFile( + resource_file_path + .clone() + .into_os_string() + .into_string() + .unwrap_or_else(|_| "unknown".to_string()), + ))); + } + } + } // Create new renderer mesh resource - let renderer_resource_handle = engine.renderer.create_mesh(&self.name, &self.mesh_data.as_ref().unwrap()).context(error_message.clone())?; + let renderer_resource_handle = engine + .renderer + .create_mesh(&self.name, &self.mesh_data.as_ref().unwrap()) + .context(error_message.clone())?; self.renderer_resource_handle = Some(renderer_resource_handle); Ok(()) } fn destroy(&mut self, engine: &mut Engine, self_handle: H) -> Result<()> { - // Destroy renderer resource if let Some(v) = self.renderer_resource_handle { engine.renderer.destroy_mesh(v).unwrap(); @@ -89,12 +150,16 @@ impl Resource for Mesh { // Find mesh rendering components that use this mesh and update them for (scene_handle, scene) in engine.scene_manager.scenes.iter_mut() { - for (entity_handle, mesh_rendering_component) in scene.get_one_component_iterator_mut::()? { + for (entity_handle, mesh_rendering_component) in + scene.get_one_component_iterator_mut::()? + { if let Some(mesh_handle) = mesh_rendering_component.mesh_handle { // If mesh rendering component has handle to this mesh if mesh_handle.data() == self_handle.data() { mesh_rendering_component.set_mesh_handle(Option::::None); - mesh_rendering_component.update_render_queue_key(&engine.resource_manager).unwrap(); + mesh_rendering_component + .update_render_queue_key(&engine.resource_manager) + .unwrap(); } } } @@ -119,6 +184,26 @@ pub struct MeshVertex { pub struct MeshData { pub vertices: Vec, pub indices: Vec, + pub aabb_min: [f32; 3], + pub aabb_max: [f32; 3], +} + +impl MeshVertex { + pub fn new( + position: [f32; 3], + texture_coordinates: [f32; 2], + normal: [f32; 3], + tangent: [f32; 3], + bitangent: [f32; 3], + ) -> Self { + Self { + position, + texture_coordinates, + normal, + tangent, + bitangent, + } + } } impl MeshData { @@ -135,11 +220,15 @@ impl MeshData { // Check data validity if models.len() > 1 { - return Err(Error::new(EngineError::InvalidModelFileMultipleMeshes(path.clone().into_os_string().into_string().unwrap()))); + return Err(Error::new(EngineError::InvalidModelFileMultipleMeshes( + path.clone().into_os_string().into_string().unwrap(), + ))); } if models.len() < 1 { - return Err(Error::new(EngineError::InvalidModelFile(path.clone().into_os_string().into_string().unwrap()))); + return Err(Error::new(EngineError::InvalidModelFile( + path.clone().into_os_string().into_string().unwrap(), + ))); } // Load vertex data from model @@ -147,19 +236,28 @@ impl MeshData { // Read vertices let mut vertices = Vec::new(); + let mut min_v = cgmath::Vector3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY); + let mut max_v = + cgmath::Vector3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY); for i in 0..mesh.positions.len() / 3 { let uv_y = *mesh.texcoords.get(i * 2 + 1).unwrap_or(&0.0); let final_uv_y = if flip_uv_y { uv_y } else { 1.0 - uv_y }; + let pos = [ + mesh.positions[i * 3], + mesh.positions[i * 3 + 1], + mesh.positions[i * 3 + 2], + ]; + let p = cgmath::Vector3::new(pos[0], pos[1], pos[2]); + min_v = cgmath::Vector3::new(min_v.x.min(p.x), min_v.y.min(p.y), min_v.z.min(p.z)); + max_v = cgmath::Vector3::new(max_v.x.max(p.x), max_v.y.max(p.y), max_v.z.max(p.z)); + vertices.push(MeshVertex { - position: [ - mesh.positions[i * 3], - mesh.positions[i * 3 + 1], - mesh.positions[i * 3 + 2], - ], - texture_coordinates: [ // Blender uses V coordinate flipped + position: pos, + texture_coordinates: [ + // Blender uses V coordinate flipped *mesh.texcoords.get(i * 2).unwrap_or(&0.0), - final_uv_y + final_uv_y, ], normal: [ mesh.normals[i * 3], @@ -203,12 +301,18 @@ impl MeshData { let bitangent = (delta_pos2 * delta_uv1.x - delta_pos1 * delta_uv2.x) * r; // Assign same tangent/bitangent to each vertex in the triangle - vertices[c[0] as usize].tangent = (tangent + cgmath::Vector3::from(vertices[c[0] as usize].tangent)).into(); - vertices[c[1] as usize].tangent = (tangent + cgmath::Vector3::from(vertices[c[1] as usize].tangent)).into(); - vertices[c[2] as usize].tangent = (tangent + cgmath::Vector3::from(vertices[c[2] as usize].tangent)).into(); - vertices[c[0] as usize].bitangent = (bitangent + cgmath::Vector3::from(vertices[c[0] as usize].bitangent)).into(); - vertices[c[1] as usize].bitangent = (bitangent + cgmath::Vector3::from(vertices[c[1] as usize].bitangent)).into(); - vertices[c[2] as usize].bitangent = (bitangent + cgmath::Vector3::from(vertices[c[2] as usize].bitangent)).into(); + vertices[c[0] as usize].tangent = + (tangent + cgmath::Vector3::from(vertices[c[0] as usize].tangent)).into(); + vertices[c[1] as usize].tangent = + (tangent + cgmath::Vector3::from(vertices[c[1] as usize].tangent)).into(); + vertices[c[2] as usize].tangent = + (tangent + cgmath::Vector3::from(vertices[c[2] as usize].tangent)).into(); + vertices[c[0] as usize].bitangent = + (bitangent + cgmath::Vector3::from(vertices[c[0] as usize].bitangent)).into(); + vertices[c[1] as usize].bitangent = + (bitangent + cgmath::Vector3::from(vertices[c[1] as usize].bitangent)).into(); + vertices[c[2] as usize].bitangent = + (bitangent + cgmath::Vector3::from(vertices[c[2] as usize].bitangent)).into(); // Prepare data for averaging tangents and bitangents triangles_included[c[0] as usize] += 1; @@ -221,14 +325,155 @@ impl MeshData { let denom = 1.0 / n as f32; let vertex = &mut vertices[i]; vertex.tangent = (Vector3f::from(vertex.tangent) * denom).normalize().into(); - vertex.bitangent = (Vector3f::from(vertex.bitangent) * denom).normalize().into(); + vertex.bitangent = (Vector3f::from(vertex.bitangent) * denom) + .normalize() + .into(); } let mesh_data = MeshData { vertices: vertices, indices: mesh.indices.clone(), + aabb_min: [min_v.x, min_v.y, min_v.z], + aabb_max: [max_v.x, max_v.y, max_v.z], }; Ok(mesh_data) } -} \ No newline at end of file +} + +fn load_meshdata_from_gltf(path: &PathBuf) -> Result { + // Import glTF file (supports .gltf and .glb) + let (doc, buffers, _images) = gltf_import(path)?; + + // Take the first mesh + let mesh = doc + .meshes() + .next() + .ok_or_else(|| Error::msg("glTF contains no meshes"))?; + + // Accumulate primitives into a single vertex/index stream + let mut positions: Vec<[f32; 3]> = Vec::new(); + let mut normals: Vec<[f32; 3]> = Vec::new(); + let mut texcoords: Vec<[f32; 2]> = Vec::new(); + let mut indices: Vec = Vec::new(); + + for primitive in mesh.primitives() { + let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()])); + + let prim_positions: Vec<[f32; 3]> = reader + .read_positions() + .ok_or_else(|| Error::msg("Missing POSITION in glTF primitive"))? + .collect(); + let prim_normals: Vec<[f32; 3]> = reader + .read_normals() + .ok_or_else(|| Error::msg("Missing NORMAL in glTF primitive"))? + .collect(); + let prim_texcoords: Vec<[f32; 2]> = reader + .read_tex_coords(0) + .map(|tc| tc.into_f32().collect()) + .unwrap_or_else(|| vec![[0.0, 0.0]; prim_positions.len()]); + + let index_base = positions.len() as u32; + positions.extend_from_slice(&prim_positions); + normals.extend_from_slice(&prim_normals); + texcoords.extend_from_slice(&prim_texcoords); + + if let Some(read_indices) = reader.read_indices() { + indices.extend(read_indices.into_u32().map(|i| i + index_base)); + } else { + // Non-indexed: generate a sequential index buffer + indices.extend((0..prim_positions.len() as u32).map(|i| i + index_base)); + } + } + + if positions.is_empty() { + return Err(Error::msg("No vertex data found in glTF mesh")); + } + + // Build vertices + let mut vertices: Vec = Vec::with_capacity(positions.len()); + let mut min_v = cgmath::Vector3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY); + let mut max_v = cgmath::Vector3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY); + + for i in 0..positions.len() { + let p = positions[i]; + let n = normals[i]; + let uv = texcoords.get(i).copied().unwrap_or([0.0, 0.0]); // glTF UVs are used as-is + + let pp = cgmath::Vector3::new(p[0], p[1], p[2]); + min_v = cgmath::Vector3::new(min_v.x.min(pp.x), min_v.y.min(pp.y), min_v.z.min(pp.z)); + max_v = cgmath::Vector3::new(max_v.x.max(pp.x), max_v.y.max(pp.y), max_v.z.max(pp.z)); + + vertices.push(MeshVertex { + position: p, + texture_coordinates: uv, + normal: n, + tangent: [0.0; 3].into(), + bitangent: [0.0; 3].into(), + }); + } + + // Compute tangents/bitangents (same approach as OBJ path) + let mut triangles_included = vec![0usize; vertices.len()]; + for c in indices.chunks(3) { + if c.len() < 3 { + continue; + } + let v0 = vertices[c[0] as usize]; + let v1 = vertices[c[1] as usize]; + let v2 = vertices[c[2] as usize]; + + let pos0: cgmath::Vector3<_> = v0.position.into(); + let pos1: cgmath::Vector3<_> = v1.position.into(); + let pos2: cgmath::Vector3<_> = v2.position.into(); + + let uv0: cgmath::Vector2<_> = v0.texture_coordinates.into(); + let uv1: cgmath::Vector2<_> = v1.texture_coordinates.into(); + let uv2: cgmath::Vector2<_> = v2.texture_coordinates.into(); + + let delta_pos1 = pos1 - pos0; + let delta_pos2 = pos2 - pos0; + let delta_uv1 = uv1 - uv0; + let delta_uv2 = uv2 - uv0; + + let r = 1.0 / (delta_uv1.x * delta_uv2.y - delta_uv1.y * delta_uv2.x); + let tangent = (delta_pos1 * delta_uv2.y - delta_pos2 * delta_uv1.y) * r; + let bitangent = (delta_pos2 * delta_uv1.x - delta_pos1 * delta_uv2.x) * r; + + vertices[c[0] as usize].tangent = + (tangent + cgmath::Vector3::from(vertices[c[0] as usize].tangent)).into(); + vertices[c[1] as usize].tangent = + (tangent + cgmath::Vector3::from(vertices[c[1] as usize].tangent)).into(); + vertices[c[2] as usize].tangent = + (tangent + cgmath::Vector3::from(vertices[c[2] as usize].tangent)).into(); + vertices[c[0] as usize].bitangent = + (bitangent + cgmath::Vector3::from(vertices[c[0] as usize].bitangent)).into(); + vertices[c[1] as usize].bitangent = + (bitangent + cgmath::Vector3::from(vertices[c[1] as usize].bitangent)).into(); + vertices[c[2] as usize].bitangent = + (bitangent + cgmath::Vector3::from(vertices[c[2] as usize].bitangent)).into(); + + triangles_included[c[0] as usize] += 1; + triangles_included[c[1] as usize] += 1; + triangles_included[c[2] as usize] += 1; + } + + for (i, n) in triangles_included.into_iter().enumerate() { + if n == 0 { + continue; + } + let denom = 1.0 / n as f32; + let vertex = &mut vertices[i]; + vertex.tangent = (Vector3f::from(vertex.tangent) * denom).normalize().into(); + vertex.bitangent = (Vector3f::from(vertex.bitangent) * denom) + .normalize() + .into(); + } + + Ok(MeshData { + vertices, + indices, + aabb_min: [min_v.x, min_v.y, min_v.z], + aabb_max: [max_v.x, max_v.y, max_v.z], + }) +} diff --git a/engine/pill_engine/src/resources/mod.rs b/engine/pill_engine/src/resources/mod.rs index c965146c..53588160 100644 --- a/engine/pill_engine/src/resources/mod.rs +++ b/engine/pill_engine/src/resources/mod.rs @@ -1,48 +1,30 @@ #![cfg_attr(debug_assertions, allow(dead_code, unused_imports, unused_variables))] -mod resource_manager; -mod mesh; -mod texture; -mod resource_storage; +mod gpu_resources; mod material; +mod mesh; +mod model; mod resource; +mod resource_manager; +mod resource_storage; mod sound; +mod texture; // --- Use --- pub use resource_manager::ResourceManager; -pub use resource::{ - Resource, - ResourceLoadType, -}; +pub use gpu_resources::GpuResources; +pub use resource::{Resource, ResourceLoadType}; pub use resource_storage::ResourceStorage; -pub use sound::{ - Sound, - SoundHandle, -}; - -pub use mesh::{ - Mesh, - MeshData, - MeshVertex, - MeshHandle -}; - -pub use texture::{ - Texture, - TextureType, - TextureHandle -}; - -pub use material::{ - Material, - MaterialTextureMap, - MaterialParameter, - MaterialTexture, - MaterialParameterMap, - MaterialHandle, - get_renderer_texture_handle_from_material_texture, -}; +pub use sound::{Sound, SoundHandle}; + +pub use mesh::{Mesh, MeshData, MeshHandle, MeshVertex}; + +pub use texture::{Texture, TextureHandle, TextureType}; + +pub use material::{PBRMaterial, PBRMaterialHandle}; + +pub use model::{Model, ModelHandle, ModelMaterialSlot}; diff --git a/engine/pill_engine/src/resources/model.rs b/engine/pill_engine/src/resources/model.rs new file mode 100644 index 00000000..f940c146 --- /dev/null +++ b/engine/pill_engine/src/resources/model.rs @@ -0,0 +1,530 @@ +use crate::{ + engine::Engine, + resources::{ + material::PBRMaterialHandle, texture::TextureHandle, Mesh, MeshData, MeshHandle, + MeshVertex, PBRMaterial, Resource, ResourceLoadType, ResourceStorage, Texture, TextureType, + }, +}; +use anyhow::{anyhow, Context, Result}; +use cgmath::InnerSpace; +use gltf::import as gltf_import; +use image::{DynamicImage, ImageBuffer, ImageFormat, Luma, Rgb, Rgba}; +use log::info; +use pill_core::{get_type_name, PillSlotMapKey, PillTypeMapKey, Vector3f}; +use std::collections::HashMap; +use std::env; +use std::io::Cursor; +use std::path::{Path, PathBuf}; + +pill_core::define_new_pill_slotmap_key! { + pub struct ModelHandle; +} + +#[readonly::make] +pub struct ModelMaterialSlot { + #[readonly] + pub mesh: MeshHandle, + #[readonly] + pub material: PBRMaterialHandle, + #[readonly] + pub translation: Vector3f, + #[readonly] + pub rotation_euler_deg: Vector3f, // XYZ degrees, matches TransformComponent expectations + #[readonly] + pub scale: Vector3f, +} + +#[readonly::make] +pub struct Model { + #[readonly] + pub name: String, + #[readonly] + pub path: PathBuf, + #[readonly] + pub meshes: Vec, + #[readonly] + pub materials: Vec, + #[readonly] + pub material_slots: Vec, +} + +impl Model { + pub fn new(name: &str, path: PathBuf) -> Self { + Self { + name: name.to_string(), + path, + meshes: Vec::new(), + materials: Vec::new(), + material_slots: Vec::new(), + } + } +} + +impl PillTypeMapKey for Model { + type Storage = ResourceStorage; +} + +impl Resource for Model { + type Handle = ModelHandle; + + fn get_name(&self) -> String { + self.name.clone() + } + + fn initialize(&mut self, engine: &mut Engine) -> Result<()> { + // Resolve absolute path + let model_path = engine.game_resources_directory_path.join(&self.path); + let (doc, buffers, images) = gltf_import(&model_path) + .with_context(|| format!("Importing glTF '{}' failed", model_path.display()))?; + + let base_dir = model_path + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| PathBuf::from(".")); + + // Diagnostics: + // - To enable verbose glTF import logging, set the runtime config key: + // LOG_GLTF_IMPORT = true + // and run with environment variables: + // RUST_LOG=info RUST_BACKTRACE=1 + // - The logs will print scenes/nodes/meshes/materials/images and, for each + // material, whether baseColor/mr/normal/emissive textures are bound. + // - If all textures show as false, your asset may use KTX2/Basis textures. + // Use a PNG/JPG variant (e.g., glTF-Sample-Models “glTF-Binary”) or add + // Basis/KTX2 transcode support before import. + // Logging flag: enabled if LOG_GLTF_IMPORT=true in config or environment + let log_import = engine.config.get_bool("LOG_GLTF_IMPORT").unwrap_or(false) + || env::var("LOG_GLTF_IMPORT") + .map(|v| { + v == "1" || v.eq_ignore_ascii_case("true") || v.eq_ignore_ascii_case("yes") + }) + .unwrap_or(false); + if log_import { + info!( + "glTF import '{}': scenes={}, nodes={}, meshes={}, materials={}, images={}", + self.name, + doc.scenes().len(), + doc.nodes().len(), + doc.meshes().len(), + doc.materials().len(), + images.len() + ); + println!( + "glTF import '{}': scenes={}, nodes={}, meshes={}, materials={}, images={}", + self.name, + doc.scenes().len(), + doc.nodes().len(), + doc.meshes().len(), + doc.materials().len(), + images.len() + ); + // Print textures and their sources up-front + for (ti, tex) in doc.textures().enumerate() { + let src = tex.source(); + let src_desc = match src.source() { + gltf::image::Source::Uri { uri, .. } => format!("uri '{}'", uri), + gltf::image::Source::View { .. } => "embedded view".to_string(), + }; + info!(" texture[{}] -> image[{}] ({})", ti, src.index(), src_desc); + println!(" texture[{}] -> image[{}] ({})", ti, src.index(), src_desc); + } + // Print materials with bound textures and factors + for (i, m) in doc.materials().enumerate() { + let p = m.pbr_metallic_roughness(); + let albedo_tex = p.base_color_texture().map(|t| t.texture().source().index()); + let mr_tex = p + .metallic_roughness_texture() + .map(|t| t.texture().source().index()); + let normal_tex = m.normal_texture().map(|t| t.texture().source().index()); + let emissive_tex = m.emissive_texture().map(|t| t.texture().source().index()); + let name = m.name().unwrap_or(""); + let bc = p.base_color_factor(); + let ef = m.emissive_factor(); + info!( + " material[{}] '{}': baseColorTex={:?}, mrTex={:?}, normalTex={:?}, emissiveTex={:?}, baseColorFactor=[{:.3},{:.3},{:.3}], metal={:.3}, rough={:.3}, emissive=[{:.3},{:.3},{:.3}]", + i, name, albedo_tex, mr_tex, normal_tex, emissive_tex, bc[0], bc[1], bc[2], p.metallic_factor(), p.roughness_factor(), ef[0], ef[1], ef[2] + ); + println!( + " material[{}] '{}': baseColorTex={:?}, mrTex={:?}, normalTex={:?}, emissiveTex={:?}, baseColorFactor=[{:.3},{:.3},{:.3}], metal={:.3}, rough={:.3}, emissive=[{:.3},{:.3},{:.3}]", + i, name, albedo_tex, mr_tex, normal_tex, emissive_tex, bc[0], bc[1], bc[2], p.metallic_factor(), p.roughness_factor(), ef[0], ef[1], ef[2] + ); + } + // Print meshes and primitive attribute overview + for (mi, m) in doc.meshes().enumerate() { + let mname = m.name().unwrap_or(""); + info!( + " mesh[{}] '{}': {} primitives", + mi, + mname, + m.primitives().len() + ); + println!( + " mesh[{}] '{}': {} primitives", + mi, + mname, + m.primitives().len() + ); + for (pi, prim) in m.primitives().enumerate() { + let attrs: Vec<_> = prim.attributes().map(|(s, _)| s.to_string()).collect(); + info!( + " prim[{}]: mode={:?}, attrs={:?}", + pi, + prim.mode(), + attrs + ); + println!( + " prim[{}]: mode={:?}, attrs={:?}", + pi, + prim.mode(), + attrs + ); + } + } + // Print nodes referencing meshes + for (ni, node) in doc.nodes().enumerate() { + if let Some(mesh) = node.mesh() { + let nname = node.name().unwrap_or(""); + let (t, r, s) = node.transform().decomposed(); + info!( + " node[{}] '{}': mesh={}, T=[{:.3},{:.3},{:.3}]", + ni, + nname, + mesh.index(), + t[0], + t[1], + t[2] + ); + println!( + " node[{}] '{}': mesh={}, T=[{:.3},{:.3},{:.3}]", + ni, + nname, + mesh.index(), + t[0], + t[1], + t[2] + ); + } + } + } + + // --- Textures and Materials --- + // Cache textures by image index to avoid duplicates (works for URI/data/embedded) + let mut image_index_to_tex: HashMap = HashMap::new(); + + let mut materials: Vec = Vec::new(); + for (mat_index, mat) in doc.materials().enumerate() { + let mat_name_owned = mat + .name() + .map(|s| s.to_string()) + .unwrap_or(format!("Mat{}", mat_index)); + let mut pbr = PBRMaterial::new(&format!("{}_{}", self.name, mat_name_owned)); + + // Factors + let pbrmr = mat.pbr_metallic_roughness(); + let bc = pbrmr.base_color_factor(); + pbr.set_base_color_factor(pill_core::Color::new(bc[0], bc[1], bc[2])); + pbr.set_metallic_factor(pbrmr.metallic_factor()); + pbr.set_roughness_factor(pbrmr.roughness_factor()); + let ef = mat.emissive_factor(); + pbr.set_emissive_factor(pill_core::Color::new(ef[0], ef[1], ef[2])); + + // Helper to load a texture from a glTF texture reference via imported images + let mut load_tex = + |tex: gltf::Texture, tex_type: TextureType| -> Result { + let img = tex.source(); + let idx = img.index(); + if let Some(h) = image_index_to_tex.get(&idx) { + if log_import { + info!(" reuse image[{}] as {:?} texture", idx, tex_type); + } + println!(" reuse image[{}] as {:?} texture", idx, tex_type); + return Ok(h.clone()); + } + let img_data = images.get(idx).ok_or_else(|| { + anyhow!("Image index {} not found in imported images", idx) + })?; + // Encode to PNG bytes for Texture::Bytes + let buf = encode_gltf_image_to_png_bytes(img_data)?; + let tex_name = format!("{}_image_{}", self.name, idx); + let texture = Texture::new(&tex_name, tex_type, ResourceLoadType::Bytes(buf)); + let h = engine.add_resource::(texture)?; + image_index_to_tex.insert(idx, h.clone()); + let src_desc = match tex.source().source() { + gltf::image::Source::Uri { uri, .. } => format!("uri '{}'", uri), + gltf::image::Source::View { .. } => "embedded view".to_string(), + }; + if log_import { + info!( + " created Texture '{}' for image[{}] ({}) as {:?}", + tex_name, idx, src_desc, tex_type + ); + } + println!( + " created Texture '{}' for image[{}] ({}) as {:?}", + tex_name, idx, src_desc, tex_type + ); + Ok(h) + }; + + // Base color texture + if let Some(ti) = pbrmr.base_color_texture() { + let th = load_tex(ti.texture(), TextureType::Gamma)?; + pbr.set_albedo_texture(th); + } + // Metallic-roughness combined texture + if let Some(ti) = pbrmr.metallic_roughness_texture() { + let th = load_tex(ti.texture(), TextureType::Linear)?; + pbr.set_metallic_roughness_texture(th); + } + // Normal map + if let Some(ti) = mat.normal_texture() { + let th = load_tex(ti.texture(), TextureType::Linear)?; + pbr.set_normal_texture(th); + } + // Emissive map + if let Some(ti) = mat.emissive_texture() { + let th = load_tex(ti.texture(), TextureType::Gamma)?; + pbr.set_emissive_texture(th); + } + + let mh = engine.add_resource::(pbr)?; + materials.push(mh); + if log_import { + let mref = engine.get_resource::(&mh)?; + info!( + "material[{}] '{}': baseColorTex={}, mrTex={}, normalTex={}, emissiveTex={}", + mat_index, + mref.name, + mref.albedo_texture.is_some(), + mref.metallic_roughness_texture.is_some(), + mref.normal_texture.is_some(), + mref.emissive_texture.is_some() + ); + println!( + "material[{}] '{}': baseColorTex={}, mrTex={}, normalTex={}, emissiveTex={}", + mat_index, + mref.name, + mref.albedo_texture.is_some(), + mref.metallic_roughness_texture.is_some(), + mref.normal_texture.is_some(), + mref.emissive_texture.is_some() + ); + } + } + + // --- Meshes (per primitive) --- + // Map (mesh_index, prim_index) -> MeshHandle + let mut prim_to_mesh: HashMap<(usize, usize), MeshHandle> = HashMap::new(); + let mut mesh_handles: Vec = Vec::new(); + for (m_i, m) in doc.meshes().enumerate() { + for (p_i, primitive) in m.primitives().enumerate() { + let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()])); + + let positions: Vec<[f32; 3]> = reader + .read_positions() + .ok_or_else(|| anyhow!("Missing POSITION in mesh {} primitive {}", m_i, p_i))? + .collect(); + let normals: Vec<[f32; 3]> = reader + .read_normals() + .ok_or_else(|| anyhow!("Missing NORMAL in mesh {} primitive {}", m_i, p_i))? + .collect(); + let texcoords: Vec<[f32; 2]> = reader + .read_tex_coords(0) + .map(|tc| tc.into_f32().collect()) + .unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]); + + // Indices + let indices: Vec = if let Some(read_indices) = reader.read_indices() { + read_indices.into_u32().collect() + } else { + (0..positions.len() as u32).collect() + }; + + // Build MeshData with tangents/bitangents and AABB + let mut vertices: Vec = Vec::with_capacity(positions.len()); + let mut min_v = cgmath::Vector3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY); + let mut max_v = + cgmath::Vector3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY); + for i in 0..positions.len() { + let p = positions[i]; + let n = normals[i]; + let uv = texcoords.get(i).copied().unwrap_or([0.0, 0.0]); + let pp = cgmath::Vector3::new(p[0], p[1], p[2]); + min_v = cgmath::Vector3::new( + min_v.x.min(pp.x), + min_v.y.min(pp.y), + min_v.z.min(pp.z), + ); + max_v = cgmath::Vector3::new( + max_v.x.max(pp.x), + max_v.y.max(pp.y), + max_v.z.max(pp.z), + ); + vertices.push(MeshDataVertexTemp { + position: p, + normal: n, + uv, + tangent: [0.0; 3], + bitangent: [0.0; 3], + }); + } + + // Tangents + let mut counts = vec![0usize; vertices.len()]; + for tri in indices.chunks(3) { + if tri.len() < 3 { + continue; + } + let v0 = vertices[tri[0] as usize]; + let v1 = vertices[tri[1] as usize]; + let v2 = vertices[tri[2] as usize]; + let pos0: cgmath::Vector3<_> = v0.position.into(); + let pos1: cgmath::Vector3<_> = v1.position.into(); + let pos2: cgmath::Vector3<_> = v2.position.into(); + let uv0: cgmath::Vector2<_> = v0.uv.into(); + let uv1: cgmath::Vector2<_> = v1.uv.into(); + let uv2: cgmath::Vector2<_> = v2.uv.into(); + let dp1 = pos1 - pos0; + let dp2 = pos2 - pos0; + let duv1 = uv1 - uv0; + let duv2 = uv2 - uv0; + let r = 1.0 / (duv1.x * duv2.y - duv1.y * duv2.x); + let t = (dp1 * duv2.y - dp2 * duv1.y) * r; + let b = (dp2 * duv1.x - dp1 * duv2.x) * r; + for &idx in tri { + let v = &mut vertices[idx as usize]; + v.tangent = (cgmath::Vector3::from(v.tangent) + t).into(); + v.bitangent = (cgmath::Vector3::from(v.bitangent) + b).into(); + counts[idx as usize] += 1; + } + } + let mut final_vertices: Vec = Vec::with_capacity(vertices.len()); + for (i, c) in counts.into_iter().enumerate() { + let denom = if c == 0 { 1.0 } else { 1.0 / c as f32 }; + let v = &mut vertices[i]; + let tan = (pill_core::Vector3f::from(v.tangent) * denom).normalize(); + let bitan = (pill_core::Vector3f::from(v.bitangent) * denom).normalize(); + final_vertices.push(MeshVertex::new( + v.position, + v.uv, + v.normal, + tan.into(), + bitan.into(), + )); + } + let data = MeshData { + vertices: final_vertices, + indices: indices.clone(), + aabb_min: [min_v.x, min_v.y, min_v.z], + aabb_max: [max_v.x, max_v.y, max_v.z], + }; + + let mesh_name = format!("{}_m{}_p{}", self.name, m_i, p_i); + let mh = engine.add_resource::(Mesh::from_mesh_data(&mesh_name, data))?; + prim_to_mesh.insert((m_i, p_i), mh.clone()); + mesh_handles.push(mh); + if log_import { + info!( + "mesh[{}] prim[{}] -> '{}' (vtx={}, idx={})", + m_i, + p_i, + mesh_name, + positions.len(), + indices.len() + ); + } + } + } + + // --- Material slots from scene nodes (local TRS) --- + let mut slots: Vec = Vec::new(); + for node in doc.nodes() { + if let Some(m) = node.mesh() { + // Node local TRS + let (t, r, s) = node.transform().decomposed(); + // Convert quat -> Euler ZYX to match TransformComponent (Rz * Ry * Rx) + let q = glam::Quat::from_xyzw(r[0], r[1], r[2], r[3]); + let (z, y, x) = q.to_euler(glam::EulerRot::ZYX); + let euler_deg = Vector3f::new(x.to_degrees(), y.to_degrees(), z.to_degrees()); + + for (p_i, primitive) in m.primitives().enumerate() { + let mesh_h = prim_to_mesh + .get(&(m.index(), p_i)) + .ok_or_else(|| anyhow!("Missing mesh handle for primitive"))? + .clone(); + // Material mapping + let mat_index = primitive.material().index().unwrap_or(0); + let mat_h = materials + .get(mat_index) + .ok_or_else(|| anyhow!("Material index out of bounds"))? + .clone(); + + slots.push(ModelMaterialSlot { + mesh: mesh_h, + material: mat_h, + translation: Vector3f::new(t[0], t[1], t[2]), + rotation_euler_deg: euler_deg, + scale: Vector3f::new(s[0], s[1], s[2]), + }); + } + } + } + + self.meshes = mesh_handles; + self.materials = materials; + self.material_slots = slots; + + if log_import { + info!("model '{}' slots: {}", self.name, self.material_slots.len()); + } + + Ok(()) + } +} + +// Internal: temp vertex used to accumulate tangents +#[derive(Clone, Copy)] +struct MeshDataVertexTemp { + position: [f32; 3], + normal: [f32; 3], + uv: [f32; 2], + tangent: [f32; 3], + bitangent: [f32; 3], +} + +fn encode_gltf_image_to_png_bytes(data: &gltf::image::Data) -> Result> { + let (w, h) = (data.width, data.height); + let dyn_img: DynamicImage = match data.format { + gltf::image::Format::R8G8B8A8 => { + let img: ImageBuffer, _> = ImageBuffer::from_raw(w, h, data.pixels.clone()) + .ok_or_else(|| anyhow!("Invalid RGBA8 image buffer"))?; + DynamicImage::ImageRgba8(img) + } + gltf::image::Format::R8G8B8 => { + let img: ImageBuffer, _> = ImageBuffer::from_raw(w, h, data.pixels.clone()) + .ok_or_else(|| anyhow!("Invalid RGB8 image buffer"))?; + DynamicImage::ImageRgb8(img) + } + gltf::image::Format::R8 => { + let img: ImageBuffer, _> = ImageBuffer::from_raw(w, h, data.pixels.clone()) + .ok_or_else(|| anyhow!("Invalid L8 image buffer"))?; + DynamicImage::ImageLuma8(img) + } + // Fallback: expand to RGBA8 + _ => { + // Try to interpret as RGBA8 by padding/truncation if length matches + let expected = (w as usize) * (h as usize) * 4; + let mut pixels = data.pixels.clone(); + pixels.resize(expected, 255u8); + let img: ImageBuffer, _> = ImageBuffer::from_raw(w, h, pixels) + .ok_or_else(|| anyhow!("Invalid fallback image buffer"))?; + DynamicImage::ImageRgba8(img) + } + }; + let mut buf = Vec::new(); + { + let mut writer = Cursor::new(&mut buf); + dyn_img.write_to(&mut writer, ImageFormat::Png)?; + } + Ok(buf.into_boxed_slice()) +} diff --git a/engine/pill_engine/src/resources/resource_manager.rs b/engine/pill_engine/src/resources/resource_manager.rs index caa6ada7..52aec336 100644 --- a/engine/pill_engine/src/resources/resource_manager.rs +++ b/engine/pill_engine/src/resources/resource_manager.rs @@ -1,55 +1,93 @@ use crate::{ - graphics::{ RendererMaterialHandle, RendererTextureHandle }, - resources::{ ResourceStorage, Resource, MaterialHandle, Material, TextureHandle, Texture, TextureType }, + graphics::RendererTextureHandle, + resources::{ + PBRMaterial, PBRMaterialHandle, Resource, ResourceStorage, Texture, TextureHandle, + TextureType, + }, }; -use pill_core::{ EngineError, get_type_name, PillSlotMapKey, PillTypeMap, PillTypeMapKey }; +use pill_core::{get_type_name, EngineError, PillSlotMapKey, PillTypeMap, PillTypeMapKey}; -use std::{ - collections::{HashMap}, - convert::TryInto, - env, - num::NonZeroU32, - path::PathBuf -}; +use crate::ecs::PostProcessParams; +use anyhow::{Context, Error, Result}; use boolinator::Boolinator; -use anyhow::{Result, Context, Error}; +use std::sync::{Arc, Mutex}; +use std::{collections::HashMap, convert::TryInto, env, num::NonZeroU32, path::PathBuf}; pub struct ResourceManager { resources: PillTypeMap, + pub gpu: crate::resources::GpuResources, + pub post_process: Arc>, } impl ResourceManager { pub fn new() -> Self { - Self { + Self { resources: PillTypeMap::new(), + gpu: crate::resources::GpuResources::new(), + post_process: Arc::new(Mutex::new(PostProcessParams { + time_s: 0.0, + focus_point: 0.0, + focus_scale: 0.0, + mb_enabled: true, + mb_debug_velocity: false, + mb_strength: 1.0, + mb_max_samples: 16, + mb_min_speed: 0.001, + mb_depth_softness: 1.0, + })), } } + pub fn gpu(&self) -> &crate::resources::GpuResources { + &self.gpu + } + + pub fn gpu_mut(&mut self) -> &mut crate::resources::GpuResources { + &mut self.gpu + } + // --- Slots --- - pub(crate) fn get_resource_slot<'a, T>(&'a self, resource_handle: &T::Handle) -> Result<&'a Option> - where T: Resource> + pub(crate) fn get_resource_slot<'a, T>( + &'a self, + resource_handle: &T::Handle, + ) -> Result<&'a Option> + where + T: Resource>, { // Get resource storage let resource_storage = self.get_resource_storage::()?; // Get resource slot - let resource_slot = resource_storage.data.get(resource_handle.clone()) - .ok_or(Error::new(EngineError::InvalidResourceHandle(get_type_name::())))?; + let resource_slot = + resource_storage + .data + .get(resource_handle.clone()) + .ok_or(Error::new(EngineError::InvalidResourceHandle( + get_type_name::(), + )))?; Ok(resource_slot) } - pub(crate) fn get_resource_slot_mut<'a, T>(&'a mut self, resource_handle: &T::Handle) -> Result<&'a mut Option> - where T: Resource> + pub(crate) fn get_resource_slot_mut<'a, T>( + &'a mut self, + resource_handle: &T::Handle, + ) -> Result<&'a mut Option> + where + T: Resource>, { // Get resource storage let resource_storage = self.get_resource_storage_mut::()?; // Get resource slot - let resource_slot = resource_storage.data.get_mut(resource_handle.clone()) - .ok_or(Error::new(EngineError::InvalidResourceHandle(get_type_name::())))?; + let resource_slot = resource_storage + .data + .get_mut(resource_handle.clone()) + .ok_or(Error::new(EngineError::InvalidResourceHandle( + get_type_name::(), + )))?; Ok(resource_slot) } @@ -57,30 +95,42 @@ impl ResourceManager { // --- Storages --- pub(crate) fn get_resource_storage(&self) -> Result<&ResourceStorage> - where T: Resource> + where + T: Resource>, { - self.resources.get::().ok_or(Error::new(EngineError::ResourceNotRegistered(get_type_name::()))) + self.resources + .get::() + .ok_or(Error::new(EngineError::ResourceNotRegistered( + get_type_name::(), + ))) } pub(crate) fn get_resource_storage_mut(&mut self) -> Result<&mut ResourceStorage> - where T: Resource> + where + T: Resource>, { - - self.resources.get_mut::().ok_or(Error::new(EngineError::ResourceNotRegistered(get_type_name::()))) + self.resources + .get_mut::() + .ok_or(Error::new(EngineError::ResourceNotRegistered( + get_type_name::(), + ))) } // --- Register - Add - Remove --- pub fn register_resource_type(&mut self, max_resource_count: usize) -> Result<()> - where T: Resource> + where + T: Resource>, { - self.resources.insert::(ResourceStorage::::new(max_resource_count)); + self.resources + .insert::(ResourceStorage::::new(max_resource_count)); Ok(()) } pub fn add_resource(&mut self, resource: T) -> Result<(T::Handle, &mut T)> - where T: Resource> + where + T: Resource>, { // Get resource storage let resource_storage = self.get_resource_storage_mut::()?; @@ -88,33 +138,49 @@ impl ResourceManager { // Check if there is space for resource if resource_storage.data.len() >= resource_storage.max_resource_count { - return Err(Error::new(EngineError::ResourceLimitReached(get_type_name::()))); + return Err(Error::new(EngineError::ResourceLimitReached( + get_type_name::(), + ))); } // Check if resource already exists if resource_storage.mapping.contains_key(&resource_name) { - return Err(Error::new(EngineError::ResourceAlreadyExists(get_type_name::(), resource_name.clone()))); + return Err(Error::new(EngineError::ResourceAlreadyExists( + get_type_name::(), + resource_name.clone(), + ))); } // Insert new resource let resource_handle = resource_storage.data.insert(Some(resource)); - let resource = resource_storage.data.get_mut(resource_handle).unwrap().as_mut().unwrap(); + let resource = resource_storage + .data + .get_mut(resource_handle) + .unwrap() + .as_mut() + .unwrap(); // Insert new mapping - resource_storage.mapping.insert(&resource_name, &resource_handle); + resource_storage + .mapping + .insert(&resource_name, &resource_handle); Ok((resource_handle, resource)) } pub fn remove_resource(&mut self, resource_handle: &T::Handle) -> Result<(T::Handle, T)> - where T: Resource> + where + T: Resource>, { // Get resource storage let resource_storage = self.get_resource_storage_mut::()?; // Remove resource - let resource = resource_storage.data.remove(*resource_handle) - .ok_or(EngineError::InvalidResourceHandle(get_type_name::()))?.expect("Critical: Resource is None"); + let resource = resource_storage + .data + .remove(*resource_handle) + .ok_or(EngineError::InvalidResourceHandle(get_type_name::()))? + .expect("Critical: Resource is None"); // Remove mapping resource_storage.mapping.remove_by_value(resource_handle); @@ -123,17 +189,28 @@ impl ResourceManager { } pub fn remove_resource_by_name(&mut self, name: &str) -> Result<(T::Handle, T)> - where T: Resource> + where + T: Resource>, { // Get resource storage let resource_storage = self.get_resource_storage_mut::()?; // Get handle by name - let resource_handle = resource_storage.mapping.get_value(&name.to_string()) - .ok_or(EngineError::InvalidResourceName(name.to_string(), get_type_name::()))?.clone(); + let resource_handle = resource_storage + .mapping + .get_value(&name.to_string()) + .ok_or(EngineError::InvalidResourceName( + name.to_string(), + get_type_name::(), + ))? + .clone(); // Remove resource - let resource = resource_storage.data.remove(resource_handle).unwrap().expect("Critical: Resource is None"); + let resource = resource_storage + .data + .remove(resource_handle) + .unwrap() + .expect("Critical: Resource is None"); // Remove mapping resource_storage.mapping.remove_by_key(&name.to_string()); @@ -144,65 +221,99 @@ impl ResourceManager { // --- Get --- pub fn get_resource_handle(&self, name: &str) -> Result - where T: Resource> + where + T: Resource>, { // Get resource storage let resource_storage = self.get_resource_storage::()?; // Get resource handle - let resource_handle = resource_storage.mapping.get_value(&name.to_string()) - .ok_or(EngineError::InvalidSceneName(name.to_string()))?.clone(); + let resource_handle = resource_storage + .mapping + .get_value(&name.to_string()) + .ok_or(EngineError::InvalidSceneName(name.to_string()))? + .clone(); Ok(resource_handle) } pub fn get_resource<'a, T>(&'a self, resource_handle: &'a T::Handle) -> Result<&'a T> - where T: Resource> + where + T: Resource>, { // Get resource - let resource = self.get_resource_slot::(resource_handle)?.as_ref().expect("Critical: Resource is None"); + let resource = self + .get_resource_slot::(resource_handle)? + .as_ref() + .expect("Critical: Resource is None"); Ok(resource) } pub fn get_resource_by_name<'a, T>(&'a self, name: &str) -> Result<&'a T> - where T: Resource> + where + T: Resource>, { // Get resource storage let resource_storage = self.get_resource_storage::()?; // Get handle by name - let resource_handle = resource_storage.mapping.get_value(&name.to_string()) - .ok_or(EngineError::InvalidResourceName(name.to_string(), get_type_name::()))?; + let resource_handle = resource_storage + .mapping + .get_value(&name.to_string()) + .ok_or(EngineError::InvalidResourceName( + name.to_string(), + get_type_name::(), + ))?; // Get resource - let resource = self.get_resource_slot::(resource_handle)?.as_ref().expect("Critical: Resource is None"); + let resource = self + .get_resource_slot::(resource_handle)? + .as_ref() + .expect("Critical: Resource is None"); Ok(resource) } - pub fn get_resource_mut<'a, T>(&'a mut self, resource_handle: &'a T::Handle) -> Result<&'a mut T> - where T: Resource> + pub fn get_resource_mut<'a, T>( + &'a mut self, + resource_handle: &'a T::Handle, + ) -> Result<&'a mut T> + where + T: Resource>, { // Get resource - let resource = self.get_resource_slot_mut::(resource_handle)?.as_mut().expect("Critical: Resource is None"); + let resource = self + .get_resource_slot_mut::(resource_handle)? + .as_mut() + .expect("Critical: Resource is None"); Ok(resource) } pub fn get_resource_by_name_mut<'a, T>(&'a mut self, name: &str) -> Result<&'a mut T> - where T: Resource> + where + T: Resource>, { // Get resource storage let resource_storage = self.get_resource_storage_mut::()?; // Get handle by name - let resource_handle = resource_storage.mapping.get_value(&name.to_string()) - .ok_or(EngineError::InvalidResourceName(name.to_string(), get_type_name::()))?.clone(); + let resource_handle = resource_storage + .mapping + .get_value(&name.to_string()) + .ok_or(EngineError::InvalidResourceName( + name.to_string(), + get_type_name::(), + ))? + .clone(); // Get resource - let resource = self.get_resource_slot_mut::(&resource_handle)?.as_mut().expect("Critical: Resource is None"); + let resource = self + .get_resource_slot_mut::(&resource_handle)? + .as_mut() + .expect("Critical: Resource is None"); Ok(resource) } -} \ No newline at end of file +} diff --git a/engine/pill_engine/src/resources/resource_storage.rs b/engine/pill_engine/src/resources/resource_storage.rs index 8c76bd04..bcb027e7 100644 --- a/engine/pill_engine/src/resources/resource_storage.rs +++ b/engine/pill_engine/src/resources/resource_storage.rs @@ -1,21 +1,25 @@ -use crate::{resources::Resource, config::RESOURCE_VERSION_LIMIT}; +use crate::{config::RESOURCE_VERSION_LIMIT, resources::Resource}; -use pill_core::{ PillSlotMap, PillSlotMapKey, PillTwinMap }; +use pill_core::{PillSlotMap, PillSlotMapKey, PillTwinMap}; use std::collections::HashMap; pub struct ResourceStorage { - pub data: PillSlotMap>, + pub data: PillSlotMap>, pub mapping: PillTwinMap, pub max_resource_count: usize, } impl ResourceStorage { - pub fn new(max_resource_count: usize) -> Self { - Self { - data: PillSlotMap::>::with_capacity_and_key_and_version_limit(max_resource_count, RESOURCE_VERSION_LIMIT as u32).unwrap(), + pub fn new(max_resource_count: usize) -> Self { + Self { + data: PillSlotMap::>::with_capacity_and_key_and_version_limit( + max_resource_count, + RESOURCE_VERSION_LIMIT as u32, + ) + .unwrap(), mapping: PillTwinMap::::new(), max_resource_count, } } -} \ No newline at end of file +} diff --git a/engine/pill_engine/src/resources/sound.rs b/engine/pill_engine/src/resources/sound.rs index 9eca3c2d..5e96836d 100644 --- a/engine/pill_engine/src/resources/sound.rs +++ b/engine/pill_engine/src/resources/sound.rs @@ -1,7 +1,7 @@ use crate::{ engine::Engine, graphics::{ RendererTextureHandle }, - resources::{ ResourceStorage, Resource, ResourceLoadType, Material }, + resources::{ ResourceStorage, Resource, ResourceLoadType, PBRMaterial }, ecs::{ DeferredUpdateManagerPointer, AudioSourceComponent, SoundType, AudioManagerComponent }, config::*, }; diff --git a/engine/pill_engine/src/resources/texture.rs b/engine/pill_engine/src/resources/texture.rs index 056c833d..33eddb1e 100644 --- a/engine/pill_engine/src/resources/texture.rs +++ b/engine/pill_engine/src/resources/texture.rs @@ -1,26 +1,26 @@ use crate::{ - engine::Engine, - graphics::{ RendererTextureHandle }, - resources::{ ResourceStorage, Resource, ResourceLoadType, Material }, - ecs::{ DeferredUpdateManagerPointer }, config::*, + ecs::DeferredUpdateManagerPointer, + engine::Engine, + graphics::RendererTextureHandle, + resources::{PBRMaterial, Resource, ResourceLoadType, ResourceStorage}, }; -use pill_core::{ PillSlotMapKey, PillTypeMapKey, PillStyle, get_type_name }; +use pill_core::{get_type_name, PillSlotMapKey, PillStyle, PillTypeMapKey}; -use std::collections::HashSet; -use std::path::{ Path, PathBuf }; -use anyhow::{ Result, Context, Error }; +use anyhow::{Context, Error, Result}; use bytemuck::{Pod, Zeroable}; // For derive macros to work -use readonly::make; // For #[readonly::make] to resolve +use readonly::make; +use std::collections::HashSet; +use std::path::{Path, PathBuf}; // For #[readonly::make] to resolve pill_core::define_new_pill_slotmap_key! { pub struct TextureHandle; } #[derive(Clone, Copy, Debug)] pub enum TextureType { - Color, - Normal, + Gamma, // sRGB + Linear, } #[readonly::make] @@ -35,7 +35,11 @@ pub struct Texture { } impl Texture { - pub fn new(name: &str, texture_type: TextureType, resource_load_type: ResourceLoadType) -> Self { + pub fn new( + name: &str, + texture_type: TextureType, + resource_load_type: ResourceLoadType, + ) -> Self { Self { name: name.to_string(), load_type: resource_load_type, @@ -57,26 +61,36 @@ impl Resource for Texture { } fn initialize(&mut self, engine: &mut Engine) -> Result<()> { - let error_message = format!("Initializing {} {} failed", "Resource".gobj_style(), get_type_name::().sobj_style()); + let error_message = format!( + "Initializing {} {} failed", + "Resource".gobj_style(), + get_type_name::().sobj_style() + ); // Create new renderer texture resource let image_data = match &self.load_type { ResourceLoadType::Path(path) => { // Check if path to asset is correct let resource_file_path = engine.game_resources_directory_path.join(path); - pill_core::validate_asset_path(&resource_file_path, &["png", "jpg", "gif", "tif"])?; + pill_core::validate_asset_path( + &resource_file_path, + &["png", "jpg", "gif", "tif", "hdr"], + )?; // Load data image::open(&resource_file_path)? - }, + } ResourceLoadType::Bytes(bytes) => { // Load data image::load_from_memory(bytes)? - }, + } }; // Create renderer texture resource - let renderer_resource_handle = engine.renderer.create_texture(&self.name, &image_data, self.texture_type).context(error_message.clone())?; + let renderer_resource_handle = engine + .renderer + .create_texture(&self.name, &image_data, self.texture_type) + .context(error_message.clone())?; self.renderer_resource_handle = Some(renderer_resource_handle); Ok(()) @@ -88,32 +102,6 @@ impl Resource for Texture { engine.renderer.destroy_texture(v).unwrap(); } - // Take resource storage from engine - let resource_storage = engine.resource_manager.get_resource_storage_mut::().expect("Critical: Resource not registered"); - let materials = &mut resource_storage.data; - - // Find materials that use this texture and update them - for material_slot in materials.iter_mut() { - let material = material_slot.1.as_mut().expect("Critical: Resource is None"); - - // Update texture slots - let mut material_updated = false; - for texture_slot in material.get_textures().data.iter_mut() { - if let Some(texture_handle) = texture_slot.1.texture_handle { - // If material texture has handle to this texture - if texture_handle.data() == self_handle.data() { - texture_slot.1.texture_handle = None; - texture_slot.1.renderer_texture_handle = None; - material_updated = true; - } - } - } - - if material_updated { - engine.renderer.update_material_textures(material.renderer_resource_handle.unwrap(), &material.textures).unwrap(); - } - } - Ok(()) } -} \ No newline at end of file +} diff --git a/engine/pill_launcher/res/templates/pill_default/src/game.rs b/engine/pill_launcher/res/templates/pill_default/src/game.rs index c9be30c9..7c6ae385 100644 --- a/engine/pill_launcher/res/templates/pill_default/src/game.rs +++ b/engine/pill_launcher/res/templates/pill_default/src/game.rs @@ -30,21 +30,21 @@ impl PillGame for Game { engine.add_system("PillRotation", pill_rotation_system)?; // Add meshes - let pill_mesh = Mesh::new("Pill", "models/Pill.obj".into()); + let pill_mesh = Mesh::new("Pill", "models/pill.obj".into()); let pill_mesh_handle = engine.add_resource(pill_mesh)?; // Add textures - let pill_color_texture = Texture::new("PillColor", TextureType::Color, ResourceLoadType::Path("textures/PillColor.png".into())); + let pill_color_texture = Texture::new("PillColor", TextureType::Color, ResourceLoadType::Path("textures/pill_color.png".into())); let pill_color_texture_handle = engine.add_resource::(pill_color_texture)?; - let pill_normal_texture = Texture::new("PillNormal", TextureType::Normal, ResourceLoadType::Path("textures/PillNormal.png".into())); + let pill_normal_texture = Texture::new("PillNormal", TextureType::Normal, ResourceLoadType::Path("textures/pill_normal.png".into())); let pill_normal_texture_handle = engine.add_resource::(pill_normal_texture)?; // Add materials let mut pill_material = Material::new("Pill"); - pill_material.set_texture("Color", pill_color_texture_handle)?; - pill_material.set_texture("Normal", pill_normal_texture_handle)?; - pill_material.set_color("Tint", Color::new( 1.0, 1.0, 1.0))?; - pill_material.set_scalar("Specularity", 0.5)?; + pill_material.set_texture("color", pill_color_texture_handle)?; + pill_material.set_texture("normal", pill_normal_texture_handle)?; + pill_material.set_color("tint", Color::new( 1.0, 1.0, 1.0))?; + pill_material.set_scalar("specularity", 0.5)?; let pill_material_handle = engine.add_resource::(pill_material)?; // Create camera entity diff --git a/engine/pill_launcher/src/main.rs b/engine/pill_launcher/src/main.rs index ae2137f0..f5614023 100644 --- a/engine/pill_launcher/src/main.rs +++ b/engine/pill_launcher/src/main.rs @@ -172,7 +172,7 @@ fn remove_files_starting_with(directory_path: &PathBuf, file_name_prefix: &str) // --- Actions --- fn create_game_project(game_project_parent_directory_path: &PathBuf, game_name: &String) -> Result<()> { - const TEMPLATE_NAME: &str = "Pill-Default"; + const TEMPLATE_NAME: &str = "pill_default"; let game_project_directory_path = game_project_parent_directory_path.join(game_name); if game_project_directory_path.exists() { diff --git a/engine/pill_renderer/Cargo.toml b/engine/pill_renderer/Cargo.toml deleted file mode 100644 index 4898e93b..00000000 --- a/engine/pill_renderer/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "pill_renderer" -version = "0.1.0" -edition = "2021" - -[dependencies] -pill_core = {path = "../pill_core"} -pill_engine = {path = "../pill_engine", features = ["internal"]} - -# Data containers -slotmap = "1.0.6" -slab = "0.4.5" - -# Loaders -image = "0.23" -tobj = "3.0" - -# Errors -anyhow = "1.0" - -# Logging -env_logger = "0.9.0" -log = "0.4" - -# Other -winit = "0.29" -cgmath = "0.18" -wgpu = { version = "0.20.1", features = [ "spirv" ] } -futures = "0.3" -pollster = "0.2" -bytemuck = { version = "1.4", features = [ "derive" ] } -config = "0.11" -naga = { version = "0.19", features = ["glsl-in", "wgsl-out"] } - -# egui -egui = "0.28.1" -egui-wgpu = { version = "0.28.1", features = ["winit"] } -egui-winit = "0.28.1" \ No newline at end of file diff --git a/engine/pill_renderer/src/lib.rs b/engine/pill_renderer/src/lib.rs deleted file mode 100644 index 071ca3d3..00000000 --- a/engine/pill_renderer/src/lib.rs +++ /dev/null @@ -1,20 +0,0 @@ -#![cfg_attr(debug_assertions, allow(dead_code, unused_imports))] - -pub mod renderer; -pub mod resources; -pub mod renderer_resource_storage; -pub mod instance; -pub mod egui; -pub mod mesh_drawer; - -// --- Use --- - -pub use renderer::*; - -pub use instance::{ - Instance, -}; - -pub use renderer_resource_storage::{ - RendererResourceStorage, -}; diff --git a/engine/pill_renderer/src/mesh_drawer.rs b/engine/pill_renderer/src/mesh_drawer.rs deleted file mode 100644 index ecd52ad9..00000000 --- a/engine/pill_renderer/src/mesh_drawer.rs +++ /dev/null @@ -1,200 +0,0 @@ -use std::{num::NonZeroU32, ops::Range}; - -use pill_core::{RendererError, Timer}; -use pill_engine::{ internal::{ RenderQueueItem, RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, TransformComponent, RENDER_QUEUE_KEY_ORDER}, ComponentStorage}; -use crate::{resources::RendererCamera, Instance, RendererResourceStorage,INITIAL_INSTANCE_VECTOR_CAPACITY}; - -use anyhow::{Error, Result}; - -pub struct MeshDrawer { - max_instance_batch_size: u32, - instances: Vec::, - instance_buffer: wgpu::Buffer, -} - -impl MeshDrawer { - pub fn new(device: &wgpu::Device, max_instance_batch_size: u32) -> Self { - - // Create instance buffer - let buffer_size = (size_of::() * max_instance_batch_size as usize) as u64; - let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("instance_buffer"), - size: buffer_size, - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - MeshDrawer { - max_instance_batch_size, - instances: Vec::::with_capacity(INITIAL_INSTANCE_VECTOR_CAPACITY), - instance_buffer, - } - } - - pub fn record_draw_commands( - &mut self, - // Resources - queue: &wgpu::Queue, - device: &wgpu::Device, - renderer_resource_storage: &RendererResourceStorage, - color_attachment: wgpu::RenderPassColorAttachment, - depth_stencil_attachment: wgpu::RenderPassDepthStencilAttachment, - // Rendring data - camera: &RendererCamera, - render_queue: &Vec::, - transform_component_storage: &ComponentStorage, - timer: &mut Timer - ) -> Result<()> { - timer.record("Prepare render pass"); - - let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("mesh_drawer_encoder"), - }); - - let mut instance_batch_number = 0; - let mut rendering_context_change_number = 0; - - for (i, instance_batch) in render_queue.chunks(self.max_instance_batch_size as usize).enumerate() { - - let batch_size = instance_batch.len(); - instance_batch_number += 1; - - timer.begin_context(&format!("Prepare draw instance batch {}", i)); - - timer.record(&format!("Write instance buffer")); - - // Prepare instance data and load it to buffer - self.instances.clear(); - self.instances.reserve(instance_batch.len()); // Pre-allocate exact capacity - - for render_queue_item in instance_batch { - let transform_slot = transform_component_storage.data - .get(render_queue_item.entity_index as usize) - .ok_or_else(|| Error::new(RendererError::Other))?; - let transform_component = transform_slot - .as_ref() - .ok_or_else(|| Error::new(RendererError::Other))?; - //println!("Creating new instance with transform component: {:?} {:?}", i, transform_component.position); - self.instances.push(Instance::new(transform_component)); - } - - - - queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&self.instances)); // Update instance buffer - - - let mut current_rendering_order: u8 = 0; - let mut current_pipeline_handle: Option = None; - let mut current_material_handle: Option = None; - let mut current_mesh_handle: Option = None; - let mut current_mesh_index_count: u32 = 0; // Number of indices in the current mesh - - - // Start encoding render pass - let load_op = if i == 0 { - wgpu::LoadOp::Clear(wgpu::Color::BLACK) -} else { - wgpu::LoadOp::Load -}; - -let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("render_pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - ops: wgpu::Operations { - load: load_op, - store: wgpu::StoreOp::Store, - }, - ..color_attachment.clone() - })], - depth_stencil_attachment: Some(depth_stencil_attachment.clone()), - timestamp_writes: None, - occlusion_query_set: None, -}); - - - render_pass.set_vertex_buffer(1, self.instance_buffer.slice(..)); // Set instance buffer - - timer.record("Record draw instances commands"); - - // Reset instance range for each batch - // Start inclusive, end exclusive (e.g. 0..3 means indices 0, 1, 2. e.g. 5..7 means indices 5, 6) - let mut accumulated_instance_range: Range = 0..0; - let mut accumulated_instance_count: u32 = 0; - for (j, render_queue_item) in instance_batch.iter().enumerate() { - let render_queue_key_fields = pill_engine::internal::decompose_render_queue_key(render_queue_item.key); - - // Recreate resource handles - let renderer_material_handle = RendererMaterialHandle::new(render_queue_key_fields.material_index.into(), NonZeroU32::new(render_queue_key_fields.material_version.into()).unwrap()); - let renderer_mesh_handle = RendererMeshHandle::new(render_queue_key_fields.mesh_index.into(), NonZeroU32::new(render_queue_key_fields.mesh_version.into()).unwrap()); - - - // Check rendering order - if current_rendering_order > render_queue_key_fields.order { - if accumulated_instance_count > 0 { - render_pass.draw_indexed(0..current_mesh_index_count, 0, accumulated_instance_range.clone()); - accumulated_instance_range = accumulated_instance_range.end..accumulated_instance_range.end; - } - - // Set new order - current_rendering_order = render_queue_key_fields.order; - rendering_context_change_number += 1; - } - - // Check material - if current_material_handle != Some(renderer_material_handle) { - // Render accumulated instances - if accumulated_instance_count > 0 { - render_pass.draw_indexed(0..current_mesh_index_count, 0, accumulated_instance_range.clone()); - accumulated_instance_range = accumulated_instance_range.end..accumulated_instance_range.end; - } - // Set new material - current_material_handle = Some(renderer_material_handle); - let material = renderer_resource_storage.materials.get(current_material_handle.unwrap()).unwrap(); - - // Set pipeline if new material is using different one - if current_pipeline_handle != Some(material.pipeline_handle) { - current_pipeline_handle = Some(material.pipeline_handle); - let pipeline = renderer_resource_storage.pipelines.get( current_pipeline_handle.unwrap()).unwrap(); - render_pass.set_pipeline(&pipeline.render_pipeline); - } - - render_pass.set_bind_group(0, &material.texture_bind_group, &[]); - render_pass.set_bind_group(1, &material.parameter_bind_group, &[]); - render_pass.set_bind_group(2, &camera.bind_group, &[]); - - rendering_context_change_number += 1; - } - - // Check mesh - if current_mesh_handle != Some(renderer_mesh_handle) { - // Render accumulated instances - if accumulated_instance_count > 0 { - render_pass.draw_indexed(0..current_mesh_index_count, 0, accumulated_instance_range.clone()); - accumulated_instance_range = accumulated_instance_range.end..accumulated_instance_range.end; - } - // Set new mesh - current_mesh_handle = Some(renderer_mesh_handle); - let mesh = renderer_resource_storage.meshes.get(current_mesh_handle.unwrap()).unwrap(); - current_mesh_index_count = mesh.index_count; - render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); - render_pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); - - rendering_context_change_number += 1; - } - - // Add new instance - accumulated_instance_range = accumulated_instance_range.start..accumulated_instance_range.end + 1; - accumulated_instance_count = accumulated_instance_range.end - accumulated_instance_range.start; - - // If last in batch, render accumulated instances - if j == batch_size - 1 && accumulated_instance_count > 0 { - render_pass.draw_indexed(0..current_mesh_index_count, 0, accumulated_instance_range.clone()); - } - } - - timer.end_context()?; // End "Draw instance batch" context - } -queue.submit(std::iter::once(encoder.finish())); - Ok(()) - } -} \ No newline at end of file diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs deleted file mode 100644 index ae2a8280..00000000 --- a/engine/pill_renderer/src/renderer.rs +++ /dev/null @@ -1,474 +0,0 @@ -// https://github.com/ejb004/egui-wgpu-demo/blob/master/src/lib.rs -// https://github.com/kaphula/winit-egui-wgpu-template/blob/master/src/main.rs -// https://github.com/emilk/egui/discussions/3067 - -use crate::{ - instance::Instance, mesh_drawer::MeshDrawer, renderer_resource_storage::RendererResourceStorage, resources::{ - RendererCamera, - RendererMaterial, - RendererMesh, - RendererPipeline, - RendererTexture, - Vertex - } -}; - -use pill_engine::internal::{ - PillRenderer, - EntityHandle, - RenderQueueItem, - TextureType, - MeshData, - MaterialTextureMap, - TransformComponent, - ComponentStorage, - CameraComponent, - MaterialParameterMap, - RendererCameraHandle, - RendererMaterialHandle, - RendererMeshHandle, - RendererPipelineHandle, - RendererTextureHandle, - RENDER_QUEUE_KEY_ORDER, - get_renderer_resource_handle_from_camera_component, -}; - -use pill_core::{ - PillSlotMapKey, PillSlotMapKeyData, PillStyle, RendererError, Timer -}; - -use std::{ - iter, mem::size_of, num::NonZeroU32, ops::Range, sync::Arc -}; - -use naga::front::glsl; -use naga::back::wgsl; - -use anyhow::{Error, Result}; -use log::{ info }; - -use crate::egui::EguiRenderer; - -pub const MAX_INSTANCE_BATCH_SIZE: usize = 10000; // Maximum number of instances that can be drawn in a single draw call -pub const INITIAL_INSTANCE_VECTOR_CAPACITY: usize = 10000; - -// Default resource handle - Master pipeline -pub const MASTER_PIPELINE_HANDLE: RendererPipelineHandle = RendererPipelineHandle { - 0: PillSlotMapKeyData { index: 1, version: unsafe { std::num::NonZeroU32::new_unchecked(1) } } -}; - -pub struct Renderer { - pub state: State, -} - -fn compile_glsl_to_wgsl(source: &str, stage: naga::ShaderStage) -> Result { - let mut frontend = glsl::Frontend::default(); - let options = glsl::Options::from(stage); - let module = frontend.parse(&options, source).unwrap(); - - let mut validator = naga::valid::Validator::new( - naga::valid::ValidationFlags::all(), - naga::valid::Capabilities::empty(), - ); - - let info = validator.validate(&module)?; - - let mut output = String::new(); - let mut writer = wgsl::Writer::new(&mut output, wgsl::WriterFlags::empty()); - writer.write(&module, &info)?; - - Ok(output) -} - -impl PillRenderer for Renderer { - fn new(window: Arc, config: config::Config) -> Self { - info!("Initializing {}", "Renderer".mobj_style()); - let state: State = pollster::block_on(State::new(window, config)); - - Self { - state, - } - } - - fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize) { - info!("Resizing {} resources", "Renderer".mobj_style()); - self.state.resize(new_window_size) - } - - - fn set_master_pipeline(&mut self, vertex_shader_bytes: &[u8], fragment_shader_bytes: &[u8]) -> Result<()> { - - // Create shaders - // Convert bytes to string - let vertex_shader_source = std::str::from_utf8(vertex_shader_bytes) - .map_err(|e| anyhow::anyhow!("Invalid UTF-8 in vertex shader: {}", e))?; - let fragment_shader_source = std::str::from_utf8(fragment_shader_bytes) - .map_err(|e| anyhow::anyhow!("Invalid UTF-8 in fragment shader: {}", e))?; - - -// Convert GLSL to WGSL -let vertex_wgsl = compile_glsl_to_wgsl(vertex_shader_source, naga::ShaderStage::Vertex).unwrap(); -let fragment_wgsl = compile_glsl_to_wgsl(fragment_shader_source, naga::ShaderStage::Fragment).unwrap(); - -// Create shader modules with WGSL -let vertex_shader = self.state.device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("master_vertex_shader"), - source: wgpu::ShaderSource::Wgsl(vertex_wgsl.into()), -}); - -let fragment_shader = self.state.device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("master_fragment_shader"), - source: wgpu::ShaderSource::Wgsl(fragment_wgsl.into()), -}); - - - - - - // Create master pipeline - let master_pipeline = RendererPipeline::new( - &self.state.device, - vertex_shader, - fragment_shader, - self.state.color_format, - Some(self.state.depth_format), - &[RendererMesh::data_layout_descriptor(), Instance::data_layout_descriptor()], - ).unwrap(); - - self.state.renderer_resource_storage.pipelines.insert(master_pipeline); - - Ok(()) - } - - fn create_mesh(&mut self, name: &str, mesh_data: &MeshData) -> Result { - let mesh = RendererMesh::new(&self.state.device, name, mesh_data)?; - let handle = self.state.renderer_resource_storage.meshes.insert(mesh); - - Ok(handle) - } - - fn create_texture(&mut self, name: &str, image_data: &image::DynamicImage, texture_type: TextureType) -> Result { - let texture = RendererTexture::new_texture(&self.state.device, &self.state.queue, Some(name), image_data, texture_type)?; - let handle = self.state.renderer_resource_storage.textures.insert(texture); - - Ok(handle) - } - - fn create_material(&mut self, name: &str, textures: &MaterialTextureMap, parameters: &MaterialParameterMap) -> Result { - let pipeline_handle = MASTER_PIPELINE_HANDLE; - let pipeline = self.state.renderer_resource_storage.pipelines.get(pipeline_handle).unwrap(); - - let material = RendererMaterial::new( - &self.state.device, - &self.state.queue, - &self.state.renderer_resource_storage, - name, - pipeline_handle, - &pipeline.material_texture_bind_group_layout, - textures, - &pipeline.material_parameter_bind_group_layout, - parameters, - ).unwrap(); - - let handle = self.state.renderer_resource_storage.materials.insert(material); - - Ok(handle) - } - - fn create_camera(&mut self) -> Result { - let pipeline_handle = MASTER_PIPELINE_HANDLE; - let pipeline = self.state.renderer_resource_storage.pipelines.get(pipeline_handle).unwrap(); - let camera_bind_group_layout = &pipeline.camera_bind_group_layout; - let camera = RendererCamera::new(&self.state.device, camera_bind_group_layout)?; - let handle = self.state.renderer_resource_storage.cameras.insert(camera); - - Ok(handle) - } - - fn update_material_textures(&mut self, renderer_material_handle: RendererMaterialHandle, textures: &MaterialTextureMap) -> Result<()> { - RendererMaterial::update_textures(&self.state.device, renderer_material_handle, &mut self.state.renderer_resource_storage, textures) - } - - fn update_material_parameters(&mut self, renderer_material_handle: RendererMaterialHandle, parameters: &MaterialParameterMap) -> Result<()> { - RendererMaterial::update_parameters(&self.state.device, &self.state.queue, renderer_material_handle, &mut self.state.renderer_resource_storage, parameters) - } - - fn destroy_mesh(&mut self, renderer_mesh_handle: RendererMeshHandle) -> Result<()> { - self.state.renderer_resource_storage.meshes.remove(renderer_mesh_handle).unwrap(); - - Ok(()) - } - - fn destroy_texture(&mut self, renderer_texture_handle: RendererTextureHandle) -> Result<()> { - self.state.renderer_resource_storage.textures.remove(renderer_texture_handle).unwrap(); - - Ok(()) - } - - fn destroy_material(&mut self, renderer_material_handle: RendererMaterialHandle) -> Result<()> { - self.state.renderer_resource_storage.materials.remove(renderer_material_handle).unwrap(); - - Ok(()) - } - - fn destroy_camera(&mut self, renderer_camera_handle: RendererCameraHandle) -> Result<()> { - self.state.renderer_resource_storage.cameras.remove(renderer_camera_handle).unwrap(); - - Ok(()) - } - - fn render( - &mut self, - active_camera_entity_handle: EntityHandle, - render_queue: &Vec, - camera_component_storage: &ComponentStorage, - transform_component_storage: &ComponentStorage, - egui_ui: Box, - timer: &mut Timer - ) -> Result<()> { - self.state.render( - active_camera_entity_handle, - render_queue, - camera_component_storage, - transform_component_storage, - egui_ui, - timer - ) - } - - fn pass_input_to_egui(&mut self, event: &winit::event::WindowEvent) -> Result<()> { - self.state.egui_renderer.handle_input(event); - Ok(()) - } - -} - -pub struct State { - // Resources - renderer_resource_storage: RendererResourceStorage, - // Renderer variables - surface: wgpu::Surface<'static>, - device: wgpu::Device, - queue: wgpu::Queue, - surface_configuration: wgpu::SurfaceConfiguration, - window_size: winit::dpi::PhysicalSize, - color_format: wgpu::TextureFormat, - depth_format: wgpu::TextureFormat, - depth_texture: RendererTexture, - mesh_drawer: MeshDrawer, - // Other - config: config::Config, - egui_renderer: crate::egui::EguiRenderer, -} - - -impl State { - // Creating some of the wgpu types requires async code - async fn new(window: Arc, config: config::Config) -> Self { - let window_size = window.inner_size(); - - let window_ref = window.clone(); - - let backends = wgpu::util::backend_bits_from_env().unwrap_or_default(); - let dx12_shader_compiler = wgpu::util::dx12_shader_compiler_from_env().unwrap_or_default(); - let gles_minor_version = wgpu::util::gles_minor_version_from_env().unwrap_or_default(); - - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends, - flags: wgpu::InstanceFlags::from_build_config().with_env(), - dx12_shader_compiler, - gles_minor_version, - }); - let surface = instance.create_surface(window).unwrap(); - - // Specify adapter options (Options passed here are not guaranteed to work for all devices) - let request_adapter_options = wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::default(), - compatible_surface: Some(&surface), - force_fallback_adapter: false, - }; - - // Create adapter - let adapter = instance.request_adapter(&request_adapter_options).await.unwrap(); - let adapter_info = adapter.get_info(); - info!("Using GPU: {} ({:?})", adapter_info.name, adapter_info.backend); - - let features = wgpu::Features::DEPTH_CLIP_CONTROL; - - // Create device descriptor - let device_descriptor = wgpu::DeviceDescriptor { - label: None, - required_features: features, // Allows to specify what extra features of GPU that needs to be included (e.g. depth clamping, push constants, texture compression, etc) - required_limits: wgpu::Limits::default(), // Allows to specify the limit of certain types of resources that will be used (e.g. max samplers, uniform buffers, etc) - //memory_hints: wgpu::MemoryHints::MemoryUsage, - }; - - // Create device and queue - let (device, queue) = adapter.request_device(&device_descriptor,None).await.unwrap(); - - // Specify surface configuration - let format = wgpu::TextureFormat::Rgba8UnormSrgb; - let surface_configuration = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, // Defines how the swap_chain's underlying textures will be used - format: format, // Defines how the swap_chain's textures will be stored on the gpu - width: window_size.width, - height: window_size.height, - desired_maximum_frame_latency: 2, - present_mode: wgpu::PresentMode::Mailbox, // Defines how to sync the surface with the display - alpha_mode: wgpu::CompositeAlphaMode::Auto, - view_formats: vec![format], - }; - - // Configure surface - surface.configure(&device, &surface_configuration); - - // Configure collections - let renderer_resource_storage = RendererResourceStorage::new(&config); - - // Create depth and color texture - let depth_texture = RendererTexture::new_depth_texture( - &device, - &surface_configuration, - "depth_texture" - ).unwrap(); - - let color_format = surface_configuration.format; - let depth_format = wgpu::TextureFormat::Depth32Float; - - // Create drawing state - let mesh_drawer = MeshDrawer::new(&device, MAX_INSTANCE_BATCH_SIZE as u32); - - let egui_renderer = EguiRenderer::new( - &device, - surface_configuration.format, - None, - 1, - window_ref, - ); - - // Create state - Self { - // Resources - renderer_resource_storage, - // Renderer variables - surface, - device, - queue, - surface_configuration, - window_size, - color_format, - depth_format, - depth_texture, - mesh_drawer, - // Other - config, - egui_renderer - } - } - - fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize) { - if new_window_size.width > 0 && new_window_size.height > 0 { - self.window_size = new_window_size; - self.surface_configuration.width = new_window_size.width; - self.surface_configuration.height = new_window_size.height; - self.surface.configure(&self.device, &self.surface_configuration); - self.depth_texture = RendererTexture::new_depth_texture( - &self.device, - &self.surface_configuration, - "depth_texture", - ).unwrap(); - } - } - - fn render( - &mut self, - active_camera_entity_handle: EntityHandle, - render_queue: &Vec, - camera_component_storage: &ComponentStorage, - transform_component_storage: &ComponentStorage, - egui_ui: Box, - timer: &mut Timer - ) -> Result<()> { - timer.record("Get frame"); - - // Get frame or return mapped error if failed - let frame = self.surface.get_current_texture(); - - let frame = match frame { - std::result::Result::Ok(frame) => frame, - std::result::Result::Err(error) => match error { - wgpu::SurfaceError::Lost => return Err(RendererError::SurfaceLost.into()), - wgpu::SurfaceError::OutOfMemory => return Err(RendererError::SurfaceOutOfMemory.into()), - _ => return Err(RendererError::SurfaceOther.into()), - }, - }; - - let view = frame.texture.create_view(&wgpu::TextureViewDescriptor::default()); - - timer.record("Get clear color and create render pass attachments"); - - // Get active camera and update it - let camera_storage = camera_component_storage.data.get(active_camera_entity_handle.data().index as usize).unwrap(); - let active_camera_component = camera_storage.as_ref().unwrap(); - let renderer_camera = self.renderer_resource_storage.cameras.get_mut(get_renderer_resource_handle_from_camera_component(active_camera_component)).ok_or(Error::new(RendererError::RendererResourceNotFound))?; - let camera_transform_storage = transform_component_storage.data.get(active_camera_entity_handle.data().index as usize).unwrap(); - let active_camera_transform_component = camera_transform_storage.as_ref().unwrap(); - renderer_camera.update(&self.queue, active_camera_component, active_camera_transform_component); - let renderer_camera = self.renderer_resource_storage.cameras.get(get_renderer_resource_handle_from_camera_component(active_camera_component)).unwrap(); - let clear_color = active_camera_component.clear_color; - - // Build a command buffer that can be sent to the GPU - let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("render_encoder"), - }); - - { // Additional scope to release mutable borrow of encoder done by begin_render_pass - - // Create color attachment - let color_attachment = wgpu::RenderPassColorAttachment { - view: &view, // Specifies what texture to save the colors to - resolve_target: None, // Specifies what texture will receive the resolved output - ops: wgpu::Operations { // Specifies what to do with the colors on the screen - load: wgpu::LoadOp::Clear(wgpu::Color { r: clear_color.x as f64, g: clear_color.y as f64, b: clear_color.z as f64, a: 1.0, } ), // Specifies how to handle colors stored from the previous frame - store: wgpu::StoreOp::Store, - }, - }; - - // Create depth attachment - let depth_stencil_attachment = wgpu::RenderPassDepthStencilAttachment { - view: &self.depth_texture.texture_view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Clear(1.0), - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }; - - timer.begin_context("Mesh Drawer"); - - self.mesh_drawer.record_draw_commands( - &self.queue, - &self.device, - &self.renderer_resource_storage, - color_attachment, - depth_stencil_attachment, - &renderer_camera, - &render_queue, - &transform_component_storage, - timer - )?; - - timer.end_context()?; - } - - timer.begin_context("Egui Draw"); - - timer.end_context()?; // End Egui Draw context - - timer.record("Submit commands and present frame"); - - frame.present(); - - Ok(()) - } -} diff --git a/engine/pill_renderer/src/renderer_resource_storage.rs b/engine/pill_renderer/src/renderer_resource_storage.rs deleted file mode 100644 index ff7f9bb3..00000000 --- a/engine/pill_renderer/src/renderer_resource_storage.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::resources::{ - RendererPipeline, - RendererMaterial, - RendererTexture, - RendererMesh, - RendererCamera -}; - -use pill_core::PillSlotMap; - -use pill_engine::internal::{ - RendererCameraHandle, - RendererMaterialHandle, - RendererMeshHandle, - RendererPipelineHandle, - RendererTextureHandle, -}; - -pub const MAX_PIPELINES: usize = 10; -pub const MAX_TEXTURES: usize = 10; -pub const MAX_MATERIALS: usize = 10; -pub const MAX_MESHES: usize = 10; -pub const MAX_CAMERAS: usize = 10; - -pub struct RendererResourceStorage { - pub(crate) pipelines: PillSlotMap::, - pub(crate) materials: PillSlotMap::, - pub(crate) textures: PillSlotMap, - pub(crate) meshes: PillSlotMap::, - pub(crate) cameras: PillSlotMap::, -} - -impl RendererResourceStorage { - pub fn new(config: &config::Config) -> Self { - let max_pipeline_count = config.get_int("MAX_PIPELINES").unwrap_or(MAX_PIPELINES as i64) as usize; - let max_texture_count = config.get_int("MAX_TEXTURES").unwrap_or(MAX_TEXTURES as i64) as usize; - let max_material_count = config.get_int("MAX_MATERIALS").unwrap_or(MAX_MATERIALS as i64) as usize; - let max_mesh_count = config.get_int("MAX_MESHS").unwrap_or(MAX_MESHES as i64) as usize; - let max_camera_count = config.get_int("MAX_CAMERAS").unwrap_or(MAX_CAMERAS as i64) as usize; - - RendererResourceStorage { - pipelines: PillSlotMap::::with_capacity_and_key(max_pipeline_count), - textures: PillSlotMap::::with_capacity_and_key(max_texture_count), - materials: PillSlotMap::::with_capacity_and_key(max_material_count), - meshes: PillSlotMap::::with_capacity_and_key(max_mesh_count), - cameras: PillSlotMap::::with_capacity_and_key(max_camera_count), - } - } -} \ No newline at end of file diff --git a/engine/pill_renderer/src/resources/renderer_material.rs b/engine/pill_renderer/src/resources/renderer_material.rs deleted file mode 100644 index 51dd627e..00000000 --- a/engine/pill_renderer/src/resources/renderer_material.rs +++ /dev/null @@ -1,214 +0,0 @@ -use crate::RendererResourceStorage; - -use pill_core::RendererError; -use pill_engine::internal::{ - MaterialParameterMap, - MaterialTextureMap, - RendererMaterialHandle, - RendererPipelineHandle, - MASTER_SHADER_COLOR_TEXTURE_SLOT, - MASTER_SHADER_NORMAL_TEXTURE_SLOT, - MASTER_SHADER_TINT_PARAMETER_SLOT, get_default_texture_handles, get_renderer_texture_handle_from_material_texture, MASTER_SHADER_SPECULARITY_PARAMETER_SLOT, -}; - -use wgpu::util::DeviceExt; -use anyhow::{ Result, Error}; - -// --- Material Uniform --- - -#[repr(C)] -#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -pub(crate) struct MaterialUniform { - pub(crate) tint: [f32; 3], - pub(crate) specularity: f32, -} - -impl MaterialUniform { - pub fn new() -> Self { - Self { - tint: cgmath::Vector3::::new(0.0,0.0,0.0).into(), - specularity: 0.0, - } - } -} - -// --- Material --- - -pub struct RendererMaterial { - pub name: String, - pub pipeline_handle: RendererPipelineHandle, - pub texture_bind_group: wgpu::BindGroup, - pub parameter_bind_group: wgpu::BindGroup, - pub(crate) uniform: MaterialUniform, - buffer: wgpu::Buffer, -} - -impl RendererMaterial { - pub fn new( - device: &wgpu::Device, - queue: &wgpu::Queue, - rendering_resource_storage: &RendererResourceStorage, - name: &str, - pipeline_handle: RendererPipelineHandle, - texture_bind_group_layout: &wgpu::BindGroupLayout, - textures: &MaterialTextureMap, - parameter_bind_group_layout: &wgpu::BindGroupLayout, - parameters: &MaterialParameterMap, - ) -> Result { - - // Create parameter buffer and write data to it - let mut uniform = MaterialUniform::new(); - let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("material_buffer"), - contents: bytemuck::cast_slice(&[uniform]), - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - }); - uniform.tint = parameters.get_color(MASTER_SHADER_TINT_PARAMETER_SLOT).unwrap().into(); - uniform.specularity = parameters.get_scalar(MASTER_SHADER_SPECULARITY_PARAMETER_SLOT).unwrap().into(); - queue.write_buffer(&buffer, 0, bytemuck::cast_slice(&[uniform])); - - // Create texture binding group - let texture_bind_group = Self::create_texture_bind_group( - device, - rendering_resource_storage, - &texture_bind_group_layout, - &(name.to_owned() + "_textures"), - textures, - )?; - - // Create parameter binding group - let parameter_bind_group = Self::create_parameter_bind_group( - device, - ¶meter_bind_group_layout, - &(name.to_owned() + "_parameters"), - &buffer, - )?; - - let renderer_material = Self { - name: name.to_string(), - pipeline_handle, - texture_bind_group, - parameter_bind_group, - uniform, - buffer, - }; - - Ok(renderer_material) - } - - pub fn update_textures( - device: &wgpu::Device, - material_renderer_handle: RendererMaterialHandle, - rendering_resource_storage: &mut RendererResourceStorage, - textures: &MaterialTextureMap - ) -> Result<()> { - let material = rendering_resource_storage.materials.get(material_renderer_handle).ok_or(Error::new(RendererError::RendererResourceNotFound))?; - let material_name = material.name.clone(); - let pipeline = rendering_resource_storage.pipelines.get(material.pipeline_handle).ok_or(Error::new(RendererError::RendererResourceNotFound))?; - - let texture_bind_group = RendererMaterial::create_texture_bind_group( - &device, - &rendering_resource_storage, - &pipeline.material_texture_bind_group_layout, - &(material_name.to_owned() + "_textures"), - textures - )?; - - let material = rendering_resource_storage.materials.get_mut(material_renderer_handle).ok_or(Error::new(RendererError::RendererResourceNotFound))?; - material.texture_bind_group = texture_bind_group; - Ok(()) - } - - pub fn create_texture_bind_group( - device: &wgpu::Device, - rendering_resource_storage: &RendererResourceStorage, - texture_bind_group_layout: &wgpu::BindGroupLayout, - name: &str, - textures: &MaterialTextureMap - ) -> Result { - - // Get texture renderer handles, if is it None use default texture for this type of slot - let color_texture = textures.data.get(MASTER_SHADER_COLOR_TEXTURE_SLOT).unwrap(); - let color_renderer_texture_handle = - get_renderer_texture_handle_from_material_texture(color_texture) - .unwrap_or_else(|| get_default_texture_handles(color_texture.texture_type).1); - - let normal_texture = textures.data.get(MASTER_SHADER_NORMAL_TEXTURE_SLOT).unwrap(); - let normal_renderer_textur_handle = - get_renderer_texture_handle_from_material_texture(normal_texture) - .unwrap_or_else(|| get_default_texture_handles(normal_texture.texture_type).1); - - let color_texture = rendering_resource_storage.textures.get(color_renderer_texture_handle).unwrap(); - let normal_texture = rendering_resource_storage.textures.get(normal_renderer_textur_handle).unwrap(); - - // Set texture resources to the bind group - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: &texture_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&color_texture.texture_view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&color_texture.sampler), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::TextureView(&normal_texture.texture_view), - }, - wgpu::BindGroupEntry { - binding: 3, - resource: wgpu::BindingResource::Sampler(&normal_texture.sampler), - }, - ], - label: Some(name), - }); - - Ok(bind_group) - } - - pub fn update_parameters( - device: &wgpu::Device, - queue: &wgpu::Queue, - material_renderer_handle: RendererMaterialHandle, - rendering_resource_storage: &mut RendererResourceStorage, - parameters: &MaterialParameterMap - ) -> Result<()> { - let material = rendering_resource_storage.materials.get_mut(material_renderer_handle).ok_or(Error::new(RendererError::RendererResourceNotFound))?; - let pipeline = rendering_resource_storage.pipelines.get(material.pipeline_handle).ok_or(Error::new(RendererError::RendererResourceNotFound))?; - - material.uniform.tint = parameters.get_color(MASTER_SHADER_TINT_PARAMETER_SLOT).unwrap().into(); - material.uniform.specularity = parameters.get_scalar(MASTER_SHADER_SPECULARITY_PARAMETER_SLOT).unwrap().into(); - queue.write_buffer(&material.buffer, 0, bytemuck::cast_slice(&[material.uniform])); - - material.parameter_bind_group = RendererMaterial::create_parameter_bind_group( - device, - &pipeline.material_parameter_bind_group_layout, - &(material.name.to_owned() + "_parameter"), - &material.buffer - )?; - - Ok(()) - } - - fn create_parameter_bind_group( - device: &wgpu::Device, - parameter_bind_group_layout: &wgpu::BindGroupLayout, - name: &str, - buffer: &wgpu::Buffer, - ) -> Result { - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: ¶meter_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }, - ], - label: Some(name), - }); - - Ok(bind_group) - } -} diff --git a/engine/pill_renderer/src/resources/renderer_mesh.rs b/engine/pill_renderer/src/resources/renderer_mesh.rs deleted file mode 100644 index 46828a91..00000000 --- a/engine/pill_renderer/src/resources/renderer_mesh.rs +++ /dev/null @@ -1,83 +0,0 @@ -use pill_engine::internal::{ MeshData, MeshVertex }; - -use wgpu::util::DeviceExt; -use anyhow::*; - -// --- Vertex --- - -pub trait Vertex { - // Defines how a data is layed out in memory (To specify how RenderPipeline needs to map the buffer in the shader) - fn data_layout_descriptor<'a>() -> wgpu::VertexBufferLayout<'a>; -} - -// --- Mesh --- - -pub struct RendererMesh { - pub vertex_buffer: wgpu::Buffer, - pub index_buffer: wgpu::Buffer, - pub index_count: u32, -} - -impl RendererMesh { - pub fn new(device: &wgpu::Device, name: &str, mesh_data: &MeshData) -> Result { - - let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some(&format!("{:?}_vertex_buffer", name)), - contents: bytemuck::cast_slice(&mesh_data.vertices), - usage: wgpu::BufferUsages::VERTEX, - }); - - let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some(&format!("{:?}_vertex_buffer", name)), - contents: bytemuck::cast_slice(&mesh_data.indices), - usage: wgpu::BufferUsages::INDEX, - }); - - let renderer_mesh = Self { - vertex_buffer, - index_buffer, - index_count: mesh_data.indices.len() as u32, - }; - - Ok(renderer_mesh) - } -} - -impl Vertex for RendererMesh { - fn data_layout_descriptor<'a>() -> wgpu::VertexBufferLayout<'a> { - use std::mem; - wgpu::VertexBufferLayout { - array_stride: mem::size_of::() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &[ - wgpu::VertexAttribute { // Vertex position - offset: 0, - shader_location: 0, - format: wgpu::VertexFormat::Float32x3, - }, - wgpu::VertexAttribute { // Vertex texture coordinates - offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, - shader_location: 1, - format: wgpu::VertexFormat::Float32x2, - }, - wgpu::VertexAttribute { // Vertex normal - offset: mem::size_of::<[f32; 5]>() as wgpu::BufferAddress, - shader_location: 2, - format: wgpu::VertexFormat::Float32x3, - }, - wgpu::VertexAttribute { // Vertex tangent - offset: mem::size_of::<[f32; 8]>() as wgpu::BufferAddress, - shader_location: 3, - format: wgpu::VertexFormat::Float32x3, - }, - wgpu::VertexAttribute { // Vertex bitangent - offset: mem::size_of::<[f32; 11]>() as wgpu::BufferAddress, - shader_location: 4, - format: wgpu::VertexFormat::Float32x3, - }, - ], - } - } -} - - diff --git a/engine/pill_renderer/src/resources/renderer_pipeline.rs b/engine/pill_renderer/src/resources/renderer_pipeline.rs deleted file mode 100644 index 10fa3c3b..00000000 --- a/engine/pill_renderer/src/resources/renderer_pipeline.rs +++ /dev/null @@ -1,167 +0,0 @@ -use anyhow::{ Result }; - -// --- Pipeline --- - -pub struct RendererPipeline { - pub render_pipeline: wgpu::RenderPipeline, - pub material_texture_bind_group_layout: wgpu::BindGroupLayout, - pub material_parameter_bind_group_layout: wgpu::BindGroupLayout, - pub camera_bind_group_layout: wgpu::BindGroupLayout, -} - -impl RendererPipeline { - pub fn new( - device: &wgpu::Device, - vertex_shader: wgpu::ShaderModule, - fragment_shader: wgpu::ShaderModule, - color_format: wgpu::TextureFormat, - depth_format: Option, - vertex_layouts: &[wgpu::VertexBufferLayout], - ) -> Result { - - // Define material bind group layout (Describes a set of resources and how they can be accessed by a shader) - let material_texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("material_texture_bind_group_layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { // Entry for the sampled texture at binding 0 - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, // Visible only to fragment shader - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { // Entry for the sampler at binding 1 - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, // Visible only to fragment shader - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - wgpu::BindGroupLayoutEntry { // Normal map - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: wgpu::TextureViewDimension::D2, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 3, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - }); - - let material_parameter_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("material_parameter_bind_group_layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, // Specifies if this buffer will be changing size or not - min_binding_size: None, - }, - count: None, - }, - ], - }); - - // Define camera bind group layout - let camera_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("camera_bind_group_layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, // Specifies if this buffer will be changing size or not - min_binding_size: None, - }, - count: None, - }] - }); - - // Create pipeline layout descriptor - let pipeline_layout_descriptor = wgpu::PipelineLayoutDescriptor { - label: Some("render_pipeline_layout"), - bind_group_layouts: &[ - &material_texture_bind_group_layout, - &material_parameter_bind_group_layout, - &camera_bind_group_layout, - ], - push_constant_ranges: &[], - }; - - // Create pipeline layout - let layout = device.create_pipeline_layout(&pipeline_layout_descriptor); - - // Create color target states that specifies what what color outputs wgpu should set up - let color_target_states = &[Some(wgpu::ColorTargetState { - format: color_format, - blend: Some(wgpu::BlendState { - alpha: wgpu::BlendComponent::REPLACE, - color: wgpu::BlendComponent::REPLACE, - }), - write_mask: wgpu::ColorWrites::ALL, - })]; - - let render_pipeline_descriptor = wgpu::RenderPipelineDescriptor { - label: Some("render_pipeline"), - layout: Some(&layout), - vertex: wgpu::VertexState { - module: &vertex_shader, - entry_point: "main", - buffers: vertex_layouts, // Specifies structure of vertices that will be passed to the vertex shader - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &fragment_shader, - entry_point: "main", - targets: color_target_states, - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { // Specifies how to interpret vertices when converting them into triangles - topology: wgpu::PrimitiveTopology::TriangleList, // Each three vertices will correspond to one triangle - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, // Specifies how to determine whether a given triangle is facing forward or not (FrontFace::Ccw means that a triangle is facing forward if the vertices are arranged in a counter clockwise direction) - cull_mode: Some(wgpu::Face::Back), // Triangles that are not considered facing forward are culled (not included in the render) as specified by CullMode::Back - polygon_mode: wgpu::PolygonMode::Fill, // Setting this to anything other than Fill requires Features::NON_FILL_POLYGON_MODE - conservative: false, // Requires Features::CONSERVATIVE_RASTERIZATION - unclipped_depth: true, // Requires Features::DEPTH_CLAMPING - }, - depth_stencil: depth_format.map(|format| wgpu::DepthStencilState { - format, - depth_write_enabled: true, - depth_compare: wgpu::CompareFunction::Less, // Specifies when to discard a new pixel. Using LESS means pixels will be drawn front to back - stencil: wgpu::StencilState::default(), - bias: wgpu::DepthBiasState::default(), - }), - multisample: wgpu::MultisampleState { - count: 1, // Determines how many samples pipeline will use (Multisampling) - mask: !0, // Specifies which samples should be active - alpha_to_coverage_enabled: false, - }, - multiview: None, - //cache: None, - }; - - let render_pipeline = device.create_render_pipeline(&render_pipeline_descriptor); - - let pipeline = Self { - render_pipeline, - material_texture_bind_group_layout, - material_parameter_bind_group_layout, - camera_bind_group_layout, - }; - - Ok(pipeline) - } -} diff --git a/engine/pill_renderer/src/resources/renderer_texture.rs b/engine/pill_renderer/src/resources/renderer_texture.rs deleted file mode 100644 index 6a731fd5..00000000 --- a/engine/pill_renderer/src/resources/renderer_texture.rs +++ /dev/null @@ -1,151 +0,0 @@ -use pill_engine::internal::TextureType; - -use anyhow::*; -use image::GenericImageView; -use std::{ num::NonZeroU32 }; - -// --- Texture --- - -pill_core::define_new_pill_slotmap_key! { - pub struct RendererTextureHandle; -} - -pub struct RendererTexture { - pub texture: wgpu::Texture, - pub texture_view: wgpu::TextureView, - pub sampler: wgpu::Sampler, -} - -impl RendererTexture { - pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; - - pub fn new_texture( - device: &wgpu::Device, - queue: &wgpu::Queue, - name: Option<&str>, - image_data: &image::DynamicImage, - texture_type: TextureType, - ) -> Result { - let dimensions = image_data.dimensions(); - let rgba = image_data.to_rgba8(); - - // Get size - let size = wgpu::Extent3d { - width: dimensions.0, - height: dimensions.1, - depth_or_array_layers: 1, - }; - - // Specify texture format - let format = match texture_type { - TextureType::Color => wgpu::TextureFormat::Rgba8UnormSrgb, - TextureType::Normal => wgpu::TextureFormat::Rgba8Unorm, - }; - - // Create texture - let texture = device.create_texture(&wgpu::TextureDescriptor { - label: name, - size, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format, - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, - view_formats: &[], - }); - - // Write data to texture - queue.write_texture( - wgpu::ImageCopyTexture { - aspect: wgpu::TextureAspect::All, - texture: &texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - }, - &rgba, - wgpu::ImageDataLayout { - offset: 0, - bytes_per_row: Some(4 * dimensions.0), - rows_per_image: Some(dimensions.1), - }, - size, - ); - - // Create texture view - let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - - // Create sampler - let sampler = device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::Repeat, - address_mode_v: wgpu::AddressMode::Repeat, - address_mode_w: wgpu::AddressMode::Repeat, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, - lod_min_clamp: 0.0, // Fix: Must be 0.0 or greater - lod_max_clamp: 100.0, // You can set this based on your texture's mipmap levels - ..Default::default() - }); - - // Create final texture - let texture = Self { - texture, - texture_view, - sampler, - }; - - Ok(texture) - } - - pub fn new_depth_texture( - device: &wgpu::Device, - surface_configuration: &wgpu::SurfaceConfiguration, - label: &str, - ) -> Result { - - // Get size - let size = wgpu::Extent3d { // Depth texture needs to be the same size as window - width: surface_configuration.width, - height: surface_configuration.height, - depth_or_array_layers: 1, - }; - - // Create texture - let texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some(label), - size, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: Self::DEPTH_FORMAT, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, // Rendering to this texture so RENDER_ATTACHMENT flag is needed - view_formats: &[], - }); - - // Create texture view - let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - - // Create sampler - let sampler = device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::Repeat, - address_mode_v: wgpu::AddressMode::Repeat, - address_mode_w: wgpu::AddressMode::Repeat, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Linear, - mipmap_filter: wgpu::FilterMode::Nearest, - compare: Some(wgpu::CompareFunction::LessEqual), - lod_min_clamp: 0.0, - lod_max_clamp: 100.0, - ..Default::default() - }); - - // Create final texture - let texture = Self { - texture, - texture_view, - sampler, - }; - - Ok(texture) - } -} \ No newline at end of file diff --git a/engine/pill_standalone/Cargo.toml b/engine/pill_standalone/Cargo.toml index 6ae5e7bb..b838381f 100644 --- a/engine/pill_standalone/Cargo.toml +++ b/engine/pill_standalone/Cargo.toml @@ -4,10 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -pill_core = {path = "../pill_core"} -pill_engine = {path = "../pill_engine", features = ["internal"]} -pill_renderer = {path = "../pill_renderer"} - +pill_core = { path = "../pill_core" } +pill_engine = { path = "../pill_engine", features = ["internal"] } winit = "0.29" env_logger = "0.9.0" pretty_env_logger = "0.4.0" diff --git a/engine/pill_standalone/src/lib.rs b/engine/pill_standalone/src/lib.rs new file mode 100644 index 00000000..4601fdce --- /dev/null +++ b/engine/pill_standalone/src/lib.rs @@ -0,0 +1 @@ +pub use pill_engine as engine; diff --git a/engine/pill_standalone/src/main.rs b/engine/pill_standalone/src/main.rs index ef5988db..ca7d2c03 100644 --- a/engine/pill_standalone/src/main.rs +++ b/engine/pill_standalone/src/main.rs @@ -1,22 +1,26 @@ mod file_watcher; use crate::file_watcher::FileWatcher; +use anyhow::{Context, Ok, Result}; use config::Config; -use pill_core::{ PillStyle, EngineError }; -use pill_engine::internal::*; -use pill_renderer; -use anyhow::{ Context, Ok, Result }; -use winit::{ - event::{ Event, WindowEvent, DeviceEvent }, - window::Icon, +use libloading::{Library, Symbol}; +use log::{info, warn}; +use pill_core::{EngineError, PillStyle}; +use pill_engine::internal::PillRenderer; +use pill_engine::internal::{Engine, PillGame}; +use std::ffi::c_void; +use std::{ + fs::{remove_file, rename}, + io::Write, + path::{Path, PathBuf}, + sync::Arc, + time::{Duration, Instant}, }; #[cfg(target_os = "windows")] use winit::platform::windows::IconExtWindows; -use std::{ - fs::{remove_file, rename}, io::{ Write }, path::{Path, PathBuf}, sync::Arc, time::{Duration, Instant} +use winit::{ + event::{DeviceEvent, Event, WindowEvent}, + window::Icon, }; -use log::{ info, warn }; -use libloading::{Library, Symbol}; -use std::ffi::c_void; const RELOAD_COOLDOWN: Duration = Duration::from_millis(1000); struct WindowData { @@ -74,7 +78,9 @@ fn set_log_level(config: &Config) { #[cfg(debug_assertions)] env_logger::Builder::new() .format(|buf, record| { - writeln!(buf, "[{}] {} {}:{}: {}", + writeln!( + buf, + "[{}] {} {}:{}: {}", record.level(), chrono::Local::now().format("%Y-%m-%dT%H:%M:%S"), record.file().unwrap_or("unknown"), @@ -86,7 +92,7 @@ fn set_log_level(config: &Config) { .filter_module("pill_standalone", log_level) .filter_module("pill_engine", log_level) .filter_module("pill_renderer", log_level) - .filter_module("pill_game", log_level) + .filter_module("pill_game", log_level) .init(); } @@ -108,7 +114,10 @@ pub fn load_window_icon(path: &Path) -> Option { } fn create_window(config: &Config, game_resources_directory_path: PathBuf) -> WindowData { - let window_title = config.get_str("WINDOW_TITLE").context(EngineError::InvalidGameConfig()).unwrap(); + let window_title = config + .get_str("WINDOW_TITLE") + .context(EngineError::InvalidGameConfig()) + .unwrap(); let window_width = config.get_int("WINDOW_WIDTH").unwrap_or(1280) as u32; let window_height = config.get_int("WINDOW_HEIGHT").unwrap_or(720) as u32; let window_fullscreen = config.get_bool("WINDOW_FULLSCREEN").unwrap_or(false); @@ -124,14 +133,17 @@ fn create_window(config: &Config, game_resources_directory_path: PathBuf) -> Win // Initialize other window parameters let window_size = winit::dpi::PhysicalSize::::new(window_width, window_height); let window_min_size = winit::dpi::PhysicalSize::::new(100, 100); - let window = Arc::new(winit::window::WindowBuilder::new() - .with_title(window_title) - .with_inner_size(window_size) - .with_min_inner_size(window_min_size) - .with_window_icon(window_icon.clone()) - .with_visible(false) // Temporarily hide window (this is a hack to make taskbar icon loaded correctly) - .build(&window_event_loop) - .context("Failed to initialize window").unwrap()); + let window = Arc::new( + winit::window::WindowBuilder::new() + .with_title(window_title) + .with_inner_size(window_size) + .with_min_inner_size(window_min_size) + .with_window_icon(window_icon.clone()) + .with_visible(false) // Temporarily hide window (this is a hack to make taskbar icon loaded correctly) + .build(&window_event_loop) + .context("Failed to initialize window") + .unwrap(), + ); // Possibly set window to fullscreen let window_fullscreen_mode = match window_fullscreen { @@ -139,17 +151,29 @@ fn create_window(config: &Config, game_resources_directory_path: PathBuf) -> Win let monitor_handle = window.current_monitor(); Some(winit::window::Fullscreen::Borderless(monitor_handle)) } - false => None + false => None, }; window.set_fullscreen(window_fullscreen_mode); - WindowData { window, event_loop: window_event_loop, size: window_size } + WindowData { + window, + event_loop: window_event_loop, + size: window_size, + } } fn load_game_dynamic_library(library_path: &PathBuf) -> (Library, Box) { type CreateGameFn = unsafe extern "C" fn() -> *mut c_void; - let game_dynamic_library = unsafe { Library::new(library_path).context(format!("Failed to load game dynamic library at {}", library_path.display())).unwrap() }; - let get_game_function: Symbol = unsafe { game_dynamic_library.get(b"get_game").unwrap() }; + let game_dynamic_library = unsafe { + Library::new(library_path) + .context(format!( + "Failed to load game dynamic library at {}", + library_path.display() + )) + .unwrap() + }; + let get_game_function: Symbol = + unsafe { game_dynamic_library.get(b"get_game").unwrap() }; let game = unsafe { *Box::from_raw(get_game_function() as *mut Box) }; (game_dynamic_library, game) } @@ -158,20 +182,27 @@ fn build_standalone_and_game_crates(project_paths: &ProjectPaths) -> Result<()> // Locate Pill Launcher manifest directly let launcher_manifest = project_paths .game_project_directory_path - .parent().unwrap() // …/examples - .parent().unwrap() // …/ + .parent() + .unwrap() // …/examples + .parent() + .unwrap() // …/ .join("engine") .join("pill_launcher") .join("Cargo.toml"); let output = std::process::Command::new("cargo") .args(&[ - "run", "--quiet", - "--manifest-path", launcher_manifest.to_str().unwrap(), + "run", + "--quiet", + "--manifest-path", + launcher_manifest.to_str().unwrap(), "--", - "-a", "build", - "-p", project_paths.game_project_directory_path.to_str().unwrap(), - "-c", "hot-reload", + "-a", + "build", + "-p", + project_paths.game_project_directory_path.to_str().unwrap(), + "-c", + "hot-reload", ]) // run inside the engine workspace so relative paths in Cargo.toml work .current_dir(launcher_manifest.parent().unwrap().parent().unwrap()) // …//engine @@ -206,19 +237,28 @@ fn check_and_reload_game( } // Check for game project source files changes - if let Some(paths) = file_watchers.game_project_source_files_watcher.get_changes() { + if let Some(paths) = file_watchers + .game_project_source_files_watcher + .get_changes() + { info!("Game project source file change detected: {:?}", paths); build_standalone_and_game_crates(project_paths)?; } // Check for game project resource files changes - if let Some(paths) = file_watchers.game_project_resources_files_watcher.get_changes() { + if let Some(paths) = file_watchers + .game_project_resources_files_watcher + .get_changes() + { info!("Game project resource file change detected: {:?}", paths); build_standalone_and_game_crates(project_paths)?; } // Check for game dynamic library changes - if let Some(_) = file_watchers.game_dynamic_library_files_watcher.get_changes() { + if let Some(_) = file_watchers + .game_dynamic_library_files_watcher + .get_changes() + { info!("Reloading game project..."); // Shutdown and drop engine @@ -230,18 +270,38 @@ fn check_and_reload_game( // Remove old game dynamic library file and rename new one remove_file(&project_paths.game_dynamic_library_path).unwrap(); - rename(&project_paths.game_dynamic_library_hot_reloaded_path, &project_paths.game_dynamic_library_path).unwrap(); + rename( + &project_paths.game_dynamic_library_hot_reloaded_path, + &project_paths.game_dynamic_library_path, + ) + .unwrap(); // Load new game dynamic library - let (game_library, game) = load_game_dynamic_library(&project_paths.game_dynamic_library_path); - let renderer: Box = Box::new(::new(Arc::clone(&window), config.clone())); - let mut new_engine = Engine::new(game, project_paths.game_resources_directory_path.clone(), renderer, config.clone()); + let (game_library, game) = + load_game_dynamic_library(&project_paths.game_dynamic_library_path); + let mut resource_manager = pill_engine::internal::ResourceManager::new(); + let renderer: Box = + Box::new(::new( + &mut resource_manager, + Arc::clone(&window), + config.clone(), + )); + let mut new_engine = Engine::new( + game, + project_paths.game_resources_directory_path.clone(), + renderer, + resource_manager, + config.clone(), + Arc::clone(&window), + ); new_engine.initialize(*window_size).unwrap(); *engine = Some(new_engine); *game_dynamic_library = Some(game_library); // Run again to clear changes (otherwise it will trigger reload again since file was renamed) - file_watchers.game_dynamic_library_files_watcher.get_changes(); + file_watchers + .game_dynamic_library_files_watcher + .get_changes(); *last_reload_time = now; } @@ -250,9 +310,12 @@ fn check_and_reload_game( } fn create_file_watchers(project_paths: &ProjectPaths) -> FileWatchers { - let game_dynamic_library_files_watcher = FileWatcher::new(project_paths.build_data_directory_path.clone()); - let game_project_source_files_watcher = FileWatcher::new(project_paths.game_source_directory_path.clone()).set_recursive(true); - let game_project_resources_files_watcher = FileWatcher::new(project_paths.game_resources_directory_path.clone()).set_recursive(true); + let game_dynamic_library_files_watcher = + FileWatcher::new(project_paths.build_data_directory_path.clone()); + let game_project_source_files_watcher = + FileWatcher::new(project_paths.game_source_directory_path.clone()).set_recursive(true); + let game_project_resources_files_watcher = + FileWatcher::new(project_paths.game_resources_directory_path.clone()).set_recursive(true); FileWatchers { game_dynamic_library_files_watcher, @@ -269,7 +332,6 @@ fn main_loop( config: Config, development_mode: bool, ) -> Result<()> { - // Create a file watcher to monitor game project file changes as well as game output file changes let mut file_watchers: Option = if development_mode { Some(create_file_watchers(&project_paths)) @@ -281,104 +343,99 @@ fn main_loop( let mut last_reload_time = Instant::now(); // Main program loop - let _ = window_data.event_loop.run(move |event, event_loop_window_target| { // Run function takes closure - match event { - Event::AboutToWait => { - window_data.window.request_redraw(); - } + let _ = window_data + .event_loop + .run(move |event, event_loop_window_target| { + // Run function takes closure + match event { + Event::AboutToWait => { + window_data.window.request_redraw(); + } - // Handle device events - Event::DeviceEvent { - ref event, - .. - } => { - match event { - DeviceEvent::MouseMotion { - delta, - } => { + // Handle device events + Event::DeviceEvent { ref event, .. } => match event { + DeviceEvent::MouseMotion { delta } => { if let Some(ref mut engine) = engine { engine.pass_mouse_delta_input(delta); } - }, + } _ => {} - } - } - - // Handle window events - Event::WindowEvent { - ref event, - window_id, - } - if window_id == window_data.window.id() => { - if let Some(ref mut engine) = engine { - engine.pass_input_to_egui(event); - } - - match event { - WindowEvent::RedrawRequested => { - let now = std::time::Instant::now(); - let delta_time = now - last_render_time; - last_render_time = now; + }, + + // Handle window events + Event::WindowEvent { + ref event, + window_id, + } if window_id == window_data.window.id() => { + if let Some(ref mut engine) = engine { + engine.pass_input_to_ui(event); + } - if let Some(ref mut e) = engine { - e.update(delta_time); + match event { + WindowEvent::RedrawRequested => { + let now = std::time::Instant::now(); + let delta_time = now - last_render_time; + last_render_time = now; + + if let Some(ref mut e) = engine { + e.update(delta_time); + } + + if development_mode { + check_and_reload_game( + engine, + game_dynamic_library, + &project_paths, + &mut last_reload_time, + &window_data.size, + &window_data.window, + &config, + file_watchers.as_mut().unwrap(), + ) + .unwrap(); + } } - - if development_mode { - check_and_reload_game( - engine, - game_dynamic_library, - &project_paths, - &mut last_reload_time, - &window_data.size, - &window_data.window, - &config, - file_watchers.as_mut().unwrap(), - ).unwrap(); + WindowEvent::KeyboardInput { event, .. } => { + if let Some(ref mut e) = engine { + e.pass_keyboard_key_input(&event); + } } - } - WindowEvent::KeyboardInput { event, .. } => { - if let Some(ref mut e) = engine { - e.pass_keyboard_key_input(&event); + WindowEvent::MouseInput { button, state, .. } => { + if let Some(ref mut e) = engine { + e.pass_mouse_key_input(&button, &state); + } } - } - WindowEvent::MouseInput { button, state, .. } => { - if let Some(ref mut e) = engine { - e.pass_mouse_key_input(&button, &state); + WindowEvent::MouseWheel { delta, .. } => { + if let Some(ref mut e) = engine { + e.pass_mouse_wheel_input(&delta); + } } - } - WindowEvent::MouseWheel { delta, .. } => { - if let Some(ref mut e) = engine { - e.pass_mouse_wheel_input(&delta); + WindowEvent::CursorMoved { position, .. } => { + if let Some(ref mut e) = engine { + e.pass_mouse_position_input(&position); + } } - } - WindowEvent::CursorMoved { position, .. } => { - if let Some(ref mut e) = engine { - e.pass_mouse_position_input(&position); + WindowEvent::CloseRequested => { + if let Some(mut e) = engine.take() { + e.shutdown(); + } + event_loop_window_target.exit(); } - } - WindowEvent::CloseRequested => { - if let Some(mut e) = engine.take() { - e.shutdown(); + WindowEvent::Resized(physical_size) => { + if let Some(ref mut e) = engine { + e.resize(*physical_size); + } } - event_loop_window_target.exit(); + _ => {} } - WindowEvent::Resized(physical_size) => { - if let Some(ref mut e) = engine { - e.resize(*physical_size); - } - } - _ => {} } + _ => {} } - _ => {} - } - }); + }); Ok(()) } - fn main() { // In the development build, standalone will look for the resource files in the "res" directory of the game project directory // In the release build, "res" directory is copied to /build/release/data/res (TODO: pack all resources use by game into a single data file) @@ -406,18 +463,38 @@ fn main() { let development_mode = true; - let current_directory_path = std::env::current_exe().unwrap().parent().unwrap().to_path_buf(); // Path where the executable is located - let game_project_directory_path = current_directory_path.parent().unwrap().parent().unwrap().to_path_buf(); + let current_directory_path = std::env::current_exe() + .unwrap() + .parent() + .unwrap() + .to_path_buf(); // Path where the executable is located + let game_project_directory_path = current_directory_path + .parent() + .unwrap() + .parent() + .unwrap() + .to_path_buf(); let build_data_directory_path = current_directory_path.join("data"); let game_resources_directory_path = if development_mode { - current_directory_path.parent().unwrap().parent().unwrap().join("res") // /res + current_directory_path + .parent() + .unwrap() + .parent() + .unwrap() + .join("res") // /res } else { build_data_directory_path.join("res") // /data/res }; - let game_source_directory_path = current_directory_path.parent().unwrap().parent().unwrap().join("src"); // /src + let game_source_directory_path = current_directory_path + .parent() + .unwrap() + .parent() + .unwrap() + .join("src"); // /src let config_path = game_resources_directory_path.join("config.ini"); let game_dynamic_library_path = build_data_directory_path.join(dylib("pill_game")); - let game_dynamic_library_hot_reloaded_path = build_data_directory_path.join(dylib("pill_game_hot_reloaded")); + let game_dynamic_library_hot_reloaded_path = + build_data_directory_path.join(dylib("pill_game_hot_reloaded")); let project_paths = ProjectPaths { build_data_directory_path: build_data_directory_path, @@ -431,7 +508,11 @@ fn main() { // Load config let mut config = Config::default(); - config.merge(config::File::with_name(project_paths.config_path.to_str().unwrap())).unwrap(); + config + .merge(config::File::with_name( + project_paths.config_path.to_str().unwrap(), + )) + .unwrap(); // Set log level set_log_level(&config); @@ -447,13 +528,35 @@ fn main() { game_dynamic_library = Some(game_library); // Initialize renderer and engine - let renderer: Box = Box::new(::new(Arc::clone(&window_data.window), config.clone())); - let mut engine: Option = Some(Engine::new(game, project_paths.game_resources_directory_path.clone(), renderer, config.clone())); - engine.as_mut().unwrap().initialize(window_data.size).context("Failed to initialize engine").unwrap(); + let mut resource_manager = pill_engine::internal::ResourceManager::new(); + let renderer: Box = + Box::new(::new( + &mut resource_manager, + Arc::clone(&window_data.window), + config.clone(), + )); + let mut engine: Option = Some(Engine::new( + game, + project_paths.game_resources_directory_path.clone(), + renderer, + resource_manager, + config.clone(), + Arc::clone(&window_data.window), + )); + engine + .as_mut() + .unwrap() + .initialize(window_data.size) + .context("Failed to initialize engine") + .unwrap(); // Run loop - window_data.event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll); - window_data.event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait); + window_data + .event_loop + .set_control_flow(winit::event_loop::ControlFlow::Poll); + window_data + .event_loop + .set_control_flow(winit::event_loop::ControlFlow::Wait); // Show window (now the taskbar icon will be set correctly) window_data.window.set_visible(true); @@ -466,5 +569,7 @@ fn main() { window_data, config, development_mode, - ).context("Main loop failed").unwrap(); + ) + .context("Main loop failed") + .unwrap(); } diff --git a/examples/pill_demo/game.log b/examples/pill_demo/game.log new file mode 100644 index 00000000..42f7f6e8 --- /dev/null +++ b/examples/pill_demo/game.log @@ -0,0 +1,10 @@ +Building game project from /Users/mk/dev/demo/Pill-Demo... + Finished `dev` profile [optimized + debuginfo] target(s) in 0.57s +Game built successfully! +Running game project from /Users/mk/dev/demo/Pill-Demo/build/dev... +[INFO] 2025-10-17T01:19:27 pill_standalone/src/main.rs:439: Initializing Standalone +[INFO] 2025-10-17T01:19:27 pill_renderer/src/renderer.rs:701: Initializing Renderer +[INFO] 2025-10-17T01:19:27 pill_renderer/src/renderer.rs:732: Using GPU: Apple M1 Pro (Metal) +[INFO] 2025-10-17T01:19:27 pill_engine/src/engine.rs:122: Initializing Engine +[INFO] 2025-10-17T01:19:27 pill_engine/src/engine.rs:94: Starting Game +[INFO] 2025-10-17T01:19:46 pill_renderer/src/renderer.rs:1493: Resizing Renderer resources diff --git a/examples/pill_demo/res/config.ini b/examples/pill_demo/res/config.ini new file mode 100644 index 00000000..45b98baf --- /dev/null +++ b/examples/pill_demo/res/config.ini @@ -0,0 +1,25 @@ +# GAME +TITLE=Pill-Demo + +# WINDOW +WINDOW_TITLE=Pill-Demo +WINDOW_WIDTH=1920 +WINDOW_HEIGHT=1200 +WINDOW_FULLSCREEN=false + +# ENGINE GENERAL +MAX_ENTITIES=100000 +MAX_CAMERAS=10 +MAX_CONCURRENT_2D_SOUNDS=10 +MAX_CONCURRENT_3D_SOUNDS=10 + +# ENGINE RESOURCES +MAX_TEXTURES=1000 +MAX_MATERIALS=20000 +MAX_MESHES=1000 +MAX_MODELS=100 +MAX_SOUNDS=50 + +# OTHER +LOG_LEVEL=Info +PANIC_ON_GAME_ERRORS=true diff --git a/examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2.ibl b/examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2.ibl new file mode 100644 index 00000000..5e48d5ba --- /dev/null +++ b/examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2.ibl @@ -0,0 +1,44 @@ +[Header] +ICOfile = "HDR_111_Parking_Space_2_Thumb.jpg" +Name = "HDR 111 Parking Lot 2" +Author = "HDRI Hub" +Location = "Cologne, Germany" +GEOlat = 52.6556789875030520 +GEOlong = 8.4282530844211578 +Comment = "parking space, place, concrete, trees, clear" +PREVIEWfile = "HDR_111_Parking_Lot_2_Prev.jpg" +Link = "http://www.hdri-hub.com" +Date = "2012:05:07" +Time = "16:31:59" + +[Background] +BGfile = "HDR_111_Parking_Lot_2_Bg.jpg" +BGmap = 1 +BGu = 0.000000 +BGv = 0.000000 +BGheight = 4096 + +[Enviroment] +EVfile = "HDR_111_Parking_Lot_2_Env.hdr" +EVmap = 1 +EVu = 0.000000 +EVv = 0.000000 +EVheight = 256 +EVmulti = 1.000000 +EVgamma = 2.200000 + +[Reflection] +REFfile = "HDR_111_Parking_Lot_2_Ref.hdr" +REFmap = 1 +REFu = 0.000000 +REFv = 0.000000 +REFheight = 2560 +REFmulti = 1.000000 +REFgamma = 2.200000 + +[Sun] +SUNcolor = 254,240,234 +SUNmulti = 1.586010 +SUNu = 0.492000 +SUNv = 0.352000 + diff --git a/examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2_Env.hdr b/examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2_Env.hdr new file mode 100644 index 00000000..58a75ab6 Binary files /dev/null and b/examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2_Env.hdr differ diff --git a/examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2_Prev.jpg b/examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2_Prev.jpg new file mode 100644 index 00000000..3c596cb0 Binary files /dev/null and b/examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2_Prev.jpg differ diff --git a/examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2_Thumb.jpg b/examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2_Thumb.jpg new file mode 100644 index 00000000..e9c46a76 Binary files /dev/null and b/examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2_Thumb.jpg differ diff --git a/examples/pill_demo/res/models/README.md b/examples/pill_demo/res/models/README.md new file mode 100644 index 00000000..b7ca1b9e --- /dev/null +++ b/examples/pill_demo/res/models/README.md @@ -0,0 +1,4 @@ +https://github.com/KhronosGroup/glTF-Sample-Assets/blob/main/Models/DamagedHelmet/README.md +https://github.com/KhronosGroup/glTF-Sample-Assets/blob/main/Models/SciFiHelmet/README.md +https://github.com/KhronosGroup/glTF-Sample-Assets/blob/main/Models/MetalRoughSpheres/README.md +https://github.com/KhronosGroup/glTF-Sample-Assets/blob/main/Models/Sponza/README.md diff --git a/examples/pill_demo/res/models/SciFiHelmet/keep.txt b/examples/pill_demo/res/models/SciFiHelmet/keep.txt new file mode 100644 index 00000000..e69de29b diff --git a/examples/pill_demo/res/models/pill.obj b/examples/pill_demo/res/models/pill.obj new file mode 100644 index 00000000..e66d03e9 --- /dev/null +++ b/examples/pill_demo/res/models/pill.obj @@ -0,0 +1,2170 @@ +# Blender v3.0.0 OBJ File: 'PillEngine.blend' +# www.blender.org +mtllib Pill.mtl +o Pill_Sphere.007 +v 0.000000 2.396242 -0.195090 +v 0.000000 2.339336 -0.382683 +v 0.000000 2.246927 -0.555570 +v 0.000000 2.122564 -0.707107 +v 0.000000 1.971027 -0.831470 +v 0.000000 1.798140 -0.923880 +v 0.000000 1.610547 -0.980785 +v 0.000000 -1.970790 -0.831470 +v 0.000000 -2.122327 -0.707107 +v 0.000000 -2.246690 -0.555570 +v 0.038060 2.396242 -0.191342 +v 0.074658 2.339336 -0.375330 +v 0.108386 2.246927 -0.544895 +v 0.137950 2.122564 -0.693520 +v 0.162212 1.971027 -0.815493 +v 0.180240 1.798140 -0.906127 +v 0.191342 1.610547 -0.961940 +v 0.191342 -1.610310 -0.961940 +v 0.180240 -1.797903 -0.906127 +v 0.162212 -1.970790 -0.815493 +v 0.137950 -2.122327 -0.693520 +v 0.108386 -2.246690 -0.544895 +v 0.074658 -2.339100 -0.375330 +v 0.038060 -2.396006 -0.191342 +v 0.074658 2.396242 -0.180240 +v 0.146447 2.339336 -0.353553 +v 0.212608 2.246927 -0.513280 +v 0.270598 2.122564 -0.653281 +v 0.318190 1.971027 -0.768178 +v 0.353553 1.798140 -0.853553 +v 0.375330 1.610547 -0.906127 +v 0.375330 -1.610310 -0.906127 +v 0.353553 -1.797903 -0.853553 +v 0.318190 -1.970790 -0.768178 +v 0.270598 -2.122327 -0.653281 +v 0.212608 -2.246690 -0.513280 +v 0.146447 -2.339100 -0.353553 +v 0.074658 -2.396006 -0.180240 +v 0.108386 2.396242 -0.162212 +v 0.212608 2.339336 -0.318190 +v 0.308658 2.246927 -0.461940 +v 0.392848 2.122564 -0.587938 +v 0.461940 1.971027 -0.691342 +v 0.513280 1.798140 -0.768178 +v 0.544895 1.610547 -0.815493 +v 0.544895 -1.610310 -0.815493 +v 0.513280 -1.797903 -0.768178 +v 0.461940 -1.970790 -0.691342 +v 0.392848 -2.122327 -0.587938 +v 0.308658 -2.246690 -0.461940 +v 0.212608 -2.339100 -0.318190 +v 0.108386 -2.396006 -0.162212 +v 0.137950 2.396242 -0.137950 +v 0.270598 2.339336 -0.270598 +v 0.392848 2.246927 -0.392847 +v 0.500000 2.122564 -0.500000 +v 0.587938 1.971027 -0.587938 +v 0.653281 1.798140 -0.653281 +v 0.693520 1.610547 -0.693520 +v 0.693520 -1.610310 -0.693520 +v 0.653282 -1.797903 -0.653281 +v 0.587938 -1.970790 -0.587938 +v 0.500000 -2.122327 -0.500000 +v 0.392848 -2.246690 -0.392847 +v 0.270598 -2.339100 -0.270598 +v 0.137950 -2.396006 -0.137950 +v 0.162212 2.396242 -0.108386 +v 0.318190 2.339336 -0.212607 +v 0.461940 2.246927 -0.308658 +v 0.587938 2.122564 -0.392847 +v 0.691342 1.971027 -0.461940 +v 0.768178 1.798140 -0.513280 +v 0.815493 1.610547 -0.544895 +v 0.815493 -1.610310 -0.544895 +v 0.768178 -1.797903 -0.513280 +v 0.691342 -1.970790 -0.461940 +v 0.587938 -2.122327 -0.392847 +v 0.461940 -2.246690 -0.308658 +v 0.318190 -2.339100 -0.212608 +v 0.162212 -2.396006 -0.108386 +v 0.180240 2.396242 -0.074658 +v 0.353554 2.339336 -0.146447 +v 0.513280 2.246927 -0.212607 +v 0.653281 2.122564 -0.270598 +v 0.768178 1.971027 -0.318190 +v 0.853553 1.798140 -0.353553 +v 0.906127 1.610547 -0.375330 +v 0.906127 -1.610310 -0.375330 +v 0.853553 -1.797903 -0.353553 +v 0.768178 -1.970790 -0.318190 +v 0.653281 -2.122327 -0.270598 +v 0.513280 -2.246690 -0.212607 +v 0.353554 -2.339100 -0.146447 +v 0.180240 -2.396006 -0.074658 +v 0.191342 2.396242 -0.038060 +v 0.375330 2.339336 -0.074658 +v 0.544895 2.246927 -0.108386 +v 0.693520 2.122564 -0.137950 +v 0.815493 1.971027 -0.162212 +v 0.906127 1.798140 -0.180240 +v 0.961940 1.610547 -0.191342 +v 0.961940 -1.610310 -0.191342 +v 0.906128 -1.797903 -0.180240 +v 0.815493 -1.970790 -0.162212 +v 0.693520 -2.122327 -0.137950 +v 0.544895 -2.246690 -0.108386 +v 0.375330 -2.339100 -0.074658 +v 0.191342 -2.396006 -0.038060 +v 0.195090 2.396242 0.000000 +v 0.382684 2.339336 0.000000 +v 0.555570 2.246927 0.000000 +v 0.707107 2.122564 0.000000 +v 0.831469 1.971027 0.000000 +v 0.923879 1.798140 0.000000 +v 0.980785 1.610547 0.000000 +v 0.980785 -1.610310 0.000000 +v 0.923880 -1.797903 0.000000 +v 0.831469 -1.970790 0.000000 +v 0.707107 -2.122327 0.000000 +v 0.555570 -2.246690 0.000000 +v 0.382684 -2.339100 0.000000 +v 0.195090 -2.396006 0.000000 +v 0.191342 2.396242 0.038060 +v 0.375330 2.339336 0.074658 +v 0.544895 2.246927 0.108386 +v 0.693520 2.122564 0.137950 +v 0.815493 1.971027 0.162212 +v 0.906127 1.798140 0.180240 +v 0.961940 1.610547 0.191342 +v 0.961940 -1.610310 0.191342 +v 0.906127 -1.797903 0.180240 +v 0.815493 -1.970790 0.162212 +v 0.693520 -2.122327 0.137950 +v 0.544895 -2.246690 0.108386 +v 0.375330 -2.339100 0.074658 +v 0.191342 -2.396006 0.038060 +v 0.180240 2.396242 0.074658 +v 0.353553 2.339336 0.146447 +v 0.513280 2.246927 0.212608 +v 0.653281 2.122564 0.270598 +v 0.768178 1.971027 0.318190 +v 0.853553 1.798140 0.353553 +v 0.906127 1.610547 0.375330 +v 0.906127 -1.610310 0.375330 +v 0.853553 -1.797903 0.353554 +v 0.768178 -1.970790 0.318190 +v 0.653281 -2.122327 0.270598 +v 0.513280 -2.246690 0.212608 +v 0.353554 -2.339100 0.146447 +v 0.180240 -2.396006 0.074658 +v 0.162212 2.396242 0.108386 +v 0.318190 2.339336 0.212608 +v 0.461940 2.246927 0.308658 +v 0.587938 2.122564 0.392847 +v 0.691341 1.971027 0.461940 +v 0.768178 1.798140 0.513280 +v 0.815493 1.610547 0.544895 +v 0.815493 -1.610310 0.544895 +v 0.768178 -1.797903 0.513280 +v 0.691342 -1.970790 0.461940 +v 0.587938 -2.122327 0.392847 +v 0.461940 -2.246690 0.308658 +v 0.318190 -2.339100 0.212608 +v 0.162212 -2.396006 0.108386 +v 0.137950 2.396242 0.137950 +v 0.270598 2.339336 0.270598 +v 0.392847 2.246927 0.392847 +v 0.500000 2.122564 0.500000 +v 0.587938 1.971027 0.587938 +v 0.653281 1.798140 0.653281 +v 0.693520 1.610547 0.693520 +v 0.693520 -1.610310 0.693520 +v 0.653281 -1.797903 0.653282 +v 0.587938 -1.970790 0.587938 +v 0.500000 -2.122327 0.500000 +v 0.392847 -2.246690 0.392847 +v 0.270598 -2.339100 0.270598 +v 0.137950 -2.396006 0.137950 +v 0.108386 2.396242 0.162212 +v 0.212608 2.339336 0.318190 +v 0.308658 2.246927 0.461940 +v 0.392847 2.122564 0.587938 +v 0.461940 1.971027 0.691342 +v 0.513280 1.798140 0.768178 +v 0.544895 1.610547 0.815493 +v 0.544895 -1.610310 0.815493 +v 0.513280 -1.797903 0.768178 +v 0.461940 -1.970790 0.691342 +v 0.392847 -2.122327 0.587938 +v 0.308658 -2.246690 0.461940 +v 0.212608 -2.339100 0.318190 +v 0.108386 -2.396006 0.162212 +v 0.074658 2.396242 0.180240 +v 0.146447 2.339336 0.353554 +v 0.212607 2.246927 0.513280 +v 0.270598 2.122564 0.653281 +v 0.318190 1.971027 0.768178 +v 0.353553 1.798140 0.853553 +v 0.375330 1.610547 0.906127 +v 0.375330 -1.610310 0.906127 +v 0.353553 -1.797903 0.853553 +v 0.318190 -1.970790 0.768178 +v 0.270598 -2.122327 0.653281 +v 0.212607 -2.246690 0.513280 +v 0.146447 -2.339100 0.353554 +v 0.074658 -2.396006 0.180240 +v 0.038060 2.396242 0.191342 +v 0.074658 2.339336 0.375330 +v 0.108386 2.246927 0.544895 +v 0.137950 2.122564 0.693520 +v 0.162212 1.971027 0.815493 +v 0.180240 1.798140 0.906127 +v 0.191342 1.610547 0.961939 +v 0.191342 -1.610310 0.961939 +v 0.180240 -1.797903 0.906127 +v 0.162212 -1.970790 0.815493 +v 0.137950 -2.122327 0.693520 +v 0.108386 -2.246690 0.544895 +v 0.074658 -2.339100 0.375330 +v 0.038060 -2.396006 0.191342 +v 0.000000 2.396242 0.195090 +v -0.000000 2.339336 0.382684 +v -0.000000 2.246927 0.555570 +v 0.000000 2.122564 0.707107 +v -0.000000 1.971027 0.831469 +v 0.000000 1.798140 0.923879 +v -0.000000 1.610547 0.980785 +v -0.000000 -1.610310 0.980785 +v -0.000000 -1.797903 0.923880 +v -0.000000 -1.970790 0.831469 +v 0.000000 -2.122327 0.707107 +v -0.000000 -2.246690 0.555570 +v 0.000000 -2.339100 0.382684 +v -0.000000 -2.396006 0.195090 +v -0.038060 2.396242 0.191342 +v -0.074658 2.339336 0.375330 +v -0.108386 2.246927 0.544895 +v -0.137950 2.122564 0.693520 +v -0.162212 1.971027 0.815493 +v -0.180240 1.798140 0.906127 +v -0.191342 1.610547 0.961939 +v -0.191342 -1.610310 0.961939 +v -0.180240 -1.797903 0.906127 +v -0.162212 -1.970790 0.815493 +v -0.137950 -2.122327 0.693520 +v -0.108386 -2.246690 0.544895 +v -0.074658 -2.339100 0.375330 +v -0.038060 -2.396006 0.191342 +v -0.074658 2.396242 0.180240 +v -0.146447 2.339336 0.353553 +v -0.212608 2.246927 0.513280 +v -0.270598 2.122564 0.653281 +v -0.318189 1.971027 0.768177 +v -0.353553 1.798140 0.853553 +v -0.375330 1.610547 0.906127 +v -0.375330 -1.610310 0.906127 +v -0.353553 -1.797903 0.853553 +v -0.318190 -1.970790 0.768178 +v -0.270598 -2.122327 0.653281 +v -0.212608 -2.246690 0.513280 +v -0.146447 -2.339100 0.353553 +v -0.074658 -2.396006 0.180240 +v -0.108386 2.396242 0.162212 +v -0.212608 2.339336 0.318190 +v -0.308658 2.246927 0.461940 +v -0.392847 2.122564 0.587938 +v -0.461940 1.971027 0.691341 +v -0.513280 1.798140 0.768178 +v -0.544895 1.610547 0.815493 +v -0.544895 -1.610310 0.815493 +v -0.513280 -1.797903 0.768178 +v -0.461940 -1.970790 0.691342 +v -0.392847 -2.122327 0.587938 +v -0.308658 -2.246690 0.461940 +v -0.212608 -2.339100 0.318190 +v -0.108386 -2.396006 0.162212 +v -0.137950 2.396242 0.137950 +v -0.270598 2.339336 0.270598 +v -0.392847 2.246927 0.392847 +v -0.500000 2.122564 0.500000 +v -0.587937 1.971027 0.587938 +v -0.653281 1.798140 0.653281 +v -0.693520 1.610547 0.693520 +v -0.693520 -1.610310 0.693520 +v -0.653281 -1.797903 0.653281 +v -0.587938 -1.970790 0.587938 +v -0.500000 -2.122327 0.500000 +v -0.392847 -2.246690 0.392847 +v -0.270598 -2.339100 0.270598 +v -0.137950 -2.396006 0.137950 +v -0.162212 2.396242 0.108386 +v -0.318190 2.339336 0.212608 +v -0.461940 2.246927 0.308658 +v -0.587938 2.122564 0.392847 +v -0.691341 1.971027 0.461940 +v -0.768178 1.798140 0.513280 +v -0.815493 1.610547 0.544895 +v -0.815493 -1.610310 0.544895 +v -0.768178 -1.797903 0.513280 +v -0.691341 -1.970790 0.461940 +v -0.587938 -2.122327 0.392847 +v -0.461940 -2.246690 0.308658 +v -0.318190 -2.339100 0.212608 +v -0.162212 -2.396006 0.108386 +v -0.000000 2.415457 0.000000 +v -0.180240 2.396242 0.074658 +v -0.353553 2.339336 0.146447 +v -0.513280 2.246927 0.212607 +v -0.653281 2.122564 0.270598 +v -0.768177 1.971027 0.318190 +v -0.853553 1.798140 0.353553 +v -0.906127 1.610547 0.375330 +v -0.906127 -1.610310 0.375330 +v -0.853553 -1.797903 0.353553 +v -0.768177 -1.970790 0.318190 +v -0.653281 -2.122327 0.270598 +v -0.513280 -2.246690 0.212607 +v -0.353553 -2.339100 0.146447 +v -0.180240 -2.396006 0.074658 +v -0.191342 2.396242 0.038060 +v -0.375330 2.339336 0.074658 +v -0.544895 2.246927 0.108386 +v -0.693520 2.122564 0.137950 +v -0.815493 1.971027 0.162212 +v -0.906127 1.798140 0.180240 +v -0.961939 1.610547 0.191342 +v -0.961939 -1.610310 0.191342 +v -0.906127 -1.797903 0.180240 +v -0.815493 -1.970790 0.162212 +v -0.693520 -2.122327 0.137950 +v -0.544895 -2.246690 0.108386 +v -0.375330 -2.339100 0.074658 +v -0.191342 -2.396006 0.038060 +v -0.195090 2.396242 0.000000 +v -0.382683 2.339336 0.000000 +v -0.555570 2.246927 -0.000000 +v -0.707107 2.122564 0.000000 +v -0.831469 1.971027 0.000000 +v -0.923879 1.798140 0.000000 +v -0.980785 1.610547 -0.000000 +v -0.980785 -1.610310 -0.000000 +v -0.923879 -1.797903 -0.000000 +v -0.831469 -1.970790 0.000000 +v -0.707107 -2.122327 0.000000 +v -0.555570 -2.246690 -0.000000 +v -0.382683 -2.339100 0.000000 +v -0.195090 -2.396006 0.000000 +v -0.191342 2.396242 -0.038060 +v -0.375330 2.339336 -0.074658 +v -0.544895 2.246927 -0.108386 +v -0.693520 2.122564 -0.137950 +v -0.815493 1.971027 -0.162212 +v -0.906127 1.798140 -0.180240 +v -0.961939 1.610547 -0.191342 +v -0.961939 -1.610310 -0.191342 +v -0.906127 -1.797903 -0.180240 +v -0.815493 -1.970790 -0.162212 +v -0.693520 -2.122327 -0.137950 +v -0.544895 -2.246690 -0.108386 +v -0.375330 -2.339100 -0.074658 +v -0.191342 -2.396006 -0.038060 +v -0.180240 2.396242 -0.074658 +v -0.353553 2.339336 -0.146447 +v -0.513280 2.246927 -0.212607 +v -0.653281 2.122564 -0.270598 +v -0.768177 1.971027 -0.318189 +v -0.853553 1.798140 -0.353553 +v -0.906127 1.610547 -0.375330 +v -0.906127 -1.610310 -0.375330 +v -0.853553 -1.797903 -0.353553 +v -0.768177 -1.970790 -0.318189 +v -0.653281 -2.122327 -0.270598 +v -0.513280 -2.246690 -0.212607 +v -0.353553 -2.339100 -0.146446 +v -0.180240 -2.396006 -0.074658 +v -0.162212 2.396242 -0.108386 +v -0.318189 2.339336 -0.212607 +v -0.461939 2.246927 -0.308658 +v -0.587938 2.122564 -0.392847 +v -0.691341 1.971027 -0.461939 +v -0.768177 1.798140 -0.513280 +v -0.815493 1.610547 -0.544895 +v -0.815493 -1.610310 -0.544895 +v -0.768177 -1.797903 -0.513280 +v -0.691341 -1.970790 -0.461939 +v -0.587938 -2.122327 -0.392847 +v -0.461939 -2.246690 -0.308658 +v -0.318189 -2.339100 -0.212607 +v -0.162212 -2.396006 -0.108386 +v -0.137950 2.396242 -0.137950 +v -0.270598 2.339336 -0.270598 +v -0.392847 2.246927 -0.392847 +v -0.500000 2.122564 -0.500000 +v -0.587937 1.971027 -0.587937 +v -0.653281 1.798140 -0.653281 +v -0.693519 1.610547 -0.693520 +v -0.693519 -1.610310 -0.693520 +v -0.653281 -1.797903 -0.653281 +v -0.587937 -1.970790 -0.587937 +v -0.500000 -2.122327 -0.500000 +v -0.392847 -2.246690 -0.392847 +v -0.270598 -2.339100 -0.270598 +v -0.137950 -2.396006 -0.137950 +v -0.108386 2.396242 -0.162212 +v -0.212607 2.339336 -0.318189 +v -0.308658 2.246927 -0.461939 +v -0.392847 2.122564 -0.587937 +v -0.461939 1.971027 -0.691341 +v -0.513280 1.798140 -0.768177 +v -0.544895 1.610547 -0.815493 +v -0.544895 -1.610310 -0.815493 +v -0.513280 -1.797903 -0.768178 +v -0.461939 -1.970790 -0.691341 +v -0.392847 -2.122327 -0.587937 +v -0.308658 -2.246690 -0.461939 +v -0.212607 -2.339100 -0.318189 +v -0.108386 -2.396006 -0.162212 +v -0.074658 2.396242 -0.180240 +v -0.146446 2.339336 -0.353553 +v -0.212607 2.246927 -0.513279 +v -0.270598 2.122564 -0.653281 +v -0.318189 1.971027 -0.768177 +v -0.353553 1.798140 -0.853553 +v -0.375330 1.610547 -0.906127 +v -0.375330 -1.610310 -0.906127 +v -0.353553 -1.797903 -0.853553 +v -0.318189 -1.970790 -0.768177 +v -0.270598 -2.122327 -0.653281 +v -0.212607 -2.246690 -0.513279 +v -0.146446 -2.339100 -0.353553 +v -0.074658 -2.396006 -0.180240 +v -0.038060 2.396242 -0.191342 +v -0.074658 2.339336 -0.375330 +v -0.108386 2.246927 -0.544895 +v -0.137950 2.122564 -0.693520 +v -0.162212 1.971027 -0.815493 +v -0.180240 1.798140 -0.906127 +v -0.191341 1.610547 -0.961939 +v -0.191341 -1.610310 -0.961939 +v -0.180240 -1.797903 -0.906127 +v -0.162212 -1.970790 -0.815493 +v -0.137950 -2.122327 -0.693520 +v -0.108386 -2.246690 -0.544895 +v -0.074658 -2.339100 -0.375330 +v -0.038060 -2.396006 -0.191342 +v 0.000000 -1.610310 -0.980785 +v 0.000000 -1.797903 -0.923879 +v 0.000000 -2.339100 -0.382683 +v 0.000000 -2.396006 -0.195090 +v 0.000000 -2.415220 0.000000 +v 0.194760 1.432671 -0.979122 +v 0.382035 1.432671 -0.922313 +v 0.554628 1.432671 -0.830060 +v 0.705908 1.432671 -0.705908 +v 0.830060 1.432671 -0.554628 +v 0.922313 1.432671 -0.382034 +v 0.979122 1.432671 -0.194759 +v 0.998304 1.432671 0.000000 +v 0.979122 1.432671 0.194760 +v 0.922313 1.432671 0.382035 +v 0.830059 1.432671 0.554628 +v 0.705907 1.432671 0.705908 +v 0.554628 1.432671 0.830060 +v 0.382034 1.432671 0.922313 +v 0.194759 1.432671 0.979122 +v -0.000000 1.432671 0.998304 +v -0.194760 1.432671 0.979122 +v -0.382035 1.432671 0.922312 +v -0.554628 1.432671 0.830059 +v -0.705908 1.432671 0.705907 +v -0.830059 1.432671 0.554628 +v -0.922312 1.432671 0.382034 +v -0.979122 1.432671 0.194759 +v -0.998304 1.432671 -0.000000 +v -0.979121 1.432671 -0.194760 +v -0.922312 1.432671 -0.382035 +v -0.830059 1.432671 -0.554628 +v -0.705907 1.432671 -0.705908 +v -0.554627 1.432671 -0.830059 +v -0.382034 1.432671 -0.922312 +v -0.194759 1.432671 -0.979121 +v 0.000000 1.432671 -0.998303 +v 0.194723 -1.434348 -0.978937 +v 0.381963 -1.434348 -0.922139 +v 0.554524 -1.434348 -0.829903 +v 0.705775 -1.434348 -0.705774 +v 0.829903 -1.434348 -0.554523 +v 0.922139 -1.434348 -0.381962 +v 0.978937 -1.434348 -0.194723 +v 0.998116 -1.434348 0.000000 +v 0.978937 -1.434348 0.194723 +v 0.922139 -1.434348 0.381962 +v 0.829903 -1.434348 0.554524 +v 0.705774 -1.434348 0.705774 +v 0.554523 -1.434348 0.829903 +v 0.381962 -1.434348 0.922139 +v 0.194722 -1.434348 0.978937 +v -0.000000 -1.434348 0.998115 +v -0.194723 -1.434348 0.978937 +v -0.381962 -1.434348 0.922138 +v -0.554523 -1.434348 0.829902 +v -0.705774 -1.434348 0.705774 +v -0.829903 -1.434348 0.554523 +v -0.922138 -1.434348 0.381962 +v -0.978937 -1.434348 0.194722 +v -0.998115 -1.434348 -0.000000 +v -0.978936 -1.434348 -0.194723 +v -0.922138 -1.434348 -0.381962 +v -0.829902 -1.434348 -0.554523 +v -0.705774 -1.434348 -0.705774 +v -0.554523 -1.434348 -0.829902 +v -0.381962 -1.434348 -0.922138 +v -0.194722 -1.434348 -0.978936 +v 0.000001 -1.434348 -0.998115 +vt 0.734354 0.136501 +vt 0.734354 0.167718 +vt 0.705722 0.167718 +vt 0.705722 0.136501 +vt 0.734354 0.829859 +vt 0.734354 0.861076 +vt 0.705722 0.861076 +vt 0.705722 0.829859 +vt 0.734354 0.105284 +vt 0.705722 0.105284 +vt 0.734354 0.798642 +vt 0.705722 0.798642 +vt 0.734354 0.074067 +vt 0.705722 0.074067 +vt 0.734354 0.767425 +vt 0.705722 0.767425 +vt 0.734354 0.042850 +vt 0.705722 0.042850 +vt 0.734354 0.738963 +vt 0.705722 0.738963 +vt 0.734354 0.954727 +vt 0.720038 0.985944 +vt 0.705722 0.954727 +vt 0.720038 0.011633 +vt 0.734354 0.923510 +vt 0.705722 0.923510 +vt 0.734354 0.198935 +vt 0.734354 0.230152 +vt 0.705722 0.230152 +vt 0.705722 0.198935 +vt 0.734354 0.892293 +vt 0.705722 0.892293 +vt 0.677090 0.198935 +vt 0.677090 0.167718 +vt 0.677090 0.892293 +vt 0.677090 0.861076 +vt 0.677090 0.136501 +vt 0.677090 0.829859 +vt 0.677090 0.105284 +vt 0.677090 0.798642 +vt 0.677090 0.074067 +vt 0.677090 0.767425 +vt 0.677090 0.042850 +vt 0.677090 0.738963 +vt 0.691406 0.985944 +vt 0.677090 0.954727 +vt 0.691406 0.011633 +vt 0.677090 0.923510 +vt 0.677090 0.230152 +vt 0.648457 0.074067 +vt 0.648457 0.042850 +vt 0.648457 0.767425 +vt 0.648458 0.738963 +vt 0.662774 0.985944 +vt 0.648457 0.954727 +vt 0.662774 0.011633 +vt 0.648457 0.923510 +vt 0.648457 0.230152 +vt 0.648457 0.198935 +vt 0.648457 0.892293 +vt 0.648457 0.167718 +vt 0.648457 0.861076 +vt 0.648457 0.136501 +vt 0.648457 0.829859 +vt 0.648457 0.105284 +vt 0.648457 0.798642 +vt 0.619825 0.892293 +vt 0.619826 0.861076 +vt 0.619826 0.167718 +vt 0.619826 0.136501 +vt 0.619826 0.829859 +vt 0.619826 0.105284 +vt 0.619826 0.798642 +vt 0.619826 0.074067 +vt 0.619826 0.767425 +vt 0.619825 0.042850 +vt 0.619826 0.738963 +vt 0.634142 0.985944 +vt 0.619825 0.954727 +vt 0.634142 0.011633 +vt 0.619825 0.923510 +vt 0.619826 0.230152 +vt 0.619826 0.198935 +vt 0.591194 0.767425 +vt 0.591194 0.738963 +vt 0.605509 0.985944 +vt 0.591193 0.954727 +vt 0.605509 0.011633 +vt 0.591193 0.042850 +vt 0.591193 0.923510 +vt 0.591194 0.230152 +vt 0.591194 0.198935 +vt 0.591194 0.892293 +vt 0.591194 0.167718 +vt 0.591194 0.861076 +vt 0.591194 0.136501 +vt 0.591194 0.829859 +vt 0.591194 0.105284 +vt 0.591194 0.798642 +vt 0.591194 0.074067 +vt 0.562562 0.167718 +vt 0.562562 0.136501 +vt 0.562562 0.861076 +vt 0.562562 0.829859 +vt 0.562562 0.105284 +vt 0.562562 0.798642 +vt 0.562562 0.074067 +vt 0.562562 0.767425 +vt 0.562562 0.042850 +vt 0.562562 0.738963 +vt 0.576877 0.985944 +vt 0.562562 0.954727 +vt 0.576877 0.011633 +vt 0.562562 0.923510 +vt 0.562562 0.230152 +vt 0.562562 0.198935 +vt 0.562562 0.892293 +vt 0.548245 0.011633 +vt 0.533929 0.042850 +vt 0.533929 0.258309 +vt 0.533929 0.738963 +vt 0.505298 0.738963 +vt 0.505297 0.258309 +vt 0.533929 0.954727 +vt 0.533929 0.923510 +vt 0.533929 0.230152 +vt 0.533929 0.198935 +vt 0.533929 0.892293 +vt 0.533929 0.167718 +vt 0.533929 0.861076 +vt 0.533929 0.136501 +vt 0.533929 0.829859 +vt 0.533929 0.105284 +vt 0.533929 0.798642 +vt 0.533929 0.074067 +vt 0.533929 0.767425 +vt 0.548245 0.985944 +vt 0.505298 0.136501 +vt 0.505297 0.105284 +vt 0.505297 0.829859 +vt 0.505298 0.798642 +vt 0.505297 0.074067 +vt 0.505298 0.767425 +vt 0.505297 0.042850 +vt 0.519613 0.985944 +vt 0.505297 0.954727 +vt 0.519613 0.011633 +vt 0.505297 0.923510 +vt 0.505298 0.230152 +vt 0.505297 0.198935 +vt 0.505297 0.892293 +vt 0.505297 0.167718 +vt 0.505298 0.861076 +vt 0.476665 0.954727 +vt 0.476665 0.923510 +vt 0.476665 0.230152 +vt 0.476665 0.198935 +vt 0.476665 0.892293 +vt 0.476665 0.167718 +vt 0.476665 0.861076 +vt 0.476665 0.136501 +vt 0.476665 0.829859 +vt 0.476665 0.105284 +vt 0.476665 0.798642 +vt 0.476665 0.074067 +vt 0.476665 0.767425 +vt 0.476665 0.042850 +vt 0.476665 0.738963 +vt 0.490981 0.985944 +vt 0.490981 0.011633 +vt 0.448033 0.829859 +vt 0.448033 0.798642 +vt 0.448033 0.105284 +vt 0.448033 0.074067 +vt 0.448033 0.767425 +vt 0.448033 0.042850 +vt 0.448033 0.738963 +vt 0.462349 0.985944 +vt 0.448033 0.954727 +vt 0.462349 0.011633 +vt 0.448033 0.923510 +vt 0.448033 0.230152 +vt 0.448033 0.198935 +vt 0.448033 0.892293 +vt 0.448033 0.167718 +vt 0.448033 0.861076 +vt 0.448033 0.136501 +vt 0.419401 0.230152 +vt 0.419401 0.198935 +vt 0.419401 0.923510 +vt 0.419401 0.892293 +vt 0.419401 0.167718 +vt 0.419401 0.861076 +vt 0.419401 0.136501 +vt 0.419401 0.829859 +vt 0.419401 0.105284 +vt 0.419401 0.798642 +vt 0.419401 0.074067 +vt 0.419401 0.767425 +vt 0.419401 0.042850 +vt 0.419401 0.738963 +vt 0.433717 0.985944 +vt 0.419401 0.954727 +vt 0.433717 0.011633 +vt 0.390769 0.105284 +vt 0.390769 0.074067 +vt 0.390769 0.798642 +vt 0.390769 0.767425 +vt 0.390769 0.042850 +vt 0.390769 0.738963 +vt 0.405085 0.985944 +vt 0.390769 0.954727 +vt 0.405085 0.011633 +vt 0.390769 0.923510 +vt 0.390769 0.230152 +vt 0.390769 0.198935 +vt 0.390769 0.892293 +vt 0.390769 0.167718 +vt 0.390769 0.861076 +vt 0.390769 0.136501 +vt 0.390769 0.829859 +vt 0.362137 0.923510 +vt 0.362137 0.892293 +vt 0.362137 0.198935 +vt 0.362137 0.167718 +vt 0.362137 0.861076 +vt 0.362137 0.136501 +vt 0.362137 0.829859 +vt 0.362137 0.105284 +vt 0.362137 0.798642 +vt 0.362137 0.074067 +vt 0.362137 0.767425 +vt 0.362137 0.042850 +vt 0.362137 0.738963 +vt 0.376453 0.985944 +vt 0.362137 0.954727 +vt 0.376453 0.011633 +vt 0.362137 0.230152 +vt 0.333505 0.798642 +vt 0.333505 0.767425 +vt 0.333505 0.074067 +vt 0.333505 0.042850 +vt 0.333505 0.738963 +vt 0.347821 0.985944 +vt 0.333505 0.954727 +vt 0.347821 0.011633 +vt 0.333505 0.923510 +vt 0.333505 0.230152 +vt 0.333505 0.198935 +vt 0.333505 0.892293 +vt 0.333505 0.167718 +vt 0.333505 0.861076 +vt 0.333505 0.136501 +vt 0.333505 0.829859 +vt 0.333505 0.105284 +vt 0.304873 0.198935 +vt 0.304873 0.167718 +vt 0.304873 0.892293 +vt 0.304873 0.861076 +vt 0.304873 0.136501 +vt 0.304873 0.829859 +vt 0.304873 0.105284 +vt 0.304873 0.798642 +vt 0.304873 0.074067 +vt 0.304873 0.767425 +vt 0.304873 0.042850 +vt 0.304873 0.738963 +vt 0.319189 0.985944 +vt 0.304873 0.954727 +vt 0.319189 0.011633 +vt 0.304873 0.923510 +vt 0.304873 0.230152 +vt 0.276241 0.074067 +vt 0.276241 0.042850 +vt 0.276241 0.767425 +vt 0.276241 0.738963 +vt 0.290557 0.985944 +vt 0.276241 0.954727 +vt 0.290557 0.011633 +vt 0.276241 0.923510 +vt 0.276241 0.230152 +vt 0.276241 0.198935 +vt 0.276241 0.892293 +vt 0.276241 0.167718 +vt 0.276241 0.861076 +vt 0.276241 0.136501 +vt 0.276241 0.829859 +vt 0.276241 0.105284 +vt 0.276241 0.798642 +vt 0.247609 0.167718 +vt 0.247609 0.136501 +vt 0.247609 0.861076 +vt 0.247609 0.829859 +vt 0.247609 0.105284 +vt 0.247609 0.798642 +vt 0.247609 0.074067 +vt 0.247609 0.767425 +vt 0.247609 0.042850 +vt 0.247609 0.738963 +vt 0.261925 0.985944 +vt 0.247609 0.954727 +vt 0.261925 0.011633 +vt 0.247609 0.923510 +vt 0.247609 0.230152 +vt 0.247609 0.198935 +vt 0.247609 0.892293 +vt 0.233293 0.985944 +vt 0.218977 0.954727 +vt 0.233293 0.011633 +vt 0.218977 0.042850 +vt 0.218977 0.258309 +vt 0.218977 0.738963 +vt 0.190345 0.738963 +vt 0.190345 0.258309 +vt 0.218977 0.923510 +vt 0.218977 0.230152 +vt 0.218977 0.198935 +vt 0.218977 0.892293 +vt 0.218977 0.167718 +vt 0.218977 0.861076 +vt 0.218977 0.136501 +vt 0.218977 0.829859 +vt 0.218977 0.105284 +vt 0.218977 0.798642 +vt 0.218977 0.074067 +vt 0.218977 0.767425 +vt 0.190345 0.861076 +vt 0.190345 0.829859 +vt 0.190345 0.136501 +vt 0.190345 0.105284 +vt 0.190345 0.798642 +vt 0.190345 0.074067 +vt 0.190345 0.767425 +vt 0.190345 0.042850 +vt 0.204661 0.985944 +vt 0.190345 0.954727 +vt 0.204661 0.011633 +vt 0.190345 0.923510 +vt 0.190345 0.230152 +vt 0.190345 0.198935 +vt 0.190345 0.892293 +vt 0.190345 0.167718 +vt 0.161713 0.258309 +vt 0.161713 0.738963 +vt 0.133081 0.738963 +vt 0.133081 0.258309 +vt 0.161713 0.954727 +vt 0.161713 0.923510 +vt 0.161713 0.230152 +vt 0.161713 0.198935 +vt 0.161713 0.892293 +vt 0.161713 0.167718 +vt 0.161713 0.861076 +vt 0.161713 0.136501 +vt 0.161713 0.829859 +vt 0.161713 0.105284 +vt 0.161713 0.798642 +vt 0.161713 0.074067 +vt 0.161713 0.767425 +vt 0.161713 0.042850 +vt 0.176029 0.985944 +vt 0.176029 0.011633 +vt 0.133081 0.136501 +vt 0.133081 0.105284 +vt 0.133081 0.829859 +vt 0.133081 0.798642 +vt 0.133081 0.074067 +vt 0.133081 0.767425 +vt 0.133081 0.042850 +vt 0.147397 0.985944 +vt 0.133081 0.954727 +vt 0.147397 0.011633 +vt 0.133081 0.923510 +vt 0.133081 0.230152 +vt 0.133081 0.198935 +vt 0.133081 0.892293 +vt 0.133081 0.167718 +vt 0.133081 0.861076 +vt 0.104449 0.954727 +vt 0.104449 0.923510 +vt 0.104449 0.230152 +vt 0.104449 0.198935 +vt 0.104449 0.892293 +vt 0.104449 0.167718 +vt 0.104449 0.861076 +vt 0.104449 0.136501 +vt 0.104449 0.829859 +vt 0.104449 0.105284 +vt 0.104449 0.798642 +vt 0.104449 0.074067 +vt 0.104449 0.767425 +vt 0.104449 0.042850 +vt 0.104449 0.738963 +vt 0.118765 0.985944 +vt 0.118765 0.011633 +vt 0.075817 0.829859 +vt 0.075817 0.798642 +vt 0.075817 0.105284 +vt 0.075817 0.074067 +vt 0.075817 0.767425 +vt 0.075817 0.042850 +vt 0.075817 0.738963 +vt 0.090133 0.985944 +vt 0.075817 0.954727 +vt 0.090133 0.011633 +vt 0.075817 0.923510 +vt 0.075817 0.230152 +vt 0.075817 0.198935 +vt 0.075817 0.892293 +vt 0.075817 0.167718 +vt 0.075817 0.861076 +vt 0.075817 0.136501 +vt 0.047185 0.230152 +vt 0.047185 0.198935 +vt 0.047185 0.923510 +vt 0.047185 0.892293 +vt 0.047185 0.167718 +vt 0.047185 0.861076 +vt 0.047185 0.136501 +vt 0.047185 0.829859 +vt 0.047185 0.105284 +vt 0.047185 0.798642 +vt 0.047185 0.074067 +vt 0.047185 0.767425 +vt 0.047185 0.042850 +vt 0.047185 0.738963 +vt 0.061501 0.985944 +vt 0.047185 0.954727 +vt 0.061501 0.011633 +vt 0.963410 0.074067 +vt 0.963410 0.105284 +vt 0.934778 0.105284 +vt 0.934778 0.074067 +vt 0.963410 0.767425 +vt 0.963410 0.798642 +vt 0.934778 0.798642 +vt 0.934778 0.767425 +vt 0.963410 0.042850 +vt 0.934778 0.042850 +vt 0.963410 0.738963 +vt 0.934778 0.738963 +vt 0.963410 0.954727 +vt 0.949094 0.985944 +vt 0.934778 0.954727 +vt 0.949094 0.011633 +vt 0.963410 0.923510 +vt 0.934778 0.923510 +vt 0.963410 0.198935 +vt 0.963410 0.230152 +vt 0.934778 0.230152 +vt 0.934778 0.198935 +vt 0.963410 0.892293 +vt 0.934778 0.892293 +vt 0.963410 0.167718 +vt 0.934778 0.167718 +vt 0.963410 0.861076 +vt 0.934778 0.861076 +vt 0.963410 0.136501 +vt 0.934778 0.136501 +vt 0.963410 0.829859 +vt 0.934778 0.829859 +vt 0.906146 0.198935 +vt 0.906146 0.167718 +vt 0.906146 0.892293 +vt 0.906146 0.861076 +vt 0.906146 0.136501 +vt 0.906146 0.829859 +vt 0.906146 0.105284 +vt 0.906146 0.798642 +vt 0.906146 0.074067 +vt 0.906146 0.767425 +vt 0.906146 0.042850 +vt 0.906146 0.738963 +vt 0.920462 0.985944 +vt 0.906146 0.954727 +vt 0.920462 0.011633 +vt 0.906146 0.923510 +vt 0.906146 0.230152 +vt 0.877514 0.074067 +vt 0.877514 0.042850 +vt 0.877514 0.767425 +vt 0.877514 0.738963 +vt 0.891830 0.985944 +vt 0.877514 0.954727 +vt 0.891830 0.011633 +vt 0.877514 0.923510 +vt 0.877514 0.230152 +vt 0.877514 0.198935 +vt 0.877514 0.892293 +vt 0.877514 0.167718 +vt 0.877514 0.861076 +vt 0.877514 0.136501 +vt 0.877514 0.829859 +vt 0.877514 0.105284 +vt 0.877514 0.798642 +vt 0.848882 0.892293 +vt 0.848882 0.861076 +vt 0.848882 0.167718 +vt 0.848882 0.136501 +vt 0.848882 0.829859 +vt 0.848882 0.105284 +vt 0.848882 0.798642 +vt 0.848882 0.074067 +vt 0.848882 0.767425 +vt 0.848882 0.042850 +vt 0.848882 0.738963 +vt 0.863198 0.985944 +vt 0.848882 0.954727 +vt 0.863198 0.011633 +vt 0.848882 0.923510 +vt 0.848882 0.230152 +vt 0.848882 0.198935 +vt 0.820250 0.767425 +vt 0.820250 0.738963 +vt 0.834566 0.985944 +vt 0.820250 0.954727 +vt 0.834566 0.011633 +vt 0.820250 0.042850 +vt 0.820250 0.923510 +vt 0.820250 0.230152 +vt 0.820250 0.198935 +vt 0.820250 0.892293 +vt 0.820250 0.167718 +vt 0.820250 0.861076 +vt 0.820250 0.136501 +vt 0.820250 0.829859 +vt 0.820250 0.105284 +vt 0.820250 0.798642 +vt 0.820250 0.074067 +vt 0.791618 0.167718 +vt 0.791618 0.136501 +vt 0.791618 0.861076 +vt 0.791618 0.829859 +vt 0.791618 0.105284 +vt 0.791618 0.798642 +vt 0.791618 0.074067 +vt 0.791618 0.767425 +vt 0.791618 0.042850 +vt 0.791617 0.738963 +vt 0.805934 0.985944 +vt 0.791618 0.954727 +vt 0.805934 0.011633 +vt 0.791618 0.923510 +vt 0.791618 0.230152 +vt 0.791618 0.198935 +vt 0.791618 0.892293 +vt 0.777302 0.985944 +vt 0.762986 0.954727 +vt 0.777302 0.011633 +vt 0.762986 0.042850 +vt 0.762986 0.258309 +vt 0.762986 0.738963 +vt 0.734353 0.258309 +vt 0.762986 0.923510 +vt 0.762986 0.230152 +vt 0.762986 0.198935 +vt 0.762986 0.892293 +vt 0.762986 0.167718 +vt 0.762986 0.861076 +vt 0.762986 0.136501 +vt 0.762986 0.829859 +vt 0.762986 0.105284 +vt 0.762986 0.798642 +vt 0.762986 0.074067 +vt 0.762986 0.767425 +vt 0.748670 0.985944 +vt 0.748670 0.011633 +vt 0.705722 0.258309 +vt 0.791617 0.258309 +vt 0.820250 0.258309 +vt 0.848882 0.258309 +vt 0.877514 0.258309 +vt 0.906146 0.258309 +vt 0.934778 0.258309 +vt 0.963410 0.258309 +vt 0.075817 0.258309 +vt 0.047185 0.258309 +vt 0.104449 0.258309 +vt 0.247609 0.258309 +vt 0.276241 0.258309 +vt 0.304873 0.258309 +vt 0.333505 0.258309 +vt 0.362137 0.258309 +vt 0.390769 0.258309 +vt 0.419401 0.258309 +vt 0.448033 0.258309 +vt 0.476665 0.258309 +vt 0.562562 0.258309 +vt 0.591194 0.258309 +vt 0.619826 0.258309 +vt 0.648457 0.258309 +vt 0.677090 0.258309 +vn 0.0000 -0.7040 -0.7101 +vn 0.0000 -0.5528 -0.8333 +vn 0.1626 -0.5528 -0.8173 +vn 0.1385 -0.7040 -0.6965 +vn 0.0000 0.5528 -0.8333 +vn 0.0000 0.7040 -0.7101 +vn 0.1385 0.7040 -0.6965 +vn 0.1626 0.5528 -0.8173 +vn 0.0000 -0.8286 -0.5598 +vn 0.1092 -0.8286 -0.5490 +vn 0.0000 0.3805 -0.9247 +vn 0.1804 0.3805 -0.9070 +vn 0.0000 -0.9217 -0.3879 +vn 0.0757 -0.9217 -0.3804 +vn 0.0000 0.1939 -0.9810 +vn 0.1914 0.1939 -0.9622 +vn 0.0000 -0.9796 -0.2010 +vn 0.0392 -0.9796 -0.1971 +vn 0.0000 0.0489 -0.9988 +vn 0.1948 0.0489 -0.9796 +vn 0.0000 0.9796 -0.2010 +vn 0.0000 1.0000 0.0000 +vn 0.0392 0.9796 -0.1971 +vn 0.0000 -1.0000 0.0000 +vn 0.0000 0.9217 -0.3879 +vn 0.0757 0.9217 -0.3804 +vn 0.0000 -0.3805 -0.9247 +vn 0.0000 -0.1939 -0.9810 +vn 0.1914 -0.1939 -0.9622 +vn 0.1804 -0.3805 -0.9070 +vn 0.0000 0.8286 -0.5598 +vn 0.1092 0.8286 -0.5490 +vn 0.3539 -0.3805 -0.8544 +vn 0.3189 -0.5528 -0.7699 +vn 0.2142 0.8286 -0.5171 +vn 0.2717 0.7040 -0.6561 +vn 0.2717 -0.7040 -0.6561 +vn 0.3189 0.5528 -0.7699 +vn 0.2142 -0.8286 -0.5171 +vn 0.3539 0.3805 -0.8544 +vn 0.1484 -0.9217 -0.3583 +vn 0.3754 0.1939 -0.9063 +vn 0.0769 -0.9796 -0.1856 +vn 0.3822 0.0489 -0.9228 +vn 0.0769 0.9796 -0.1856 +vn 0.1484 0.9217 -0.3583 +vn 0.3754 -0.1939 -0.9063 +vn 0.2155 -0.9217 -0.3225 +vn 0.1116 -0.9796 -0.1671 +vn 0.5450 0.1939 -0.8157 +vn 0.5549 0.0489 -0.8305 +vn 0.1116 0.9796 -0.1671 +vn 0.2155 0.9217 -0.3225 +vn 0.5450 -0.1939 -0.8157 +vn 0.5137 -0.3805 -0.7689 +vn 0.3110 0.8286 -0.4654 +vn 0.4630 -0.5528 -0.6929 +vn 0.3945 0.7040 -0.5904 +vn 0.3945 -0.7040 -0.5904 +vn 0.4630 0.5528 -0.6929 +vn 0.3110 -0.8286 -0.4654 +vn 0.5137 0.3805 -0.7689 +vn 0.3958 0.8286 -0.3958 +vn 0.5021 0.7040 -0.5021 +vn 0.5893 -0.5528 -0.5893 +vn 0.5021 -0.7040 -0.5021 +vn 0.5893 0.5528 -0.5893 +vn 0.3958 -0.8286 -0.3958 +vn 0.6539 0.3805 -0.6539 +vn 0.2743 -0.9217 -0.2743 +vn 0.6937 0.1939 -0.6937 +vn 0.1421 -0.9796 -0.1421 +vn 0.7063 0.0489 -0.7063 +vn 0.1421 0.9796 -0.1421 +vn 0.2743 0.9217 -0.2743 +vn 0.6937 -0.1939 -0.6937 +vn 0.6539 -0.3805 -0.6539 +vn 0.8157 0.1939 -0.5450 +vn 0.8305 0.0489 -0.5549 +vn 0.1671 0.9796 -0.1116 +vn 0.1671 -0.9796 -0.1116 +vn 0.3225 0.9217 -0.2155 +vn 0.8157 -0.1939 -0.5450 +vn 0.7689 -0.3805 -0.5137 +vn 0.4654 0.8286 -0.3110 +vn 0.6929 -0.5528 -0.4630 +vn 0.5904 0.7040 -0.3945 +vn 0.5904 -0.7040 -0.3945 +vn 0.6929 0.5528 -0.4630 +vn 0.4654 -0.8286 -0.3110 +vn 0.7689 0.3805 -0.5137 +vn 0.3225 -0.9217 -0.2155 +vn 0.7699 -0.5528 -0.3189 +vn 0.6561 -0.7040 -0.2717 +vn 0.6561 0.7040 -0.2717 +vn 0.7699 0.5528 -0.3189 +vn 0.5171 -0.8286 -0.2142 +vn 0.8544 0.3805 -0.3539 +vn 0.3583 -0.9217 -0.1484 +vn 0.9063 0.1939 -0.3754 +vn 0.1856 -0.9796 -0.0769 +vn 0.9228 0.0489 -0.3822 +vn 0.1856 0.9796 -0.0769 +vn 0.3583 0.9217 -0.1484 +vn 0.9063 -0.1939 -0.3754 +vn 0.8544 -0.3805 -0.3539 +vn 0.5171 0.8286 -0.2142 +vn 0.1971 -0.9796 -0.0392 +vn 0.9796 -0.0489 -0.1948 +vn 0.9796 0.0489 -0.1948 +vn 0.9988 0.0489 0.0000 +vn 0.9988 -0.0489 0.0000 +vn 0.1971 0.9796 -0.0392 +vn 0.3804 0.9217 -0.0757 +vn 0.9622 -0.1939 -0.1914 +vn 0.9070 -0.3805 -0.1804 +vn 0.5490 0.8286 -0.1092 +vn 0.8173 -0.5528 -0.1626 +vn 0.6965 0.7040 -0.1385 +vn 0.6965 -0.7040 -0.1385 +vn 0.8173 0.5528 -0.1626 +vn 0.5490 -0.8286 -0.1092 +vn 0.9070 0.3805 -0.1804 +vn 0.3804 -0.9217 -0.0757 +vn 0.9622 0.1939 -0.1914 +vn 0.7101 -0.7040 0.0000 +vn 0.5598 -0.8286 0.0000 +vn 0.8333 0.5528 0.0000 +vn 0.9247 0.3805 0.0000 +vn 0.3879 -0.9217 0.0000 +vn 0.9810 0.1939 0.0000 +vn 0.2010 -0.9796 0.0000 +vn 0.2010 0.9796 0.0000 +vn 0.3879 0.9217 0.0000 +vn 0.9810 -0.1939 0.0000 +vn 0.9247 -0.3805 0.0000 +vn 0.5598 0.8286 0.0000 +vn 0.8333 -0.5528 0.0000 +vn 0.7101 0.7040 0.0000 +vn 0.1971 0.9796 0.0392 +vn 0.3804 0.9217 0.0757 +vn 0.9622 -0.1939 0.1914 +vn 0.9070 -0.3805 0.1804 +vn 0.5490 0.8286 0.1092 +vn 0.8173 -0.5528 0.1626 +vn 0.6965 0.7040 0.1385 +vn 0.6965 -0.7040 0.1385 +vn 0.8173 0.5528 0.1626 +vn 0.5490 -0.8286 0.1092 +vn 0.9070 0.3805 0.1804 +vn 0.3804 -0.9217 0.0757 +vn 0.9622 0.1939 0.1914 +vn 0.1971 -0.9796 0.0392 +vn 0.9796 0.0489 0.1948 +vn 0.7699 0.5528 0.3189 +vn 0.8544 0.3805 0.3539 +vn 0.5171 -0.8286 0.2142 +vn 0.3583 -0.9217 0.1484 +vn 0.9063 0.1939 0.3754 +vn 0.1856 -0.9796 0.0769 +vn 0.9228 0.0489 0.3822 +vn 0.1856 0.9796 0.0769 +vn 0.3583 0.9217 0.1484 +vn 0.9063 -0.1939 0.3754 +vn 0.8544 -0.3805 0.3539 +vn 0.5171 0.8286 0.2142 +vn 0.7699 -0.5528 0.3189 +vn 0.6561 0.7040 0.2717 +vn 0.6561 -0.7040 0.2717 +vn 0.8157 -0.1939 0.5450 +vn 0.7689 -0.3805 0.5137 +vn 0.3225 0.9217 0.2155 +vn 0.4654 0.8286 0.3110 +vn 0.6929 -0.5528 0.4630 +vn 0.5904 0.7040 0.3945 +vn 0.5904 -0.7040 0.3945 +vn 0.6929 0.5528 0.4630 +vn 0.4654 -0.8286 0.3110 +vn 0.7689 0.3805 0.5137 +vn 0.3225 -0.9217 0.2155 +vn 0.8157 0.1939 0.5450 +vn 0.1671 -0.9796 0.1116 +vn 0.8305 0.0489 0.5549 +vn 0.1671 0.9796 0.1116 +vn 0.3958 -0.8286 0.3958 +vn 0.2743 -0.9217 0.2743 +vn 0.6539 0.3805 0.6539 +vn 0.6937 0.1939 0.6937 +vn 0.1421 -0.9796 0.1421 +vn 0.7063 0.0489 0.7063 +vn 0.1421 0.9796 0.1421 +vn 0.2743 0.9217 0.2743 +vn 0.6937 -0.1939 0.6937 +vn 0.6539 -0.3805 0.6539 +vn 0.3958 0.8286 0.3958 +vn 0.5893 -0.5528 0.5893 +vn 0.5021 0.7040 0.5021 +vn 0.5021 -0.7040 0.5021 +vn 0.5893 0.5528 0.5893 +vn 0.2155 0.9217 0.3225 +vn 0.3110 0.8286 0.4654 +vn 0.5137 -0.3805 0.7689 +vn 0.4630 -0.5528 0.6929 +vn 0.3945 0.7040 0.5904 +vn 0.3945 -0.7040 0.5904 +vn 0.4630 0.5528 0.6929 +vn 0.3110 -0.8286 0.4654 +vn 0.5137 0.3805 0.7689 +vn 0.2155 -0.9217 0.3225 +vn 0.5450 0.1939 0.8157 +vn 0.1116 -0.9796 0.1671 +vn 0.5549 0.0489 0.8305 +vn 0.1116 0.9796 0.1671 +vn 0.5450 -0.1939 0.8157 +vn 0.3539 0.3805 0.8544 +vn 0.3754 0.1939 0.9063 +vn 0.1484 -0.9217 0.3583 +vn 0.0769 -0.9796 0.1856 +vn 0.3822 0.0489 0.9228 +vn 0.0769 0.9796 0.1856 +vn 0.1484 0.9217 0.3583 +vn 0.3754 -0.1939 0.9063 +vn 0.3539 -0.3805 0.8544 +vn 0.2142 0.8286 0.5171 +vn 0.3189 -0.5528 0.7699 +vn 0.2717 0.7040 0.6561 +vn 0.2717 -0.7040 0.6561 +vn 0.3189 0.5528 0.7699 +vn 0.2142 -0.8286 0.5171 +vn 0.1804 -0.3805 0.9070 +vn 0.1626 -0.5528 0.8173 +vn 0.1092 0.8286 0.5490 +vn 0.1385 0.7040 0.6965 +vn 0.1385 -0.7040 0.6965 +vn 0.1626 0.5528 0.8173 +vn 0.1092 -0.8286 0.5490 +vn 0.1804 0.3805 0.9070 +vn 0.0757 -0.9217 0.3804 +vn 0.1914 0.1939 0.9622 +vn 0.0392 -0.9796 0.1971 +vn 0.1948 0.0489 0.9796 +vn 0.0392 0.9796 0.1971 +vn 0.0757 0.9217 0.3804 +vn 0.1914 -0.1939 0.9622 +vn 0.0000 -0.9217 0.3879 +vn 0.0000 -0.9796 0.2010 +vn 0.0000 0.1939 0.9810 +vn 0.0000 0.0489 0.9988 +vn 0.0000 0.9796 0.2010 +vn 0.0000 0.9217 0.3879 +vn 0.0000 -0.1939 0.9810 +vn 0.0000 -0.3805 0.9247 +vn 0.0000 0.8286 0.5598 +vn 0.0000 -0.5528 0.8333 +vn 0.0000 0.7040 0.7101 +vn 0.0000 -0.7040 0.7101 +vn 0.0000 0.5528 0.8333 +vn 0.0000 -0.8286 0.5598 +vn 0.0000 0.3805 0.9247 +vn -0.1626 -0.5528 0.8173 +vn -0.1385 -0.7040 0.6965 +vn -0.1385 0.7040 0.6965 +vn -0.1626 0.5528 0.8173 +vn -0.1092 -0.8286 0.5490 +vn -0.1804 0.3805 0.9070 +vn -0.0757 -0.9217 0.3804 +vn -0.1914 0.1939 0.9622 +vn -0.0392 -0.9796 0.1971 +vn -0.1948 0.0489 0.9796 +vn -0.0392 0.9796 0.1971 +vn -0.0757 0.9217 0.3804 +vn -0.1914 -0.1939 0.9622 +vn -0.1804 -0.3805 0.9070 +vn -0.1092 0.8286 0.5490 +vn -0.0769 0.9796 0.1856 +vn -0.0769 -0.9796 0.1856 +vn -0.3822 -0.0489 0.9228 +vn -0.3822 0.0489 0.9228 +vn -0.5549 0.0489 0.8305 +vn -0.5549 -0.0489 0.8305 +vn -0.1484 0.9217 0.3583 +vn -0.3754 -0.1939 0.9063 +vn -0.3539 -0.3805 0.8544 +vn -0.2142 0.8286 0.5171 +vn -0.3189 -0.5528 0.7699 +vn -0.2717 0.7040 0.6561 +vn -0.2717 -0.7040 0.6561 +vn -0.3189 0.5528 0.7699 +vn -0.2142 -0.8286 0.5171 +vn -0.3539 0.3805 0.8544 +vn -0.1484 -0.9217 0.3583 +vn -0.3754 0.1939 0.9063 +vn -0.3945 0.7040 0.5904 +vn -0.4630 0.5528 0.6929 +vn -0.3945 -0.7040 0.5904 +vn -0.3110 -0.8286 0.4654 +vn -0.5137 0.3805 0.7689 +vn -0.2155 -0.9217 0.3225 +vn -0.5450 0.1939 0.8157 +vn -0.1116 -0.9796 0.1671 +vn -0.1116 0.9796 0.1671 +vn -0.2155 0.9217 0.3225 +vn -0.5450 -0.1939 0.8157 +vn -0.5137 -0.3805 0.7689 +vn -0.3110 0.8286 0.4654 +vn -0.4630 -0.5528 0.6929 +vn -0.7063 -0.0489 0.7062 +vn -0.7063 0.0489 0.7063 +vn -0.8305 0.0489 0.5549 +vn -0.8305 -0.0489 0.5549 +vn -0.1421 0.9796 0.1421 +vn -0.2743 0.9217 0.2743 +vn -0.6937 -0.1939 0.6937 +vn -0.6539 -0.3805 0.6539 +vn -0.3958 0.8286 0.3958 +vn -0.5893 -0.5528 0.5893 +vn -0.5021 0.7040 0.5021 +vn -0.5021 -0.7040 0.5021 +vn -0.5893 0.5528 0.5893 +vn -0.3958 -0.8286 0.3958 +vn -0.6539 0.3805 0.6539 +vn -0.2743 -0.9217 0.2743 +vn -0.6937 0.1939 0.6937 +vn -0.1421 -0.9796 0.1421 +vn -0.5904 -0.7040 0.3945 +vn -0.4654 -0.8286 0.3110 +vn -0.6929 0.5528 0.4630 +vn -0.7689 0.3805 0.5137 +vn -0.3225 -0.9217 0.2155 +vn -0.8157 0.1939 0.5450 +vn -0.1671 -0.9796 0.1116 +vn -0.1671 0.9796 0.1116 +vn -0.3225 0.9217 0.2155 +vn -0.8157 -0.1939 0.5450 +vn -0.7689 -0.3805 0.5137 +vn -0.4654 0.8286 0.3110 +vn -0.6929 -0.5528 0.4630 +vn -0.5904 0.7040 0.3945 +vn -0.1856 0.9796 0.0769 +vn -0.3583 0.9217 0.1484 +vn -0.9063 -0.1939 0.3754 +vn -0.8544 -0.3805 0.3539 +vn -0.5171 0.8286 0.2142 +vn -0.7699 -0.5528 0.3189 +vn -0.6561 0.7040 0.2717 +vn -0.6561 -0.7040 0.2717 +vn -0.7699 0.5528 0.3189 +vn -0.5171 -0.8286 0.2142 +vn -0.8544 0.3805 0.3539 +vn -0.3583 -0.9217 0.1484 +vn -0.9063 0.1939 0.3754 +vn -0.1856 -0.9796 0.0769 +vn -0.9228 0.0489 0.3822 +vn -0.8173 0.5528 0.1626 +vn -0.9070 0.3805 0.1804 +vn -0.5490 -0.8286 0.1092 +vn -0.3804 -0.9217 0.0757 +vn -0.9622 0.1939 0.1914 +vn -0.1971 -0.9796 0.0392 +vn -0.9796 0.0489 0.1948 +vn -0.1971 0.9796 0.0392 +vn -0.3804 0.9217 0.0757 +vn -0.9622 -0.1939 0.1914 +vn -0.9070 -0.3805 0.1804 +vn -0.5490 0.8286 0.1092 +vn -0.8173 -0.5528 0.1626 +vn -0.6965 0.7040 0.1385 +vn -0.6965 -0.7040 0.1385 +vn -0.9810 -0.1939 0.0000 +vn -0.9247 -0.3805 0.0000 +vn -0.3879 0.9217 0.0000 +vn -0.5598 0.8286 0.0000 +vn -0.8333 -0.5528 0.0000 +vn -0.7101 0.7040 0.0000 +vn -0.7101 -0.7040 0.0000 +vn -0.8333 0.5528 0.0000 +vn -0.5598 -0.8286 0.0000 +vn -0.9247 0.3805 0.0000 +vn -0.3879 -0.9217 0.0000 +vn -0.9810 0.1939 0.0000 +vn -0.2010 -0.9796 0.0000 +vn -0.9988 0.0489 0.0000 +vn -0.2010 0.9796 0.0000 +vn -0.5490 -0.8286 -0.1092 +vn -0.3804 -0.9217 -0.0757 +vn -0.9070 0.3805 -0.1804 +vn -0.9622 0.1939 -0.1914 +vn -0.1971 -0.9796 -0.0392 +vn -0.9796 0.0489 -0.1948 +vn -0.1971 0.9796 -0.0392 +vn -0.3804 0.9217 -0.0757 +vn -0.9622 -0.1939 -0.1914 +vn -0.9070 -0.3805 -0.1804 +vn -0.5490 0.8286 -0.1092 +vn -0.8173 -0.5528 -0.1626 +vn -0.6965 0.7040 -0.1385 +vn -0.6965 -0.7040 -0.1385 +vn -0.8173 0.5528 -0.1626 +vn -0.8544 -0.3805 -0.3539 +vn -0.7699 -0.5528 -0.3189 +vn -0.5171 0.8286 -0.2142 +vn -0.6561 0.7040 -0.2717 +vn -0.6561 -0.7040 -0.2717 +vn -0.7699 0.5528 -0.3189 +vn -0.5171 -0.8286 -0.2142 +vn -0.8544 0.3805 -0.3539 +vn -0.3583 -0.9217 -0.1484 +vn -0.9063 0.1939 -0.3754 +vn -0.1856 -0.9796 -0.0769 +vn -0.9228 0.0489 -0.3822 +vn -0.1856 0.9796 -0.0769 +vn -0.3583 0.9217 -0.1484 +vn -0.9063 -0.1939 -0.3754 +vn -0.3225 -0.9217 -0.2155 +vn -0.1671 -0.9796 -0.1116 +vn -0.8157 0.1939 -0.5450 +vn -0.8305 0.0489 -0.5549 +vn -0.1671 0.9796 -0.1116 +vn -0.3225 0.9217 -0.2155 +vn -0.8157 -0.1939 -0.5450 +vn -0.7689 -0.3805 -0.5137 +vn -0.4654 0.8286 -0.3110 +vn -0.6929 -0.5528 -0.4630 +vn -0.5904 0.7040 -0.3945 +vn -0.5904 -0.7040 -0.3945 +vn -0.6929 0.5528 -0.4630 +vn -0.4654 -0.8286 -0.3110 +vn -0.7689 0.3805 -0.5137 +vn -0.3958 0.8286 -0.3958 +vn -0.5021 0.7040 -0.5021 +vn -0.5893 -0.5528 -0.5893 +vn -0.5021 -0.7040 -0.5021 +vn -0.5893 0.5528 -0.5893 +vn -0.3958 -0.8286 -0.3958 +vn -0.6539 0.3805 -0.6539 +vn -0.2743 -0.9217 -0.2743 +vn -0.6937 0.1939 -0.6937 +vn -0.1421 -0.9796 -0.1421 +vn -0.7063 0.0489 -0.7063 +vn -0.1421 0.9796 -0.1421 +vn -0.2743 0.9217 -0.2743 +vn -0.6937 -0.1939 -0.6937 +vn -0.6539 -0.3805 -0.6539 +vn -0.5450 0.1939 -0.8157 +vn -0.5549 0.0489 -0.8305 +vn -0.1116 0.9796 -0.1671 +vn -0.1116 -0.9796 -0.1671 +vn -0.2155 0.9217 -0.3225 +vn -0.5450 -0.1939 -0.8157 +vn -0.5137 -0.3805 -0.7689 +vn -0.3110 0.8286 -0.4654 +vn -0.4630 -0.5528 -0.6929 +vn -0.3945 0.7040 -0.5904 +vn -0.3945 -0.7040 -0.5904 +vn -0.4630 0.5528 -0.6929 +vn -0.3110 -0.8286 -0.4654 +vn -0.5137 0.3805 -0.7689 +vn -0.2155 -0.9217 -0.3225 +vn -0.3189 -0.5528 -0.7699 +vn -0.2717 -0.7040 -0.6561 +vn -0.2717 0.7040 -0.6561 +vn -0.3189 0.5528 -0.7699 +vn -0.2142 -0.8286 -0.5171 +vn -0.3539 0.3805 -0.8544 +vn -0.1484 -0.9217 -0.3583 +vn -0.3754 0.1939 -0.9063 +vn -0.0769 -0.9796 -0.1856 +vn -0.3822 0.0489 -0.9228 +vn -0.0769 0.9796 -0.1856 +vn -0.1484 0.9217 -0.3583 +vn -0.3754 -0.1939 -0.9063 +vn -0.3539 -0.3805 -0.8544 +vn -0.2142 0.8286 -0.5171 +vn -0.0392 0.9796 -0.1971 +vn -0.0392 -0.9796 -0.1971 +vn -0.1948 -0.0489 -0.9796 +vn -0.1948 0.0489 -0.9796 +vn 0.0000 -0.0489 -0.9988 +vn -0.0757 0.9217 -0.3804 +vn -0.1914 -0.1939 -0.9622 +vn -0.1804 -0.3805 -0.9070 +vn -0.1092 0.8286 -0.5490 +vn -0.1626 -0.5528 -0.8173 +vn -0.1385 0.7040 -0.6965 +vn -0.1385 -0.7040 -0.6965 +vn -0.1626 0.5528 -0.8173 +vn -0.1092 -0.8286 -0.5490 +vn -0.1804 0.3805 -0.9070 +vn -0.0757 -0.9217 -0.3804 +vn -0.1914 0.1939 -0.9622 +vn 0.1948 -0.0489 -0.9796 +vn -0.3822 -0.0489 -0.9228 +vn -0.5549 -0.0489 -0.8305 +vn -0.7062 -0.0489 -0.7063 +vn -0.8305 -0.0489 -0.5549 +vn -0.9228 -0.0489 -0.3822 +vn -0.9796 -0.0489 -0.1948 +vn -0.9988 -0.0489 0.0000 +vn -0.9796 -0.0489 0.1948 +vn -0.9228 -0.0489 0.3822 +vn -0.1948 -0.0489 0.9796 +vn 0.0000 -0.0489 0.9988 +vn 0.1948 -0.0489 0.9796 +vn 0.3822 -0.0489 0.9228 +vn 0.5549 -0.0489 0.8305 +vn 0.7062 -0.0489 0.7063 +vn 0.8305 -0.0489 0.5549 +vn 0.9228 -0.0489 0.3822 +vn 0.9796 -0.0489 0.1948 +vn 0.9228 -0.0489 -0.3822 +vn 0.8305 -0.0489 -0.5549 +vn 0.7062 -0.0489 -0.7062 +vn 0.5549 -0.0489 -0.8305 +vn 0.3822 -0.0489 -0.9228 +usemtl Material +s 1 +f 9/1/1 8/2/2 20/3/3 21/4/4 +f 5/5/5 4/6/6 14/7/7 15/8/8 +f 10/9/9 9/1/1 21/4/4 22/10/10 +f 6/11/11 5/5/5 15/8/8 16/12/12 +f 448/13/13 10/9/9 22/10/10 23/14/14 +f 7/15/15 6/11/11 16/12/12 17/16/16 +f 449/17/17 448/13/13 23/14/14 24/18/18 +f 482/19/19 7/15/15 17/16/16 451/20/20 +f 1/21/21 305/22/22 11/23/23 +f 450/24/24 449/17/17 24/18/18 +f 2/25/25 1/21/21 11/23/23 12/26/26 +f 447/27/27 446/28/28 18/29/29 19/30/30 +f 3/31/31 2/25/25 12/26/26 13/32/32 +f 8/2/2 447/27/27 19/30/30 20/3/3 +f 4/6/6 3/31/31 13/32/32 14/7/7 +f 20/3/3 19/30/30 33/33/33 34/34/34 +f 14/7/7 13/32/32 27/35/35 28/36/36 +f 21/4/4 20/3/3 34/34/34 35/37/37 +f 15/8/8 14/7/7 28/36/36 29/38/38 +f 22/10/10 21/4/4 35/37/37 36/39/39 +f 16/12/12 15/8/8 29/38/38 30/40/40 +f 23/14/14 22/10/10 36/39/39 37/41/41 +f 17/16/16 16/12/12 30/40/40 31/42/42 +f 24/18/18 23/14/14 37/41/41 38/43/43 +f 451/20/20 17/16/16 31/42/42 452/44/44 +f 11/23/23 305/45/22 25/46/45 +f 450/47/24 24/18/18 38/43/43 +f 12/26/26 11/23/23 25/46/45 26/48/46 +f 19/30/30 18/29/29 32/49/47 33/33/33 +f 13/32/32 12/26/26 26/48/46 27/35/35 +f 38/43/43 37/41/41 51/50/48 52/51/49 +f 452/44/44 31/42/42 45/52/50 453/53/51 +f 25/46/45 305/54/22 39/55/52 +f 450/56/24 38/43/43 52/51/49 +f 26/48/46 25/46/45 39/55/52 40/57/53 +f 33/33/33 32/49/47 46/58/54 47/59/55 +f 27/35/35 26/48/46 40/57/53 41/60/56 +f 34/34/34 33/33/33 47/59/55 48/61/57 +f 28/36/36 27/35/35 41/60/56 42/62/58 +f 35/37/37 34/34/34 48/61/57 49/63/59 +f 29/38/38 28/36/36 42/62/58 43/64/60 +f 36/39/39 35/37/37 49/63/59 50/65/61 +f 30/40/40 29/38/38 43/64/60 44/66/62 +f 37/41/41 36/39/39 50/65/61 51/50/48 +f 31/42/42 30/40/40 44/66/62 45/52/50 +f 42/62/58 41/60/56 55/67/63 56/68/64 +f 49/63/59 48/61/57 62/69/65 63/70/66 +f 43/64/60 42/62/58 56/68/64 57/71/67 +f 50/65/61 49/63/59 63/70/66 64/72/68 +f 44/66/62 43/64/60 57/71/67 58/73/69 +f 51/50/48 50/65/61 64/72/68 65/74/70 +f 45/52/50 44/66/62 58/73/69 59/75/71 +f 52/51/49 51/50/48 65/74/70 66/76/72 +f 453/53/51 45/52/50 59/75/71 454/77/73 +f 39/55/52 305/78/22 53/79/74 +f 450/80/24 52/51/49 66/76/72 +f 40/57/53 39/55/52 53/79/74 54/81/75 +f 47/59/55 46/58/54 60/82/76 61/83/77 +f 41/60/56 40/57/53 54/81/75 55/67/63 +f 48/61/57 47/59/55 61/83/77 62/69/65 +f 454/77/73 59/75/71 73/84/78 455/85/79 +f 53/79/74 305/86/22 67/87/80 +f 450/88/24 66/76/72 80/89/81 +f 54/81/75 53/79/74 67/87/80 68/90/82 +f 61/83/77 60/82/76 74/91/83 75/92/84 +f 55/67/63 54/81/75 68/90/82 69/93/85 +f 62/69/65 61/83/77 75/92/84 76/94/86 +f 56/68/64 55/67/63 69/93/85 70/95/87 +f 63/70/66 62/69/65 76/94/86 77/96/88 +f 57/71/67 56/68/64 70/95/87 71/97/89 +f 64/72/68 63/70/66 77/96/88 78/98/90 +f 58/73/69 57/71/67 71/97/89 72/99/91 +f 65/74/70 64/72/68 78/98/90 79/100/92 +f 59/75/71 58/73/69 72/99/91 73/84/78 +f 66/76/72 65/74/70 79/100/92 80/89/81 +f 77/96/88 76/94/86 90/101/93 91/102/94 +f 71/97/89 70/95/87 84/103/95 85/104/96 +f 78/98/90 77/96/88 91/102/94 92/105/97 +f 72/99/91 71/97/89 85/104/96 86/106/98 +f 79/100/92 78/98/90 92/105/97 93/107/99 +f 73/84/78 72/99/91 86/106/98 87/108/100 +f 80/89/81 79/100/92 93/107/99 94/109/101 +f 455/85/79 73/84/78 87/108/100 456/110/102 +f 67/87/80 305/111/22 81/112/103 +f 450/113/24 80/89/81 94/109/101 +f 68/90/82 67/87/80 81/112/103 82/114/104 +f 75/92/84 74/91/83 88/115/105 89/116/106 +f 69/93/85 68/90/82 82/114/104 83/117/107 +f 76/94/86 75/92/84 89/116/106 90/101/93 +f 70/95/87 69/93/85 83/117/107 84/103/95 +f 450/118/24 94/109/101 108/119/108 +f 489/120/109 457/121/110 458/122/111 490/123/112 +f 82/114/104 81/112/103 95/124/113 96/125/114 +f 89/116/106 88/115/105 102/126/115 103/127/116 +f 83/117/107 82/114/104 96/125/114 97/128/117 +f 90/101/93 89/116/106 103/127/116 104/129/118 +f 84/103/95 83/117/107 97/128/117 98/130/119 +f 91/102/94 90/101/93 104/129/118 105/131/120 +f 85/104/96 84/103/95 98/130/119 99/132/121 +f 92/105/97 91/102/94 105/131/120 106/133/122 +f 86/106/98 85/104/96 99/132/121 100/134/123 +f 93/107/99 92/105/97 106/133/122 107/135/124 +f 87/108/100 86/106/98 100/134/123 101/136/125 +f 94/109/101 93/107/99 107/135/124 108/119/108 +f 456/110/102 87/108/100 101/136/125 457/121/110 +f 81/112/103 305/137/22 95/124/113 +f 106/133/122 105/131/120 119/138/126 120/139/127 +f 100/134/123 99/132/121 113/140/128 114/141/129 +f 107/135/124 106/133/122 120/139/127 121/142/130 +f 101/136/125 100/134/123 114/141/129 115/143/131 +f 108/119/108 107/135/124 121/142/130 122/144/132 +f 457/121/110 101/136/125 115/143/131 458/122/111 +f 95/124/113 305/145/22 109/146/133 +f 450/147/24 108/119/108 122/144/132 +f 96/125/114 95/124/113 109/146/133 110/148/134 +f 103/127/116 102/126/115 116/149/135 117/150/136 +f 97/128/117 96/125/114 110/148/134 111/151/137 +f 104/129/118 103/127/116 117/150/136 118/152/138 +f 98/130/119 97/128/117 111/151/137 112/153/139 +f 105/131/120 104/129/118 118/152/138 119/138/126 +f 99/132/121 98/130/119 112/153/139 113/140/128 +f 110/148/134 109/146/133 123/154/140 124/155/141 +f 117/150/136 116/149/135 130/156/142 131/157/143 +f 111/151/137 110/148/134 124/155/141 125/158/144 +f 118/152/138 117/150/136 131/157/143 132/159/145 +f 112/153/139 111/151/137 125/158/144 126/160/146 +f 119/138/126 118/152/138 132/159/145 133/161/147 +f 113/140/128 112/153/139 126/160/146 127/162/148 +f 120/139/127 119/138/126 133/161/147 134/163/149 +f 114/141/129 113/140/128 127/162/148 128/164/150 +f 121/142/130 120/139/127 134/163/149 135/165/151 +f 115/143/131 114/141/129 128/164/150 129/166/152 +f 122/144/132 121/142/130 135/165/151 136/167/153 +f 458/122/111 115/143/131 129/166/152 459/168/154 +f 109/146/133 305/169/22 123/154/140 +f 450/170/24 122/144/132 136/167/153 +f 128/164/150 127/162/148 141/171/155 142/172/156 +f 135/165/151 134/163/149 148/173/157 149/174/158 +f 129/166/152 128/164/150 142/172/156 143/175/159 +f 136/167/153 135/165/151 149/174/158 150/176/160 +f 459/168/154 129/166/152 143/175/159 460/177/161 +f 123/154/140 305/178/22 137/179/162 +f 450/180/24 136/167/153 150/176/160 +f 124/155/141 123/154/140 137/179/162 138/181/163 +f 131/157/143 130/156/142 144/182/164 145/183/165 +f 125/158/144 124/155/141 138/181/163 139/184/166 +f 132/159/145 131/157/143 145/183/165 146/185/167 +f 126/160/146 125/158/144 139/184/166 140/186/168 +f 133/161/147 132/159/145 146/185/167 147/187/169 +f 127/162/148 126/160/146 140/186/168 141/171/155 +f 134/163/149 133/161/147 147/187/169 148/173/157 +f 145/183/165 144/182/164 158/188/170 159/189/171 +f 139/184/166 138/181/163 152/190/172 153/191/173 +f 146/185/167 145/183/165 159/189/171 160/192/174 +f 140/186/168 139/184/166 153/191/173 154/193/175 +f 147/187/169 146/185/167 160/192/174 161/194/176 +f 141/171/155 140/186/168 154/193/175 155/195/177 +f 148/173/157 147/187/169 161/194/176 162/196/178 +f 142/172/156 141/171/155 155/195/177 156/197/179 +f 149/174/158 148/173/157 162/196/178 163/198/180 +f 143/175/159 142/172/156 156/197/179 157/199/181 +f 150/176/160 149/174/158 163/198/180 164/200/182 +f 460/177/161 143/175/159 157/199/181 461/201/183 +f 137/179/162 305/202/22 151/203/184 +f 450/204/24 150/176/160 164/200/182 +f 138/181/163 137/179/162 151/203/184 152/190/172 +f 163/198/180 162/196/178 176/205/185 177/206/186 +f 157/199/181 156/197/179 170/207/187 171/208/188 +f 164/200/182 163/198/180 177/206/186 178/209/189 +f 461/201/183 157/199/181 171/208/188 462/210/190 +f 151/203/184 305/211/22 165/212/191 +f 450/213/24 164/200/182 178/209/189 +f 152/190/172 151/203/184 165/212/191 166/214/192 +f 159/189/171 158/188/170 172/215/193 173/216/194 +f 153/191/173 152/190/172 166/214/192 167/217/195 +f 160/192/174 159/189/171 173/216/194 174/218/196 +f 154/193/175 153/191/173 167/217/195 168/219/197 +f 161/194/176 160/192/174 174/218/196 175/220/198 +f 155/195/177 154/193/175 168/219/197 169/221/199 +f 162/196/178 161/194/176 175/220/198 176/205/185 +f 156/197/179 155/195/177 169/221/199 170/207/187 +f 167/217/195 166/214/192 180/222/200 181/223/201 +f 174/218/196 173/216/194 187/224/202 188/225/203 +f 168/219/197 167/217/195 181/223/201 182/226/204 +f 175/220/198 174/218/196 188/225/203 189/227/205 +f 169/221/199 168/219/197 182/226/204 183/228/206 +f 176/205/185 175/220/198 189/227/205 190/229/207 +f 170/207/187 169/221/199 183/228/206 184/230/208 +f 177/206/186 176/205/185 190/229/207 191/231/209 +f 171/208/188 170/207/187 184/230/208 185/232/210 +f 178/209/189 177/206/186 191/231/209 192/233/211 +f 462/210/190 171/208/188 185/232/210 463/234/212 +f 165/212/191 305/235/22 179/236/213 +f 450/237/24 178/209/189 192/233/211 +f 166/214/192 165/212/191 179/236/213 180/222/200 +f 173/216/194 172/215/193 186/238/214 187/224/202 +f 185/232/210 184/230/208 198/239/215 199/240/216 +f 192/233/211 191/231/209 205/241/217 206/242/218 +f 463/234/212 185/232/210 199/240/216 464/243/219 +f 179/236/213 305/244/22 193/245/220 +f 450/246/24 192/233/211 206/242/218 +f 180/222/200 179/236/213 193/245/220 194/247/221 +f 187/224/202 186/238/214 200/248/222 201/249/223 +f 181/223/201 180/222/200 194/247/221 195/250/224 +f 188/225/203 187/224/202 201/249/223 202/251/225 +f 182/226/204 181/223/201 195/250/224 196/252/226 +f 189/227/205 188/225/203 202/251/225 203/253/227 +f 183/228/206 182/226/204 196/252/226 197/254/228 +f 190/229/207 189/227/205 203/253/227 204/255/229 +f 184/230/208 183/228/206 197/254/228 198/239/215 +f 191/231/209 190/229/207 204/255/229 205/241/217 +f 202/251/225 201/249/223 215/256/230 216/257/231 +f 196/252/226 195/250/224 209/258/232 210/259/233 +f 203/253/227 202/251/225 216/257/231 217/260/234 +f 197/254/228 196/252/226 210/259/233 211/261/235 +f 204/255/229 203/253/227 217/260/234 218/262/236 +f 198/239/215 197/254/228 211/261/235 212/263/237 +f 205/241/217 204/255/229 218/262/236 219/264/238 +f 199/240/216 198/239/215 212/263/237 213/265/239 +f 206/242/218 205/241/217 219/264/238 220/266/240 +f 464/243/219 199/240/216 213/265/239 465/267/241 +f 193/245/220 305/268/22 207/269/242 +f 450/270/24 206/242/218 220/266/240 +f 194/247/221 193/245/220 207/269/242 208/271/243 +f 201/249/223 200/248/222 214/272/244 215/256/230 +f 195/250/224 194/247/221 208/271/243 209/258/232 +f 220/266/240 219/264/238 233/273/245 234/274/246 +f 465/267/241 213/265/239 227/275/247 466/276/248 +f 207/269/242 305/277/22 221/278/249 +f 450/279/24 220/266/240 234/274/246 +f 208/271/243 207/269/242 221/278/249 222/280/250 +f 215/256/230 214/272/244 228/281/251 229/282/252 +f 209/258/232 208/271/243 222/280/250 223/283/253 +f 216/257/231 215/256/230 229/282/252 230/284/254 +f 210/259/233 209/258/232 223/283/253 224/285/255 +f 217/260/234 216/257/231 230/284/254 231/286/256 +f 211/261/235 210/259/233 224/285/255 225/287/257 +f 218/262/236 217/260/234 231/286/256 232/288/258 +f 212/263/237 211/261/235 225/287/257 226/289/259 +f 219/264/238 218/262/236 232/288/258 233/273/245 +f 213/265/239 212/263/237 226/289/259 227/275/247 +f 231/286/256 230/284/254 244/290/260 245/291/261 +f 225/287/257 224/285/255 238/292/262 239/293/263 +f 232/288/258 231/286/256 245/291/261 246/294/264 +f 226/289/259 225/287/257 239/293/263 240/295/265 +f 233/273/245 232/288/258 246/294/264 247/296/266 +f 227/275/247 226/289/259 240/295/265 241/297/267 +f 234/274/246 233/273/245 247/296/266 248/298/268 +f 466/276/248 227/275/247 241/297/267 467/299/269 +f 221/278/249 305/300/22 235/301/270 +f 450/302/24 234/274/246 248/298/268 +f 222/280/250 221/278/249 235/301/270 236/303/271 +f 229/282/252 228/281/251 242/304/272 243/305/273 +f 223/283/253 222/280/250 236/303/271 237/306/274 +f 230/284/254 229/282/252 243/305/273 244/290/260 +f 224/285/255 223/283/253 237/306/274 238/292/262 +f 235/301/270 305/307/22 249/308/275 +f 450/309/24 248/298/268 262/310/276 +f 500/311/277 468/312/278 469/313/279 501/314/280 +f 236/303/271 235/301/270 249/308/275 250/315/281 +f 243/305/273 242/304/272 256/316/282 257/317/283 +f 237/306/274 236/303/271 250/315/281 251/318/284 +f 244/290/260 243/305/273 257/317/283 258/319/285 +f 238/292/262 237/306/274 251/318/284 252/320/286 +f 245/291/261 244/290/260 258/319/285 259/321/287 +f 239/293/263 238/292/262 252/320/286 253/322/288 +f 246/294/264 245/291/261 259/321/287 260/323/289 +f 240/295/265 239/293/263 253/322/288 254/324/290 +f 247/296/266 246/294/264 260/323/289 261/325/291 +f 241/297/267 240/295/265 254/324/290 255/326/292 +f 248/298/268 247/296/266 261/325/291 262/310/276 +f 467/299/269 241/297/267 255/326/292 468/312/278 +f 253/322/288 252/320/286 266/327/293 267/328/294 +f 260/323/289 259/321/287 273/329/295 274/330/296 +f 254/324/290 253/322/288 267/328/294 268/331/297 +f 261/325/291 260/323/289 274/330/296 275/332/298 +f 255/326/292 254/324/290 268/331/297 269/333/299 +f 262/310/276 261/325/291 275/332/298 276/334/300 +f 468/312/278 255/326/292 269/333/299 469/313/279 +f 249/308/275 305/335/22 263/336/301 +f 450/337/24 262/310/276 276/334/300 +f 250/315/281 249/308/275 263/336/301 264/338/302 +f 257/317/283 256/316/282 270/339/303 271/340/304 +f 251/318/284 250/315/281 264/338/302 265/341/305 +f 258/319/285 257/317/283 271/340/304 272/342/306 +f 252/320/286 251/318/284 265/341/305 266/327/293 +f 259/321/287 258/319/285 272/342/306 273/329/295 +f 502/343/307 470/344/308 471/345/309 503/346/310 +f 264/338/302 263/336/301 277/347/311 278/348/312 +f 271/340/304 270/339/303 284/349/313 285/350/314 +f 265/341/305 264/338/302 278/348/312 279/351/315 +f 272/342/306 271/340/304 285/350/314 286/352/316 +f 266/327/293 265/341/305 279/351/315 280/353/317 +f 273/329/295 272/342/306 286/352/316 287/354/318 +f 267/328/294 266/327/293 280/353/317 281/355/319 +f 274/330/296 273/329/295 287/354/318 288/356/320 +f 268/331/297 267/328/294 281/355/319 282/357/321 +f 275/332/298 274/330/296 288/356/320 289/358/322 +f 269/333/299 268/331/297 282/357/321 283/359/323 +f 276/334/300 275/332/298 289/358/322 290/360/324 +f 469/313/279 269/333/299 283/359/323 470/344/308 +f 263/336/301 305/361/22 277/347/311 +f 450/362/24 276/334/300 290/360/324 +f 288/356/320 287/354/318 301/363/325 302/364/326 +f 282/357/321 281/355/319 295/365/327 296/366/328 +f 289/358/322 288/356/320 302/364/326 303/367/329 +f 283/359/323 282/357/321 296/366/328 297/368/330 +f 290/360/324 289/358/322 303/367/329 304/369/331 +f 470/344/308 283/359/323 297/368/330 471/345/309 +f 277/347/311 305/370/22 291/371/332 +f 450/372/24 290/360/324 304/369/331 +f 278/348/312 277/347/311 291/371/332 292/373/333 +f 285/350/314 284/349/313 298/374/334 299/375/335 +f 279/351/315 278/348/312 292/373/333 293/376/336 +f 286/352/316 285/350/314 299/375/335 300/377/337 +f 280/353/317 279/351/315 293/376/336 294/378/338 +f 287/354/318 286/352/316 300/377/337 301/363/325 +f 281/355/319 280/353/317 294/378/338 295/365/327 +f 292/373/333 291/371/332 306/379/339 307/380/340 +f 299/375/335 298/374/334 313/381/341 314/382/342 +f 293/376/336 292/373/333 307/380/340 308/383/343 +f 300/377/337 299/375/335 314/382/342 315/384/344 +f 294/378/338 293/376/336 308/383/343 309/385/345 +f 301/363/325 300/377/337 315/384/344 316/386/346 +f 295/365/327 294/378/338 309/385/345 310/387/347 +f 302/364/326 301/363/325 316/386/346 317/388/348 +f 296/366/328 295/365/327 310/387/347 311/389/349 +f 303/367/329 302/364/326 317/388/348 318/390/350 +f 297/368/330 296/366/328 311/389/349 312/391/351 +f 304/369/331 303/367/329 318/390/350 319/392/352 +f 471/345/309 297/368/330 312/391/351 472/393/353 +f 291/371/332 305/394/22 306/379/339 +f 450/395/24 304/369/331 319/392/352 +f 311/389/349 310/387/347 324/396/354 325/397/355 +f 318/390/350 317/388/348 331/398/356 332/399/357 +f 312/391/351 311/389/349 325/397/355 326/400/358 +f 319/392/352 318/390/350 332/399/357 333/401/359 +f 472/393/353 312/391/351 326/400/358 473/402/360 +f 306/379/339 305/403/22 320/404/361 +f 450/405/24 319/392/352 333/401/359 +f 307/380/340 306/379/339 320/404/361 321/406/362 +f 314/382/342 313/381/341 327/407/363 328/408/364 +f 308/383/343 307/380/340 321/406/362 322/409/365 +f 315/384/344 314/382/342 328/408/364 329/410/366 +f 309/385/345 308/383/343 322/409/365 323/411/367 +f 316/386/346 315/384/344 329/410/366 330/412/368 +f 310/387/347 309/385/345 323/411/367 324/396/354 +f 317/388/348 316/386/346 330/412/368 331/398/356 +f 328/408/364 327/407/363 341/413/369 342/414/370 +f 322/409/365 321/406/362 335/415/371 336/416/372 +f 329/410/366 328/408/364 342/414/370 343/417/373 +f 323/411/367 322/409/365 336/416/372 337/418/374 +f 330/412/368 329/410/366 343/417/373 344/419/375 +f 324/396/354 323/411/367 337/418/374 338/420/376 +f 331/398/356 330/412/368 344/419/375 345/421/377 +f 325/397/355 324/396/354 338/420/376 339/422/378 +f 332/399/357 331/398/356 345/421/377 346/423/379 +f 326/400/358 325/397/355 339/422/378 340/424/380 +f 333/401/359 332/399/357 346/423/379 347/425/381 +f 473/402/360 326/400/358 340/424/380 474/426/382 +f 320/404/361 305/427/22 334/428/383 +f 450/429/24 333/401/359 347/425/381 +f 321/406/362 320/404/361 334/428/383 335/415/371 +f 346/430/379 345/431/377 359/432/384 360/433/385 +f 340/434/380 339/435/378 353/436/386 354/437/387 +f 347/438/381 346/430/379 360/433/385 361/439/388 +f 474/440/382 340/434/380 354/437/387 475/441/389 +f 334/442/383 305/443/22 348/444/390 +f 450/445/24 347/438/381 361/439/388 +f 335/446/371 334/442/383 348/444/390 349/447/391 +f 342/448/370 341/449/369 355/450/392 356/451/393 +f 336/452/372 335/446/371 349/447/391 350/453/394 +f 343/454/373 342/448/370 356/451/393 357/455/395 +f 337/456/374 336/452/372 350/453/394 351/457/396 +f 344/458/375 343/454/373 357/455/395 358/459/397 +f 338/460/376 337/456/374 351/457/396 352/461/398 +f 345/431/377 344/458/375 358/459/397 359/432/384 +f 339/435/378 338/460/376 352/461/398 353/436/386 +f 357/455/395 356/451/393 370/462/399 371/463/400 +f 351/457/396 350/453/394 364/464/401 365/465/402 +f 358/459/397 357/455/395 371/463/400 372/466/403 +f 352/461/398 351/457/396 365/465/402 366/467/404 +f 359/432/384 358/459/397 372/466/403 373/468/405 +f 353/436/386 352/461/398 366/467/404 367/469/406 +f 360/433/385 359/432/384 373/468/405 374/470/407 +f 354/437/387 353/436/386 367/469/406 368/471/408 +f 361/439/388 360/433/385 374/470/407 375/472/409 +f 475/441/389 354/437/387 368/471/408 476/473/410 +f 348/444/390 305/474/22 362/475/411 +f 450/476/24 361/439/388 375/472/409 +f 349/447/391 348/444/390 362/475/411 363/477/412 +f 356/451/393 355/450/392 369/478/413 370/462/399 +f 350/453/394 349/447/391 363/477/412 364/464/401 +f 375/472/409 374/470/407 388/479/414 389/480/415 +f 476/473/410 368/471/408 382/481/416 477/482/417 +f 362/475/411 305/483/22 376/484/418 +f 450/485/24 375/472/409 389/480/415 +f 363/477/412 362/475/411 376/484/418 377/486/419 +f 370/462/399 369/478/413 383/487/420 384/488/421 +f 364/464/401 363/477/412 377/486/419 378/489/422 +f 371/463/400 370/462/399 384/488/421 385/490/423 +f 365/465/402 364/464/401 378/489/422 379/491/424 +f 372/466/403 371/463/400 385/490/423 386/492/425 +f 366/467/404 365/465/402 379/491/424 380/493/426 +f 373/468/405 372/466/403 386/492/425 387/494/427 +f 367/469/406 366/467/404 380/493/426 381/495/428 +f 374/470/407 373/468/405 387/494/427 388/479/414 +f 368/471/408 367/469/406 381/495/428 382/481/416 +f 379/491/424 378/489/422 392/496/429 393/497/430 +f 386/492/425 385/490/423 399/498/431 400/499/432 +f 380/493/426 379/491/424 393/497/430 394/500/433 +f 387/494/427 386/492/425 400/499/432 401/501/434 +f 381/495/428 380/493/426 394/500/433 395/502/435 +f 388/479/414 387/494/427 401/501/434 402/503/436 +f 382/481/416 381/495/428 395/502/435 396/504/437 +f 389/480/415 388/479/414 402/503/436 403/505/438 +f 477/482/417 382/481/416 396/504/437 478/506/439 +f 376/484/418 305/507/22 390/508/440 +f 450/509/24 389/480/415 403/505/438 +f 377/486/419 376/484/418 390/508/440 391/510/441 +f 384/488/421 383/487/420 397/511/442 398/512/443 +f 378/489/422 377/486/419 391/510/441 392/496/429 +f 385/490/423 384/488/421 398/512/443 399/498/431 +f 478/506/439 396/504/437 410/513/444 479/514/445 +f 390/508/440 305/515/22 404/516/446 +f 450/517/24 403/505/438 417/518/447 +f 391/510/441 390/508/440 404/516/446 405/519/448 +f 398/512/443 397/511/442 411/520/449 412/521/450 +f 392/496/429 391/510/441 405/519/448 406/522/451 +f 399/498/431 398/512/443 412/521/450 413/523/452 +f 393/497/430 392/496/429 406/522/451 407/524/453 +f 400/499/432 399/498/431 413/523/452 414/525/454 +f 394/500/433 393/497/430 407/524/453 408/526/455 +f 401/501/434 400/499/432 414/525/454 415/527/456 +f 395/502/435 394/500/433 408/526/455 409/528/457 +f 402/503/436 401/501/434 415/527/456 416/529/458 +f 396/504/437 395/502/435 409/528/457 410/513/444 +f 403/505/438 402/503/436 416/529/458 417/518/447 +f 414/525/454 413/523/452 427/530/459 428/531/460 +f 408/526/455 407/524/453 421/532/461 422/533/462 +f 415/527/456 414/525/454 428/531/460 429/534/463 +f 409/528/457 408/526/455 422/533/462 423/535/464 +f 416/529/458 415/527/456 429/534/463 430/536/465 +f 410/513/444 409/528/457 423/535/464 424/537/466 +f 417/518/447 416/529/458 430/536/465 431/538/467 +f 479/514/445 410/513/444 424/537/466 480/539/468 +f 404/516/446 305/540/22 418/541/469 +f 450/542/24 417/518/447 431/538/467 +f 405/519/448 404/516/446 418/541/469 419/543/470 +f 412/521/450 411/520/449 425/544/471 426/545/472 +f 406/522/451 405/519/448 419/543/470 420/546/473 +f 413/523/452 412/521/450 426/545/472 427/530/459 +f 407/524/453 406/522/451 420/546/473 421/532/461 +f 418/541/469 305/547/22 432/548/474 +f 450/549/24 431/538/467 445/550/475 +f 513/551/476 481/552/477 482/19/19 514/553/478 +f 419/543/470 418/541/469 432/548/474 433/554/479 +f 426/545/472 425/544/471 439/555/480 440/556/481 +f 420/546/473 419/543/470 433/554/479 434/557/482 +f 427/530/459 426/545/472 440/556/481 441/558/483 +f 421/532/461 420/546/473 434/557/482 435/559/484 +f 428/531/460 427/530/459 441/558/483 442/560/485 +f 422/533/462 421/532/461 435/559/484 436/561/486 +f 429/534/463 428/531/460 442/560/485 443/562/487 +f 423/535/464 422/533/462 436/561/486 437/563/488 +f 430/536/465 429/534/463 443/562/487 444/564/489 +f 424/537/466 423/535/464 437/563/488 438/565/490 +f 431/538/467 430/536/465 444/564/489 445/550/475 +f 480/539/468 424/537/466 438/565/490 481/552/477 +f 436/561/486 435/559/484 4/6/6 5/5/5 +f 443/562/487 442/560/485 9/1/1 10/9/9 +f 437/563/488 436/561/486 5/5/5 6/11/11 +f 444/564/489 443/562/487 10/9/9 448/13/13 +f 438/565/490 437/563/488 6/11/11 7/15/15 +f 445/550/475 444/564/489 448/13/13 449/17/17 +f 481/552/477 438/565/490 7/15/15 482/19/19 +f 432/548/474 305/566/22 1/21/21 +f 450/567/24 445/550/475 449/17/17 +f 433/554/479 432/548/474 1/21/21 2/25/25 +f 440/556/481 439/555/480 446/28/28 447/27/27 +f 434/557/482 433/554/479 2/25/25 3/31/31 +f 441/558/483 440/556/481 447/27/27 8/2/2 +f 435/559/484 434/557/482 3/31/31 4/6/6 +f 442/560/485 441/558/483 8/2/2 9/1/1 +f 514/553/478 482/19/19 451/20/20 483/568/491 +f 480/539/468 481/552/477 513/551/476 512/569/492 +f 511/570/493 479/514/445 480/539/468 512/569/492 +f 510/571/494 478/506/439 479/514/445 511/570/493 +f 509/572/495 477/482/417 478/506/439 510/571/494 +f 508/573/496 476/473/410 477/482/417 509/572/495 +f 507/574/497 475/441/389 476/473/410 508/573/496 +f 506/575/498 474/440/382 475/441/389 507/574/497 +f 505/576/499 473/402/360 474/426/382 506/577/498 +f 504/578/500 472/393/353 473/402/360 505/576/499 +f 503/346/310 471/345/309 472/393/353 504/578/500 +f 469/313/279 470/344/308 502/343/307 501/314/280 +f 467/299/269 468/312/278 500/311/277 499/579/501 +f 498/580/502 466/276/248 467/299/269 499/579/501 +f 497/581/503 465/267/241 466/276/248 498/580/502 +f 496/582/504 464/243/219 465/267/241 497/581/503 +f 495/583/505 463/234/212 464/243/219 496/582/504 +f 494/584/506 462/210/190 463/234/212 495/583/505 +f 493/585/507 461/201/183 462/210/190 494/584/506 +f 492/586/508 460/177/161 461/201/183 493/585/507 +f 491/587/509 459/168/154 460/177/161 492/586/508 +f 490/123/112 458/122/111 459/168/154 491/587/509 +f 456/110/102 457/121/110 489/120/109 488/588/510 +f 487/589/511 455/85/79 456/110/102 488/588/510 +f 486/590/512 454/77/73 455/85/79 487/589/511 +f 485/591/513 453/53/51 454/77/73 486/590/512 +f 484/592/514 452/44/44 453/53/51 485/591/513 +f 439/555/480 513/551/476 514/553/478 446/28/28 +f 425/544/471 512/569/492 513/551/476 439/555/480 +f 411/520/449 511/570/493 512/569/492 425/544/471 +f 397/511/442 510/571/494 511/570/493 411/520/449 +f 383/487/420 509/572/495 510/571/494 397/511/442 +f 369/478/413 508/573/496 509/572/495 383/487/420 +f 355/450/392 507/574/497 508/573/496 369/478/413 +f 341/449/369 506/575/498 507/574/497 355/450/392 +f 327/407/363 505/576/499 506/577/498 341/413/369 +f 313/381/341 504/578/500 505/576/499 327/407/363 +f 298/374/334 503/346/310 504/578/500 313/381/341 +f 284/349/313 502/343/307 503/346/310 298/374/334 +f 270/339/303 501/314/280 502/343/307 284/349/313 +f 256/316/282 500/311/277 501/314/280 270/339/303 +f 242/304/272 499/579/501 500/311/277 256/316/282 +f 228/281/251 498/580/502 499/579/501 242/304/272 +f 214/272/244 497/581/503 498/580/502 228/281/251 +f 200/248/222 496/582/504 497/581/503 214/272/244 +f 186/238/214 495/583/505 496/582/504 200/248/222 +f 172/215/193 494/584/506 495/583/505 186/238/214 +f 158/188/170 493/585/507 494/584/506 172/215/193 +f 144/182/164 492/586/508 493/585/507 158/188/170 +f 130/156/142 491/587/509 492/586/508 144/182/164 +f 116/149/135 490/123/112 491/587/509 130/156/142 +f 102/126/115 489/120/109 490/123/112 116/149/135 +f 88/115/105 488/588/510 489/120/109 102/126/115 +f 74/91/83 487/589/511 488/588/510 88/115/105 +f 60/82/76 486/590/512 487/589/511 74/91/83 +f 46/58/54 485/591/513 486/590/512 60/82/76 +f 32/49/47 484/592/514 485/591/513 46/58/54 +f 18/29/29 483/568/491 484/592/514 32/49/47 +f 446/28/28 514/553/478 483/568/491 18/29/29 +f 483/568/491 451/20/20 452/44/44 484/592/514 diff --git a/examples/pill_demo/res/models/sponza/keep.txt b/examples/pill_demo/res/models/sponza/keep.txt new file mode 100644 index 00000000..e69de29b diff --git a/examples/pill_demo/res/textures/pill_color.png b/examples/pill_demo/res/textures/pill_color.png new file mode 100644 index 00000000..8262f8f3 Binary files /dev/null and b/examples/pill_demo/res/textures/pill_color.png differ diff --git a/examples/pill_demo/res/textures/pill_logo.png b/examples/pill_demo/res/textures/pill_logo.png new file mode 100644 index 00000000..d4e5bc35 Binary files /dev/null and b/examples/pill_demo/res/textures/pill_logo.png differ diff --git a/examples/pill_demo/res/textures/pill_normal.png b/examples/pill_demo/res/textures/pill_normal.png new file mode 100644 index 00000000..879f9b0b Binary files /dev/null and b/examples/pill_demo/res/textures/pill_normal.png differ diff --git a/examples/pill_demo/src/game.rs b/examples/pill_demo/src/game.rs new file mode 100644 index 00000000..737e1c30 --- /dev/null +++ b/examples/pill_demo/src/game.rs @@ -0,0 +1,763 @@ +use cgmath::InnerSpace; +use pill_engine::{define_component, game::*}; +use rand::Rng; + +// Draft import for renderer vNext API (as per DESIGN.md) +// use pill_engine::pill_renderer as pr; + +// Define custom component +pub struct PillComponent {} + +impl Component for PillComponent {} + +impl PillTypeMapKey for PillComponent { + type Storage = ComponentStorage; +} + +// Component for motion blur test pill +define_component!(MovingPillComponent { + target_position: Vector3f, + velocity: f32, + rotation_speed: f32, +}); + +// Game +pub struct Game {} + +impl PillGame for Game { + fn start(&self, engine: &mut Engine) -> Result<()> { + // let _scene_benchmark = create_scene_benchmark(engine, "Benchmark 60k pills")?; + // let _scene_pills = create_scene_pills(engine, "Pills")?; + // let _scene_dof_proof = create_scene_dof_proof(engine, "DoF Proof")?; + // let _scene_mb_proof = create_scene_mb_proof(engine, "MB Proof")?; + // let _scene_pbr_grid: SceneHandle = create_scene_pbr_grid(engine, "PBR Grid")?; + // let _scene_gltf_glb: SceneHandle = create_scene_gltf_glb(engine, "GLTF glb")?; + // let _scene_gltf_gltf: SceneHandle = create_scene_gltf_gltf(engine, "GLTF gltf")?; + // let _scene_gltf_pbr: SceneHandle = create_scene_gltf_pbr(engine, "GLTF PBR Spheres")?; // TODO: fix + let _scene_gltf_sponza: SceneHandle = create_scene_gltf_sponza(engine, "GLTF Sponza")?; + engine.set_active_scene(_scene_gltf_sponza)?; + Ok(()) + } +} + +fn create_scene_dof_proof(engine: &mut Engine, name: &str) -> Result { + let scene = engine.create_scene(name)?; + + // Register components + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + + // Add systems + engine.add_system("PillRotation", pill_rotation_system)?; + + // Mesh + let pill_mesh_handle = get_or_add_mesh(engine, "Pill", "models/pill.obj")?; + + // Textures (same as normal pills scene so PBR bindings are valid) + let pill_color_texture = Texture::new( + "PillColor", + TextureType::Gamma, + ResourceLoadType::Path("textures/pill_color.png".into()), + ); + let pill_color_texture_handle = engine.add_resource::(pill_color_texture)?; + let pill_normal_texture = Texture::new( + "PillNormal", + TextureType::Linear, + ResourceLoadType::Path("textures/pill_normal.png".into()), + ); + let pill_normal_texture_handle = engine.add_resource::(pill_normal_texture)?; + + // Create a few distinct materials so pills are easy to distinguish on-screen. + let tints = [ + Color::new(1.0, 0.2, 0.2), + Color::new(0.2, 1.0, 0.2), + Color::new(0.2, 0.2, 1.0), + Color::new(1.0, 1.0, 0.2), + Color::new(1.0, 0.2, 1.0), + ]; + let mut materials: Vec = Vec::new(); + for (i, tint) in tints.iter().enumerate() { + let mut mat: PBRMaterial = PBRMaterial::new(&format!("DoFProofMat{}", i)); + mat.set_albedo_texture(pill_color_texture_handle.clone()); + mat.set_normal_texture(pill_normal_texture_handle.clone()); + mat.set_base_color_factor(*tint); + mat.set_metallic_factor(0.0); + mat.set_roughness_factor(0.4); + let handle = engine.add_resource::(mat)?; + materials.push(handle); + } + + // Camera: look straight down +Z from z=-16 so depth ordering is obvious. + let camera = engine.create_entity(scene)?; + let cam_xform = TransformComponent::builder() + .position(Vector3f::new(0.0, 0.0, -16.0)) + .rotation(Vector3f::new(0.0, 0.0, 0.0)) + .build(); + engine.add_component_to_entity(scene, camera, cam_xform)?; + let camera_component = CameraComponent::builder().enabled(true).build(); + engine.add_component_to_entity(scene, camera, camera_component)?; + + // Five pills at fixed depths along view direction; sizes/positions make it easy to verify focus changes. + // Distances from camera are approx: 6, 16, 26, 46, 86 units. + let zs = [-10.0, 0.0, 10.0, 30.0, 70.0]; + let xs: [f32; 5] = [-5.0, -3.0, 0.0, 15.0, 50.0]; + for i in 0..zs.len() { + let pill = engine.create_entity(scene)?; + let transform_component = TransformComponent::builder() + .scale(Vector3f::new(1.2, 1.8, 1.2)) + .rotation(Vector3f::new(0.0, 0.0, 0.0)) + .position(Vector3f::new(xs[i], 0.0, zs[i])) + .build(); + engine.add_component_to_entity(scene, pill, transform_component)?; + let mesh_rendering_component: MeshRenderingComponent = MeshRenderingComponent::builder() + .mesh(&pill_mesh_handle) + .material(&materials[i]) + .build(); + engine.add_component_to_entity(scene, pill, mesh_rendering_component)?; + engine.add_component_to_entity(scene, pill, PillComponent {})?; + } + + Ok(scene) +} + +fn create_scene_mb_proof(engine: &mut Engine, name: &str) -> Result { + let scene = engine.create_scene(name)?; + + // Register components + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + + // Add systems + engine.add_system("MotionBlurMovement", motion_blur_movement_system)?; + + // Mesh + let pill_mesh_handle = get_or_add_mesh(engine, "Pill", "models/pill.obj")?; + + // Textures + let pill_color_texture = Texture::new( + "PillColor", + TextureType::Gamma, + ResourceLoadType::Path("textures/pill_color.png".into()), + ); + let pill_color_texture_handle = engine.add_resource::(pill_color_texture)?; + let pill_normal_texture = Texture::new( + "PillNormal", + TextureType::Linear, + ResourceLoadType::Path("textures/pill_normal.png".into()), + ); + let pill_normal_texture_handle = engine.add_resource::(pill_normal_texture)?; + + // Create material for the moving pill (bright color to make motion blur visible) + let mut mat: PBRMaterial = PBRMaterial::new("MotionBlurMat"); + mat.set_albedo_texture(pill_color_texture_handle.clone()); + mat.set_normal_texture(pill_normal_texture_handle.clone()); + mat.set_base_color_factor(Color::new(1.0, 0.8, 0.2)); // Bright yellow-orange + mat.set_metallic_factor(0.1); + mat.set_roughness_factor(0.3); + let material_handle = engine.add_resource::(mat)?; + + // Camera: positioned to see the motion clearly + let camera = engine.create_entity(scene)?; + let cam_xform = TransformComponent::builder() + .position(Vector3f::new(0.0, 0.0, -20.0)) + .rotation(Vector3f::new(0.0, 0.0, 0.0)) + .build(); + engine.add_component_to_entity(scene, camera, cam_xform)?; + let camera_component = CameraComponent::builder().enabled(true).build(); + engine.add_component_to_entity(scene, camera, camera_component)?; + + // Multiple moving pills for motion blur testing + let num_pills = 12; + let positions: Vec = (0..num_pills) + .map(|i| { + let angle = i as f32 * (std::f32::consts::TAU / num_pills as f32); + let radius = 6.0; + Vector3f::new(angle.cos() * radius, 0.0, angle.sin() * radius) + }) + .collect(); + + for (i, pos) in positions.iter().enumerate() { + let angle = i as f32 * (std::f32::consts::TAU / num_pills as f32); + let pill = engine.create_entity(scene)?; + let transform_component = TransformComponent::builder() + .scale(Vector3f::new(1.5, 2.0, 1.5)) + .rotation(Vector3f::new(0.0, 0.0, 0.0)) + .position(*pos) + .build(); + engine.add_component_to_entity(scene, pill, transform_component)?; + + let mesh_rendering_component: MeshRenderingComponent = MeshRenderingComponent::builder() + .mesh(&pill_mesh_handle) + .material(&material_handle) + .build(); + engine.add_component_to_entity(scene, pill, mesh_rendering_component)?; + + // Spread targets around a different circular offset + let angle_offset = angle + std::f32::consts::PI / 2.0; + let target_radius = 5.0; + let target = Vector3f::new( + angle_offset.cos() * target_radius, + 0.0, + angle_offset.sin() * target_radius, + ); + + let moving_pill_component = MovingPillComponent { + target_position: target, + velocity: 6.0 + i as f32 * 0.3, // vary speed a bit for visualization + rotation_speed: 0.0, // Could add spin if you wish + }; + engine.add_component_to_entity(scene, pill, moving_pill_component)?; + } + + Ok(scene) +} + +fn create_scene_benchmark(engine: &mut Engine, name: &str) -> Result { + let scene = engine.create_scene(name)?; + + // Register components + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + + // Add systems + engine.add_system("PillRotation", pill_rotation_system)?; + + // Add meshes + let pill_mesh_handle = get_or_add_mesh(engine, "Pill", "models/pill.obj")?; + // Add textures + let pill_color_texture = Texture::new( + "PillColor", + TextureType::Gamma, + ResourceLoadType::Path("textures/pill_color.png".into()), + ); + let pill_color_texture_handle = engine.add_resource::(pill_color_texture)?; + let pill_normal_texture = Texture::new( + "PillNormal", + TextureType::Linear, + ResourceLoadType::Path("textures/pill_normal.png".into()), + ); + let pill_normal_texture_handle = engine.add_resource::(pill_normal_texture)?; + + // Material properties showcase: create a small set of materials to reuse + let mut rng = rand::thread_rng(); + let mut materials = Vec::new(); + for j in 0..10 { + let mut mat: PBRMaterial = PBRMaterial::new(&format!("PillMat{}", j)); + mat.set_albedo_texture(pill_color_texture_handle.clone()); + mat.set_normal_texture(pill_normal_texture_handle.clone()); + let tint = Color::new( + rng.gen_range(0.2..=0.8), + rng.gen_range(0.2..=0.8), + rng.gen_range(0.2..=0.8), + ); + let metallic = rng.gen_range(0.0..=1.0); + let roughness = rng.gen_range(0.0..=1.0); + mat.set_base_color_factor(tint); + mat.set_metallic_factor(metallic); + mat.set_roughness_factor(roughness); + let handle = engine.add_resource::(mat)?; + materials.push(handle); + } + + // Create camera entity + let camera = engine.create_entity(scene)?; + let transform_component = TransformComponent::builder() + .position(Vector3f::new(0.0, 0.0, -16.0)) + .rotation(Vector3f::new(0.0, 0.0, -20.0)) + .build(); + engine.add_component_to_entity(scene, camera, transform_component)?; + let camera_component = CameraComponent::builder().enabled(true).build(); + engine.add_component_to_entity(scene, camera, camera_component)?; + + // Create pill entity + for i in 0..60000 { + let pill = engine.create_entity(scene)?; + let posx = rng.gen_range(-10.0..=20.0); + let posy = rng.gen_range(-10.0..=10.0); + let posz = rng.gen_range(-1.0..=1.0); + let rotx = rng.gen_range(-180.0..=180.0); + let transform_component = TransformComponent::builder() + .rotation(Vector3f::new(rotx, 0.0, 0.0)) + .position(Vector3f::new(posx, posy, posz)) + .build(); + engine.add_component_to_entity(scene, pill, transform_component)?; + let mesh_rendering_component = MeshRenderingComponent::builder() + .mesh(&pill_mesh_handle) + .material(&materials[i as usize % materials.len()]) + .build(); + engine.add_component_to_entity(scene, pill, mesh_rendering_component)?; + engine.add_component_to_entity(scene, pill, PillComponent {})?; + } + + Ok(scene) +} + +fn create_scene_pills(engine: &mut Engine, name: &str) -> Result { + let scene = engine.create_scene(name)?; + + // Register components + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + + // Add systems + engine.add_system("PillRotation", pill_rotation_system)?; + + // Add meshes + let pill_mesh_handle = get_or_add_mesh(engine, "Pill", "models/pill.obj")?; + // Add textures + let pill_color_texture = Texture::new( + "PillColor", + TextureType::Gamma, + ResourceLoadType::Path("textures/pill_color.png".into()), + ); + let pill_color_texture_handle = engine.add_resource::(pill_color_texture)?; + let pill_normal_texture = Texture::new( + "PillNormal", + TextureType::Linear, + ResourceLoadType::Path("textures/pill_normal.png".into()), + ); + let pill_normal_texture_handle = engine.add_resource::(pill_normal_texture)?; + + // Material properties showcase: create a small set of materials to reuse + let mut rng = rand::thread_rng(); + let mut materials = Vec::new(); + for j in 0..10 { + let mut mat: PBRMaterial = PBRMaterial::new(&format!("PillMat{}", j)); + mat.set_albedo_texture(pill_color_texture_handle.clone()); + mat.set_normal_texture(pill_normal_texture_handle.clone()); + let tint = Color::new( + rng.gen_range(0.2..=0.8), + rng.gen_range(0.2..=0.8), + rng.gen_range(0.2..=0.8), + ); + let metallic = rng.gen_range(0.0..=1.0); + let roughness = rng.gen_range(0.0..=1.0); + mat.set_base_color_factor(tint); + mat.set_metallic_factor(metallic); + mat.set_roughness_factor(roughness); + let handle = engine.add_resource::(mat)?; + materials.push(handle); + } + + // Create camera entity + let camera = engine.create_entity(scene)?; + let transform_component = TransformComponent::builder() + .position(Vector3f::new(0.0, 0.0, -16.0)) + .rotation(Vector3f::new(0.0, 0.0, -20.0)) + .build(); + engine.add_component_to_entity(scene, camera, transform_component)?; + let camera_component = CameraComponent::builder().enabled(true).build(); + engine.add_component_to_entity(scene, camera, camera_component)?; + + // Create pill entity + for i in 0..3000 { + let pill = engine.create_entity(scene)?; + let posx = rng.gen_range(-100.0..=100.0); + let posy = rng.gen_range(-100.0..=100.0); + let posz = rng.gen_range(-10.0..=100.0); + let rotx = rng.gen_range(-180.0..=180.0); + let roty = rng.gen_range(-180.0..=180.0); + let rotz = rng.gen_range(-180.0..=180.0); + let transform_component = TransformComponent::builder() + .scale(Vector3f::new(0.75, 1.2, 0.75)) + .rotation(Vector3f::new(rotx, roty, rotz)) + .position(Vector3f::new(posx, posy, posz)) + .build(); + engine.add_component_to_entity(scene, pill, transform_component)?; + let mesh_rendering_component = MeshRenderingComponent::builder() + .mesh(&pill_mesh_handle) + .material(&materials[i as usize % materials.len()]) + .build(); + engine.add_component_to_entity(scene, pill, mesh_rendering_component)?; + engine.add_component_to_entity(scene, pill, PillComponent {})?; + } + + Ok(scene) +} + +fn create_scene_pbr_grid(engine: &mut Engine, name: &str) -> Result { + let scene = engine.create_scene(name)?; + + // Register components + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + + // Camera + let camera = engine.create_entity(scene)?; + let camera_transform = TransformComponent::builder() + .position(Vector3f::new(0.0, 0.0, -16.0)) + .rotation(Vector3f::new(0.0, 0.0, -20.0)) + .build(); + engine.add_component_to_entity(scene, camera, camera_transform)?; + let camera_component = CameraComponent::builder().enabled(true).build(); + engine.add_component_to_entity(scene, camera, camera_component)?; + + // Mesh (use pill as sphere substitute) + let pill_mesh_handle = get_or_add_mesh(engine, "Pill", "models/pill.obj")?; + + // Create 5x5 grid where: + // - X axis: roughness [0.0 .. 1.0] + // - Y axis: metallic [0.0 .. 1.0] + // - Base color: (0.35, 0.35, 0.35) + let grid_size = 5; + let spacing = 2.5f32; + for y in 0..grid_size { + for x in 0..grid_size { + let roughness = (x as f32) / ((grid_size - 1) as f32); + let metallic = (y as f32) / ((grid_size - 1) as f32); + + let mut mat: PBRMaterial = PBRMaterial::new(&format!("PBR_{}_{}", x, y)); + mat.set_base_color_factor(Color::new(0.35, 0.35, 0.35)); + mat.set_metallic_factor(metallic); + mat.set_roughness_factor(roughness); + let mat_handle = engine.add_resource::(mat)?; + + let entity = engine.create_entity(scene)?; + let pos_x = (x as f32 - ((grid_size as f32 - 1.0) * 0.5)) * spacing; + let pos_y = (y as f32 - ((grid_size as f32 - 1.0) * 0.5)) * spacing; + let transform = TransformComponent::builder() + .position(Vector3f::new(pos_x, pos_y, 0.0)) + .rotation(Vector3f::new(45.0, 45.0, 45.0)) + .build(); + engine.add_component_to_entity(scene, entity, transform)?; + + let mesh_render = MeshRenderingComponent::builder() + .mesh(&pill_mesh_handle) + .material(&mat_handle) + .build(); + engine.add_component_to_entity(scene, entity, mesh_render)?; + } + } + + Ok(scene) +} + +fn create_scene_gltf_glb(engine: &mut Engine, name: &str) -> Result { + let scene = engine.create_scene(name)?; + + // Register components + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + + engine.add_system("PillRotation", pill_rotation_system)?; + + // Camera + let camera = engine.create_entity(scene)?; + let camera_transform = TransformComponent::builder() + .position(Vector3f::new(0.0, 0.0, -2.0)) + .rotation(Vector3f::new(0.0, 0.0, 0.0)) + .build(); + engine.add_component_to_entity(scene, camera, camera_transform)?; + let camera_component = CameraComponent::builder().enabled(true).build(); + engine.add_component_to_entity(scene, camera, camera_component)?; + + // Load Model from GLTF and assemble + // https://github.com/KhronosGroup/glTF-Sample-Models/tree/d7a3cc8e51d7c573771ae77a57f16b0662a905c6/2.0/DamagedHelmet + let model_handle = get_or_add_model(engine, "DamagedHelmet", "models/DamagedHelmet.glb")?; + let entities = assemble_model_into_scene(engine, scene, &model_handle)?; + for entity in entities { + engine.add_component_to_entity(scene, entity, PillComponent {})?; + } + + Ok(scene) +} + +fn create_scene_gltf_pbr(engine: &mut Engine, name: &str) -> Result { + let scene = engine.create_scene(name)?; + + // Register components + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + + // Camera + let camera = engine.create_entity(scene)?; + let camera_transform = TransformComponent::builder() + .position(Vector3f::new(0.0, -30.0, 0.0)) + .rotation(Vector3f::new(-90.0, 0.0, 0.0)) + .build(); + engine.add_component_to_entity(scene, camera, camera_transform)?; + let camera_component = CameraComponent::builder().enabled(true).fov(45.0).build(); + engine.add_component_to_entity(scene, camera, camera_component)?; + + // Load Model from GLTF and assemble + // https://github.com/KhronosGroup/glTF-Sample-Assets/blob/ef86ca2c5996146cf1b14f7478842db4eee920b1/Models/MetalRoughSpheres/glTF-Binary/MetalRoughSpheres.glb + let model_handle = + get_or_add_model(engine, "MetalRoughSpheres", "models/MetalRoughSpheres.glb")?; + let _entities = assemble_model_into_scene(engine, scene, &model_handle)?; + + Ok(scene) +} + +fn create_scene_gltf_gltf(engine: &mut Engine, name: &str) -> Result { + let scene = engine.create_scene(name)?; + + // Register components + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + + engine.add_system("PillRotation", pill_rotation_system)?; + + // Camera + let camera = engine.create_entity(scene)?; + let camera_transform = TransformComponent::builder() + .position(Vector3f::new(0.0, 0.0, -3.0)) + .rotation(Vector3f::new(0.0, 0.0, 0.0)) + .build(); + engine.add_component_to_entity(scene, camera, camera_transform)?; + let camera_component = CameraComponent::builder().enabled(true).build(); + engine.add_component_to_entity(scene, camera, camera_component)?; + + // Load Model from GLTF and assemble + // https://github.com/KhronosGroup/glTF-Sample-Assets/blob/ef86ca2c5996146cf1b14f7478842db4eee920b1/Models/SciFiHelmet/README.md + let model_handle = + get_or_add_model(engine, "SciFiHelmet", "models/SciFiHelmet/SciFiHelmet.gltf")?; + let entities = assemble_model_into_scene(engine, scene, &model_handle)?; + for entity in entities { + engine.add_component_to_entity(scene, entity, PillComponent {})?; + } + + Ok(scene) +} + +fn create_scene_gltf_sponza(engine: &mut Engine, name: &str) -> Result { + let scene = engine.create_scene(name)?; + + // Register components + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + + // Camera + let camera = engine.create_entity(scene)?; + let camera_transform = TransformComponent::builder() + .position(Vector3f::new(5.0, 1.0, 0.0)) + .rotation(Vector3f::new(0.0, -90.0, 0.0)) + .build(); + engine.add_component_to_entity(scene, camera, camera_transform)?; + let camera_component = CameraComponent::builder().enabled(true).build(); + engine.add_component_to_entity(scene, camera, camera_component)?; + + // Load Model from GLTF and assemble + // https://github.com/KhronosGroup/glTF-Sample-Assets/blob/ef86ca2c5996146cf1b14f7478842db4eee920b1/Models/SciFiHelmet/README.md + let model_handle = get_or_add_model(engine, "Sponza", "models/sponza/Sponza.gltf")?; + let entities = assemble_model_into_scene(engine, scene, &model_handle)?; + // for entity in entities { + // engine.add_component_to_entity(scene, entity, PillComponent {})?; + // } + + Ok(scene) +} + +fn get_or_add_model(engine: &mut Engine, name: &str, path: &str) -> Result { + match engine.get_resource_handle::(name) { + Ok(handle) => Ok(handle), + Err(_) => { + let model = Model::new(name, path.into()); + engine.add_resource(model) + } + } +} + +fn assemble_model_into_scene( + engine: &mut Engine, + scene: SceneHandle, + model_handle: &ModelHandle, +) -> Result> { + // Snapshot slots to avoid holding an immutable borrow of engine while mutating it + let (material_handles, slots): ( + Vec, + Vec<(MeshHandle, PBRMaterialHandle, Vector3f, Vector3f, Vector3f)>, + ) = { + let model = engine.get_resource::(model_handle)?; + let material_handles = model.materials.clone(); + let slots = model + .material_slots + .iter() + .map(|s| { + ( + s.mesh.clone(), + s.material.clone(), + s.translation, + s.rotation_euler_deg, + s.scale, + ) + }) + .collect(); + (material_handles, slots) + }; + + // Update emissive for each material handle + for mh in material_handles { + if let Ok(mat) = engine.get_resource_mut::(&mh) { + let cur = mat.emissive; + mat.set_emissive_factor(cur * 100.0); + } + } + let mut entities: Vec = Vec::new(); + for (mesh_h, material_h, translation, rotation_deg, scale) in slots { + let entity = engine.create_entity(scene)?; + let transform = TransformComponent::builder() + .position(translation) + .rotation(rotation_deg) + .scale(scale) + .build(); + engine.add_component_to_entity(scene, entity, transform)?; + let mesh_render = MeshRenderingComponent::builder() + .mesh(&mesh_h) + .material(&material_h) + .build(); + engine.add_component_to_entity(scene, entity, mesh_render)?; + entities.push(entity); + } + Ok(entities) +} + +fn get_or_add_mesh(engine: &mut Engine, name: &str, path: &str) -> Result { + match engine.get_resource_handle::(name) { + Ok(handle) => Ok(handle), + Err(_) => { + let mesh = Mesh::new(name, path.into()); + engine.add_resource(mesh) + } + } +} + +fn pill_rotation_system(engine: &mut Engine) -> Result<()> { + let delta_time = engine.get_global_component::()?.delta_time; + let input_component = engine.get_global_component_mut::()?; + + // Exit on Escape + if input_component.get_key_pressed(KeyboardKey::Escape) { + // TODO: engine.request_shutdown(); + // TODO: standalone handle request_shutdown + std::process::exit(0); + } + + // Rotate pill if spacebar is not pressed + if !input_component.get_key_pressed(KeyboardKey::Space) { + for (_, transform_component, _) in + engine.iterate_two_components_mut::()? + { + transform_component.rotate_around_axis(90.0 * delta_time, Vector3f::new(0.0, 1.0, 0.0)); + } + } + + Ok(()) +} + +fn pick_random_target_opposite_hemisphere( + rng: &mut impl Rng, + cur_pos: Vector3f, + prev_dir: Vector3f, +) -> Vector3f { + let min_distance = 4.0; + let radius = 13.0; + let mut new_target = cur_pos; + + for _ in 0..20 { + let u: f32 = rng.gen(); + let v: f32 = rng.gen(); + let theta = 2.0 * std::f32::consts::PI * u; + let phi = v.acos(); + + let mut rand_dir = + Vector3f::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos()); + if rand_dir.dot(prev_dir) > 0.0 { + rand_dir = -rand_dir; + } + + let new_distance = rng.gen_range(min_distance..=radius); + new_target = cur_pos + rand_dir * new_distance; + + new_target.x = new_target.x.max(-12.0).min(12.0); + new_target.y = new_target.y.max(-8.0).min(8.0); + new_target.z = new_target.z.max(-3.0).min(20.0); + + if (new_target - cur_pos).magnitude() >= min_distance { + break; + } + } + + new_target +} + +fn motion_blur_movement_system(engine: &mut Engine) -> Result<()> { + let delta_time = engine.get_global_component::()?.delta_time; + let mut rng = rand::thread_rng(); + + for (_, transform_component, moving_pill_component) in + engine.iterate_two_components_mut::()? + { + let cur_pos = transform_component.position; + let last_dir_raw = cur_pos - moving_pill_component.target_position; + let last_dir = if last_dir_raw.magnitude2() > 0.0001 { + last_dir_raw.normalize() + } else { + Vector3f::new(0.0, 0.0, 1.0) + }; + + let direction_to_target = moving_pill_component.target_position - cur_pos; + let distance_to_target = direction_to_target.magnitude(); + let step = moving_pill_component.velocity * delta_time; + + if distance_to_target <= step { + transform_component.set_position(moving_pill_component.target_position); + let reached_pos = moving_pill_component.target_position; + + moving_pill_component.target_position = + pick_random_target_opposite_hemisphere(&mut rng, reached_pos, last_dir); + moving_pill_component.velocity = rng.gen_range(8.0..=25.0); + continue; + } + + if distance_to_target > 0.0 { + let normalized_direction = direction_to_target / distance_to_target; + let movement = normalized_direction * step; + let new_position = cur_pos + movement; + transform_component.set_position(new_position); + } + + // Update rotation + // transform_component.rotate_around_axis( + // moving_pill_component.rotation_speed * delta_time, + // Vector3f::new(0.0, 1.0, 0.0), + // ); + } + + Ok(()) +} diff --git a/examples/pill_demo/src/lib.rs b/examples/pill_demo/src/lib.rs new file mode 100644 index 00000000..c0d7b224 --- /dev/null +++ b/examples/pill_demo/src/lib.rs @@ -0,0 +1,4 @@ +mod game; +use pill_engine::game::create_game; + +create_game!(crate::game::Game {}, pill_engine::game::PillGame); \ No newline at end of file