diff options
Diffstat (limited to 'engine/src')
| -rw-r--r-- | engine/src/asset.rs | 168 | ||||
| -rw-r--r-- | engine/src/data_types/matrix.rs | 170 | ||||
| -rw-r--r-- | engine/src/data_types/vector.rs | 141 | ||||
| -rw-r--r-- | engine/src/lib.rs | 5 | ||||
| -rw-r--r-- | engine/src/material.rs | 40 | ||||
| -rw-r--r-- | engine/src/model.rs | 41 | ||||
| -rw-r--r-- | engine/src/opengl/glsl.rs | 616 | ||||
| -rw-r--r-- | engine/src/opengl/mod.rs | 1 | ||||
| -rw-r--r-- | engine/src/renderer.rs | 299 | ||||
| -rw-r--r-- | engine/src/renderer/object.rs | 13 | ||||
| -rw-r--r-- | engine/src/renderer/opengl.rs | 769 | ||||
| -rw-r--r-- | engine/src/renderer/opengl/glsl/fragment.glsl | 73 | ||||
| -rw-r--r-- | engine/src/renderer/opengl/glsl/light.glsl | 133 | ||||
| -rw-r--r-- | engine/src/renderer/opengl/glsl/vertex.glsl | 24 | ||||
| -rw-r--r-- | engine/src/renderer/opengl/glsl/vertex_data.glsl | 11 | ||||
| -rw-r--r-- | engine/src/renderer/opengl/graphics_mesh.rs | 2 | ||||
| -rw-r--r-- | engine/src/shader.rs | 818 | ||||
| -rw-r--r-- | engine/src/shader/cursor.rs | 160 | ||||
| -rw-r--r-- | engine/src/shader/default.rs | 363 | ||||
| -rw-r--r-- | engine/src/util.rs | 3 |
20 files changed, 2258 insertions, 1592 deletions
diff --git a/engine/src/asset.rs b/engine/src/asset.rs index e78b6d8..b089b73 100644 --- a/engine/src/asset.rs +++ b/engine/src/asset.rs @@ -15,12 +15,24 @@ use std::sync::mpsc::{ channel as mpsc_channel, }; -use ecs::Sole; -use ecs::phase::PRE_UPDATE as PRE_UPDATE_PHASE; +use ecs::pair::{ChildOf, Pair}; +use ecs::phase::{PRE_UPDATE as PRE_UPDATE_PHASE, Phase}; use ecs::sole::Single; +use ecs::{Sole, declare_entity}; use crate::work_queue::{Work, WorkQueue}; +declare_entity!( + pub HANDLE_ASSETS_PHASE, + ( + Phase, + Pair::builder() + .relation::<ChildOf>() + .target_id(*PRE_UPDATE_PHASE) + .build() + ) +); + /// Asset label. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Label<'a> @@ -125,6 +137,7 @@ pub struct Assets import_work_queue: WorkQueue<ImportWorkUserData>, import_work_msg_receiver: MpscReceiver<ImportWorkMessage>, import_work_msg_sender: MpscSender<ImportWorkMessage>, + events: Events, } impl Assets @@ -142,6 +155,7 @@ impl Assets import_work_queue: WorkQueue::new(), import_work_msg_receiver, import_work_msg_sender, + events: Events::default(), } } @@ -173,13 +187,15 @@ impl Assets where 'this: 'handle, { - let LookupEntry::Occupied(asset_index) = - *self.asset_lookup.borrow().get(&handle.id.label_hash)? + let asset_lookup = self.asset_lookup.borrow(); + + let LookupEntry::Occupied(asset_index, _) = + asset_lookup.get(&handle.id.label_hash)? else { return None; }; - let stored_asset = self.assets.get(asset_index).expect("Not possible"); + let stored_asset = self.assets.get(*asset_index).expect("Not possible"); let Some(asset) = stored_asset.strong.downcast_ref::<Asset>() else { tracing::error!("Wrong asset type"); @@ -189,6 +205,68 @@ impl Assets Some(asset) } + #[tracing::instrument(skip_all, fields(asset_type=type_name::<Asset>()))] + pub fn get_handle_to_loaded<'label, Asset: 'static + Send + Sync>( + &self, + label: impl Into<Label<'label>>, + ) -> Option<Handle<Asset>> + { + let label = label.into(); + + let label_hash = LabelHash::new(&label); + + let asset_lookup = self.asset_lookup.borrow(); + + let LookupEntry::Occupied(asset_index, _) = asset_lookup.get(&label_hash)? else { + return None; + }; + + let stored_asset = self.assets.get(*asset_index).expect("Not possible"); + + if stored_asset.strong.downcast_ref::<Asset>().is_none() { + tracing::error!("Wrong asset type"); + return None; + }; + + Some(Handle::new(label_hash)) + } + + pub fn is_loaded_and_has_type<Asset: 'static + Send + Sync>( + &self, + handle: &Handle<Asset>, + ) -> bool + { + let asset_lookup = self.asset_lookup.borrow(); + + let Some(LookupEntry::Occupied(asset_index, _)) = + asset_lookup.get(&handle.id.label_hash) + else { + return false; + }; + + let stored_asset = self.assets.get(*asset_index).expect("Not possible"); + + stored_asset.strong.downcast_ref::<Asset>().is_some() + } + + pub fn get_label<Asset: 'static + Send + Sync>( + &self, + handle: &Handle<Asset>, + ) -> Option<LabelOwned> + { + let lookup_entry = self + .asset_lookup + .borrow() + .get(&handle.id.label_hash)? + .clone(); + + let LookupEntry::Occupied(_, label) = lookup_entry else { + return None; + }; + + Some(label) + } + #[tracing::instrument(skip(self))] pub fn load<'i, Asset: 'static + Send + Sync>( &self, @@ -216,9 +294,9 @@ impl Assets return Handle::new(label_hash); }; - match *lookup_entry { - LookupEntry::Occupied(asset_index) => { - let stored_asset = self.assets.get(asset_index).expect("Not possible"); + match lookup_entry { + LookupEntry::Occupied(asset_index, _) => { + let stored_asset = self.assets.get(*asset_index).expect("Not possible"); if stored_asset.strong.downcast_ref::<Asset>().is_none() { tracing::error!("Wrong asset type {}", type_name::<Asset>()); @@ -261,9 +339,9 @@ impl Assets return Handle::new(label_hash); }; - match *lookup_entry { - LookupEntry::Occupied(asset_index) => { - let stored_asset = self.assets.get(asset_index).expect("Not possible"); + match lookup_entry { + LookupEntry::Occupied(asset_index, _) => { + let stored_asset = self.assets.get(*asset_index).expect("Not possible"); if stored_asset.strong.downcast_ref::<Asset>().is_none() { tracing::error!( @@ -323,7 +401,7 @@ impl Assets if matches!( self.asset_lookup.get_mut().get(&label_hash), - Some(LookupEntry::Occupied(_)) + Some(LookupEntry::Occupied(_, _)) ) { tracing::error!("Asset already exists"); @@ -338,11 +416,15 @@ impl Assets self.asset_lookup .get_mut() - .insert(label_hash, LookupEntry::Occupied(index)); + .insert(label_hash, LookupEntry::Occupied(index, label.to_owned())); if label.name.is_some() { - let parent_asset_label_hash = - LabelHash::new(&Label { path: label.path, name: None }); + let parent_asset_label = Label { + path: label.path.as_ref().into(), + name: None, + }; + + let parent_asset_label_hash = LabelHash::new(&parent_asset_label); if matches!( self.asset_lookup.get_mut().get(&parent_asset_label_hash), @@ -360,14 +442,26 @@ impl Assets self.asset_lookup.get_mut().insert( parent_asset_label_hash, - LookupEntry::Occupied(self.assets.len() - 1), + LookupEntry::Occupied( + self.assets.len() - 1, + parent_asset_label.to_owned(), + ), ); } } + self.events + .curr_tick_events + .push(Event::Stored(Id { label_hash }, label.to_owned())); + Handle::new(label_hash) } + pub fn events(&self) -> &Events + { + &self.events + } + fn is_pending(asset_lookup: &HashMap<LabelHash, LookupEntry>, label: &Label) -> bool { if label.name.is_some() { @@ -587,6 +681,11 @@ pub struct Handle<Asset: 'static> impl<Asset: 'static> Handle<Asset> { + pub fn from_id(id: Id) -> Self + { + Self { id, _pd: PhantomData } + } + pub fn id(&self) -> Id { self.id @@ -616,6 +715,29 @@ pub struct Id label_hash: LabelHash, } +#[derive(Debug, Default)] +pub struct Events +{ + curr_tick_events: Vec<Event>, + last_tick_events: Vec<Event>, +} + +impl Events +{ + pub fn last_tick_events(&self) -> impl Iterator<Item = &Event> + { + self.last_tick_events.iter() + } +} + +/// Asset event. +#[derive(Debug)] +pub enum Event +{ + /// Asset stored. + Stored(Id, LabelOwned), +} + #[derive(Debug, thiserror::Error)] enum ImporterError { @@ -718,12 +840,20 @@ impl ecs::extension::Extension for Extension { let _ = collector.add_sole(self.assets); - collector.add_system(*PRE_UPDATE_PHASE, add_received_assets); + collector.add_declared_entity(&HANDLE_ASSETS_PHASE); + + collector.add_system(*HANDLE_ASSETS_PHASE, add_received_assets); } } fn add_received_assets(mut assets: Single<Assets>) { + let Events { curr_tick_events, last_tick_events } = &mut assets.events; + + std::mem::swap(last_tick_events, curr_tick_events); + + curr_tick_events.clear(); + while let Some(import_work_msg) = assets.import_work_msg_receiver.try_recv().ok() { match import_work_msg { ImportWorkMessage::Store { do_store, label, asset } => { @@ -770,10 +900,10 @@ enum ImportWorkMessage }, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] enum LookupEntry { - Occupied(usize), + Occupied(usize, LabelOwned), Pending, } diff --git a/engine/src/data_types/matrix.rs b/engine/src/data_types/matrix.rs index b754b62..39aeea0 100644 --- a/engine/src/data_types/matrix.rs +++ b/engine/src/data_types/matrix.rs @@ -1,4 +1,6 @@ -use crate::vector::Vec3; +use std::ops::Mul; + +use crate::vector::{Vec3, Vec4, VecN}; #[derive(Debug, Clone)] pub struct Matrix<Value, const ROWS: usize, const COLUMNS: usize> @@ -20,17 +22,24 @@ impl<Value, const ROWS: usize, const COLUMNS: usize> Matrix<Value, ROWS, COLUMNS } } + pub fn from_columns<ColumnVec>(columns: [ColumnVec; COLUMNS]) -> Self + where + ColumnVec: VecN<Value, ROWS>, + { + Self { + items: columns.map(|column| column.into_array()), + } + } + /// Sets the value at the specified cell. pub fn set_cell(&mut self, row: usize, column: usize, value: Value) { self.items[column][row] = value; } - /// Returns the internal 2D array as a pointer. - #[must_use] - pub fn as_ptr(&self) -> *const Value + pub fn items(&self) -> &[[Value; ROWS]; COLUMNS] { - self.items[0].as_ptr() + &self.items } } @@ -119,4 +128,155 @@ impl Matrix<f32, 4, 4> self.set_cell(3, 3, 1.0); } + + pub fn inverse(&self) -> Self + { + let coef_00 = + self.items[2][2] * self.items[3][3] - self.items[3][2] * self.items[2][3]; + let coef_02 = + self.items[1][2] * self.items[3][3] - self.items[3][2] * self.items[1][3]; + let coef_03 = + self.items[1][2] * self.items[2][3] - self.items[2][2] * self.items[1][3]; + + let coef_04 = + self.items[2][1] * self.items[3][3] - self.items[3][1] * self.items[2][3]; + let coef_06 = + self.items[1][1] * self.items[3][3] - self.items[3][1] * self.items[1][3]; + let coef_07 = + self.items[1][1] * self.items[2][3] - self.items[2][1] * self.items[1][3]; + + let coef_08 = + self.items[2][1] * self.items[3][2] - self.items[3][1] * self.items[2][2]; + let coef_10 = + self.items[1][1] * self.items[3][2] - self.items[3][1] * self.items[1][2]; + let coef_11 = + self.items[1][1] * self.items[2][2] - self.items[2][1] * self.items[1][2]; + + let coef_12 = + self.items[2][0] * self.items[3][3] - self.items[3][0] * self.items[2][3]; + let coef_14 = + self.items[1][0] * self.items[3][3] - self.items[3][0] * self.items[1][3]; + let coef_15 = + self.items[1][0] * self.items[2][3] - self.items[2][0] * self.items[1][3]; + + let coef_16 = + self.items[2][0] * self.items[3][2] - self.items[3][0] * self.items[2][2]; + let coef_18 = + self.items[1][0] * self.items[3][2] - self.items[3][0] * self.items[1][2]; + let coef_19 = + self.items[1][0] * self.items[2][2] - self.items[2][0] * self.items[1][2]; + + let coef_20 = + self.items[2][0] * self.items[3][1] - self.items[3][0] * self.items[2][1]; + let coef_22 = + self.items[1][0] * self.items[3][1] - self.items[3][0] * self.items[1][1]; + let coef_23 = + self.items[1][0] * self.items[2][1] - self.items[2][0] * self.items[1][1]; + + let fac_0 = Vec4 { + x: coef_00, + y: coef_00, + z: coef_02, + w: coef_03, + }; + let fac_1 = Vec4 { + x: coef_04, + y: coef_04, + z: coef_06, + w: coef_07, + }; + let fac_2 = Vec4 { + x: coef_08, + y: coef_08, + z: coef_10, + w: coef_11, + }; + let fac_3 = Vec4 { + x: coef_12, + y: coef_12, + z: coef_14, + w: coef_15, + }; + let fac_4 = Vec4 { + x: coef_16, + y: coef_16, + z: coef_18, + w: coef_19, + }; + let fac_5 = Vec4 { + x: coef_20, + y: coef_20, + z: coef_22, + w: coef_23, + }; + + let vec_0 = Vec4 { + x: self.items[1][0], + y: self.items[0][0], + z: self.items[0][0], + w: self.items[0][0], + }; + let vec_1 = Vec4 { + x: self.items[1][1], + y: self.items[0][1], + z: self.items[0][1], + w: self.items[0][1], + }; + let vec_2 = Vec4 { + x: self.items[1][2], + y: self.items[0][2], + z: self.items[0][2], + w: self.items[0][2], + }; + let vec_3 = Vec4 { + x: self.items[1][3], + y: self.items[0][3], + z: self.items[0][3], + w: self.items[0][3], + }; + + let inv_0 = vec_1 * fac_0 - vec_2 * fac_1 + vec_3 * fac_2; + let inv_1 = vec_0 * fac_0 - vec_2 * fac_3 + vec_3 * fac_4; + let inv_2 = vec_0 * fac_1 - vec_1 * fac_3 + vec_3 * fac_5; + let inv_3 = vec_0 * fac_2 - vec_1 * fac_4 + vec_2 * fac_5; + + let sign_a = Vec4 { x: 1.0, y: -1.0, z: 1.0, w: -1.0 }; + let sign_b = Vec4 { x: -1.0, y: 1.0, z: -1.0, w: 1.0 }; + + let inverse = Self::from_columns([ + inv_0 * sign_a, + inv_1 * sign_b, + inv_2 * sign_a, + inv_3 * sign_b, + ]); + + let row_0 = Vec4 { + x: inverse.items[0][0], + y: inverse.items[1][0], + z: inverse.items[2][0], + w: inverse.items[3][0], + }; + + let dot_0 = Vec4::<f32>::from(self.items[0]) * row_0; + + let dot_1 = (dot_0.x + dot_0.y) + (dot_0.z + dot_0.w); + + let one_over_determinant = 1.0 / dot_1; + + inverse * one_over_determinant + } +} + +impl Mul<f32> for Matrix<f32, 4, 4> +{ + type Output = Self; + + fn mul(self, scalar: f32) -> Self::Output + { + Self { + items: self + .items + .map(|column| (Vec4::from(column) * scalar).into_array()), + } + } } diff --git a/engine/src/data_types/vector.rs b/engine/src/data_types/vector.rs index dc6df30..1a4e49e 100644 --- a/engine/src/data_types/vector.rs +++ b/engine/src/data_types/vector.rs @@ -2,6 +2,12 @@ use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign}; use crate::color::Color; +/// A vector of `Value`s with `N` number of elements. +pub trait VecN<Value, const N: usize>: sealed::Sealed +{ + fn into_array(self) -> [Value; N]; +} + #[derive(Debug, Default, Clone, Copy, PartialEq)] pub struct Vec2<Value> { @@ -97,6 +103,16 @@ where } } +impl<Value> VecN<Value, 2> for Vec2<Value> +{ + fn into_array(self) -> [Value; 2] + { + [self.x, self.y] + } +} + +impl<Value> sealed::Sealed for Vec2<Value> {} + #[derive(Debug, Default, Clone, Copy, PartialEq)] pub struct Vec3<Value> { @@ -365,3 +381,128 @@ impl<Value> From<Color<Value>> for Vec3<Value> } } } + +impl<Value> VecN<Value, 3> for Vec3<Value> +{ + fn into_array(self) -> [Value; 3] + { + [self.x, self.y, self.z] + } +} + +impl<Value> sealed::Sealed for Vec3<Value> {} + +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub struct Vec4<Value> +{ + pub x: Value, + pub y: Value, + pub z: Value, + pub w: Value, +} + +impl<Value> Mul for Vec4<Value> +where + Value: Mul<Value, Output = Value>, +{ + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output + { + Self::Output { + x: self.x * rhs.x, + y: self.y * rhs.y, + z: self.z * rhs.z, + w: self.w * rhs.w, + } + } +} + +impl<Value> Add for Vec4<Value> +where + Value: Add<Value, Output = Value>, +{ + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output + { + Self::Output { + x: self.x + rhs.x, + y: self.y + rhs.y, + z: self.z + rhs.z, + w: self.w + rhs.w, + } + } +} + +impl<Value> Sub for Vec4<Value> +where + Value: Sub<Value, Output = Value>, +{ + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output + { + Self::Output { + x: self.x - rhs.x, + y: self.y - rhs.y, + z: self.z - rhs.z, + w: self.w - rhs.w, + } + } +} + +impl<Value> Mul<Value> for Vec4<Value> +where + Value: Mul<Value, Output = Value> + Clone, +{ + type Output = Self; + + fn mul(mut self, rhs: Value) -> Self::Output + { + self.x = self.x * rhs.clone(); + self.y = self.y * rhs.clone(); + self.z = self.z * rhs.clone(); + self.w = self.w * rhs.clone(); + + self + } +} + +impl<Value: Clone> From<Value> for Vec4<Value> +{ + fn from(value: Value) -> Self + { + Self { + x: value.clone(), + y: value.clone(), + z: value.clone(), + w: value, + } + } +} + +impl<Value> From<[Value; 4]> for Vec4<Value> +{ + fn from(values: [Value; 4]) -> Self + { + let [x, y, z, w] = values; + + Self { x, y, z, w } + } +} + +impl<Value> VecN<Value, 4> for Vec4<Value> +{ + fn into_array(self) -> [Value; 4] + { + [self.x, self.y, self.z, self.w] + } +} + +impl<Value> sealed::Sealed for Vec4<Value> {} + +mod sealed +{ + pub trait Sealed {} +} diff --git a/engine/src/lib.rs b/engine/src/lib.rs index b470cdc..560d288 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -13,8 +13,8 @@ use ecs::{SoleAlreadyExistsError, World}; use crate::asset::{Assets, Extension as AssetExtension}; use crate::delta_time::{DeltaTime, LastUpdate, update as update_delta_time}; +use crate::shader::Extension as ShaderExtension; -mod opengl; mod util; mod work_queue; @@ -35,6 +35,7 @@ pub mod model; pub mod projection; pub mod reflection; pub mod renderer; +pub mod shader; pub mod texture; pub mod transform; pub mod windowing; @@ -76,9 +77,11 @@ impl Engine crate::model::asset::add_importers(&mut assets); crate::material::asset::add_importers(&mut assets); + crate::shader::add_asset_importers(&mut assets); crate::image::set_asset_importers(&mut assets); world.add_extension(AssetExtension { assets }); + world.add_extension(ShaderExtension); Self { world } } diff --git a/engine/src/material.rs b/engine/src/material.rs index 2b9a8ca..94ab24e 100644 --- a/engine/src/material.rs +++ b/engine/src/material.rs @@ -21,9 +21,9 @@ pub struct Material impl Material { - pub fn builder() -> Builder + pub const fn builder() -> Builder { - Builder::default() + Builder::new() } } @@ -51,7 +51,7 @@ pub struct Builder impl Builder { #[must_use] - pub fn new() -> Self + pub const fn new() -> Self { Self { ambient: Color::WHITE_F32, @@ -125,7 +125,7 @@ impl Builder /// # Panics /// Will panic if no ambient map, diffuse map or specular map is set. #[must_use] - pub fn build(self) -> Material + pub const fn build(self) -> Material { Material { ambient: self.ambient, @@ -149,8 +149,8 @@ impl Default for Builder builder! { /// Material flags. -#[builder(name = FlagsBuilder, derives = (Debug, Default, Clone))] -#[derive(Debug, Default, Clone, Component)] +#[builder(name = FlagsBuilder, derives = (Debug, Clone))] +#[derive(Debug, Clone, Component)] #[non_exhaustive] pub struct Flags { @@ -163,8 +163,32 @@ pub struct Flags impl Flags { #[must_use] - pub fn builder() -> FlagsBuilder + pub const fn builder() -> FlagsBuilder { - FlagsBuilder::default() + FlagsBuilder::new() + } +} + +impl Default for Flags +{ + fn default() -> Self + { + Self::builder().build() + } +} + +impl FlagsBuilder +{ + pub const fn new() -> Self + { + Self { use_ambient_color: false } + } +} + +impl Default for FlagsBuilder +{ + fn default() -> Self + { + Self::new() } } diff --git a/engine/src/model.rs b/engine/src/model.rs index 4c88f8f..ebf623f 100644 --- a/engine/src/model.rs +++ b/engine/src/model.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use ecs::Component; -use crate::asset::Handle as AssetHandle; +use crate::asset::{Assets, Handle as AssetHandle}; use crate::material::Material; use crate::material::asset::Map as MaterialAssetMap; use crate::mesh::Mesh; @@ -40,6 +40,38 @@ impl Spec { SpecBuilder::default() } + + pub fn find_first_material<'assets>( + &'assets self, + assets: &'assets Assets, + ) -> MaterialSearchResult<'assets> + { + let Some(material_name) = self.material_names.first() else { + return MaterialSearchResult::NoMaterials; + }; + + let material_asset = match &self.materials { + Materials::Maps(material_asset_map_assets) => material_asset_map_assets + .iter() + .find_map(|mat_asset_map_asset| { + let mat_asset_map = assets.get(mat_asset_map_asset)?; + + mat_asset_map.assets.get(material_name) + }), + Materials::Direct(material_assets) => material_assets.get(material_name), + }; + + let Some(material_asset) = material_asset else { + return MaterialSearchResult::NotFound; + }; + + if assets.get(material_asset).is_none() { + tracing::trace!("Missing material asset"); + return MaterialSearchResult::NotFound; + } + + MaterialSearchResult::Found(material_asset) + } } #[derive(Debug, Default, Clone)] @@ -152,3 +184,10 @@ impl Default for Materials Self::Maps(Vec::new()) } } + +pub enum MaterialSearchResult<'a> +{ + Found(&'a AssetHandle<Material>), + NotFound, + NoMaterials, +} diff --git a/engine/src/opengl/glsl.rs b/engine/src/opengl/glsl.rs deleted file mode 100644 index 6fd5638..0000000 --- a/engine/src/opengl/glsl.rs +++ /dev/null @@ -1,616 +0,0 @@ -use std::borrow::Cow; -use std::fmt::{Display, Formatter}; -use std::path::{Path, PathBuf}; -use std::string::FromUtf8Error; - -const PREINCLUDE_DIRECTIVE: &str = "#preinclude"; - -pub fn preprocess<'content>( - shader_content: impl Into<Cow<'content, str>>, - read_file: &impl Fn(&Path) -> Result<Vec<u8>, std::io::Error>, -) -> Result<Cow<'content, str>, PreprocessingError> -{ - do_preprocess(shader_content, SpanPath::Original, read_file) -} - -fn do_preprocess<'content>( - shader_content: impl Into<Cow<'content, str>>, - shader_path: SpanPath<'_>, - read_file: &impl Fn(&Path) -> Result<Vec<u8>, std::io::Error>, -) -> Result<Cow<'content, str>, PreprocessingError> -{ - let shader_content = shader_content.into(); - - let mut preincludes = shader_content - .match_indices(PREINCLUDE_DIRECTIVE) - .peekable(); - - if preincludes.peek().is_none() { - // Shader content contains no preincludes - return Ok(shader_content.into()); - }; - - let mut preprocessed = shader_content.to_string(); - - let mut curr = shader_content.find(PREINCLUDE_DIRECTIVE); - - let mut last_start = 0; - let mut span_line_offset = 0; - - while let Some(preinclude_start) = curr { - let replacement_job = handle_preinclude( - &preprocessed, - &shader_path, - preinclude_start, - span_line_offset, - )?; - - let path = replacement_job.path.clone(); - - let mut included = - String::from_utf8(read_file(&replacement_job.path).map_err(|err| { - PreprocessingError::ReadIncludedShaderFailed { - source: err, - path: replacement_job.path.clone(), - } - })?) - .map_err(|err| { - PreprocessingError::IncludedShaderInvalidUtf8 { - source: err, - path: path.clone(), - } - })?; - - if let Some(first_line) = included.lines().next() { - if first_line.starts_with("#version") { - included = included - .chars() - .skip_while(|character| *character != '\n') - .collect(); - } - } - - let included_preprocessed = do_preprocess( - &included, - SpanPath::Path(replacement_job.path.as_path().into()), - read_file, - )?; - - let start = replacement_job.start_index; - let end = replacement_job.end_index; - - preprocessed.replace_range(start..end, &included_preprocessed); - - curr = preprocessed[last_start + 1..] - .find(PREINCLUDE_DIRECTIVE) - .map(|index| index + 1); - - last_start = preinclude_start + included_preprocessed.len(); - - span_line_offset += included_preprocessed.lines().count(); - } - - Ok(preprocessed.into()) -} - -fn handle_preinclude( - shader_content: &str, - shader_path: &SpanPath<'_>, - preinclude_start_index: usize, - span_line_offset: usize, -) -> Result<ReplacementJob, PreprocessingError> -{ - let expect_token = |token: char, index: usize| { - let token_found = shader_content.chars().nth(index).ok_or_else(|| { - PreprocessingError::ExpectedToken { - expected: token, - span: Span::new( - shader_content, - shader_path.to_owned(), - index, - span_line_offset, - preinclude_start_index, - ), - } - })?; - - if token_found != token { - return Err(PreprocessingError::InvalidToken { - expected: token, - found: token_found, - span: Span::new( - shader_content, - shader_path.to_owned(), - index, - span_line_offset, - preinclude_start_index, - ), - }); - } - - Ok(()) - }; - - let space_index = preinclude_start_index + PREINCLUDE_DIRECTIVE.len(); - let quote_open_index = space_index + 1; - - expect_token(' ', space_index)?; - expect_token('"', quote_open_index)?; - - let buf = shader_content[quote_open_index + 1..] - .chars() - .take_while(|character| *character != '"') - .map(|character| character as u8) - .collect::<Vec<_>>(); - - if buf.is_empty() { - return Err(PreprocessingError::ExpectedToken { - expected: '"', - span: Span::new( - shader_content, - shader_path.to_owned(), - shader_content.len() - 1, - span_line_offset, - preinclude_start_index, - ), - }); - } - - let path_len = buf.len(); - - let path = PathBuf::from(String::from_utf8(buf).map_err(|err| { - PreprocessingError::PreincludePathInvalidUtf8 { - source: err, - span: Span::new( - shader_content, - shader_path.to_owned(), - quote_open_index + 1, - span_line_offset, - preinclude_start_index, - ), - } - })?); - - Ok(ReplacementJob { - start_index: preinclude_start_index, - end_index: quote_open_index + 1 + path_len + 1, - path, - }) -} - -struct ReplacementJob -{ - start_index: usize, - end_index: usize, - path: PathBuf, -} - -#[derive(Debug, thiserror::Error)] -pub enum PreprocessingError -{ - #[error( - "Invalid token at line {}, column {} of {}. Expected '{}', found '{}'", - span.line, - span.column, - span.path, - expected, - found - )] - InvalidToken - { - expected: char, - found: char, - span: Span, - }, - - #[error( - "Expected token '{}' at line {}, column {} of {}. Found eof", - expected, - span.line, - span.column, - span.path - )] - ExpectedToken - { - expected: char, span: Span - }, - - #[error( - "Preinclude path at line {}, column {} of {} is invalid UTF-8", - span.line, - span.column, - span.path - )] - PreincludePathInvalidUtf8 - { - #[source] - source: FromUtf8Error, - span: Span, - }, - - #[error("Failed to read included shader")] - ReadIncludedShaderFailed - { - #[source] - source: std::io::Error, - path: PathBuf, - }, - - #[error("Included shader is not valid UTF-8")] - IncludedShaderInvalidUtf8 - { - #[source] - source: FromUtf8Error, - path: PathBuf, - }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] -pub struct Span -{ - pub line: usize, - pub column: usize, - pub path: SpanPath<'static>, -} - -impl Span -{ - fn new( - file_content: &str, - path: SpanPath<'static>, - char_index: usize, - line_offset: usize, - line_start_index: usize, - ) -> Self - { - let line = find_line_of_index(file_content, char_index) + 1 - - line_offset.saturating_sub(1); - - Self { - line, - column: char_index - line_start_index + 1, - path, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SpanPath<'a> -{ - Original, - Path(Cow<'a, Path>), -} - -impl<'a> SpanPath<'a> -{ - fn to_owned(&self) -> SpanPath<'static> - { - match self { - Self::Original => SpanPath::Original, - Self::Path(path) => SpanPath::Path(Cow::Owned(path.to_path_buf().into())), - } - } -} - -impl<'a> Display for SpanPath<'a> -{ - fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result - { - match self { - Self::Original => write!(formatter, "original file"), - Self::Path(path) => write!(formatter, "file {}", path.display()), - } - } -} - -impl<'a, PathLike> PartialEq<PathLike> for SpanPath<'a> -where - PathLike: AsRef<Path>, -{ - fn eq(&self, other: &PathLike) -> bool - { - match self { - Self::Original => false, - Self::Path(path) => path == other.as_ref(), - } - } -} - -fn find_line_of_index(text: &str, index: usize) -> usize -{ - text.chars() - .take(index + 1) - .enumerate() - .filter(|(_, character)| *character == '\n') - .count() -} - -#[cfg(test)] -mod tests -{ - use std::ffi::OsStr; - use std::path::Path; - - use super::{preprocess, PreprocessingError}; - use crate::opengl::glsl::SpanPath; - - #[test] - fn preprocess_no_directives_is_same() - { - assert_eq!( - preprocess("#version 330 core\n", &|_| { unreachable!() }).unwrap(), - "#version 330 core\n" - ); - } - - #[test] - fn preprocess_with_directives_works() - { - assert_eq!( - preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"foo.glsl\"\n", - "\n", - "void main() {}", - ), - &|_| { Ok(b"out vec4 FragColor;".to_vec()) } - ) - .unwrap(), - concat!( - "#version 330 core\n", - "\n", - "out vec4 FragColor;\n", - "\n", - "void main() {}", - ) - ); - - assert_eq!( - preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ), - &|_| { Ok(b"out vec4 FragColor;".to_vec()) } - ) - .unwrap(), - concat!( - "#version 330 core\n", - "\n", - "out vec4 FragColor;\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ) - ); - - assert_eq!( - preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "#preinclude \"foo.glsl\"\n", - "\n", - "void main() {}", - ), - &|path| { - if path == OsStr::new("bar.glsl") { - Ok(b"out vec4 FragColor;".to_vec()) - } else { - Ok(concat!( - "uniform sampler2D input_texture;\n", - "in vec2 in_texture_coords;" - ) - .as_bytes() - .to_vec()) - } - }, - ) - .unwrap(), - concat!( - "#version 330 core\n", - "\n", - "out vec4 FragColor;\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "uniform sampler2D input_texture;\n", - "in vec2 in_texture_coords;\n", - "\n", - "void main() {}", - ) - ); - } - - #[test] - fn preprocess_invalid_directive_does_not_work() - { - let res = preprocess( - concat!( - "#version 330 core\n", - "\n", - // Missing " - "#preinclude foo.glsl\"\n", - "\n", - "void main() {}", - ), - &|_| Ok(b"out vec4 FragColor;".to_vec()), - ); - - let Err(PreprocessingError::InvalidToken { expected, found, span }) = res else { - panic!( - "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}" - ); - }; - - assert_eq!(expected, '"'); - assert_eq!(found, 'f'); - assert_eq!(span.line, 3); - assert_eq!(span.column, 13); - assert_eq!(span.path, SpanPath::Original); - } - - #[test] - fn preprocess_error_has_correct_span() - { - let res = preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "#preinclude \"foo.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ), - &|path| { - if path == OsStr::new("bar.glsl") { - Ok(concat!( - "out vec4 FragColor;\n", - "in vec2 in_texture_coords;\n", - "in float foo;" - ) - .as_bytes() - .to_vec()) - } else if path == OsStr::new("foo.glsl") { - Ok(concat!( - "uniform sampler2D input_texture;\n", - "\n", - // Missing space before first " - "#preinclude\"shared_types.glsl\"\n", - ) - .as_bytes() - .to_vec()) - } else { - panic!(concat!( - "Expected read function to be called with ", - "either path bar.glsl or foo.glsl" - )); - } - }, - ); - - let Err(PreprocessingError::InvalidToken { expected, found, span }) = res else { - panic!( - "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}" - ); - }; - - assert_eq!(expected, ' '); - assert_eq!(found, '"'); - assert_eq!(span.line, 3); - assert_eq!(span.column, 12); - assert_eq!(span.path, SpanPath::Path(Path::new("foo.glsl").into())); - } - - #[test] - fn preprocess_included_shader_with_include_works() - { - assert_eq!( - preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ), - &|path| { - if path == OsStr::new("bar.glsl") { - Ok(concat!( - "#preinclude \"foo.glsl\"\n", - "\n", - "out vec4 FragColor;" - ) - .as_bytes() - .to_vec()) - } else { - Ok(concat!( - "uniform sampler2D input_texture;\n", - "in vec2 in_texture_coords;" - ) - .as_bytes() - .to_vec()) - } - } - ) - .unwrap(), - concat!( - "#version 330 core\n", - "\n", - "uniform sampler2D input_texture;\n", - "in vec2 in_texture_coords;\n", - "\n", - "out vec4 FragColor;\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ) - ); - } - - #[test] - fn preprocess_included_shader_with_include_error_span_is_correct() - { - let res = preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ), - &|path| { - if path == OsStr::new("bar.glsl") { - Ok(concat!( - // ' instead of " - "#preinclude 'foo.glsl\"\n", - "\n", - "out vec4 FragColor;" - ) - .as_bytes() - .to_vec()) - } else { - Ok(concat!( - "uniform sampler2D input_texture;\n", - "in vec2 in_texture_coords;" - ) - .as_bytes() - .to_vec()) - } - }, - ); - - let Err(PreprocessingError::InvalidToken { expected, found, span }) = res else { - panic!( - "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}" - ); - }; - - assert_eq!(expected, '"'); - assert_eq!(found, '\''); - assert_eq!(span.line, 1); - assert_eq!(span.column, 13); - assert_eq!(span.path, Path::new("bar.glsl")); - } -} diff --git a/engine/src/opengl/mod.rs b/engine/src/opengl/mod.rs deleted file mode 100644 index 2208ac6..0000000 --- a/engine/src/opengl/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod glsl; diff --git a/engine/src/renderer.rs b/engine/src/renderer.rs index 2a66a68..6c20102 100644 --- a/engine/src/renderer.rs +++ b/engine/src/renderer.rs @@ -1,5 +1,7 @@ use std::any::type_name; use std::collections::VecDeque; +use std::path::Path; +use std::sync::LazyLock; use std::sync::atomic::{AtomicU64, Ordering}; use bitflags::bitflags; @@ -10,18 +12,28 @@ use ecs::query::term::Without; use ecs::sole::Single; use ecs::{Component, Query, declare_entity}; -use crate::asset::{Assets, Handle as AssetHandle}; +use crate::asset::{Assets, Handle as AssetHandle, Label as AssetLabel}; use crate::builder; -use crate::camera::{Active as ActiveCamera, Camera}; +use crate::color::Color; use crate::data_types::dimens::Dimens; use crate::draw_flags::{DrawFlags, NoDraw, PolygonModeConfig}; -use crate::lighting::{DirectionalLight, GlobalLight, PointLight}; -use crate::material::{Flags as MaterialFlags, Material}; +use crate::image::Image; use crate::mesh::Mesh; -use crate::model::{Materials as ModelMaterials, Model, Spec as ModelSpec}; +use crate::model::{MaterialSearchResult, Model}; use crate::renderer::object::{Id as ObjectId, Store as ObjectStore}; -use crate::texture::Texture; -use crate::transform::{Scale, Transform, WorldPosition}; +use crate::shader::cursor::{ + BindingLocation as ShaderBindingLocation, + BindingValue as ShaderBindingValue, + Cursor as ShaderCursor, +}; +use crate::shader::default::ASSET_LABEL as DEFAULT_SHADER_ASSET_LABEL; +use crate::shader::{ + Context as ShaderContext, + ModuleSource as ShaderModuleSource, + Program as ShaderProgram, + Shader, +}; +use crate::texture::{Properties as TextureProperties, Texture}; use crate::windowing::window::Window; pub mod object; @@ -29,8 +41,14 @@ pub mod opengl; static NEXT_SURFACE_ID: AtomicU64 = AtomicU64::new(0); +pub static DEFAULT_TEXTURE_ASSET_LABEL: LazyLock<AssetLabel> = + LazyLock::new(|| AssetLabel { + path: Path::new("").into(), + name: Some("default_texture".into()), + }); + declare_entity!( - pub RENDER_PHASE, + pub PRE_RENDER_PHASE, ( Phase, Pair::builder() @@ -40,6 +58,17 @@ declare_entity!( ) ); +declare_entity!( + pub RENDER_PHASE, + ( + Phase, + Pair::builder() + .relation::<ChildOf>() + .target_id(*PRE_RENDER_PHASE) + .build() + ) +); + builder! { /// Window graphics properties. #[builder(name=GraphicsPropertiesBuilder, derives=(Debug, Clone))] @@ -131,23 +160,10 @@ pub enum Command MakeCurrent(SurfaceId), ClearBuffers(BufferClearMask), SwapBuffers(SurfaceId), - UseShader, // TODO: Add ability to specify shader - ActivateShader, - UseCamera(Camera, WorldPosition), - ApplyTransform - { - transform: Transform, - window_size: Dimens<u32>, - }, - SetShaderDirectionalLights(Vec<DirectionalLight>), - SetShaderPointLights(Vec<(PointLight, WorldPosition)>), + CreateShaderProgram(ObjectId, ShaderProgram), + ActivateShader(ObjectId), + SetShaderBinding(ShaderBindingLocation, ShaderBindingValue), CreateTexture(Texture), - UseMaterial - { - material_asset: Option<AssetHandle<Material>>, - material_flags: MaterialFlags, - global_light: GlobalLight, - }, DrawMesh { mesh_asset: AssetHandle<Mesh>, @@ -202,12 +218,19 @@ pub struct CtxUsedByWindow; type RenderableEntity<'a> = ( &'a Model, - Option<&'a MaterialFlags>, - Option<&'a WorldPosition>, - Option<&'a Scale>, Option<&'a DrawFlags>, + Option<&'a Shader>, + Option<&'a mut PendingShaderBindings>, ); +pub fn init(mut assets: Single<Assets>) +{ + assets.store_with_label( + DEFAULT_TEXTURE_ASSET_LABEL.clone(), + Image::from_color(Dimens { width: 1, height: 1 }, Color::WHITE_U8), + ); +} + #[tracing::instrument(skip_all)] pub fn enqueue_commands( renderer_ctx_query: Query<( @@ -216,16 +239,15 @@ pub fn enqueue_commands( &[Pair<CtxUsedByWindow, Wildcard>], )>, renderable_query: Query<RenderableEntity<'_>, (Without<NoDraw>,)>, - point_light_query: Query<(&PointLight, &WorldPosition)>, - directional_light_query: Query<(&DirectionalLight,)>, - camera_query: Query<(&Camera, &WorldPosition, &ActiveCamera)>, - global_light: Single<GlobalLight>, assets: Single<Assets>, + shader_context: Single<ShaderContext>, mut actions: Actions, ) { - let Some((camera, camera_world_pos, _)) = camera_query.iter().next() else { - tracing::warn!("No current camera. Nothing will be rendered"); + let Some(default_shader_asset) = assets + .get_handle_to_loaded::<ShaderModuleSource>(DEFAULT_SHADER_ASSET_LABEL.clone()) + else { + tracing::error!("Default shader asset is not loaded"); return; }; @@ -240,7 +262,7 @@ pub fn enqueue_commands( continue; }; - let Some(window) = window_ent.get::<Window>() else { + if window_ent.get::<Window>().is_none() { tracing::debug!( window_entity_id=%window_ent_id, "Window entity does not have a {} component", @@ -270,15 +292,38 @@ pub fn enqueue_commands( command_queue.push(Command::MakeCurrent(surface_spec.id)); - command_queue - .push(Command::UseCamera(camera.clone(), camera_world_pos.clone())); + let default_texture_asset = assets + .get_handle_to_loaded::<Image>(DEFAULT_TEXTURE_ASSET_LABEL.clone()) + .expect("Not possible"); + + if !object_store + .contains_with_id(&ObjectId::Asset(default_texture_asset.id())) + { + command_queue.push(Command::CreateTexture(Texture { + asset_handle: default_texture_asset, + properties: TextureProperties::default(), + })); + } command_queue.push(Command::ClearBuffers( BufferClearMask::COLOR | BufferClearMask::DEPTH, )); - for (model, material_flags, world_pos, scale, draw_flags) in &renderable_query + for (model, draw_flags, shader, mut pending_shader_bindings) in + &renderable_query { + let shader_asset = match &shader { + Some(shader) => &shader.asset_handle, + None => &default_shader_asset, + }; + + if pending_shader_bindings.as_ref().map_or_else( + || true, + |pending_shader_bindings| pending_shader_bindings.bindings.is_empty(), + ) { + continue; + } + let Some(model_spec) = assets.get(&model.spec_asset) else { continue; }; @@ -289,103 +334,69 @@ pub fn enqueue_commands( debug_assert!(model_spec.material_names.len() <= 1); - let model_material_asset = - match find_first_model_material(model_spec, &assets) { - MaterialSearchResult::Found(model_material_asset) => { - Some(model_material_asset.clone()) - } - MaterialSearchResult::NotFound => { - continue; - } - MaterialSearchResult::NoMaterials => None, - }; + let model_material_asset = match model_spec.find_first_material(&assets) { + MaterialSearchResult::Found(model_material_asset) => { + model_material_asset.clone() + // Some(model_material_asset.clone()) + } + MaterialSearchResult::NotFound + | MaterialSearchResult::NoMaterials => { + // MaterialSearchResult::NotFound => { + continue; + } // MaterialSearchResult::NoMaterials => None, + }; - if let Some(model_material_asset) = &model_material_asset { - let Some(model_material) = assets.get(model_material_asset) else { - unreachable!(); + if !object_store.contains_with_id(&ObjectId::Asset(shader_asset.id())) { + let Some(shader_program) = + shader_context.get_program(&shader_asset.id()) + else { + tracing::error!( + "Shader context doesn't have a program for shader asset {:?}", + assets.get_label(&shader_asset) + ); + continue; }; - if let Some(ambient_map) = &model_material.ambient_map { - if assets.get(&ambient_map.asset_handle).is_none() { - continue; - } - } + command_queue.push(Command::CreateShaderProgram( + ObjectId::Asset(shader_asset.id()), + shader_program.clone(), + )); + } - if let Some(diffuse_map) = &model_material.diffuse_map { - if assets.get(&diffuse_map.asset_handle).is_none() { - continue; - } - } + command_queue + .push(Command::ActivateShader(ObjectId::Asset(shader_asset.id()))); - if let Some(specular_map) = &model_material.specular_map { - if assets.get(&specular_map.asset_handle).is_none() { - continue; - } + let Some(model_material) = assets.get(&model_material_asset) else { + // TODO: Handle this case since it may occur + unreachable!(); + }; + + for texture in [ + &model_material.ambient_map, + &model_material.diffuse_map, + &model_material.specular_map, + ] + .into_iter() + .flatten() + { + if !object_store + .contains_with_id(&ObjectId::Asset(texture.asset_handle.id())) + { + command_queue.push(Command::CreateTexture(texture.clone())); } } - command_queue.push(Command::UseShader); - - command_queue.push(Command::ApplyTransform { - transform: Transform { - position: world_pos - .as_deref() - .cloned() - .unwrap_or_default() - .position, - scale: scale.as_deref().cloned().unwrap_or_default().scale, - }, - window_size: *window.inner_size(), - }); - - command_queue.push(Command::SetShaderDirectionalLights( - directional_light_query - .iter() - .map(|(dir_light,)| dir_light.clone()) - .collect::<Vec<_>>(), - )); - - command_queue.push(Command::SetShaderPointLights( - point_light_query - .iter() - .map(|(point_light, point_light_world_pos)| { - (point_light.clone(), point_light_world_pos.clone()) - }) - .collect::<Vec<_>>(), - )); - - if let Some(model_material_asset) = &model_material_asset { - let Some(model_material) = assets.get(model_material_asset) else { - unreachable!(); - }; - - for texture in [ - &model_material.specular_map, - &model_material.diffuse_map, - &model_material.specular_map, - ] - .into_iter() - .flatten() + if let Some(pending_shader_bindings) = &mut pending_shader_bindings { + for (shader_binding_loc, shader_binding_val) in + pending_shader_bindings.bindings.drain(..) { - if !object_store - .contains_with_id(&ObjectId::Asset(texture.asset_handle.id())) - { - command_queue.push(Command::CreateTexture(texture.clone())); - } + command_queue.push(Command::SetShaderBinding( + shader_binding_loc, + shader_binding_val, + )); } } - command_queue.push(Command::UseMaterial { - material_asset: model_material_asset, - material_flags: material_flags - .as_deref() - .cloned() - .unwrap_or_default(), - global_light: global_light.clone(), - }); - - command_queue.push(Command::ActivateShader); - if let Some(draw_flags) = draw_flags.as_deref() && draw_flags.polygon_mode_config != PolygonModeConfig::default() { @@ -410,39 +421,23 @@ pub fn enqueue_commands( } } -enum MaterialSearchResult<'a> +#[derive(Default, Clone, Component)] +pub struct PendingShaderBindings { - Found(&'a AssetHandle<Material>), - NotFound, - NoMaterials, + pub bindings: Vec<(ShaderBindingLocation, ShaderBindingValue)>, } -fn find_first_model_material<'assets>( - model_spec: &'assets ModelSpec, - assets: &'assets Assets, -) -> MaterialSearchResult<'assets> +impl<'a> Extend<(ShaderCursor<'a>, ShaderBindingValue)> for PendingShaderBindings { - let Some(material_name) = model_spec.material_names.first() else { - return MaterialSearchResult::NoMaterials; - }; - - let Some(material_asset) = (match &model_spec.materials { - ModelMaterials::Maps(material_asset_map_assets) => material_asset_map_assets - .iter() - .find_map(|mat_asset_map_asset| { - let mat_asset_map = assets.get(mat_asset_map_asset)?; - - mat_asset_map.assets.get(material_name) - }), - ModelMaterials::Direct(material_assets) => material_assets.get(material_name), - }) else { - return MaterialSearchResult::NotFound; - }; - - if assets.get(material_asset).is_none() { - tracing::trace!("Missing material asset"); - return MaterialSearchResult::NotFound; + fn extend<Iter: IntoIterator<Item = (ShaderCursor<'a>, ShaderBindingValue)>>( + &mut self, + iter: Iter, + ) + { + self.bindings.extend(iter.into_iter().map( + |(shader_cursor, shader_binding_val)| { + (shader_cursor.into_binding_location(), shader_binding_val) + }, + )) } - - MaterialSearchResult::Found(material_asset) } diff --git a/engine/src/renderer/object.rs b/engine/src/renderer/object.rs index d8bb2e3..357bd6a 100644 --- a/engine/src/renderer/object.rs +++ b/engine/src/renderer/object.rs @@ -38,6 +38,17 @@ impl Store Some(obj) } + pub fn get_shader_program_obj(&self, id: &Id) -> Option<&Object> + { + let obj = self.get_obj(id)?; + + if !matches!(obj.kind(), Kind::ShaderProgram) { + return None; + } + + Some(obj) + } + pub fn contains_with_id(&self, id: &Id) -> bool { self.objects.contains_key(id) @@ -108,5 +119,5 @@ impl Object pub enum Kind { Texture, - Mesh, + ShaderProgram, } diff --git a/engine/src/renderer/opengl.rs b/engine/src/renderer/opengl.rs index 4bd67a4..c72a344 100644 --- a/engine/src/renderer/opengl.rs +++ b/engine/src/renderer/opengl.rs @@ -1,16 +1,14 @@ //! OpenGL renderer. use std::any::type_name; +use std::borrow::Cow; use std::collections::HashMap; -use std::ffi::CString; -use std::io::{Error as IoError, ErrorKind as IoErrorKind}; -use std::path::Path; use ecs::actions::Actions; use ecs::entity::obtainer::Obtainer as EntityObtainer; use ecs::event::component::{Changed, Removed}; use ecs::pair::{ChildOf, Pair, Wildcard}; -use ecs::phase::Phase; +use ecs::phase::{Phase, START as START_PHASE}; use ecs::query::term::Without; use ecs::sole::Single; use ecs::system::observer::Observe; @@ -44,6 +42,7 @@ use opengl_bindings::shader::{ Kind as ShaderKind, Program as GlShaderProgram, Shader as GlShader, + // UniformLocation as GlUniformLocation, }; use opengl_bindings::texture::{ Filtering as GlTextureFiltering, @@ -59,21 +58,13 @@ use opengl_bindings::vertex_array::{ }; use opengl_bindings::{ContextWithFns, CurrentContextWithFns}; use safer_ffi::layout::ReprC; +use zerocopy::{Immutable, IntoBytes}; use crate::asset::{Assets, Id as AssetId}; -use crate::camera::Camera; -use crate::color::Color; use crate::data_types::dimens::Dimens; use crate::image::{ColorType as ImageColorType, Image}; -use crate::lighting::{DirectionalLight, GlobalLight, PointLight}; -use crate::material::{Flags as MaterialFlags, Material}; use crate::matrix::Matrix; use crate::model::Model; -use crate::opengl::glsl::{ - PreprocessingError as GlslPreprocessingError, - preprocess as glsl_preprocess, -}; -use crate::projection::{ClipVolume, Projection}; use crate::renderer::object::{ Id as RendererObjectId, Kind as RendererObjectKind, @@ -91,17 +82,23 @@ use crate::renderer::{ CommandQueue as RendererCommandQueue, CtxUsedByWindow as RendererCtxUsedByWindow, GraphicsProperties, + PRE_RENDER_PHASE, RENDER_PHASE, SurfaceId, SurfaceSpec, WindowUsingRendererCtx, }; +use crate::shader::cursor::BindingValue as ShaderBindingValue; +use crate::shader::{ + Error as ShaderError, + Program as ShaderProgram, + Stage as ShaderStage, +}; use crate::texture::{ Filtering as TextureFiltering, Properties as TextureProperties, Wrapping as TextureWrapping, }; -use crate::transform::WorldPosition; use crate::vector::{Vec2, Vec3}; use crate::windowing::Context as WindowingContext; use crate::windowing::window::{ @@ -115,12 +112,6 @@ mod glutin_compat; mod graphics_mesh; mod vertex; -const AMBIENT_MAP_TEXTURE_UNIT: u32 = 0; -const DIFFUSE_MAP_TEXTURE_UNIT: u32 = 1; -const SPECULAR_MAP_TEXTURE_UNIT: u32 = 2; - -const DEFAULT_TEXTURE_OBJECT_ID: RendererObjectId = RendererObjectId::Other(0xaa); - declare_entity!( pub POST_RENDER_PHASE, (Phase, Pair::builder().relation::<ChildOf>().target_id(*RENDER_PHASE).build()) @@ -136,9 +127,9 @@ struct WindowGlConfig struct GraphicsContext { gl_context: ContextWithFns, - shader_program: Option<GlShaderProgram>, graphics_mesh_store: GraphicsMeshStore, surfaces: HashMap<SurfaceId, GlutinSurface<GlutinWindowSurface>>, + uniform_buffer_objs: HashMap<u32, opengl_bindings::buffer::Buffer<u8>>, } #[derive(Debug, Default)] @@ -155,9 +146,12 @@ impl ecs::extension::Extension for Extension { fn collect(self, mut collector: ecs::extension::Collector<'_>) { + collector.add_declared_entity(&PRE_RENDER_PHASE); collector.add_declared_entity(&RENDER_PHASE); collector.add_declared_entity(&POST_RENDER_PHASE); + collector.add_system(*START_PHASE, super::init); + collector.add_system(*RENDER_PHASE, super::enqueue_commands); collector.add_system(*RENDER_PHASE, handle_commands); @@ -617,9 +611,9 @@ fn init_window_graphics( let renderer_ctx_ent_id = actions.spawn(( GraphicsContext { gl_context, - shader_program: None, graphics_mesh_store: GraphicsMeshStore::default(), surfaces: HashMap::from([(surface_id, surface)]), + uniform_buffer_objs: HashMap::new(), }, RendererObjectStore::default(), RendererCommandQueue::default(), @@ -657,17 +651,17 @@ fn handle_commands( { let GraphicsContext { ref gl_context, - ref mut shader_program, ref mut graphics_mesh_store, ref surfaces, + ref mut uniform_buffer_objs, } = *graphics_ctx; let mut opt_curr_gl_ctx: Option<CurrentContextWithFns> = None; - let mut opt_curr_camera: Option<(Camera, WorldPosition)> = None; + let mut activated_gl_shader_program: Option<GlShaderProgram> = None; for command in command_queue.drain() { - let tracing_span = tracing::info_span!("handle_cmd", command = ?command); + let tracing_span = tracing::info_span!("handle_cmd"); let _tracing_span_enter = tracing_span.enter(); match command { @@ -724,89 +718,148 @@ fn handle_commands( tracing::error!("Failed to swap buffers: {err}"); } } - RendererCommand::UseShader => { + RendererCommand::CreateShaderProgram( + shader_program_obj_id, + shader_program, + ) => { let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { tracing::error!("No GL context is current"); continue; }; - let _shader_program = shader_program.get_or_insert_with(|| { - create_default_shader_program(&curr_gl_ctx).unwrap() - }); - } - RendererCommand::ActivateShader => { - let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { - tracing::error!("No GL context is current"); + if renderer_object_store.contains_with_id(&shader_program_obj_id) { + tracing::error!( + object_id=?shader_program_obj_id, + "Object store already contains a object with this ID" + ); continue; - }; + } - let Some(shader_program) = shader_program else { - tracing::error!("Shader does not exist"); - continue; - }; + let gl_shader_program = + match create_shader_program(&curr_gl_ctx, &shader_program) { + Ok(gl_shader_program) => gl_shader_program, + Err(err) => { + tracing::error!("Failed to create shader program: {err}"); + continue; + } + }; - shader_program.activate(&curr_gl_ctx); - } - RendererCommand::UseCamera(camera, camera_world_pos) => { - opt_curr_camera = Some((camera, camera_world_pos)); + renderer_object_store.insert( + shader_program_obj_id, + RendererObject::from_raw( + gl_shader_program.into_raw(), + RendererObjectKind::ShaderProgram, + ), + ); } - RendererCommand::ApplyTransform { transform, window_size } => { + RendererCommand::ActivateShader(shader_program_obj_id) => { let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { tracing::error!("No GL context is current"); continue; }; - let Some(shader_program) = shader_program else { - tracing::error!("Shader does not exist"); + let Some(shader_program_obj) = renderer_object_store + .get_shader_program_obj(&shader_program_obj_id) + else { + tracing::error!( + "Shader object does not exist or has a wrong kind" + ); continue; }; - let Some((camera, camera_world_pos)) = &opt_curr_camera else { - tracing::error!("No current camera"); - continue; - }; + let gl_shader_program = + GlShaderProgram::from_raw(shader_program_obj.as_raw()); - apply_transformation_matrices( - &curr_gl_ctx, - Transformation { - position: transform.position, - scale: transform.scale, - }, - shader_program, - &camera, - &camera_world_pos, - &window_size, - ); + gl_shader_program.activate(&curr_gl_ctx); + + activated_gl_shader_program = Some(gl_shader_program); } - RendererCommand::SetShaderDirectionalLights(directional_lights) => { + RendererCommand::SetShaderBinding(binding_location, binding_value) => { let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { tracing::error!("No GL context is current"); continue; }; - let Some(shader_program) = shader_program else { - tracing::error!("Shader does not exist"); + if activated_gl_shader_program.is_none() { + tracing::error!("No shader program is activated"); continue; - }; + } - set_shader_directional_lights( - curr_gl_ctx, - shader_program, - &directional_lights, - ); - } - RendererCommand::SetShaderPointLights(point_lights) => { - let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { - tracing::error!("No GL context is current"); - continue; - }; + if let ShaderBindingValue::Texture(texture_asset) = &binding_value { + let Some(texture_obj) = renderer_object_store.get_texture_obj( + &RendererObjectId::Asset(texture_asset.id()), + ) else { + tracing::error!( + "Texture {:?} does not exist in renderer object store", + assets.get_label(texture_asset) + ); + continue; + }; + + let gl_texture = GlTexture::from_raw(texture_obj.as_raw()); + + gl_texture.bind_to_texture_unit( + curr_gl_ctx, + binding_location.binding_index, + ); + + // gl_shader_program.set_uniform_at_location( + // curr_gl_ctx, + // GlUniformLocation::from_number( + // binding_location.binding_index as i32, + // ), + // &binding_location.binding_index, + // ); - let Some(shader_program) = shader_program else { - tracing::error!("Shader does not exist"); continue; - }; + } - set_shader_point_lights(curr_gl_ctx, shader_program, &point_lights); + let binding_index = binding_location.binding_index; + + let uniform_buffer = + uniform_buffer_objs.entry(binding_index).or_insert_with(|| { + let uniform_buf = + opengl_bindings::buffer::Buffer::<u8>::new(curr_gl_ctx); + + uniform_buf + .init( + curr_gl_ctx, + binding_location.binding_size, + opengl_bindings::buffer::Usage::Dynamic, + ) + .unwrap(); + + uniform_buf.bind_to_indexed_target( + curr_gl_ctx, + opengl_bindings::buffer::BindingTarget::UniformBuffer, + binding_index as u32, + ); + + uniform_buf + }); + + let fvec3_value; + + uniform_buffer + .store_at_byte_offset( + curr_gl_ctx, + binding_location.byte_offset, + match binding_value { + ShaderBindingValue::Uint(ref value) => value.as_bytes(), + ShaderBindingValue::Int(ref value) => value.as_bytes(), + ShaderBindingValue::Float(ref value) => value.as_bytes(), + ShaderBindingValue::FVec3(value) => { + fvec3_value = CF32Vec3::from(value); + + fvec3_value.as_bytes() + } + ShaderBindingValue::FMat4x4(ref value) => { + value.items().as_bytes() + } + ShaderBindingValue::Texture(_) => unreachable!(), + }, + ) + .unwrap(); } RendererCommand::CreateTexture(texture) => { let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { @@ -815,6 +868,7 @@ fn handle_commands( }; let Some(texture_image) = assets.get(&texture.asset_handle) else { + tracing::error!("Texture asset is not loaded",); continue; }; @@ -828,58 +882,14 @@ fn handle_commands( tracing::error!("Failed to create texture object: {err}"); } } - RendererCommand::UseMaterial { - material_asset, - material_flags, - global_light, - } => { - let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { - tracing::error!("No GL context is current"); - continue; - }; - - let Some(shader_program) = shader_program else { - tracing::error!("Shader does not exist"); - continue; - }; - - let Some((_, camera_world_pos)) = &opt_curr_camera else { - tracing::error!("No current camera"); - continue; - }; - - let material = match material_asset.as_ref() { - Some(material_asset) => { - let Some(material) = assets.get(&material_asset) else { - continue; - }; - - material - } - None => &Material::default(), - }; - - set_shader_material( - curr_gl_ctx, - material, - &material_flags, - &global_light, - shader_program, - camera_world_pos, - ); - - bind_material_textures( - curr_gl_ctx, - material, - &mut renderer_object_store, - ); - } RendererCommand::DrawMesh { mesh_asset } => { let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { tracing::error!("No GL context is current"); continue; }; + // tracing::warn!("ARGH! Drawing mesh"); + let graphics_mesh = match graphics_mesh_store.graphics_meshes.get(&mesh_asset.id()) { Some(graphics_mesh) => graphics_mesh, @@ -930,21 +940,6 @@ fn handle_commands( } } -fn create_default_texture(current_context: &CurrentContextWithFns<'_>) -> GlTexture -{ - match create_gl_texture( - current_context, - &Image::from_color(Dimens { width: 1, height: 1 }, Color::WHITE_U8), - &TextureProperties::default(), - ) { - Ok(gl_texture) => gl_texture, - Err( - GlTextureGenerateError::SizeWidthValueTooLarge { value: _, max_value: _ } - | GlTextureGenerateError::SizeHeightValueTooLarge { value: _, max_value: _ }, - ) => unreachable!(), - } -} - #[tracing::instrument(skip_all)] fn create_texture_object( curr_gl_ctx: &CurrentContextWithFns<'_>, @@ -975,53 +970,6 @@ fn create_texture_object( Ok(()) } -fn bind_material_textures( - current_context: &CurrentContextWithFns<'_>, - material: &Material, - renderer_object_store: &mut RendererObjectStore, -) -{ - let material_texture_maps = [ - (&material.ambient_map, AMBIENT_MAP_TEXTURE_UNIT), - (&material.diffuse_map, DIFFUSE_MAP_TEXTURE_UNIT), - (&material.specular_map, SPECULAR_MAP_TEXTURE_UNIT), - ]; - - for (texture, texture_unit) in material_texture_maps { - let Some(texture) = texture else { - let default_texture_obj = renderer_object_store - .entry(DEFAULT_TEXTURE_OBJECT_ID) - .or_insert_with(|| { - RendererObject::from_raw( - create_default_texture(current_context).into_raw(), - RendererObjectKind::Texture, - ) - }); - - let gl_texture = GlTexture::from_raw(default_texture_obj.as_raw()); - - gl_texture.bind_to_texture_unit(current_context, texture_unit); - - continue; - }; - - let texture_object_id = RendererObjectId::Asset(texture.asset_handle.id()); - - let Some(texture_obj) = renderer_object_store.get_texture_obj(&texture_object_id) - else { - tracing::error!( - texture_object_id=?texture_object_id, - "Texture object does not exist" - ); - continue; - }; - - let gl_texture = GlTexture::from_raw(texture_obj.as_raw()); - - gl_texture.bind_to_texture_unit(current_context, texture_unit); - } -} - fn set_viewport( current_context: &CurrentContextWithFns<'_>, position: Vec2<u32>, @@ -1104,21 +1052,60 @@ fn create_gl_texture( Ok(gl_texture) } -const VERTEX_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/vertex.glsl"); -const FRAGMENT_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/fragment.glsl"); - -const VERTEX_DATA_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/vertex_data.glsl"); -const LIGHT_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/light.glsl"); - -fn create_default_shader_program( +fn create_shader_program( current_context: &CurrentContextWithFns<'_>, + shader_program: &ShaderProgram, ) -> Result<GlShaderProgram, CreateShaderError> { + let shader_program_reflection = shader_program.reflection(0).expect("Not possible"); + + let (vs_entry_point_index, vs_entry_point_reflection) = shader_program_reflection + .entry_points() + .enumerate() + .find(|(_, entry_point)| entry_point.stage() == ShaderStage::Vertex) + .ok_or_else(|| { + CreateShaderError::NoShaderStageEntrypointFound(ShaderStage::Vertex) + })?; + + let vertex_shader_entry_point_code = shader_program + .get_entry_point_code(vs_entry_point_index.try_into().expect( + "Vertex shader entry point index does not fit in 32-bit unsigned int", + )) + .map_err(|err| CreateShaderError::GetShaderEntryPointCodeFailed { + err, + stage: ShaderStage::Vertex, + entrypoint: vs_entry_point_reflection + .name() + .map(|name| name.to_string().into()) + .unwrap_or("(none)".into()), + })?; + + let (fs_entry_point_index, fs_entry_point_reflection) = shader_program_reflection + .entry_points() + .enumerate() + .find(|(_, entry_point)| entry_point.stage() == ShaderStage::Fragment) + .ok_or_else(|| { + CreateShaderError::NoShaderStageEntrypointFound(ShaderStage::Fragment) + })?; + + let fragment_shader_entry_point_code = shader_program + .get_entry_point_code(fs_entry_point_index.try_into().expect( + "Fragment shader entry point index does not fit in 32-bit unsigned int", + )) + .map_err(|err| CreateShaderError::GetShaderEntryPointCodeFailed { + err, + stage: ShaderStage::Fragment, + entrypoint: fs_entry_point_reflection + .name() + .map(|name| name.to_string().into()) + .unwrap_or("(none)".into()), + })?; + let vertex_shader = GlShader::new(current_context, ShaderKind::Vertex); vertex_shader.set_source( current_context, - &*glsl_preprocess(VERTEX_GLSL_SHADER_SRC, &get_glsl_shader_content)?, + &vertex_shader_entry_point_code.as_str().unwrap(), )?; vertex_shader.compile(current_context)?; @@ -1127,7 +1114,7 @@ fn create_default_shader_program( fragment_shader.set_source( current_context, - &*glsl_preprocess(FRAGMENT_GLSL_SHADER_SRC, &get_glsl_shader_content)?, + &fragment_shader_entry_point_code.as_str().unwrap(), )?; fragment_shader.compile(current_context)?; @@ -1145,340 +1132,22 @@ fn create_default_shader_program( #[derive(Debug, thiserror::Error)] enum CreateShaderError { - #[error(transparent)] - ShaderError(#[from] GlShaderError), - - #[error(transparent)] - PreprocessingError(#[from] GlslPreprocessingError), -} - -fn get_glsl_shader_content(path: &Path) -> Result<Vec<u8>, std::io::Error> -{ - if path == Path::new("vertex_data.glsl") { - return Ok(VERTEX_DATA_GLSL_SHADER_SRC.as_bytes().to_vec()); - } - - if path == Path::new("light.glsl") { - return Ok(LIGHT_GLSL_SHADER_SRC.as_bytes().to_vec()); - } - - Err(IoError::new( - IoErrorKind::NotFound, - format!("Content for shader file {} not found", path.display()), - )) -} - -fn apply_transformation_matrices( - current_context: &CurrentContextWithFns<'_>, - transformation: Transformation, - gl_shader_program: &mut GlShaderProgram, - camera: &Camera, - camera_world_pos: &WorldPosition, - window_size: &Dimens<u32>, -) -{ - gl_shader_program.set_uniform( - current_context, - c"model", - &opengl_bindings::data_types::Matrix { - items: create_transformation_matrix(transformation).items, - }, - ); - - let view_matrix = create_view_matrix(camera, &camera_world_pos.position); - - gl_shader_program.set_uniform( - current_context, - c"view", - &opengl_bindings::data_types::Matrix { items: view_matrix.items }, - ); - - #[allow(clippy::cast_precision_loss)] - let proj_matrix = match &camera.projection { - Projection::Perspective(perspective_proj) => perspective_proj.to_matrix_rh( - window_size.width as f32 / window_size.height as f32, - ClipVolume::NegOneToOne, - ), - Projection::Orthographic(orthographic_proj) => orthographic_proj - .to_matrix_rh(&camera_world_pos.position, ClipVolume::NegOneToOne), - }; - - gl_shader_program.set_uniform( - current_context, - c"projection", - &opengl_bindings::data_types::Matrix { items: proj_matrix.items }, - ); -} - -fn set_shader_directional_lights( - curr_gl_ctx: &CurrentContextWithFns<'_>, - gl_shader_program: &mut GlShaderProgram, - directional_lights: &[DirectionalLight], -) -{ - debug_assert!( - directional_lights.len() < 64, - "Shader cannot handle more than 64 directional lights" - ); - - for (dir_light_index, dir_light) in directional_lights.iter().enumerate() { - let direction: opengl_bindings::data_types::Vec3<_> = dir_light.direction.into(); - - gl_shader_program.set_uniform( - curr_gl_ctx, - &create_light_uniform_name( - "directional_lights", - dir_light_index, - "direction", - ), - &direction, - ); - - set_light_phong_uniforms( - curr_gl_ctx, - gl_shader_program, - "directional_lights", - dir_light_index, - dir_light, - ); - } - - // There probably won't be more than 2147483648 directional lights - #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] - gl_shader_program.set_uniform( - curr_gl_ctx, - c"directional_light_cnt", - &(directional_lights.len() as i32), - ); -} - -fn set_shader_point_lights( - curr_gl_ctx: &CurrentContextWithFns<'_>, - gl_shader_program: &mut GlShaderProgram, - point_lights: &[(PointLight, WorldPosition)], -) -{ - debug_assert!( - point_lights.len() < 64, - "Shader cannot handle more than 64 point lights" - ); - - for (point_light_index, (point_light, point_light_world_pos)) in - point_lights.iter().enumerate() - { - let pos: opengl_bindings::data_types::Vec3<_> = - (point_light_world_pos.position + point_light.local_position).into(); - - gl_shader_program.set_uniform( - curr_gl_ctx, - &create_light_uniform_name("point_lights", point_light_index, "position"), - &pos, - ); - - set_light_phong_uniforms( - curr_gl_ctx, - gl_shader_program, - "point_lights", - point_light_index, - &*point_light, - ); - - set_light_attenuation_uniforms( - curr_gl_ctx, - gl_shader_program, - "point_lights", - point_light_index, - &*point_light, - ); - } - - // There probably won't be more than 2147483648 point lights - #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] - gl_shader_program.set_uniform( - curr_gl_ctx, - c"point_light_cnt", - &(point_lights.len() as i32), - ); -} - -fn set_shader_material( - curr_gl_ctx: &CurrentContextWithFns<'_>, - material: &Material, - material_flags: &MaterialFlags, - global_light: &GlobalLight, - gl_shader_program: &mut GlShaderProgram, - camera_world_pos: &WorldPosition, -) -{ - let ambient: opengl_bindings::data_types::Vec3<_> = - Vec3::from(if material_flags.use_ambient_color { - material.ambient.clone() - } else { - global_light.ambient.clone() - }) - .into(); - - gl_shader_program.set_uniform(curr_gl_ctx, c"material.ambient", &ambient); - - let diffuse: opengl_bindings::data_types::Vec3<_> = - Vec3::from(material.diffuse.clone()).into(); - - gl_shader_program.set_uniform(curr_gl_ctx, c"material.diffuse", &diffuse); - - let specular: opengl_bindings::data_types::Vec3<_> = - Vec3::from(material.specular.clone()).into(); - - #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform(curr_gl_ctx, c"material.specular", &specular); - - #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform( - curr_gl_ctx, - c"material.ambient_map", - &(AMBIENT_MAP_TEXTURE_UNIT as i32), - ); - - #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform( - curr_gl_ctx, - c"material.diffuse_map", - &(DIFFUSE_MAP_TEXTURE_UNIT as i32), - ); - - #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform( - curr_gl_ctx, - c"material.specular_map", - &(SPECULAR_MAP_TEXTURE_UNIT as i32), - ); - - gl_shader_program.set_uniform( - curr_gl_ctx, - c"material.shininess", - &material.shininess, - ); - - let view_pos: opengl_bindings::data_types::Vec3<_> = camera_world_pos.position.into(); - - gl_shader_program.set_uniform(curr_gl_ctx, c"view_pos", &view_pos); -} - -fn set_light_attenuation_uniforms( - current_context: &CurrentContextWithFns<'_>, - gl_shader_program: &mut GlShaderProgram, - light_array: &str, - light_index: usize, - light: &PointLight, -) -{ - gl_shader_program.set_uniform( - current_context, - &create_light_uniform_name( - light_array, - light_index, - "attenuation_props.constant", - ), - &light.attenuation_params.constant, - ); - - gl_shader_program.set_uniform( - current_context, - &create_light_uniform_name(light_array, light_index, "attenuation_props.linear"), - &light.attenuation_params.linear, - ); - - gl_shader_program.set_uniform( - current_context, - &create_light_uniform_name( - light_array, - light_index, - "attenuation_props.quadratic", - ), - &light.attenuation_params.quadratic, - ); -} - -fn set_light_phong_uniforms( - current_context: &CurrentContextWithFns<'_>, - gl_shader_program: &mut GlShaderProgram, - light_array: &str, - light_index: usize, - light: &impl Light, -) -{ - gl_shader_program.set_uniform( - current_context, - &create_light_uniform_name(light_array, light_index, "phong.diffuse"), - &opengl_bindings::data_types::Vec3 { - x: light.diffuse().red, - y: light.diffuse().green, - z: light.diffuse().blue, - }, - ); - - gl_shader_program.set_uniform( - current_context, - &create_light_uniform_name(light_array, light_index, "phong.specular"), - &opengl_bindings::data_types::Vec3 { - x: light.specular().red, - y: light.specular().green, - z: light.specular().blue, - }, - ); -} - -trait Light -{ - fn diffuse(&self) -> &Color<f32>; - fn specular(&self) -> &Color<f32>; -} - -impl Light for PointLight -{ - fn diffuse(&self) -> &Color<f32> - { - &self.diffuse - } - - fn specular(&self) -> &Color<f32> - { - &self.specular - } -} - -impl Light for DirectionalLight -{ - fn diffuse(&self) -> &Color<f32> - { - &self.diffuse - } - - fn specular(&self) -> &Color<f32> + #[error( + "Failed to get code of shader program entry point {entrypoint} of stage {stage:?}" + )] + GetShaderEntryPointCodeFailed { - &self.specular - } -} - -fn create_light_uniform_name( - light_array: &str, - light_index: usize, - light_field: &str, -) -> CString -{ - unsafe { - CString::from_vec_with_nul_unchecked( - format!("{light_array}[{light_index}].{light_field}\0").into(), - ) - } -} - -fn create_view_matrix(camera: &Camera, camera_pos: &Vec3<f32>) -> Matrix<f32, 4, 4> -{ - let mut view = Matrix::new(); + #[source] + err: ShaderError, + stage: ShaderStage, + entrypoint: Cow<'static, str>, + }, - view.look_at(&camera_pos, &camera.target, &camera.global_up); + #[error("No entrypoint was found for shader stage {0:?}")] + NoShaderStageEntrypointFound(ShaderStage), - view + #[error(transparent)] + ShaderError(#[from] GlShaderError), } #[tracing::instrument(skip_all)] @@ -1524,23 +1193,6 @@ fn opengl_debug_message_cb( }; } -#[derive(Debug)] -struct Transformation -{ - position: Vec3<f32>, - scale: Vec3<f32>, -} - -fn create_transformation_matrix(transformation: Transformation) -> Matrix<f32, 4, 4> -{ - let mut matrix = Matrix::new_identity(); - - matrix.translate(&transformation.position); - matrix.scale(&transformation.scale); - - matrix -} - #[inline] fn texture_wrapping_to_gl(texture_wrapping: TextureWrapping) -> GlTextureWrapping { @@ -1569,7 +1221,8 @@ impl<Value: ReprC + Copy> From<Vec2<Value>> for opengl_bindings::data_types::Vec } } -impl<Value: ReprC + Copy> From<Vec3<Value>> for opengl_bindings::data_types::Vec3<Value> +impl<Value: ReprC + IntoBytes + Copy> From<Vec3<Value>> + for opengl_bindings::data_types::Vec3<Value> { fn from(vec3: Vec3<Value>) -> Self { @@ -1577,6 +1230,15 @@ impl<Value: ReprC + Copy> From<Vec3<Value>> for opengl_bindings::data_types::Vec } } +impl<Value: ReprC + Copy> From<Matrix<Value, 4, 4>> + for opengl_bindings::data_types::Matrix<Value, 4, 4> +{ + fn from(matrix: Matrix<Value, 4, 4>) -> Self + { + Self { items: matrix.items } + } +} + impl<Value: Copy> From<Dimens<Value>> for opengl_bindings::data_types::Dimens<Value> { fn from(dimens: Dimens<Value>) -> Self @@ -1611,3 +1273,20 @@ impl From<crate::draw_flags::PolygonModeFace> for opengl_bindings::misc::Polygon } } } + +#[derive(Debug, IntoBytes, Immutable)] +#[repr(C)] +pub struct CF32Vec3 +{ + x: f32, + y: f32, + z: f32, +} + +impl From<Vec3<f32>> for CF32Vec3 +{ + fn from(src: Vec3<f32>) -> Self + { + Self { x: src.x, y: src.y, z: src.z } + } +} diff --git a/engine/src/renderer/opengl/glsl/fragment.glsl b/engine/src/renderer/opengl/glsl/fragment.glsl deleted file mode 100644 index 5bf5ff2..0000000 --- a/engine/src/renderer/opengl/glsl/fragment.glsl +++ /dev/null @@ -1,73 +0,0 @@ -#version 330 core - -#preinclude "light.glsl" -#preinclude "vertex_data.glsl" - -#define MAX_LIGHT_CNT 64 - -out vec4 FragColor; - -in VertexData vertex_data; - -uniform vec3 view_pos; -uniform sampler2D input_texture; -uniform Material material; - -uniform PointLight point_lights[MAX_LIGHT_CNT]; -uniform int point_light_cnt; - -uniform DirectionalLight directional_lights[MAX_LIGHT_CNT]; -uniform int directional_light_cnt; - -void main() -{ - vec3 ambient_light = calc_ambient_light(material, vertex_data.texture_coords); - - vec3 directional_light_sum = vec3(0.0, 0.0, 0.0); - - for (int dl_index = 0; dl_index < directional_light_cnt; dl_index++) { - CalculatedLight calculated_dir_light; - - calc_light( - // Negated since we want the light to point from the light direction - normalize(-directional_lights[dl_index].direction), - directional_lights[dl_index].phong, - vertex_data, - view_pos, - material, - calculated_dir_light - ); - - directional_light_sum += - calculated_dir_light.diffuse + calculated_dir_light.specular; - } - - vec3 point_light_sum = vec3(0.0, 0.0, 0.0); - - for (int pl_index = 0; pl_index < point_light_cnt; pl_index++) { - vec3 light_direction = - normalize(point_lights[pl_index].position - vertex_data.world_space_pos); - - CalculatedLight calculated_point_light; - - calc_light( - light_direction, - point_lights[pl_index].phong, - vertex_data, - view_pos, - material, - calculated_point_light - ); - - float attenuation = - calc_attenuation(point_lights[pl_index], vertex_data.world_space_pos); - - calculated_point_light.diffuse *= attenuation; - calculated_point_light.specular *= attenuation; - - point_light_sum += - calculated_point_light.diffuse + calculated_point_light.specular; - } - - FragColor = vec4((ambient_light + directional_light_sum + point_light_sum), 1.0); -} diff --git a/engine/src/renderer/opengl/glsl/light.glsl b/engine/src/renderer/opengl/glsl/light.glsl deleted file mode 100644 index f12b5fe..0000000 --- a/engine/src/renderer/opengl/glsl/light.glsl +++ /dev/null @@ -1,133 +0,0 @@ -#version 330 core - -#ifndef LIGHT_GLSL -#define LIGHT_GLSL - -#preinclude "vertex_data.glsl" - -struct Material -{ - vec3 ambient; - vec3 diffuse; - vec3 specular; - sampler2D ambient_map; - sampler2D diffuse_map; - sampler2D specular_map; - float shininess; -}; - -struct LightPhong -{ - vec3 diffuse; - vec3 specular; -}; - -struct AttenuationProperties -{ - float constant; - float linear; - float quadratic; -}; - -struct PointLight -{ - LightPhong phong; - vec3 position; - AttenuationProperties attenuation_props; -}; - -struct DirectionalLight -{ - LightPhong phong; - vec3 direction; -}; - -struct CalculatedLight -{ - vec3 diffuse; - vec3 specular; -}; - -vec3 calc_ambient_light(in Material material, in vec2 texture_coords) -{ - return vec3(texture(material.ambient_map, texture_coords)) * material.ambient; -} - -vec3 calc_diffuse_light( - in Material material, - in LightPhong light_phong, - in vec3 light_dir, - in vec3 norm, - in vec2 texture_coords -) -{ - float diff = max(dot(norm, light_dir), 0.0); - - return light_phong.diffuse * ( - diff * (vec3(texture(material.diffuse_map, texture_coords)) * material.diffuse) - ); -} - -vec3 calc_specular_light( - in Material material, - in LightPhong light_phong, - in vec3 light_dir, - in vec3 norm, - in vec3 view_pos, - in vec3 frag_pos, - in vec2 texture_coords -) -{ - vec3 view_direction = normalize(view_pos - frag_pos); - - vec3 halfway_direction = normalize(light_dir + view_direction); - - float spec = - pow(max(dot(norm, halfway_direction), 0.0), material.shininess); - - return light_phong.specular * ( - spec * (vec3(texture(material.specular_map, texture_coords)) * material.specular) - ); -} - -float calc_attenuation(in PointLight point_light, in vec3 position) -{ - float light_distance = length(point_light.position - position); - - return 1.0 / (point_light.attenuation_props.constant - + point_light.attenuation_props.linear - * light_distance + point_light.attenuation_props.quadratic - * pow(light_distance, 2)); -} - -void calc_light( - in vec3 light_direction, - in LightPhong light_phong, - in VertexData vertex_data, - in vec3 view_pos, - in Material material, - out CalculatedLight calculated_light -) -{ - vec3 norm = normalize(vertex_data.world_space_normal); - - calculated_light.diffuse = calc_diffuse_light( - material, - light_phong, - light_direction, - norm, - vertex_data.texture_coords - ); - - calculated_light.specular = calc_specular_light( - material, - light_phong, - light_direction, - norm, - view_pos, - vertex_data.world_space_pos, - vertex_data.texture_coords - ); -} - -#endif diff --git a/engine/src/renderer/opengl/glsl/vertex.glsl b/engine/src/renderer/opengl/glsl/vertex.glsl deleted file mode 100644 index b57caa6..0000000 --- a/engine/src/renderer/opengl/glsl/vertex.glsl +++ /dev/null @@ -1,24 +0,0 @@ -#version 330 core - -#preinclude "vertex_data.glsl" - -layout (location = 0) in vec3 pos; -layout (location = 1) in vec2 texture_coords; -layout (location = 2) in vec3 normal; - -out VertexData vertex_data; - -uniform mat4 model; -uniform mat4 view; -uniform mat4 projection; - -void main() -{ - gl_Position = projection * view * model * vec4(pos, 1.0); - - vertex_data.world_space_pos = vec3(model * vec4(pos, 1.0)); - vertex_data.texture_coords = texture_coords; - - // TODO: Do this using CPU for performance increase - vertex_data.world_space_normal = mat3(transpose(inverse(model))) * normal; -} diff --git a/engine/src/renderer/opengl/glsl/vertex_data.glsl b/engine/src/renderer/opengl/glsl/vertex_data.glsl deleted file mode 100644 index 486d445..0000000 --- a/engine/src/renderer/opengl/glsl/vertex_data.glsl +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef VERTEX_DATA_GLSL -#define VERTEX_DATA_GLSL - -struct VertexData -{ - vec2 texture_coords; - vec3 world_space_pos; - vec3 world_space_normal; -}; - -#endif diff --git a/engine/src/renderer/opengl/graphics_mesh.rs b/engine/src/renderer/opengl/graphics_mesh.rs index dc839a8..8e0e3cc 100644 --- a/engine/src/renderer/opengl/graphics_mesh.rs +++ b/engine/src/renderer/opengl/graphics_mesh.rs @@ -29,7 +29,7 @@ impl GraphicsMesh mesh: &Mesh, ) -> Result<Self, Error> { - tracing::trace!( + tracing::info!( "Creating vertex array, vertex buffer{}", if mesh.indices().is_some() { " and index buffer" diff --git a/engine/src/shader.rs b/engine/src/shader.rs new file mode 100644 index 0000000..65872f1 --- /dev/null +++ b/engine/src/shader.rs @@ -0,0 +1,818 @@ +use std::any::type_name; +use std::borrow::Cow; +use std::collections::HashMap; +use std::fmt::Debug; +use std::path::Path; +use std::str::Utf8Error; + +use bitflags::bitflags; +use ecs::pair::{ChildOf, Pair}; +use ecs::phase::{Phase, START as START_PHASE}; +use ecs::sole::Single; +use ecs::{Component, Sole, declare_entity}; +use shader_slang::{ + Blob as SlangBlob, + ComponentType as SlangComponentType, + DebugInfoLevel as SlangDebugInfoLevel, + EntryPoint as SlangEntryPoint, + GlobalSession as SlangGlobalSession, + Module as SlangModule, + ParameterCategory as SlangParameterCategory, + Session as SlangSession, + TypeKind as SlangTypeKind, +}; + +use crate::asset::{ + Assets, + Event as AssetEvent, + HANDLE_ASSETS_PHASE, + Handle as AssetHandle, + Id as AssetId, + Submitter as AssetSubmitter, +}; +use crate::builder; +use crate::renderer::PRE_RENDER_PHASE; +use crate::shader::default::{ + ASSET_LABEL, + enqueue_set_shader_bindings as default_shader_enqueue_set_shader_bindings, +}; + +pub mod cursor; +pub mod default; + +#[derive(Debug, Clone, Component)] +pub struct Shader +{ + pub asset_handle: AssetHandle<ModuleSource>, +} + +/// Shader module. +#[derive(Debug)] +pub struct ModuleSource +{ + pub name: Cow<'static, str>, + pub file_path: Cow<'static, Path>, + pub source: Cow<'static, str>, + pub link_entrypoints: EntrypointFlags, +} + +bitflags! { + #[derive(Debug, Clone, Copy, Default)] + pub struct EntrypointFlags: usize + { + const FRAGMENT = 1 << 0; + const VERTEX = 1 << 1; + } +} + +pub struct Module +{ + inner: SlangModule, +} + +impl Module +{ + pub fn entry_points(&self) -> impl ExactSizeIterator<Item = EntryPoint> + { + self.inner + .entry_points() + .map(|entry_point| EntryPoint { inner: entry_point }) + } + + pub fn get_entry_point(&self, entry_point: &str) -> Option<EntryPoint> + { + let entry_point = self.inner.find_entry_point_by_name(entry_point)?; + + Some(EntryPoint { inner: entry_point }) + } +} + +pub struct EntryPoint +{ + inner: SlangEntryPoint, +} + +impl EntryPoint +{ + pub fn function_name(&self) -> Option<&str> + { + self.inner.function_reflection().name() + } +} + +pub struct EntryPointReflection<'a> +{ + inner: &'a shader_slang::reflection::EntryPoint, +} + +impl<'a> EntryPointReflection<'a> +{ + pub fn name(&self) -> Option<&str> + { + self.inner.name() + } + + pub fn name_override(&self) -> Option<&str> + { + self.inner.name_override() + } + + pub fn stage(&self) -> Stage + { + Stage::from_slang_stage(self.inner.stage()) + } + + pub fn parameters(&self) -> impl ExactSizeIterator<Item = VariableLayout<'a>> + { + self.inner + .parameters() + .map(|param| VariableLayout { inner: param }) + } + + pub fn var_layout(&self) -> Option<VariableLayout<'a>> + { + Some(VariableLayout { inner: self.inner.var_layout()? }) + } +} + +#[derive(Clone)] +pub struct Program +{ + inner: SlangComponentType, +} + +impl Program +{ + pub fn link(&self) -> Result<Program, Error> + { + let linked_program = self.inner.link()?; + + Ok(Program { inner: linked_program }) + } + + pub fn get_entry_point_code(&self, entry_point_index: u32) -> Result<Blob, Error> + { + let blob = self.inner.entry_point_code(entry_point_index.into(), 0)?; + + Ok(Blob { inner: blob }) + } + + pub fn reflection(&self, target: u32) -> Result<ProgramReflection<'_>, Error> + { + let reflection = self.inner.layout(target as i64)?; + + Ok(ProgramReflection { inner: reflection }) + } +} + +impl Debug for Program +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + formatter + .debug_struct(type_name::<Self>()) + .finish_non_exhaustive() + } +} + +pub struct ProgramReflection<'a> +{ + inner: &'a shader_slang::reflection::Shader, +} + +impl<'a> ProgramReflection<'a> +{ + pub fn get_entry_point_by_index(&self, index: u32) + -> Option<EntryPointReflection<'a>> + { + Some(EntryPointReflection { + inner: self.inner.entry_point_by_index(index)?, + }) + } + + pub fn get_entry_point_by_name(&self, name: &str) + -> Option<EntryPointReflection<'a>> + { + Some(EntryPointReflection { + inner: self.inner.find_entry_point_by_name(name)?, + }) + } + + pub fn entry_points(&self) + -> impl ExactSizeIterator<Item = EntryPointReflection<'a>> + { + self.inner + .entry_points() + .map(|entry_point| EntryPointReflection { inner: entry_point }) + } + + pub fn global_params_type_layout(&self) -> Option<TypeLayout<'a>> + { + Some(TypeLayout { + inner: self.inner.global_params_type_layout()?, + }) + } + + pub fn global_params_var_layout(&self) -> Option<VariableLayout<'a>> + { + Some(VariableLayout { + inner: self.inner.global_params_var_layout()?, + }) + } + + pub fn get_type(&self, name: &str) -> Option<TypeReflection<'a>> + { + Some(TypeReflection { + inner: self.inner.find_type_by_name(name)?, + }) + } + + pub fn get_type_layout(&self, ty: &TypeReflection<'a>) -> Option<TypeLayout<'a>> + { + Some(TypeLayout { + inner: self + .inner + .type_layout(&ty.inner, shader_slang::LayoutRules::Default)?, + }) + } +} + +#[derive(Clone, Copy)] +pub struct VariableLayout<'a> +{ + inner: &'a shader_slang::reflection::VariableLayout, +} + +impl<'a> VariableLayout<'a> +{ + pub fn name(&self) -> Option<&str> + { + self.inner.name() + } + + pub fn semantic_name(&self) -> Option<&str> + { + self.inner.semantic_name() + } + + pub fn binding_index(&self) -> u32 + { + self.inner + .offset(shader_slang::ParameterCategory::DescriptorTableSlot) as u32 + + // self.inner.binding_index() + } + + pub fn binding_space(&self) -> u32 + { + self.inner.binding_space() + } + + pub fn semantic_index(&self) -> usize + { + self.inner.semantic_index() + } + + pub fn offset(&self) -> usize + { + self.inner.offset(shader_slang::ParameterCategory::Uniform) + } + + pub fn ty(&self) -> Option<TypeReflection<'a>> + { + self.inner.ty().map(|ty| TypeReflection { inner: ty }) + } + + pub fn type_layout(&self) -> Option<TypeLayout<'a>> + { + Some(TypeLayout { inner: self.inner.type_layout()? }) + } +} + +#[derive(Clone, Copy)] +pub struct TypeLayout<'a> +{ + inner: &'a shader_slang::reflection::TypeLayout, +} + +impl<'a> TypeLayout<'a> +{ + pub fn get_field_by_name(&self, name: &str) -> Option<VariableLayout<'a>> + { + let index = self.inner.find_field_index_by_name(name); + + if index < 0 { + return None; + } + + let index = u32::try_from(index.cast_unsigned()).expect("Should not happend"); + + let field = self.inner.field_by_index(index)?; + + Some(VariableLayout { inner: field }) + } + + pub fn binding_range_descriptor_set_index(&self, index: i64) -> i64 + { + self.inner.binding_range_descriptor_set_index(index) + } + + pub fn get_field_binding_range_offset_by_name(&self, name: &str) -> Option<u64> + { + let field_index = self.inner.find_field_index_by_name(name); + + if field_index < 0 { + return None; + } + + let field_binding_range_offset = + self.inner.field_binding_range_offset(field_index); + + if field_binding_range_offset < 0 { + return None; + } + + Some(field_binding_range_offset.cast_unsigned()) + } + + pub fn ty(&self) -> Option<TypeReflection<'a>> + { + self.inner.ty().map(|ty| TypeReflection { inner: ty }) + } + + pub fn fields(&self) -> impl ExactSizeIterator<Item = VariableLayout<'a>> + { + self.inner + .fields() + .map(|field| VariableLayout { inner: field }) + } + + pub fn field_cnt(&self) -> u32 + { + self.inner.field_count() + } + + pub fn element_type_layout(&self) -> Option<TypeLayout<'a>> + { + self.inner + .element_type_layout() + .map(|type_layout| TypeLayout { inner: type_layout }) + } + + pub fn element_var_layout(&self) -> Option<VariableLayout<'a>> + { + self.inner + .element_var_layout() + .map(|var_layout| VariableLayout { inner: var_layout }) + } + + pub fn container_var_layout(&self) -> Option<VariableLayout<'a>> + { + self.inner + .container_var_layout() + .map(|var_layout| VariableLayout { inner: var_layout }) + } + + pub fn uniform_size(&self) -> Option<usize> + { + // tracing::debug!( + // "uniform_size: {:?} categories: {:?}", + // self.inner.name(), + // self.inner.categories().collect::<Vec<_>>(), + // ); + + if !self + .inner + .categories() + .any(|category| category == SlangParameterCategory::Uniform) + { + return None; + } + + // let category = self.inner.categories().next().unwrap(); + + // println!( + // "AARGH size Category: {category:?} Category count: {}", + // self.inner.category_count() + // ); + + // Some(self.inner.size(category)) + + Some(self.inner.size(SlangParameterCategory::Uniform)) + } + + pub fn stride(&self) -> usize + { + self.inner.stride(self.inner.categories().next().unwrap()) + } +} + +pub struct TypeReflection<'a> +{ + inner: &'a shader_slang::reflection::Type, +} + +impl TypeReflection<'_> +{ + pub fn kind(&self) -> TypeKind + { + TypeKind::from_slang_type_kind(self.inner.kind()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum TypeKind +{ + None, + Struct, + Enum, + Array, + Matrix, + Vector, + Scalar, + ConstantBuffer, + Resource, + SamplerState, + TextureBuffer, + ShaderStorageBuffer, + ParameterBlock, + GenericTypeParameter, + Interface, + OutputStream, + MeshOutput, + Specialized, + Feedback, + Pointer, + DynamicResource, + Count, +} + +impl TypeKind +{ + fn from_slang_type_kind(type_kind: SlangTypeKind) -> Self + { + match type_kind { + SlangTypeKind::None => Self::None, + SlangTypeKind::Struct => Self::Struct, + SlangTypeKind::Enum => Self::Enum, + SlangTypeKind::Array => Self::Array, + SlangTypeKind::Matrix => Self::Matrix, + SlangTypeKind::Vector => Self::Vector, + SlangTypeKind::Scalar => Self::Scalar, + SlangTypeKind::ConstantBuffer => Self::ConstantBuffer, + SlangTypeKind::Resource => Self::Resource, + SlangTypeKind::SamplerState => Self::SamplerState, + SlangTypeKind::TextureBuffer => Self::TextureBuffer, + SlangTypeKind::ShaderStorageBuffer => Self::ShaderStorageBuffer, + SlangTypeKind::ParameterBlock => Self::ParameterBlock, + SlangTypeKind::GenericTypeParameter => Self::GenericTypeParameter, + SlangTypeKind::Interface => Self::Interface, + SlangTypeKind::OutputStream => Self::OutputStream, + SlangTypeKind::MeshOutput => Self::MeshOutput, + SlangTypeKind::Specialized => Self::Specialized, + SlangTypeKind::Feedback => Self::Feedback, + SlangTypeKind::Pointer => Self::Pointer, + SlangTypeKind::DynamicResource => Self::DynamicResource, + SlangTypeKind::Count => Self::Count, + } + } +} + +pub struct Blob +{ + inner: SlangBlob, +} + +impl Blob +{ + pub fn as_bytes(&self) -> &[u8] + { + self.inner.as_slice() + } + + pub fn as_str(&self) -> Result<&str, Utf8Error> + { + self.inner.as_str() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum Stage +{ + None, + Vertex, + Hull, + Domain, + Geometry, + Fragment, + Compute, + RayGeneration, + Intersection, + AnyHit, + ClosestHit, + Miss, + Callable, + Mesh, + Amplification, + Dispatch, + Count, +} + +impl Stage +{ + fn from_slang_stage(stage: shader_slang::Stage) -> Self + { + match stage { + shader_slang::Stage::None => Self::None, + shader_slang::Stage::Vertex => Self::Vertex, + shader_slang::Stage::Hull => Self::Hull, + shader_slang::Stage::Domain => Self::Domain, + shader_slang::Stage::Geometry => Self::Geometry, + shader_slang::Stage::Fragment => Self::Fragment, + shader_slang::Stage::Compute => Self::Compute, + shader_slang::Stage::RayGeneration => Self::RayGeneration, + shader_slang::Stage::Intersection => Self::Intersection, + shader_slang::Stage::AnyHit => Self::AnyHit, + shader_slang::Stage::ClosestHit => Self::ClosestHit, + shader_slang::Stage::Miss => Self::Miss, + shader_slang::Stage::Callable => Self::Callable, + shader_slang::Stage::Mesh => Self::Mesh, + shader_slang::Stage::Amplification => Self::Amplification, + shader_slang::Stage::Dispatch => Self::Dispatch, + shader_slang::Stage::Count => Self::Count, + } + } +} + +builder! { +#[builder(name = SettingsBuilder, derives=(Debug))] +#[derive(Debug)] +#[non_exhaustive] +pub struct Settings +{ + link_entrypoints: EntrypointFlags, +} +} + +#[derive(Sole)] +pub struct Context +{ + _global_session: SlangGlobalSession, + session: SlangSession, + modules: HashMap<AssetId, Module>, + programs: HashMap<AssetId, Program>, +} + +impl Context +{ + pub fn get_module(&self, asset_id: &AssetId) -> Option<&Module> + { + self.modules.get(asset_id) + } + + pub fn get_program(&self, asset_id: &AssetId) -> Option<&Program> + { + self.programs.get(asset_id) + } + + pub fn compose_into_program( + &self, + modules: &[&Module], + entry_points: &[&EntryPoint], + ) -> Result<Program, Error> + { + let components = + modules + .iter() + .map(|module| SlangComponentType::from(module.inner.clone())) + .chain(entry_points.iter().map(|entry_point| { + SlangComponentType::from(entry_point.inner.clone()) + })) + .collect::<Vec<_>>(); + + let program = self.session.create_composite_component_type(&components)?; + + Ok(Program { inner: program }) + } +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct Error(#[from] shader_slang::Error); + +pub(crate) fn add_asset_importers(assets: &mut Assets) +{ + assets.set_importer::<_, _>(["slang"], import_slang_asset); +} + +fn import_slang_asset( + asset_submitter: &mut AssetSubmitter<'_>, + file_path: &Path, + settings: Option<&'_ Settings>, +) -> Result<(), ImportError> +{ + let file_name = file_path + .file_name() + .ok_or(ImportError::NoPathFileName)? + .to_str() + .ok_or(ImportError::PathFileNameNotUtf8)?; + + let file_path_canonicalized = file_path + .canonicalize() + .map_err(ImportError::CanonicalizePathFailed)?; + + asset_submitter.submit_store(ModuleSource { + name: file_name.to_owned().into(), + file_path: file_path_canonicalized.into(), + source: std::fs::read_to_string(file_path) + .map_err(ImportError::ReadFileFailed)? + .into(), + link_entrypoints: settings + .map(|settings| settings.link_entrypoints) + .unwrap_or_default(), + }); + + Ok(()) +} + +#[derive(Debug, thiserror::Error)] +enum ImportError +{ + #[error("Failed to read file")] + ReadFileFailed(#[source] std::io::Error), + + #[error("Asset path does not have a file name")] + NoPathFileName, + + #[error("Asset path file name is not valid UTF8")] + PathFileNameNotUtf8, + + #[error("Failed to canonicalize asset path")] + CanonicalizePathFailed(#[source] std::io::Error), +} + +declare_entity!( + IMPORT_SHADERS_PHASE, + ( + Phase, + Pair::builder() + .relation::<ChildOf>() + .target_id(*HANDLE_ASSETS_PHASE) + .build() + ) +); + +pub(crate) struct Extension; + +impl ecs::extension::Extension for Extension +{ + fn collect(self, mut collector: ecs::extension::Collector<'_>) + { + let Some(global_session) = SlangGlobalSession::new() else { + tracing::error!("Unable to create global shader-slang session"); + return; + }; + + let session_options = shader_slang::CompilerOptions::default() + .optimization(shader_slang::OptimizationLevel::None) + .matrix_layout_column(true) + .debug_information(SlangDebugInfoLevel::Maximal) + .no_mangle(true); + + let target_desc = shader_slang::TargetDesc::default() + .format(shader_slang::CompileTarget::Glsl) + // .format(shader_slang::CompileTarget::Spirv) + .profile(global_session.find_profile("glsl_330")); + // .profile(global_session.find_profile("spirv_1_5")); + + let targets = [target_desc]; + + let session_desc = shader_slang::SessionDesc::default() + .targets(&targets) + .search_paths(&[""]) + .options(&session_options); + + let Some(session) = global_session.create_session(&session_desc) else { + tracing::error!("Failed to create shader-slang session"); + return; + }; + + collector + .add_sole(Context { + _global_session: global_session, + session, + modules: HashMap::new(), + programs: HashMap::new(), + }) + .ok(); + + collector.add_declared_entity(&IMPORT_SHADERS_PHASE); + + collector.add_system(*START_PHASE, initialize); + collector.add_system(*IMPORT_SHADERS_PHASE, load_modules); + + collector.add_system( + *PRE_RENDER_PHASE, + default_shader_enqueue_set_shader_bindings, + ); + } +} + +fn initialize(mut assets: Single<Assets>) +{ + assets.store_with_label( + ASSET_LABEL.clone(), + ModuleSource { + name: "default_shader.slang".into(), + file_path: Path::new("@engine/default_shader").into(), + source: include_str!("../res/default_shader.slang").into(), + link_entrypoints: EntrypointFlags::VERTEX | EntrypointFlags::FRAGMENT, + }, + ); +} + +#[tracing::instrument(skip_all)] +fn load_modules(mut context: Single<Context>, assets: Single<Assets>) +{ + for AssetEvent::Stored(asset_id, asset_label) in assets.events().last_tick_events() { + let asset_handle = AssetHandle::<ModuleSource>::from_id(*asset_id); + + if !assets.is_loaded_and_has_type(&asset_handle) { + continue; + } + + let Some(module_source) = assets.get(&asset_handle) else { + unreachable!(); + }; + + tracing::debug!(asset_label=?asset_label, "Loading shader module"); + + let module = match load_module(&context.session, module_source) { + Ok(module) => module, + Err(err) => { + tracing::error!("Failed to load shader module: {err}"); + continue; + } + }; + + if !module_source.link_entrypoints.is_empty() { + assert!(context.programs.get(asset_id).is_none()); + + let Some(vertex_shader_entry_point) = module.get_entry_point("vertex_main") + else { + tracing::error!( + "Shader module does not contain a vertex shader entry point" + ); + continue; + }; + + let Some(fragment_shader_entry_point) = + module.get_entry_point("fragment_main") + else { + tracing::error!( + "Shader module don't contain a fragment_main entry point" + ); + continue; + }; + + let shader_program = match context.compose_into_program( + &[&module], + &[&vertex_shader_entry_point, &fragment_shader_entry_point], + ) { + Ok(shader_program) => shader_program, + Err(err) => { + tracing::error!("Failed to compose shader into program: {err}"); + continue; + } + }; + + let linked_shader_program = match shader_program.link() { + Ok(linked_shader_program) => linked_shader_program, + Err(err) => { + tracing::error!("Failed to link shader: {err}"); + continue; + } + }; + + context.programs.insert(*asset_id, linked_shader_program); + } + + context.modules.insert(*asset_id, module); + } +} + +fn load_module( + session: &SlangSession, + module_source: &ModuleSource, +) -> Result<Module, Error> +{ + let module = session.load_module_from_source_string( + &module_source.name, + &module_source.file_path.to_string_lossy(), + &module_source.source, + )?; + + Ok(Module { inner: module }) +} diff --git a/engine/src/shader/cursor.rs b/engine/src/shader/cursor.rs new file mode 100644 index 0000000..b5ba4e0 --- /dev/null +++ b/engine/src/shader/cursor.rs @@ -0,0 +1,160 @@ +use crate::asset::Handle as AssetHandle; +use crate::image::Image; +use crate::matrix::Matrix; +use crate::shader::{TypeKind, TypeLayout, VariableLayout}; +use crate::vector::Vec3; + +/// Shader cursor +#[derive(Clone)] +pub struct Cursor<'a> +{ + type_layout: TypeLayout<'a>, + binding_location: BindingLocation, +} + +impl<'a> Cursor<'a> +{ + pub fn new(var_layout: VariableLayout<'a>) -> Self + { + let binding_location = BindingLocation { + binding_index: var_layout.binding_index(), + binding_size: 0, + byte_offset: var_layout.offset(), + }; + + Self { + type_layout: var_layout.type_layout().unwrap(), + binding_location, + } + } + + pub fn field(&self, name: &str) -> Self + { + let Some(field_var_layout) = self.type_layout.get_field_by_name(name) else { + panic!("Field '{name}' does not exist"); + }; + + let field_type_kind = field_var_layout.ty().unwrap().kind(); + + let field_var_layout = match field_type_kind { + TypeKind::ConstantBuffer => field_var_layout + .type_layout() + .expect("Constant buffer field has no type layout") + .element_var_layout() + .expect("Constant buffer field type layout has no element var layout"), + TypeKind::Array + | TypeKind::Matrix + | TypeKind::Scalar + | TypeKind::Vector + | TypeKind::Struct + | TypeKind::Resource => field_var_layout, + type_kind => unimplemented!("Type kind {type_kind:?} is not yet supported"), + }; + + Self { + type_layout: field_var_layout.type_layout().unwrap(), + binding_location: BindingLocation { + binding_index: self.binding_location.binding_index + + field_var_layout.binding_index(), + binding_size: if field_type_kind == TypeKind::ConstantBuffer { + field_var_layout + .type_layout() + .unwrap() + .uniform_size() + .unwrap() + } else { + self.binding_location.binding_size + }, + byte_offset: self.binding_location.byte_offset + + field_var_layout.offset(), + }, + } + } + + pub fn element(mut self, index: usize) -> Self + { + let element_type_layout = self.type_layout.element_type_layout().unwrap(); + + self.binding_location.byte_offset += index * element_type_layout.stride(); + + self.type_layout = element_type_layout; + + self + } + + pub fn with_field(self, name: &str, func: impl FnOnce(Self) -> Self) -> Self + { + let _ = func(self.field(name)); + + self + } + + pub fn binding_location(&self) -> &BindingLocation + { + &self.binding_location + } + + pub fn into_binding_location(self) -> BindingLocation + { + self.binding_location + } +} + +#[derive(Debug, Clone)] +pub struct BindingLocation +{ + pub binding_index: u32, + pub binding_size: usize, + pub byte_offset: usize, +} + +#[derive(Debug, Clone)] +pub enum BindingValue +{ + Uint(u32), + Int(i32), + Float(f32), + FVec3(Vec3<f32>), + FMat4x4(Matrix<f32, 4, 4>), + Texture(AssetHandle<Image>), +} + +impl From<u32> for BindingValue +{ + fn from(value: u32) -> Self + { + BindingValue::Uint(value) + } +} + +impl From<i32> for BindingValue +{ + fn from(value: i32) -> Self + { + BindingValue::Int(value) + } +} + +impl From<f32> for BindingValue +{ + fn from(value: f32) -> Self + { + BindingValue::Float(value) + } +} + +impl From<Vec3<f32>> for BindingValue +{ + fn from(vec: Vec3<f32>) -> Self + { + BindingValue::FVec3(vec) + } +} + +impl From<Matrix<f32, 4, 4>> for BindingValue +{ + fn from(matrix: Matrix<f32, 4, 4>) -> Self + { + BindingValue::FMat4x4(matrix) + } +} diff --git a/engine/src/shader/default.rs b/engine/src/shader/default.rs new file mode 100644 index 0000000..28bbdc9 --- /dev/null +++ b/engine/src/shader/default.rs @@ -0,0 +1,363 @@ +use std::path::Path; +use std::sync::LazyLock; + +use ecs::Query; +use ecs::actions::Actions; +use ecs::query::term::Without; +use ecs::sole::Single; + +use crate::asset::{Assets, Label as AssetLabel}; +use crate::camera::{Active as ActiveCamera, Camera}; +use crate::data_types::dimens::Dimens; +use crate::draw_flags::NoDraw; +use crate::lighting::{DirectionalLight, GlobalLight, PointLight}; +use crate::material::{Flags as MaterialFlags, Material}; +use crate::matrix::Matrix; +use crate::model::{MaterialSearchResult, Model}; +use crate::projection::{ClipVolume as ProjectionClipVolume, Projection}; +use crate::renderer::{DEFAULT_TEXTURE_ASSET_LABEL, PendingShaderBindings}; +use crate::shader::cursor::{BindingValue as ShaderBindingValue, Cursor as ShaderCursor}; +use crate::shader::{ + Context as ShaderContext, + ModuleSource as ShaderModuleSource, + Shader, +}; +use crate::transform::{Scale, Transform, WorldPosition}; +use crate::vector::Vec3; +use crate::windowing::window::Window; + +pub static ASSET_LABEL: LazyLock<AssetLabel> = LazyLock::new(|| AssetLabel { + path: Path::new("").into(), + name: Some("default_shader".into()), +}); + +pub fn enqueue_set_shader_bindings( + renderable_query: Query<RenderableEntity<'_>, (Without<NoDraw>,)>, + camera_query: Query<(&Camera, &WorldPosition, &ActiveCamera)>, + window_query: Query<(&Window,)>, + point_light_query: Query<(&PointLight, &WorldPosition)>, + directional_light_query: Query<(&DirectionalLight,)>, + assets: Single<Assets>, + shader_context: Single<ShaderContext>, + global_light: Single<GlobalLight>, + mut actions: Actions, +) +{ + let Some((camera, camera_world_pos, _)) = camera_query.iter().next() else { + tracing::warn!("No current camera"); + return; + }; + + let Some((window,)) = window_query.iter().next() else { + // tracing::warn!("No window"); + return; + }; + + let default_shader_asset = assets + .get_handle_to_loaded::<ShaderModuleSource>(ASSET_LABEL.clone()) + .unwrap(); + + for ( + entity_id, + (model, material_flags, world_pos, scale, shader, mut pending_shader_bindings), + ) in renderable_query.iter_with_euids() + { + let shader_asset = match &shader { + Some(shader) => &shader.asset_handle, + None => &default_shader_asset, + }; + + if shader_asset.id() != default_shader_asset.id() { + continue; + } + + let Some(shader_program) = shader_context.get_program(&shader_asset.id()) else { + continue; + }; + + let Some(model_spec) = assets.get(&model.spec_asset) else { + continue; + }; + + let has_pending_shader_bindings_comp = pending_shader_bindings.is_some(); + + let shader_bindings = match pending_shader_bindings.as_deref_mut() { + Some(pending_shader_bindings) => pending_shader_bindings, + None => &mut PendingShaderBindings::default(), + }; + + let shader_cursor = ShaderCursor::new( + shader_program + .reflection(0) + .unwrap() + .global_params_var_layout() + .unwrap(), + ); + + let model_matrix = create_model_matrix(Transform { + position: world_pos.as_deref().cloned().unwrap_or_default().position, + scale: scale.as_deref().cloned().unwrap_or_default().scale, + }); + + let inverted_model_matrix = model_matrix.inverse(); + + let model_material = match model_spec.find_first_material(&assets) { + MaterialSearchResult::Found(model_material_asset) => { + let Some(model_material) = assets.get(&model_material_asset) else { + continue; + }; + + model_material + } + MaterialSearchResult::NotFound => { + continue; + } + MaterialSearchResult::NoMaterials => &const { Material::builder().build() }, + }; + + if [ + &model_material.ambient_map, + &model_material.diffuse_map, + &model_material.specular_map, + ] + .into_iter() + .flatten() + .any(|texture| !assets.is_loaded_and_has_type(&texture.asset_handle)) + { + continue; + } + + let material_flags = material_flags + .as_deref() + .unwrap_or(&const { MaterialFlags::builder().build() }); + + let model_3d_shader_cursor = shader_cursor.field("Uniforms").field("model_3d"); + let lighting_shader_cursor = shader_cursor.field("Uniforms").field("lighting"); + let material_shader_cursor = lighting_shader_cursor.field("material"); + + shader_bindings.extend([ + (model_3d_shader_cursor.field("model"), model_matrix.into()), + ( + model_3d_shader_cursor.field("model_inverted"), + inverted_model_matrix.into(), + ), + ( + model_3d_shader_cursor.field("view"), + create_view_matrix(&camera, &camera_world_pos.position).into(), + ), + ( + model_3d_shader_cursor.field("projection"), + create_projection_matrix( + &camera, + &camera_world_pos.position, + window.inner_size(), + ) + .into(), + ), + ( + lighting_shader_cursor.field("view_pos"), + camera_world_pos.position.into(), + ), + ( + material_shader_cursor.field("ambient"), + Vec3::from( + if material_flags.use_ambient_color { + &model_material.ambient + } else { + &global_light.ambient + } + .clone(), + ) + .into(), + ), + ( + material_shader_cursor.field("diffuse"), + Vec3::from(model_material.diffuse.clone()).into(), + ), + ( + material_shader_cursor.field("specular"), + Vec3::from(model_material.specular.clone()).into(), + ), + ( + material_shader_cursor.field("shininess"), + model_material.shininess.into(), + ), + ( + lighting_shader_cursor.field("directional_light_cnt"), + u32::try_from(directional_light_query.iter().count()) + .expect( + "Directional light count does not fit in 32-bit unsigned integer", + ) + .into(), + ), + ( + lighting_shader_cursor.field("point_light_cnt"), + u32::try_from(point_light_query.iter().count()) + .expect("Point light count does not fit in 32-bit unsigned integer") + .into(), + ), + ]); + + shader_bindings.extend(point_light_query.iter().enumerate().flat_map( + |(point_light_index, (point_light, point_light_world_pos))| { + let point_light_shader_cursor = lighting_shader_cursor + .field("point_lights") + .element(point_light_index); + + let phong_shader_cursor = point_light_shader_cursor.field("phong"); + + let attenuation_props_shader_cursor = + point_light_shader_cursor.field("attenuation_props"); + + [ + ( + phong_shader_cursor.field("diffuse"), + Vec3::from(point_light.diffuse.clone()).into(), + ), + ( + phong_shader_cursor.field("specular"), + Vec3::from(point_light.specular.clone()).into(), + ), + ( + point_light_shader_cursor.field("position"), + (point_light_world_pos.position + point_light.local_position) + .into(), + ), + ( + attenuation_props_shader_cursor.field("constant"), + point_light.attenuation_params.constant.into(), + ), + ( + attenuation_props_shader_cursor.field("linear"), + point_light.attenuation_params.linear.into(), + ), + ( + attenuation_props_shader_cursor.field("quadratic"), + point_light.attenuation_params.quadratic.into(), + ), + ] + }, + )); + + shader_bindings.extend(directional_light_query.iter().enumerate().flat_map( + |(directional_light_index, (directional_light,))| { + let directional_light_shader_cursor = lighting_shader_cursor + .field("directional_lights") + .element(directional_light_index); + + let phong_shader_cursor = directional_light_shader_cursor.field("phong"); + + [ + ( + phong_shader_cursor.field("diffuse"), + Vec3::from(directional_light.diffuse.clone()).into(), + ), + ( + phong_shader_cursor.field("specular"), + Vec3::from(directional_light.specular.clone()).into(), + ), + ( + directional_light_shader_cursor.field("direction"), + directional_light.direction.into(), + ), + ] + }, + )); + + shader_bindings.bindings.push(( + shader_cursor.field("ambient_map").into_binding_location(), + ShaderBindingValue::Texture( + model_material + .ambient_map + .as_ref() + .map(|ambient_map| ambient_map.asset_handle.clone()) + .unwrap_or_else(|| { + assets + .get_handle_to_loaded(DEFAULT_TEXTURE_ASSET_LABEL.clone()) + .expect("Not possible") + }), + ), + )); + + shader_bindings.bindings.push(( + shader_cursor.field("diffuse_map").into_binding_location(), + ShaderBindingValue::Texture( + model_material + .diffuse_map + .as_ref() + .map(|diffuse_map| diffuse_map.asset_handle.clone()) + .unwrap_or_else(|| { + assets + .get_handle_to_loaded(DEFAULT_TEXTURE_ASSET_LABEL.clone()) + .expect("Not possible") + }), + ), + )); + + shader_bindings.bindings.push(( + shader_cursor.field("specular_map").into_binding_location(), + ShaderBindingValue::Texture( + model_material + .specular_map + .as_ref() + .map(|specular_map| specular_map.asset_handle.clone()) + .unwrap_or_else(|| { + assets + .get_handle_to_loaded(DEFAULT_TEXTURE_ASSET_LABEL.clone()) + .expect("Not possible") + }), + ), + )); + + if !has_pending_shader_bindings_comp { + actions.add_components(entity_id, (shader_bindings.clone(),)); + } + } +} + +fn create_model_matrix(transform: Transform) -> Matrix<f32, 4, 4> +{ + let mut matrix = Matrix::new_identity(); + + matrix.translate(&transform.position); + matrix.scale(&transform.scale); + + matrix +} + +fn create_view_matrix(camera: &Camera, camera_world_pos: &Vec3<f32>) +-> Matrix<f32, 4, 4> +{ + let mut view = Matrix::new(); + + // tracing::debug!("Camera target: {:?}", camera.target); + + view.look_at(camera_world_pos, &camera.target, &camera.global_up); + + view +} + +fn create_projection_matrix( + camera: &Camera, + camera_world_pos: &Vec3<f32>, + window_size: &Dimens<u32>, +) -> Matrix<f32, 4, 4> +{ + match &camera.projection { + Projection::Perspective(perspective_proj) => perspective_proj.to_matrix_rh( + window_size.width as f32 / window_size.height as f32, + ProjectionClipVolume::NegOneToOne, + ), + Projection::Orthographic(orthographic_proj) => orthographic_proj + .to_matrix_rh(camera_world_pos, ProjectionClipVolume::NegOneToOne), + } +} + +type RenderableEntity<'a> = ( + &'a Model, + Option<&'a MaterialFlags>, + Option<&'a WorldPosition>, + Option<&'a Scale>, + Option<&'a Shader>, + Option<&'a mut PendingShaderBindings>, +); diff --git a/engine/src/util.rs b/engine/src/util.rs index 9174734..0c81c71 100644 --- a/engine/src/util.rs +++ b/engine/src/util.rs @@ -158,7 +158,8 @@ macro_rules! builder { )* #[must_use] - $visibility fn build(self) -> $name { + $visibility const fn build(self) -> $name + { $name { $( $field: self.$field, |
