diff options
Diffstat (limited to 'engine/src')
28 files changed, 1843 insertions, 678 deletions
diff --git a/engine/src/asset.rs b/engine/src/asset.rs new file mode 100644 index 0000000..db4d23c --- /dev/null +++ b/engine/src/asset.rs @@ -0,0 +1,777 @@ +use std::any::{type_name, Any}; +use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::HashMap; +use std::convert::Infallible; +use std::ffi::{OsStr, OsString}; +use std::fmt::{Debug, Display}; +use std::hash::{DefaultHasher, Hash, Hasher}; +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; +use std::sync::mpsc::{ + channel as mpsc_channel, + Receiver as MpscReceiver, + Sender as MpscSender, +}; +use std::sync::Arc; + +use ecs::phase::PRE_UPDATE as PRE_UPDATE_PHASE; +use ecs::sole::Single; +use ecs::Sole; + +use crate::work_queue::{Work, WorkQueue}; + +/// Asset label. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Label<'a> +{ + pub path: Cow<'a, Path>, + pub name: Option<Cow<'a, str>>, +} + +impl Label<'_> +{ + pub fn to_owned(&self) -> LabelOwned + { + LabelOwned { + path: self.path.to_path_buf(), + name: self.name.as_ref().map(|name| name.to_string()), + } + } +} + +impl<'a> From<&'a Path> for Label<'a> +{ + fn from(path: &'a Path) -> Self + { + Self { path: path.into(), name: None } + } +} + +impl From<PathBuf> for Label<'_> +{ + fn from(path: PathBuf) -> Self + { + Self { path: path.into(), name: None } + } +} + +impl<'a> From<&'a LabelOwned> for Label<'a> +{ + fn from(label: &'a LabelOwned) -> Self + { + Self { + path: (&label.path).into(), + name: label.name.as_ref().map(|name| Cow::Borrowed(name.as_str())), + } + } +} + +impl Display for Label<'_> +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + write!(formatter, "{}", self.path.display())?; + + if let Some(name) = &self.name { + formatter.write_str("::")?; + formatter.write_str(&name)?; + } + + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LabelOwned +{ + pub path: PathBuf, + pub name: Option<String>, +} + +impl LabelOwned +{ + pub fn to_label(&self) -> Label<'_> + { + Label { + path: (&self.path).into(), + name: self.name.as_ref().map(|name| Cow::Borrowed(name.as_str())), + } + } +} + +impl Display for LabelOwned +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + write!(formatter, "{}", self.path.display())?; + + if let Some(name) = &self.name { + formatter.write_str("::")?; + formatter.write_str(&name)?; + } + + Ok(()) + } +} + +#[derive(Debug, Sole)] +pub struct Assets +{ + assets: Vec<StoredAsset>, + asset_lookup: RefCell<HashMap<LabelHash, LookupEntry>>, + importers: Vec<WrappedImporterFn>, + importer_lookup: HashMap<OsString, usize>, + import_work_queue: WorkQueue<ImportWorkUserData>, + import_work_msg_receiver: MpscReceiver<ImportWorkMessage>, + import_work_msg_sender: MpscSender<ImportWorkMessage>, +} + +impl Assets +{ + pub fn with_capacity(capacity: usize) -> Self + { + let (import_work_msg_sender, import_work_msg_receiver) = + mpsc_channel::<ImportWorkMessage>(); + + Self { + assets: Vec::with_capacity(capacity), + asset_lookup: RefCell::new(HashMap::with_capacity(capacity)), + importers: Vec::new(), + importer_lookup: HashMap::new(), + import_work_queue: WorkQueue::new(), + import_work_msg_receiver, + import_work_msg_sender, + } + } + + pub fn set_importer<'file_ext, AssetSettings, Err>( + &mut self, + file_extensions: impl IntoIterator<Item: Into<Cow<'file_ext, str>>>, + func: impl Fn(&mut Submitter<'_>, &Path, Option<&AssetSettings>) -> Result<(), Err>, + ) where + AssetSettings: 'static, + Err: std::error::Error + 'static, + { + self.importers.push(WrappedImporterFn::new(func)); + + let importer_index = self.importers.len() - 1; + + self.importer_lookup + .extend(file_extensions.into_iter().map(|file_ext| { + let file_ext: Cow<str> = file_ext.into(); + + (file_ext.into_owned().into(), importer_index) + })); + } + + #[tracing::instrument(skip_all, fields(asset_type=type_name::<Asset>()))] + pub fn get<'this, 'handle, Asset: 'static + Send + Sync>( + &'this self, + handle: &'handle Handle<Asset>, + ) -> Option<&'handle Asset> + where + 'this: 'handle, + { + let LookupEntry::Occupied(asset_index) = + *self.asset_lookup.borrow().get(&handle.id.label_hash)? + else { + return None; + }; + + 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"); + return None; + }; + + Some(asset) + } + + #[tracing::instrument(skip(self))] + pub fn load<'i, Asset: 'static + Send + Sync>( + &self, + label: impl Into<Label<'i>> + Debug, + ) -> Handle<Asset> + { + let label = label.into(); + + let label_hash = LabelHash::new(&label); + + let mut asset_lookup = self.asset_lookup.borrow_mut(); + + if Self::is_pending(&asset_lookup, &label) { + return Handle::new(label_hash); + } + + let Some(lookup_entry) = asset_lookup.get(&label_hash) else { + self.add_import_work::<Infallible>( + &label, + label_hash, + None, + &mut asset_lookup, + ); + + return Handle::new(label_hash); + }; + + 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>()); + } + } + LookupEntry::Pending => {} + } + + Handle::new(label_hash) + } + + #[tracing::instrument(skip(self))] + pub fn load_with_settings<'i, Asset, AssetSettings>( + &self, + label: impl Into<Label<'i>> + Debug, + asset_settings: AssetSettings, + ) -> Handle<Asset> + where + Asset: Send + Sync + 'static, + AssetSettings: Send + Sync + Debug + 'static, + { + let label = label.into(); + + let label_hash = LabelHash::new(&label); + + let mut asset_lookup = self.asset_lookup.borrow_mut(); + + if Self::is_pending(&asset_lookup, &label) { + return Handle::new(label_hash); + } + + let Some(lookup_entry) = asset_lookup.get(&label_hash) else { + self.add_import_work::<AssetSettings>( + &label, + label_hash, + Some(asset_settings), + &mut asset_lookup, + ); + + return Handle::new(label_hash); + }; + + 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 {} for asset", + type_name::<Asset>() + ); + } + } + LookupEntry::Pending => {} + } + + Handle::new(label_hash) + } + + pub fn store_with_name<'name, Asset: 'static + Send + Sync>( + &mut self, + name: impl Into<Cow<'name, str>>, + asset: Asset, + ) -> Handle<Asset> + { + self.store_with_label( + Label { + path: Path::new("").into(), + name: Some(name.into()), + }, + asset, + ) + } + + #[tracing::instrument(skip(self, asset), fields(asset_type=type_name::<Asset>()))] + pub fn store_with_label<'i, Asset: 'static + Send + Sync>( + &mut self, + label: impl Into<Label<'i>> + Debug, + asset: Asset, + ) -> Handle<Asset> + { + let label = label.into(); + + let label_hash = LabelHash::new(&label); + + if matches!( + self.asset_lookup.get_mut().get(&label_hash), + Some(LookupEntry::Occupied(_)) + ) { + tracing::error!("Asset already exists"); + + return Handle::new(label_hash); + } + + tracing::debug!("Storing asset"); + + self.assets.push(StoredAsset::new(asset)); + + let index = self.assets.len() - 1; + + self.asset_lookup + .get_mut() + .insert(label_hash, LookupEntry::Occupied(index)); + + if label.name.is_some() { + let parent_asset_label_hash = + LabelHash::new(&Label { path: label.path, name: None }); + + if matches!( + self.asset_lookup.get_mut().get(&parent_asset_label_hash), + Some(LookupEntry::Pending) + ) { + self.asset_lookup.get_mut().remove(&parent_asset_label_hash); + } else if self + .asset_lookup + .get_mut() + .get(&parent_asset_label_hash) + .is_none() + { + self.assets + .push(StoredAsset::new::<Option<Infallible>>(None)); + + self.asset_lookup.get_mut().insert( + parent_asset_label_hash, + LookupEntry::Occupied(self.assets.len() - 1), + ); + } + } + + Handle::new(label_hash) + } + + fn is_pending(asset_lookup: &HashMap<LabelHash, LookupEntry>, label: &Label) -> bool + { + if label.name.is_some() { + if let Some(LookupEntry::Pending) = + asset_lookup.get(&LabelHash::new(&Label { + path: label.path.as_ref().into(), + name: None, + })) + { + return true; + } + } + + if let Some(LookupEntry::Pending) = asset_lookup.get(&LabelHash::new(label)) { + return true; + }; + + false + } + + fn add_import_work<AssetSettings>( + &self, + label: &Label<'_>, + label_hash: LabelHash, + asset_settings: Option<AssetSettings>, + asset_lookup: &mut HashMap<LabelHash, LookupEntry>, + ) where + AssetSettings: Any + Send + Sync, + { + let Some(file_ext) = label.path.extension() else { + tracing::error!("Asset file is missing a file extension"); + return; + }; + + let Some(importer) = self.get_importer(file_ext) else { + tracing::error!( + "No importer exists for asset file extension {}", + file_ext.to_string_lossy() + ); + return; + }; + + self.import_work_queue.add_work(Work { + func: |ImportWorkUserData { + import_work_msg_sender, + asset_path, + asset_settings, + importer, + }| { + if let Err(err) = importer.call( + import_work_msg_sender, + asset_path.as_path(), + asset_settings.as_deref(), + ) { + tracing::error!( + "Failed to load asset {}: {err}", + asset_path.display() + ); + } + }, + user_data: ImportWorkUserData { + import_work_msg_sender: self.import_work_msg_sender.clone(), + asset_path: label.path.to_path_buf(), + asset_settings: asset_settings.map(|asset_settings| { + Box::new(asset_settings) as Box<dyn Any + Send + Sync> + }), + importer: importer.clone(), + }, + }); + + asset_lookup.insert(label_hash, LookupEntry::Pending); + + if label.name.is_some() { + asset_lookup.insert( + LabelHash::new(&Label { + path: label.path.as_ref().into(), + name: None, + }), + LookupEntry::Pending, + ); + } + } + + fn get_importer(&self, file_ext: &OsStr) -> Option<&WrappedImporterFn> + { + let index = *self.importer_lookup.get(file_ext)?; + + Some(self.importers.get(index).expect("Not possible")) + } +} + +impl Default for Assets +{ + fn default() -> Self + { + Self::with_capacity(0) + } +} + +pub struct Submitter<'path> +{ + import_work_msg_sender: MpscSender<ImportWorkMessage>, + asset_path: &'path Path, +} + +impl Submitter<'_> +{ + pub fn submit_load_other<'label, Asset: Send + Sync + 'static>( + &self, + label: impl Into<Label<'label>>, + ) -> Handle<Asset> + { + let label = label.into(); + + let _ = self.import_work_msg_sender.send(ImportWorkMessage::Load { + do_load: |assets, label, _asset_settings| { + let _ = assets.load::<Asset>(label); + }, + label: label.to_owned(), + asset_settings: None, + }); + + Handle::new(LabelHash::new(&label)) + } + + pub fn submit_load_other_with_settings<'label, Asset, AssetSettings>( + &self, + label: impl Into<Label<'label>>, + asset_settings: AssetSettings, + ) -> Handle<Asset> + where + Asset: Send + Sync + 'static, + AssetSettings: Send + Sync + Debug + 'static, + { + let label = label.into(); + + let _ = self.import_work_msg_sender.send(ImportWorkMessage::Load { + do_load: |assets, label, asset_settings| { + let asset_settings = *asset_settings + .expect("Not possible") + .downcast::<AssetSettings>() + .expect("Not possible"); + + let _ = assets + .load_with_settings::<Asset, AssetSettings>(label, asset_settings); + }, + label: label.to_owned(), + asset_settings: Some(Box::new(asset_settings)), + }); + + Handle::new(LabelHash::new(&label)) + } + + pub fn submit_store<Asset: Send + Sync + 'static>( + &self, + asset: Asset, + ) -> Handle<Asset> + { + let label = LabelOwned { + path: self.asset_path.into(), + name: None, + }; + + let label_hash = LabelHash::new(&label.to_label()); + + let _ = self.import_work_msg_sender.send(ImportWorkMessage::Store { + do_store: |assets, label, boxed_asset| { + let Ok(asset) = boxed_asset.downcast::<Asset>() else { + unreachable!(); + }; + + assets.store_with_label::<Asset>(&label, *asset); + }, + label, + asset: Box::new(asset), + }); + + Handle::new(label_hash) + } + + pub fn submit_store_named<Asset: Send + Sync + 'static>( + &self, + name: impl AsRef<str>, + asset: Asset, + ) -> Handle<Asset> + { + let label = LabelOwned { + path: self.asset_path.into(), + name: Some(name.as_ref().into()), + }; + + let label_hash = LabelHash::new(&label.to_label()); + + let _ = self.import_work_msg_sender.send(ImportWorkMessage::Store { + do_store: |assets, label, boxed_asset| { + let Ok(asset) = boxed_asset.downcast::<Asset>() else { + unreachable!(); + }; + + assets.store_with_label::<Asset>(&label, *asset); + }, + label, + asset: Box::new(asset), + }); + + Handle::new(label_hash) + } +} + +/// Asset handle. +#[derive(Debug)] +pub struct Handle<Asset: 'static> +{ + id: Id, + _pd: PhantomData<Asset>, +} + +impl<Asset: 'static> Handle<Asset> +{ + pub fn id(&self) -> Id + { + self.id + } + + fn new(label_hash: LabelHash) -> Self + { + Self { + id: Id { label_hash }, + _pd: PhantomData, + } + } +} + +impl<Asset: 'static> Clone for Handle<Asset> +{ + fn clone(&self) -> Self + { + Self { id: self.id, _pd: PhantomData } + } +} + +/// Asset ID. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id +{ + label_hash: LabelHash, +} + +#[derive(Debug, thiserror::Error)] +enum ImporterError +{ + #[error("Settings has a incorrect type")] + IncorrectAssetSettingsType(PathBuf), + + #[error(transparent)] + Other(Box<dyn std::error::Error>), +} + +#[derive(Debug, Clone)] +struct WrappedImporterFn +{ + wrapper_func: fn( + MpscSender<ImportWorkMessage>, + &Path, + Option<&(dyn Any + Send + Sync)>, + ) -> Result<(), ImporterError>, +} + +impl WrappedImporterFn +{ + fn new<InnerFunc, AssetSettings, Err>(inner_func_param: InnerFunc) -> Self + where + InnerFunc: + Fn(&mut Submitter<'_>, &Path, Option<&AssetSettings>) -> Result<(), Err>, + AssetSettings: 'static, + Err: std::error::Error + 'static, + { + assert_eq!(size_of::<InnerFunc>(), 0); + + let wrapper_func = + |import_work_msg_sender: MpscSender<ImportWorkMessage>, + asset_path: &Path, + asset_settings: Option<&(dyn Any + Send + Sync)>| { + let inner_func = unsafe { std::mem::zeroed::<InnerFunc>() }; + + let asset_settings = asset_settings + .map(|asset_settings| { + asset_settings + .downcast_ref::<AssetSettings>() + .ok_or_else(|| { + ImporterError::IncorrectAssetSettingsType( + asset_path.to_path_buf(), + ) + }) + }) + .transpose()?; + + inner_func( + &mut Submitter { import_work_msg_sender, asset_path }, + asset_path, + asset_settings, + ) + .map_err(|err| ImporterError::Other(Box::new(err)))?; + + Ok(()) + }; + + std::mem::forget(inner_func_param); + + Self { wrapper_func } + } + + fn call( + &self, + import_work_msg_sender: MpscSender<ImportWorkMessage>, + asset_path: &Path, + asset_settings: Option<&(dyn Any + Send + Sync)>, + ) -> Result<(), ImporterError> + { + (self.wrapper_func)(import_work_msg_sender, asset_path, asset_settings) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct LabelHash(u64); + +impl LabelHash +{ + fn new(label: &Label<'_>) -> Self + { + let mut hasher = DefaultHasher::new(); + + label.hash(&mut hasher); + + Self(hasher.finish()) + } +} + +#[derive(Debug, Default)] +pub(crate) struct Extension +{ + pub assets: Assets, +} + +impl ecs::extension::Extension for Extension +{ + fn collect(self, mut collector: ecs::extension::Collector<'_>) + { + let _ = collector.add_sole(self.assets); + + collector.add_system(*PRE_UPDATE_PHASE, add_received_assets); + } +} + +fn add_received_assets(mut assets: Single<Assets>) +{ + while let Some(import_work_msg) = assets.import_work_msg_receiver.try_recv().ok() { + match import_work_msg { + ImportWorkMessage::Store { do_store, label, asset } => { + do_store(&mut assets, label, asset); + } + ImportWorkMessage::Load { do_load, label, asset_settings } => { + do_load( + &assets, + Label { + path: label.path.as_path().into(), + name: label.name.as_deref().map(|name| name.into()), + }, + asset_settings, + ); + } + } + } +} + +#[derive(Debug)] +struct ImportWorkUserData +{ + import_work_msg_sender: MpscSender<ImportWorkMessage>, + asset_path: PathBuf, + asset_settings: Option<Box<dyn Any + Send + Sync>>, + importer: WrappedImporterFn, +} + +#[derive(Debug)] +enum ImportWorkMessage +{ + Store + { + do_store: fn(&mut Assets, LabelOwned, Box<dyn Any + Send + Sync>), + label: LabelOwned, + asset: Box<dyn Any + Send + Sync>, + }, + + Load + { + do_load: fn(&Assets, Label<'_>, Option<Box<dyn Any + Send + Sync>>), + label: LabelOwned, + asset_settings: Option<Box<dyn Any + Send + Sync>>, + }, +} + +#[derive(Debug, Clone, Copy)] +enum LookupEntry +{ + Occupied(usize), + Pending, +} + +#[derive(Debug)] +struct StoredAsset +{ + strong: Arc<dyn Any + Send + Sync>, +} + +impl StoredAsset +{ + fn new<Asset: Any + Send + Sync>(asset: Asset) -> Self + { + let strong = Arc::new(asset); + + Self { strong } + } +} diff --git a/engine/src/camera/fly.rs b/engine/src/camera/fly.rs index 087f727..d6eac62 100644 --- a/engine/src/camera/fly.rs +++ b/engine/src/camera/fly.rs @@ -7,8 +7,8 @@ use ecs::{Component, Query}; use crate::camera::{Active as ActiveCamera, Camera}; use crate::delta_time::DeltaTime; use crate::input::{Cursor, CursorFlags, Key, KeyState, Keys}; -use crate::transform::Position; -use crate::util::builder; +use crate::transform::WorldPosition; +use crate::builder; use crate::vector::{Vec2, Vec3}; builder! { @@ -75,7 +75,7 @@ pub struct Options } fn update( - camera_query: Query<(&mut Camera, &mut Position, &mut Fly, &ActiveCamera)>, + camera_query: Query<(&mut Camera, &mut WorldPosition, &mut Fly, &ActiveCamera)>, keys: Single<Keys>, cursor: Single<Cursor>, cursor_flags: Single<CursorFlags>, @@ -84,7 +84,7 @@ fn update( options: Local<Options>, ) { - for (mut camera, mut camera_pos, mut fly_camera, _) in &camera_query { + for (mut camera, mut camera_world_pos, mut fly_camera, _) in &camera_query { if cursor.has_moved && cursor_flags.is_first_move.flag { tracing::debug!("First cursor move"); @@ -122,29 +122,30 @@ fn update( camera.global_up = cam_right.cross(&direction).normalize(); if keys.get_key_state(Key::W) == KeyState::Pressed { - camera_pos.position += + camera_world_pos.position += direction * fly_camera.speed * delta_time.as_secs_f32(); } if keys.get_key_state(Key::S) == KeyState::Pressed { - camera_pos.position -= + camera_world_pos.position -= direction * fly_camera.speed * delta_time.as_secs_f32(); } if keys.get_key_state(Key::A) == KeyState::Pressed { let cam_left = -direction.cross(&Vec3::UP).normalize(); - camera_pos.position += cam_left * fly_camera.speed * delta_time.as_secs_f32(); + camera_world_pos.position += + cam_left * fly_camera.speed * delta_time.as_secs_f32(); } if keys.get_key_state(Key::D) == KeyState::Pressed { let cam_right = direction.cross(&Vec3::UP).normalize(); - camera_pos.position += + camera_world_pos.position += cam_right * fly_camera.speed * delta_time.as_secs_f32(); } - camera.target = camera_pos.position + direction; + camera.target = camera_world_pos.position + direction; } } diff --git a/engine/src/data_types/dimens.rs b/engine/src/data_types/dimens.rs index 5002436..d8d0247 100644 --- a/engine/src/data_types/dimens.rs +++ b/engine/src/data_types/dimens.rs @@ -6,6 +6,22 @@ pub struct Dimens<Value> pub height: Value, } +impl<Value: Clone> From<Value> for Dimens<Value> +{ + fn from(value: Value) -> Self + { + Self { width: value.clone(), height: value } + } +} + +impl<Value> From<(Value, Value)> for Dimens<Value> +{ + fn from(value: (Value, Value)) -> Self + { + Self { width: value.0, height: value.1 } + } +} + /// 3D dimensions. #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] pub struct Dimens3<Value> @@ -14,3 +30,27 @@ pub struct Dimens3<Value> pub height: Value, pub depth: Value, } + +impl<Value: Clone> From<Value> for Dimens3<Value> +{ + fn from(value: Value) -> Self + { + Self { + width: value.clone(), + height: value.clone(), + depth: value, + } + } +} + +impl<Value: Clone> From<(Value, Value, Value)> for Dimens3<Value> +{ + fn from(value: (Value, Value, Value)) -> Self + { + Self { + width: value.0, + height: value.1, + depth: value.2, + } + } +} diff --git a/engine/src/data_types/vector.rs b/engine/src/data_types/vector.rs index 802a4a7..100c709 100644 --- a/engine/src/data_types/vector.rs +++ b/engine/src/data_types/vector.rs @@ -3,6 +3,7 @@ use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign}; use crate::color::Color; #[derive(Debug, Default, Clone, Copy, PartialEq)] +#[repr(C)] pub struct Vec2<Value> { pub x: Value, diff --git a/engine/src/draw_flags.rs b/engine/src/draw_flags.rs index df5eed1..426f865 100644 --- a/engine/src/draw_flags.rs +++ b/engine/src/draw_flags.rs @@ -1,6 +1,6 @@ use ecs::Component; -use crate::util::builder; +use crate::builder; builder! { /// Flags for how a object should be drawn. diff --git a/engine/src/file_format/wavefront/mtl.rs b/engine/src/file_format/wavefront/mtl.rs index d90dbcf..f3c7a64 100644 --- a/engine/src/file_format/wavefront/mtl.rs +++ b/engine/src/file_format/wavefront/mtl.rs @@ -2,7 +2,7 @@ //! //! File format documentation: <https://paulbourke.net/dataformats/mtl> -use std::path::Path; +use std::path::{Path, PathBuf}; use crate::color::Color; use crate::file_format::wavefront::common::{ @@ -11,8 +11,6 @@ use crate::file_format::wavefront::common::{ ParsingError, Statement, }; -use crate::material::{Builder as MaterialBuilder, Material}; -use crate::texture::{Error as TextureError, Texture}; /// Parses the content of a Wavefront `.mtl`. /// @@ -50,18 +48,41 @@ pub fn parse(obj_content: &str) -> Result<Vec<NamedMaterial>, Error> } #[derive(Debug, Clone)] +#[non_exhaustive] pub struct NamedMaterial { pub name: String, - pub material: Material, + pub ambient: Color<f32>, + pub diffuse: Color<f32>, + pub specular: Color<f32>, + pub ambient_map: Option<TextureMap>, + pub diffuse_map: Option<TextureMap>, + pub specular_map: Option<TextureMap>, + pub shininess: f32, +} + +impl Default for NamedMaterial +{ + fn default() -> Self + { + Self { + name: String::new(), + ambient: Color::WHITE_F32, + diffuse: Color::WHITE_F32, + specular: Color::WHITE_F32, + ambient_map: None, + diffuse_map: None, + specular_map: None, + shininess: 0.0, + } + } } #[derive(Debug, Clone)] -pub struct UnfinishedNamedMaterial +#[non_exhaustive] +pub struct TextureMap { - name: String, - material_builder: MaterialBuilder, - ready: bool, + pub path: PathBuf, } #[derive(Debug, thiserror::Error)] @@ -70,8 +91,14 @@ pub enum Error #[error(transparent)] ParsingError(#[from] ParsingError), - #[error("Failed to open texture")] - TextureError(#[from] TextureError), + #[error( + "A material start statement (newmtl) is expected before statement at line {}", + line_no + )] + ExpectedMaterialStartStmtBeforeStmt + { + line_no: usize + }, #[error( "Unsupported number of arguments ({arg_count}) to {keyword} at line {line_no}" @@ -100,59 +127,52 @@ fn statements_to_materials( { let mut materials = Vec::<NamedMaterial>::with_capacity(material_cnt); - let mut curr_material = UnfinishedNamedMaterial { - name: String::new(), - material_builder: MaterialBuilder::new(), - ready: false, - }; - for (line_no, statement) in statements { if statement.keyword == Keyword::Newmtl { - if curr_material.ready { - tracing::debug!("Building material"); - - let material = curr_material.material_builder.clone().build(); - - materials.push(NamedMaterial { name: curr_material.name, material }); - } - let name = statement.get_text_arg(0, line_no)?; - curr_material.name = name.to_string(); - curr_material.ready = true; + materials.push(NamedMaterial { + name: name.to_string(), + ..Default::default() + }); continue; } - if !curr_material.ready { - // Discard statements not belonging to a material - continue; + let Some(curr_material) = materials.last_mut() else { + return Err(Error::ExpectedMaterialStartStmtBeforeStmt { line_no }); }; match statement.keyword { Keyword::Ka => { let color = get_color_from_statement(&statement, line_no)?; - tracing::debug!("Adding ambient color"); + tracing::debug!( + "Adding ambient color {color:?} to material {}", + curr_material.name + ); - curr_material.material_builder = - curr_material.material_builder.ambient(color); + curr_material.ambient = color; } Keyword::Kd => { let color = get_color_from_statement(&statement, line_no)?; - tracing::debug!("Adding diffuse color"); + tracing::debug!( + "Adding diffuse color {color:?} to material {}", + curr_material.name + ); - curr_material.material_builder = - curr_material.material_builder.diffuse(color); + curr_material.diffuse = color; } Keyword::Ks => { let color = get_color_from_statement(&statement, line_no)?; - tracing::debug!("Adding specular color"); + tracing::debug!( + "Adding specular color {color:?} to material {}", + curr_material.name + ); - curr_material.material_builder = - curr_material.material_builder.specular(color); + curr_material.specular = color; } Keyword::MapKa => { if statement.arguments.len() > 1 { @@ -165,51 +185,75 @@ fn statements_to_materials( let texture_file_path = statement.get_text_arg(0, line_no)?; - let texture = Texture::open(Path::new(texture_file_path))?; - - tracing::debug!("Adding ambient map"); + tracing::debug!( + "Adding ambient map {texture_file_path} to material {}", + curr_material.name + ); - let texture_id = texture.id(); - - curr_material.material_builder = curr_material - .material_builder - .texture(texture) - .ambient_map(texture_id); + curr_material.ambient_map = Some(TextureMap { + path: Path::new(texture_file_path).to_path_buf(), + }); } Keyword::MapKd => { - let texture = get_map_from_texture(&statement, line_no)?; + if statement.arguments.len() > 1 { + return Err(Error::UnsupportedArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no, + }); + } - tracing::debug!("Adding diffuse map"); + let texture_file_path = statement.get_text_arg(0, line_no)?; - let texture_id = texture.id(); + tracing::debug!( + "Adding diffuse map {texture_file_path} to material {}", + curr_material.name + ); - curr_material.material_builder = curr_material - .material_builder - .texture(texture) - .diffuse_map(texture_id); + curr_material.diffuse_map = Some(TextureMap { + path: Path::new(texture_file_path).to_path_buf(), + }); } Keyword::MapKs => { - let texture = get_map_from_texture(&statement, line_no)?; + if statement.arguments.len() > 1 { + return Err(Error::UnsupportedArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no, + }); + } - tracing::debug!("Adding specular map"); + let texture_file_path = statement.get_text_arg(0, line_no)?; - let texture_id = texture.id(); + tracing::debug!( + "Adding specular map {texture_file_path} to material {}", + curr_material.name + ); - curr_material.material_builder = curr_material - .material_builder - .texture(texture) - .specular_map(texture_id); + curr_material.specular_map = Some(TextureMap { + path: Path::new(texture_file_path).to_path_buf(), + }); } - Keyword::Newmtl => {} - } - } + Keyword::Ns => { + if statement.arguments.len() != 1 { + return Err(Error::UnsupportedArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no, + }); + } - if curr_material.ready { - tracing::debug!("Building last material"); + let shininess = statement.get_float_arg(0, line_no)?; - let material = curr_material.material_builder.build(); + tracing::debug!( + "Adding shininess {shininess} to material {}", + curr_material.name + ); - materials.push(NamedMaterial { name: curr_material.name, material }); + curr_material.shininess = shininess; + } + Keyword::Newmtl => {} + } } Ok(materials) @@ -235,24 +279,6 @@ fn get_color_from_statement( Ok(Color { red, green, blue }) } -fn get_map_from_texture( - statement: &Statement<Keyword>, - line_no: usize, -) -> Result<Texture, Error> -{ - if statement.arguments.len() > 1 { - return Err(Error::UnsupportedArgumentCount { - keyword: statement.keyword.to_string(), - arg_count: statement.arguments.len(), - line_no, - }); - } - - let texture_file_path = statement.get_text_arg(0, line_no)?; - - Ok(Texture::open(Path::new(texture_file_path))?) -} - keyword! { #[derive(Debug, PartialEq, Eq, Clone, Copy)] enum Keyword { @@ -271,5 +297,7 @@ keyword! { #[keyword(rename = "map_Ks")] MapKs, + + Ns, } } diff --git a/engine/src/file_format/wavefront/obj.rs b/engine/src/file_format/wavefront/obj.rs index 6ca11c2..88d566c 100644 --- a/engine/src/file_format/wavefront/obj.rs +++ b/engine/src/file_format/wavefront/obj.rs @@ -13,10 +13,9 @@ use crate::file_format::wavefront::common::{ Statement, Triplet, }; -use crate::mesh::Mesh; +use crate::mesh::{Mesh, Vertex}; use crate::util::try_option; use crate::vector::{Vec2, Vec3}; -use crate::vertex::{Builder as VertexBuilder, Vertex}; /// Parses the content of a Wavefront `.obj`. /// @@ -168,7 +167,7 @@ impl FaceVertex /// - The face's vertex normal cannot be found in the given [`Obj`] pub fn to_vertex(&self, obj: &Obj) -> Result<Vertex, Error> { - let mut vertex_builder = VertexBuilder::default(); + let mut vertex_builder = Vertex::builder(); let vertex_pos = *obj.vertex_positions.get(self.position as usize - 1).ok_or( Error::FaceVertexPositionNotFound { vertex_pos_index: self.position }, diff --git a/engine/src/image.rs b/engine/src/image.rs new file mode 100644 index 0000000..0e04412 --- /dev/null +++ b/engine/src/image.rs @@ -0,0 +1,184 @@ +use std::fs::File; +use std::io::BufReader; +use std::path::Path; + +use image_rs::GenericImageView as _; + +use crate::asset::{Assets, Submitter as AssetSubmitter}; +use crate::color::Color; +use crate::data_types::dimens::Dimens; +use crate::builder; + +#[derive(Debug)] +pub struct Image +{ + inner: image_rs::DynamicImage, +} + +impl Image +{ + pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> + { + let buffered_reader = + BufReader::new(File::open(&path).map_err(Error::ReadFailed)?); + + let image_reader = image_rs::io::Reader::with_format( + buffered_reader, + image_rs::ImageFormat::from_path(path) + .map_err(|_| Error::UnsupportedFormat)?, + ); + + Ok(Self { + inner: image_reader + .decode() + .map_err(|err| Error::DecodeFailed(DecodeError(err)))?, + }) + } + + pub fn from_color(dimens: impl Into<Dimens<u32>>, color: impl Into<Color<u8>>) + -> Self + { + let dimens: Dimens<u32> = dimens.into(); + + let color: Color<u8> = color.into(); + + Self { + inner: image_rs::RgbImage::from_pixel( + dimens.width, + dimens.height, + image_rs::Rgb([color.red, color.green, color.blue]), + ) + .into(), + } + } + + pub fn dimensions(&self) -> Dimens<u32> + { + self.inner.dimensions().into() + } + + pub fn color_type(&self) -> ColorType + { + self.inner.color().into() + } + + pub fn as_bytes(&self) -> &[u8] + { + self.inner.as_bytes() + } +} + +builder! { +#[builder(name = SettingsBuilder, derives=(Debug, Clone))] +#[derive(Debug, Default, Clone)] +#[non_exhaustive] +pub struct Settings { +} +} + +impl Settings +{ + pub fn builder() -> SettingsBuilder + { + SettingsBuilder::default() + } +} + +impl Default for SettingsBuilder +{ + fn default() -> Self + { + Settings::default().into() + } +} + +/// An enumeration over supported color types and bit depths +#[derive(Copy, PartialEq, Eq, Debug, Clone, Hash)] +#[non_exhaustive] +pub enum ColorType +{ + /// Pixel is 8-bit luminance + L8, + + /// Pixel is 8-bit luminance with an alpha channel + La8, + + /// Pixel contains 8-bit R, G and B channels + Rgb8, + + /// Pixel is 8-bit RGB with an alpha channel + Rgba8, + + /// Pixel is 16-bit luminance + L16, + + /// Pixel is 16-bit luminance with an alpha channel + La16, + + /// Pixel is 16-bit RGB + Rgb16, + + /// Pixel is 16-bit RGBA + Rgba16, + + /// Pixel is 32-bit float RGB + Rgb32F, + + /// Pixel is 32-bit float RGBA + Rgba32F, +} + +impl From<image_rs::ColorType> for ColorType +{ + fn from(color_type: image_rs::ColorType) -> Self + { + match color_type { + image_rs::ColorType::L8 => Self::L8, + image_rs::ColorType::La8 => Self::La8, + image_rs::ColorType::Rgb8 => Self::Rgb8, + image_rs::ColorType::Rgba8 => Self::Rgba8, + image_rs::ColorType::L16 => Self::L16, + image_rs::ColorType::La16 => Self::La16, + image_rs::ColorType::Rgb16 => Self::Rgb16, + image_rs::ColorType::Rgba16 => Self::Rgba16, + image_rs::ColorType::Rgb32F => Self::Rgb32F, + image_rs::ColorType::Rgba32F => Self::Rgba32F, + _ => { + panic!("Unrecognized image_rs::ColorType variant"); + } + } + } +} + +pub fn set_asset_importers(assets: &mut Assets) +{ + assets.set_importer::<_, _>(["png", "jpg"], import_asset); +} + +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + #[error("Failed to read image file")] + ReadFailed(#[source] std::io::Error), + + #[error("Failed to decode image")] + DecodeFailed(DecodeError), + + #[error("Unsupported image format")] + UnsupportedFormat, +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct DecodeError(image_rs::ImageError); + +fn import_asset( + asset_submitter: &mut AssetSubmitter<'_>, + path: &Path, + _settings: Option<&'_ Settings>, +) -> Result<(), Error> +{ + asset_submitter.submit_store::<Image>(Image::open(path)?); + + Ok(()) +} diff --git a/engine/src/input.rs b/engine/src/input.rs index 95de048..d6a82f6 100644 --- a/engine/src/input.rs +++ b/engine/src/input.rs @@ -1,8 +1,8 @@ use std::collections::HashMap; use ecs::extension::Collector as ExtensionCollector; +use ecs::pair::{ChildOf, Pair}; use ecs::phase::{Phase, PRE_UPDATE as PRE_UPDATE_PHASE, START as START_PHASE}; -use ecs::relationship::{ChildOf, Relationship}; use ecs::sole::Single; use ecs::{static_entity, Sole}; @@ -18,10 +18,7 @@ pub use reexports::*; static_entity!( SET_PREV_KEY_STATE_PHASE, - ( - Phase, - <Relationship<ChildOf, Phase>>::new(*WINDOW_UPDATE_PHASE) - ) + (Phase, Pair::new::<ChildOf>(*WINDOW_UPDATE_PHASE)) ); #[derive(Debug, Sole)] diff --git a/engine/src/lib.rs b/engine/src/lib.rs index a9a5a97..6ccba53 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -1,35 +1,40 @@ #![deny(clippy::all, clippy::pedantic)] #![allow(clippy::needless_pass_by_value)] -use ecs::component::{Component, Sequence as ComponentSequence}; +use ecs::component::Sequence as ComponentSequence; use ecs::extension::Extension; +use ecs::pair::Pair; use ecs::phase::PRE_UPDATE as PRE_UPDATE_PHASE; use ecs::sole::Sole; use ecs::system::{Into, System}; use ecs::uid::Uid; use ecs::{SoleAlreadyExistsError, World}; +use crate::asset::{Assets, Extension as AssetExtension}; use crate::delta_time::{update as update_delta_time, DeltaTime, LastUpdate}; mod opengl; mod util; +mod work_queue; +pub mod asset; pub mod camera; pub mod collision; pub mod data_types; pub mod delta_time; pub mod draw_flags; pub mod file_format; +pub mod image; pub mod input; pub mod lighting; pub mod material; pub mod math; pub mod mesh; +pub mod model; pub mod projection; pub mod renderer; pub mod texture; pub mod transform; -pub mod vertex; pub mod window; pub extern crate ecs; @@ -37,6 +42,8 @@ pub extern crate ecs; pub(crate) use crate::data_types::matrix; pub use crate::data_types::{color, vector}; +const INITIAL_ASSET_CAPACITY: usize = 128; + #[derive(Debug)] pub struct Engine { @@ -60,6 +67,13 @@ impl Engine .initialize((LastUpdate::default(),)), ); + let mut assets = Assets::with_capacity(INITIAL_ASSET_CAPACITY); + + crate::model::set_asset_importers(&mut assets); + crate::image::set_asset_importers(&mut assets); + + world.add_extension(AssetExtension { assets }); + Self { world } } @@ -79,12 +93,11 @@ impl Engine self.world.register_system(phase_euid, system); } - pub fn register_observer_system<'this, SystemImpl, Event>( + pub fn register_observer_system<'this, SystemImpl>( &'this mut self, system: impl System<'this, SystemImpl>, - event: Event, - ) where - Event: Component, + event: Pair<Uid, Uid>, + ) { self.world.register_observer_system(system, event); } @@ -104,7 +117,7 @@ impl Engine } /// Runs the event loop. - pub fn start(&self) + pub fn start(&mut self) { self.world.start_loop(); } diff --git a/engine/src/lighting.rs b/engine/src/lighting.rs index 48adb0e..4406ed5 100644 --- a/engine/src/lighting.rs +++ b/engine/src/lighting.rs @@ -2,7 +2,7 @@ use ecs::{Component, Sole}; use crate::color::Color; use crate::data_types::vector::Vec3; -use crate::util::builder; +use crate::builder; builder! { #[builder(name = PointLightBuilder, derives = (Debug, Clone))] @@ -10,7 +10,8 @@ builder! { #[non_exhaustive] pub struct PointLight { - pub position: Vec3<f32>, + /// Position in local space. + pub local_position: Vec3<f32>, pub diffuse: Color<f32>, pub specular: Color<f32>, pub attenuation_params: AttenuationParams, @@ -31,7 +32,7 @@ impl Default for PointLight fn default() -> Self { Self { - position: Vec3::default(), + local_position: Vec3::default(), diffuse: Color { red: 0.5, green: 0.5, blue: 0.5 }, specular: Color { red: 1.0, green: 1.0, blue: 1.0 }, attenuation_params: AttenuationParams::default(), diff --git a/engine/src/material.rs b/engine/src/material.rs index e368519..56ff15f 100644 --- a/engine/src/material.rs +++ b/engine/src/material.rs @@ -1,21 +1,19 @@ use ecs::Component; use crate::color::Color; -use crate::data_types::dimens::Dimens; -use crate::texture::{Id as TextureId, Texture}; -use crate::util::builder; +use crate::texture::Texture; +use crate::builder; -#[derive(Debug, Clone, Component)] +#[derive(Debug, Clone)] #[non_exhaustive] pub struct Material { pub ambient: Color<f32>, pub diffuse: Color<f32>, pub specular: Color<f32>, - pub ambient_map: TextureId, - pub diffuse_map: TextureId, - pub specular_map: TextureId, - pub textures: Vec<Texture>, + pub ambient_map: Option<Texture>, + pub diffuse_map: Option<Texture>, + pub specular_map: Option<Texture>, pub shininess: f32, } @@ -27,17 +25,24 @@ impl Material } } +impl Default for Material +{ + fn default() -> Self + { + Self::builder().build() + } +} + /// [`Material`] builder. #[derive(Debug, Clone)] pub struct Builder { - ambient: Option<Color<f32>>, - diffuse: Option<Color<f32>>, - specular: Option<Color<f32>>, - ambient_map: Option<TextureId>, - diffuse_map: Option<TextureId>, - specular_map: Option<TextureId>, - textures: Vec<Texture>, + ambient: Color<f32>, + diffuse: Color<f32>, + specular: Color<f32>, + ambient_map: Option<Texture>, + diffuse_map: Option<Texture>, + specular_map: Option<Texture>, shininess: f32, } @@ -47,13 +52,12 @@ impl Builder pub fn new() -> Self { Self { - ambient: None, - diffuse: None, - specular: None, + ambient: Color::WHITE_F32, + diffuse: Color::WHITE_F32, + specular: Color::WHITE_F32, ambient_map: None, diffuse_map: None, specular_map: None, - textures: Vec::new(), shininess: 32.0, } } @@ -61,7 +65,7 @@ impl Builder #[must_use] pub fn ambient(mut self, ambient: Color<f32>) -> Self { - self.ambient = Some(ambient); + self.ambient = ambient; self } @@ -69,7 +73,7 @@ impl Builder #[must_use] pub fn diffuse(mut self, diffuse: Color<f32>) -> Self { - self.diffuse = Some(diffuse); + self.diffuse = diffuse; self } @@ -77,13 +81,13 @@ impl Builder #[must_use] pub fn specular(mut self, specular: Color<f32>) -> Self { - self.specular = Some(specular); + self.specular = specular; self } #[must_use] - pub fn ambient_map(mut self, ambient_map: TextureId) -> Self + pub fn ambient_map(mut self, ambient_map: Texture) -> Self { self.ambient_map = Some(ambient_map); @@ -91,7 +95,7 @@ impl Builder } #[must_use] - pub fn diffuse_map(mut self, diffuse_map: TextureId) -> Self + pub fn diffuse_map(mut self, diffuse_map: Texture) -> Self { self.diffuse_map = Some(diffuse_map); @@ -99,7 +103,7 @@ impl Builder } #[must_use] - pub fn specular_map(mut self, specular_map: TextureId) -> Self + pub fn specular_map(mut self, specular_map: Texture) -> Self { self.specular_map = Some(specular_map); @@ -107,22 +111,6 @@ impl Builder } #[must_use] - pub fn textures(mut self, textures: impl IntoIterator<Item = Texture>) -> Self - { - self.textures = textures.into_iter().collect(); - - self - } - - #[must_use] - pub fn texture(mut self, texture: Texture) -> Self - { - self.textures.push(texture); - - self - } - - #[must_use] pub fn shininess(mut self, shininess: f32) -> Self { self.shininess = shininess; @@ -135,43 +123,15 @@ impl Builder /// # Panics /// Will panic if no ambient map, diffuse map or specular map is set. #[must_use] - pub fn build(mut self) -> Material + pub fn build(self) -> Material { - let ambient_map = self.ambient_map.unwrap_or_else(|| { - let texture = create_1x1_white_texture(); - let texture_id = texture.id(); - - self.textures.push(texture); - - texture_id - }); - - let diffuse_map = self.diffuse_map.unwrap_or_else(|| { - let texture = create_1x1_white_texture(); - let texture_id = texture.id(); - - self.textures.push(texture); - - texture_id - }); - - let specular_map = self.specular_map.unwrap_or_else(|| { - let texture = create_1x1_white_texture(); - let texture_id = texture.id(); - - self.textures.push(texture); - - texture_id - }); - Material { - ambient: self.ambient.unwrap_or(Color::WHITE_F32), - diffuse: self.diffuse.unwrap_or(Color::WHITE_F32), - specular: self.specular.unwrap_or(Color::WHITE_F32), - ambient_map, - diffuse_map, - specular_map, - textures: self.textures, + ambient: self.ambient, + diffuse: self.diffuse, + specular: self.specular, + ambient_map: self.ambient_map, + diffuse_map: self.diffuse_map, + specular_map: self.specular_map, shininess: self.shininess, } } @@ -206,8 +166,3 @@ impl Flags FlagsBuilder::default() } } - -fn create_1x1_white_texture() -> Texture -{ - Texture::new_from_color(&Dimens { width: 1, height: 1 }, &Color::WHITE_U8) -} diff --git a/engine/src/mesh.rs b/engine/src/mesh.rs index 917e7f7..fb977af 100644 --- a/engine/src/mesh.rs +++ b/engine/src/mesh.rs @@ -1,11 +1,9 @@ -use ecs::Component; - -use crate::vector::Vec3; -use crate::vertex::Vertex; +use crate::builder; +use crate::vector::{Vec2, Vec3}; pub mod cube; -#[derive(Debug, Clone, Component)] +#[derive(Debug, Clone, Default)] pub struct Mesh { vertices: Vec<Vertex>, @@ -77,6 +75,27 @@ impl Mesh } } +builder! { +#[builder(name = VertexBuilder, derives = (Debug, Default, Clone))] +#[derive(Debug, Clone, Default, PartialEq)] +#[non_exhaustive] +pub struct Vertex +{ + pub pos: Vec3<f32>, + pub texture_coords: Vec2<f32>, + pub normal: Vec3<f32>, +} +} + +impl Vertex +{ + #[must_use] + pub fn builder() -> VertexBuilder + { + VertexBuilder::default() + } +} + #[derive(Debug, Clone)] pub struct DirectionPositions<'mesh> { diff --git a/engine/src/mesh/cube.rs b/engine/src/mesh/cube.rs index c29ce0b..e91cf0e 100644 --- a/engine/src/mesh/cube.rs +++ b/engine/src/mesh/cube.rs @@ -1,8 +1,8 @@ +use crate::data_types::dimens::Dimens3; use crate::math::calc_triangle_surface_normal; -use crate::mesh::Mesh; -use crate::util::builder; +use crate::mesh::{Mesh, Vertex}; +use crate::builder; use crate::vector::{Vec2, Vec3}; -use crate::vertex::{Builder as VertexBuilder, Vertex}; builder! { /// Cube mesh creation specification. @@ -27,6 +27,18 @@ impl CreationSpec } } +impl CreationSpecBuilder +{ + pub fn dimens(mut self, dimens: Dimens3<f32>) -> Self + { + self.width = dimens.width; + self.height = dimens.height; + self.depth = dimens.depth; + + self + } +} + /// Describes a single side of a cube (obviously). #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum Side @@ -192,7 +204,7 @@ impl FaceVertices .expect("Vertex normal index is out of bounds") .clone(); - VertexBuilder::default() + Vertex::builder() .pos(vertex_pos) .normal(vertex_normal) .build() diff --git a/engine/src/model.rs b/engine/src/model.rs new file mode 100644 index 0000000..9f5840c --- /dev/null +++ b/engine/src/model.rs @@ -0,0 +1,176 @@ +use std::borrow::Cow; +use std::collections::HashMap; +use std::fs::read_to_string; +use std::path::Path; + +use ecs::Component; + +use crate::asset::{Assets, Handle as AssetHandle, Submitter as AssetSubmitter}; +use crate::material::Material; +use crate::mesh::Mesh; +use crate::texture::Texture; + +#[derive(Debug, Clone, Component)] +#[non_exhaustive] +pub struct Model +{ + pub asset_handle: AssetHandle<Data>, +} + +impl Model +{ + pub fn new(asset_handle: AssetHandle<Data>) -> Self + { + Self { asset_handle } + } +} + +#[derive(Debug, Default, Clone)] +#[non_exhaustive] +pub struct Data +{ + pub mesh: Mesh, + pub materials: HashMap<String, Material>, +} + +impl Data +{ + pub fn builder() -> DataBuilder + { + DataBuilder::default() + } +} + +#[derive(Debug, Default, Clone)] +pub struct DataBuilder +{ + mesh: Mesh, + materials: HashMap<String, Material>, +} + +impl DataBuilder +{ + pub fn mesh(mut self, mesh: Mesh) -> Self + { + self.mesh = mesh; + + self + } + + pub fn material<'name>( + mut self, + name: impl Into<Cow<'name, str>>, + material: Material, + ) -> Self + { + self.materials.insert(name.into().into_owned(), material); + + self + } + + pub fn build(self) -> Data + { + Data { + mesh: self.mesh, + materials: self.materials, + } + } +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct Settings {} + +#[derive(Debug, thiserror::Error)] +enum Error +{ + #[error("Failed to read model file")] + ReadModelFileFailed(#[source] std::io::Error), + + #[error("Failed to read material file")] + ReadMaterialFileFailed(#[source] std::io::Error), + + #[error("Failed to parse model file")] + ParsingFailed(#[from] ParsingError), +} + +pub fn set_asset_importers(assets: &mut Assets) +{ + assets.set_importer(["obj"], import_wavefront_obj_asset); +} + +#[derive(Debug, thiserror::Error)] +enum ParsingError +{ + #[error(transparent)] + Obj(#[from] crate::file_format::wavefront::obj::Error), + + #[error(transparent)] + Mtl(#[from] crate::file_format::wavefront::mtl::Error), +} + +fn import_wavefront_obj_asset( + asset_submitter: &mut AssetSubmitter<'_>, + path: &Path, + _settings: Option<&'_ Settings>, +) -> Result<(), Error> +{ + let obj = crate::file_format::wavefront::obj::parse( + &read_to_string(path).map_err(Error::ReadModelFileFailed)?, + ) + .map_err(|err| Error::ParsingFailed(ParsingError::Obj(err)))?; + + let mesh = obj + .to_mesh() + .map_err(|err| Error::ParsingFailed(ParsingError::Obj(err)))?; + + let mut materials = + HashMap::<String, Material>::with_capacity(obj.mtl_libs.iter().flatten().count()); + + for mtl_lib_path in obj.mtl_libs.iter().flatten() { + materials.extend(import_mtl(asset_submitter, &mtl_lib_path)?); + } + + asset_submitter.submit_store(Data { mesh, materials }); + + Ok(()) +} + +fn import_mtl<'a>( + asset_submitter: &'a AssetSubmitter<'_>, + path: &Path, +) -> Result<impl Iterator<Item = (String, Material)> + 'a, Error> +{ + let named_materials = crate::file_format::wavefront::mtl::parse( + &read_to_string(path).map_err(Error::ReadMaterialFileFailed)?, + ) + .map_err(|err| Error::ParsingFailed(ParsingError::Mtl(err)))?; + + Ok(named_materials.into_iter().map(|named_material| { + let mut material_builder = Material::builder() + .ambient(named_material.ambient) + .diffuse(named_material.diffuse) + .specular(named_material.specular) + .shininess(named_material.shininess); + + if let Some(ambient_map) = named_material.ambient_map { + material_builder = material_builder.ambient_map(Texture::new( + asset_submitter.submit_load_other(ambient_map.path.as_path()), + )); + } + + if let Some(diffuse_map) = named_material.diffuse_map { + material_builder = material_builder.diffuse_map(Texture::new( + asset_submitter.submit_load_other(diffuse_map.path.as_path()), + )); + } + + if let Some(specular_map) = named_material.specular_map { + material_builder = material_builder.specular_map(Texture::new( + asset_submitter.submit_load_other(specular_map.path.as_path()), + )); + } + + (named_material.name, material_builder.build()) + })) +} diff --git a/engine/src/opengl/buffer.rs b/engine/src/opengl/buffer.rs index 68a75fb..eded553 100644 --- a/engine/src/opengl/buffer.rs +++ b/engine/src/opengl/buffer.rs @@ -1,5 +1,6 @@ use std::marker::PhantomData; use std::mem::size_of_val; +use std::ptr::null; #[derive(Debug)] pub struct Buffer<Item> @@ -35,19 +36,40 @@ impl<Item> Buffer<Item> } } - pub fn object(&self) -> gl::types::GLuint + pub fn store_mapped<Value>( + &mut self, + values: &[Value], + usage: Usage, + mut map_func: impl FnMut(&Value) -> Item, + ) { - self.buf + unsafe { + #[allow(clippy::cast_possible_wrap)] + gl::NamedBufferData( + self.buf, + (size_of::<Item>() * values.len()) as gl::types::GLsizeiptr, + null(), + usage.into_gl(), + ); + } + + for (index, value) in values.iter().enumerate() { + let item = map_func(value); + + unsafe { + gl::NamedBufferSubData( + self.buf, + (index * size_of::<Item>()) as gl::types::GLintptr, + size_of::<Item>() as gl::types::GLsizeiptr, + (&raw const item).cast(), + ); + } + } } -} -impl<Item> Drop for Buffer<Item> -{ - fn drop(&mut self) + pub fn object(&self) -> gl::types::GLuint { - unsafe { - gl::DeleteBuffers(1, &self.buf); - } + self.buf } } diff --git a/engine/src/opengl/shader.rs b/engine/src/opengl/shader.rs index 36dc1a4..a626fc7 100644 --- a/engine/src/opengl/shader.rs +++ b/engine/src/opengl/shader.rs @@ -159,82 +159,105 @@ impl Program } } - pub fn set_uniform_matrix_4fv(&mut self, name: &CStr, matrix: &Matrix<f32, 4, 4>) + pub fn set_uniform(&mut self, name: &CStr, var: &impl UniformVariable) { - let uniform_location = - unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) }; + let location = UniformLocation(unsafe { + gl::GetUniformLocation(self.program, name.as_ptr().cast()) + }); + + var.set(self, location); + } + + fn get_info_log(&self) -> String + { + let mut buf = vec![gl::types::GLchar::default(); 512]; unsafe { - gl::ProgramUniformMatrix4fv( + #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] + gl::GetProgramInfoLog( self.program, - uniform_location, - 1, - gl::FALSE, - matrix.as_ptr(), + buf.len() as gl::types::GLsizei, + null_mut(), + buf.as_mut_ptr(), ); } + + let info_log = unsafe { CStr::from_ptr(buf.as_ptr()) }; + + unsafe { String::from_utf8_unchecked(info_log.to_bytes().to_vec()) } } +} - pub fn set_uniform_vec_3fv(&mut self, name: &CStr, vec: &Vec3<f32>) +impl Drop for Program +{ + fn drop(&mut self) { - let uniform_location = - unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) }; - unsafe { - gl::ProgramUniform3fv(self.program, uniform_location, 1, vec.as_ptr()); + gl::DeleteProgram(self.program); } } +} - pub fn set_uniform_1fv(&mut self, name: &CStr, num: f32) - { - let uniform_location = - unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) }; +pub trait UniformVariable +{ + fn set(&self, program: &mut Program, uniform_location: UniformLocation); +} +impl UniformVariable for f32 +{ + fn set(&self, program: &mut Program, uniform_location: UniformLocation) + { unsafe { - gl::ProgramUniform1fv(self.program, uniform_location, 1, &num); + gl::ProgramUniform1f(program.program, uniform_location.0, *self); } } +} - pub fn set_uniform_1i(&mut self, name: &CStr, num: i32) +impl UniformVariable for i32 +{ + fn set(&self, program: &mut Program, uniform_location: UniformLocation) { - let uniform_location = - unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) }; - unsafe { - gl::ProgramUniform1i(self.program, uniform_location, num); + gl::ProgramUniform1i(program.program, uniform_location.0, *self); } } +} - fn get_info_log(&self) -> String +impl UniformVariable for Vec3<f32> +{ + fn set(&self, program: &mut Program, uniform_location: UniformLocation) { - let mut buf = vec![gl::types::GLchar::default(); 512]; - unsafe { - #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] - gl::GetProgramInfoLog( - self.program, - buf.len() as gl::types::GLsizei, - null_mut(), - buf.as_mut_ptr(), + gl::ProgramUniform3f( + program.program, + uniform_location.0, + self.x, + self.y, + self.z, ); } - - let info_log = unsafe { CStr::from_ptr(buf.as_ptr()) }; - - unsafe { String::from_utf8_unchecked(info_log.to_bytes().to_vec()) } } } -impl Drop for Program +impl UniformVariable for Matrix<f32, 4, 4> { - fn drop(&mut self) + fn set(&self, program: &mut Program, uniform_location: UniformLocation) { unsafe { - gl::DeleteProgram(self.program); + gl::ProgramUniformMatrix4fv( + program.program, + uniform_location.0, + 1, + gl::FALSE, + self.as_ptr(), + ); } } } +#[derive(Debug)] +pub struct UniformLocation(gl::types::GLint); + /// Shader error. #[derive(Debug, thiserror::Error)] pub enum Error diff --git a/engine/src/opengl/texture.rs b/engine/src/opengl/texture.rs index 52c8554..80a5f37 100644 --- a/engine/src/opengl/texture.rs +++ b/engine/src/opengl/texture.rs @@ -1,5 +1,4 @@ use crate::data_types::dimens::Dimens; -use crate::texture::Properties; #[derive(Debug)] pub struct Texture @@ -20,10 +19,10 @@ impl Texture Self { texture } } - pub fn bind(&self) + pub fn bind_to_texture_unit(&self, texture_unit: u32) { unsafe { - gl::BindTexture(gl::TEXTURE_2D, self.texture); + gl::BindTextureUnit(texture_unit, self.texture); } } @@ -41,16 +40,9 @@ impl Texture } } - pub fn apply_properties(&mut self, properties: &Properties) - { - self.set_wrap(properties.wrap); - self.set_magnifying_filter(properties.magnifying_filter); - self.set_minifying_filter(properties.minifying_filter); - } - pub fn set_wrap(&mut self, wrapping: Wrapping) { - let wrapping_gl = wrapping.to_gl(); + let wrapping_gl = wrapping as gl::types::GLenum; #[allow(clippy::cast_possible_wrap)] unsafe { @@ -61,7 +53,7 @@ impl Texture pub fn set_magnifying_filter(&mut self, filtering: Filtering) { - let filtering_gl = filtering.to_gl(); + let filtering_gl = filtering as gl::types::GLenum; #[allow(clippy::cast_possible_wrap)] unsafe { @@ -75,7 +67,7 @@ impl Texture pub fn set_minifying_filter(&mut self, filtering: Filtering) { - let filtering_gl = filtering.to_gl(); + let filtering_gl = filtering as gl::types::GLenum; #[allow(clippy::cast_possible_wrap)] unsafe { @@ -132,43 +124,21 @@ impl Drop for Texture /// Texture wrapping. #[derive(Debug, Clone, Copy)] +#[repr(u32)] pub enum Wrapping { - Repeat, - MirroredRepeat, - ClampToEdge, - ClampToBorder, -} - -impl Wrapping -{ - fn to_gl(self) -> gl::types::GLenum - { - match self { - Self::Repeat => gl::REPEAT, - Self::MirroredRepeat => gl::MIRRORED_REPEAT, - Self::ClampToEdge => gl::CLAMP_TO_EDGE, - Self::ClampToBorder => gl::CLAMP_TO_BORDER, - } - } + Repeat = gl::REPEAT, + MirroredRepeat = gl::MIRRORED_REPEAT, + ClampToEdge = gl::CLAMP_TO_EDGE, + ClampToBorder = gl::CLAMP_TO_BORDER, } #[derive(Debug, Clone, Copy)] +#[repr(u32)] pub enum Filtering { - Nearest, - Linear, -} - -impl Filtering -{ - fn to_gl(self) -> gl::types::GLenum - { - match self { - Self::Linear => gl::LINEAR, - Self::Nearest => gl::NEAREST, - } - } + Nearest = gl::NEAREST, + Linear = gl::LINEAR, } /// Texture pixel data format. @@ -197,44 +167,3 @@ impl PixelDataFormat } } } - -pub fn set_active_texture_unit(texture_unit: TextureUnit) -{ - unsafe { - gl::ActiveTexture(texture_unit.into_gl()); - } -} - -macro_rules! texture_unit_enum { - (cnt=$cnt: literal) => { - seq_macro::seq!(N in 0..$cnt { - #[derive(Debug, Clone, Copy)] - pub enum TextureUnit { - #( - No~N, - )* - } - - impl TextureUnit { - fn into_gl(self) -> gl::types::GLenum { - match self { - #( - Self::No~N => gl::TEXTURE~N, - )* - } - } - - pub fn from_num(num: usize) -> Option<Self> { - match num { - #( - N => Some(Self::No~N), - )* - _ => None - } - } - } - }); - }; -} - -texture_unit_enum!(cnt = 31); diff --git a/engine/src/opengl/vertex_array.rs b/engine/src/opengl/vertex_array.rs index e1e1a15..1f8a870 100644 --- a/engine/src/opengl/vertex_array.rs +++ b/engine/src/opengl/vertex_array.rs @@ -1,10 +1,6 @@ use std::mem::size_of; use crate::opengl::buffer::Buffer; -use crate::vertex::Vertex; - -#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] -const VERTEX_STRIDE: i32 = size_of::<Vertex>() as i32; #[derive(Debug)] pub struct VertexArray @@ -59,10 +55,10 @@ impl VertexArray } } - pub fn bind_vertex_buffer( + pub fn bind_vertex_buffer<VertexT>( &mut self, binding_index: u32, - vertex_buffer: &Buffer<Vertex>, + vertex_buffer: &Buffer<VertexT>, offset: isize, ) { @@ -72,7 +68,7 @@ impl VertexArray binding_index, vertex_buffer.object(), offset, - VERTEX_STRIDE, + size_of::<VertexT>() as i32, ); } } @@ -127,16 +123,6 @@ impl VertexArray } } -impl Drop for VertexArray -{ - fn drop(&mut self) - { - unsafe { - gl::DeleteVertexArrays(1, &self.array); - } - } -} - #[derive(Debug)] pub enum PrimitiveKind { diff --git a/engine/src/projection.rs b/engine/src/projection.rs index faa741f..115ca39 100644 --- a/engine/src/projection.rs +++ b/engine/src/projection.rs @@ -1,6 +1,6 @@ use crate::data_types::dimens::Dimens3; use crate::matrix::Matrix; -use crate::util::builder; +use crate::builder; use crate::vector::Vec3; #[derive(Debug)] diff --git a/engine/src/renderer.rs b/engine/src/renderer.rs index 2544919..17bc925 100644 --- a/engine/src/renderer.rs +++ b/engine/src/renderer.rs @@ -1 +1,7 @@ +use ecs::pair::{ChildOf, Pair}; +use ecs::phase::{Phase, UPDATE as UPDATE_PHASE}; +use ecs::static_entity; + pub mod opengl; + +static_entity!(pub RENDER_PHASE, (Phase, Pair::new::<ChildOf>(*UPDATE_PHASE))); diff --git a/engine/src/renderer/opengl.rs b/engine/src/renderer/opengl.rs index 5665860..cfd046f 100644 --- a/engine/src/renderer/opengl.rs +++ b/engine/src/renderer/opengl.rs @@ -3,26 +3,29 @@ use std::collections::HashMap; use std::ffi::{c_void, CString}; use std::io::{Error as IoError, ErrorKind as IoErrorKind}; -use std::ops::Deref; use std::path::Path; use std::process::abort; use ecs::actions::Actions; use ecs::component::local::Local; -use ecs::phase::{PRESENT as PRESENT_PHASE, START as START_PHASE}; +use ecs::component::Handle as ComponentHandle; +use ecs::phase::START as START_PHASE; use ecs::query::term::Without; use ecs::sole::Single; use ecs::system::{Into as _, System}; use ecs::{Component, Query}; +use crate::asset::{Assets, Id as AssetId}; 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::image::{ColorType as ImageColorType, Image}; use crate::lighting::{DirectionalLight, GlobalLight, PointLight}; use crate::material::{Flags as MaterialFlags, Material}; use crate::matrix::Matrix; use crate::mesh::Mesh; +use crate::model::Model; use crate::opengl::buffer::{Buffer, Usage as BufferUsage}; use crate::opengl::debug::{ enable_debug_output, @@ -44,9 +47,10 @@ use crate::opengl::shader::{ Shader as GlShader, }; use crate::opengl::texture::{ - set_active_texture_unit, + Filtering as GlTextureFiltering, + PixelDataFormat as GlTexturePixelDataFormat, Texture as GlTexture, - TextureUnit, + Wrapping as GlTextureWrapping, }; use crate::opengl::vertex_array::{ DataType as VertexArrayDataType, @@ -62,21 +66,31 @@ use crate::opengl::{ ContextFlags, }; use crate::projection::{ClipVolume, Projection}; -use crate::texture::{Id as TextureId, Texture}; -use crate::transform::{Position, Scale}; +use crate::renderer::opengl::vertex::{AttributeComponentType, Vertex}; +use crate::renderer::RENDER_PHASE; +use crate::texture::{ + Filtering as TextureFiltering, + Properties as TextureProperties, + Wrapping as TextureWrapping, +}; +use crate::transform::{Scale, WorldPosition}; use crate::util::{defer, Defer, RefOrValue}; use crate::vector::{Vec2, Vec3}; -use crate::vertex::{AttributeComponentType, Vertex}; use crate::window::Window; +mod vertex; + +const AMBIENT_MAP_TEXTURE_UNIT: u32 = 0; +const DIFFUSE_MAP_TEXTURE_UNIT: u32 = 1; +const SPECULAR_MAP_TEXTURE_UNIT: u32 = 2; + type RenderableEntity<'a> = ( - &'a Mesh, - &'a Material, - &'a Option<MaterialFlags>, - &'a Option<Position>, - &'a Option<Scale>, - &'a Option<DrawFlags>, - &'a Option<GlObjects>, + &'a Model, + Option<&'a MaterialFlags>, + Option<&'a WorldPosition>, + Option<&'a Scale>, + Option<&'a DrawFlags>, + Option<&'a GlObjects>, ); #[derive(Debug, Default)] @@ -90,7 +104,7 @@ impl ecs::extension::Extension for Extension collector.add_system(*START_PHASE, initialize); collector.add_system( - *PRESENT_PHASE, + *RENDER_PHASE, render .into_system() .initialize((GlobalGlObjects::default(),)), @@ -131,33 +145,31 @@ fn initialize(window: Single<Window>) enable(Capability::MultiSample); } +#[tracing::instrument(skip_all)] #[allow(clippy::too_many_arguments)] fn render( query: Query<RenderableEntity<'_>, (Without<NoDraw>,)>, - point_light_query: Query<(&PointLight,)>, + point_light_query: Query<(&PointLight, &WorldPosition)>, directional_lights: Query<(&DirectionalLight,)>, - camera_query: Query<(&Camera, &Position, &ActiveCamera)>, + camera_query: Query<(&Camera, &WorldPosition, &ActiveCamera)>, window: Single<Window>, global_light: Single<GlobalLight>, + assets: Single<Assets>, mut gl_objects: Local<GlobalGlObjects>, mut actions: Actions, ) { - let Some((camera, camera_pos, _)) = camera_query.iter().next() else { + let Some((camera, camera_world_pos, _)) = camera_query.iter().next() else { tracing::warn!("No current camera. Nothing will be rendered"); return; }; - let point_lights = point_light_query - .iter() - .map(|(point_light,)| point_light) - .collect::<Vec<_>>(); - let directional_lights = directional_lights.iter().collect::<Vec<_>>(); let GlobalGlObjects { shader_program, textures: gl_textures, + default_1x1_texture: default_1x1_gl_texture, } = &mut *gl_objects; let shader_program = @@ -165,18 +177,23 @@ fn render( clear_buffers(BufferClearMask::COLOR | BufferClearMask::DEPTH); - for ( + 'subject_loop: for ( euid, - (mesh, material, material_flags, position, scale, draw_flags, gl_objects), + (model, material_flags, position, scale, draw_flags, gl_objects), ) in query.iter_with_euids() { + let Some(model_data) = assets.get(&model.asset_handle) else { + tracing::trace!("Missing model asset"); + continue; + }; + let material_flags = material_flags .map(|material_flags| material_flags.clone()) .unwrap_or_default(); let gl_objs = match gl_objects.as_deref() { Some(gl_objs) => RefOrValue::Ref(gl_objs), - None => RefOrValue::Value(Some(GlObjects::new(&mesh))), + None => RefOrValue::Value(Some(GlObjects::new(&model_data.mesh))), }; defer!(|gl_objs| { @@ -192,34 +209,82 @@ fn render( }, shader_program, &camera, - &camera_pos, + &camera_world_pos, window.size().expect("Failed to get window size"), ); + if model_data.materials.len() > 1 { + tracing::warn!(concat!( + "Multiple model materials are not supported ", + "so only the first material will be used" + )); + } + + let material = match model_data.materials.values().next() { + Some(material) => material, + None => { + tracing::warn!("Model has no materials. Using default material"); + + &Material::default() + } + }; + apply_light( &material, &material_flags, &global_light, shader_program, - point_lights.as_slice(), + (point_light_query.iter(), point_light_query.iter().count()), directional_lights .iter() .map(|(dir_light,)| &**dir_light) .collect::<Vec<_>>() .as_slice(), - &camera_pos, + &camera_world_pos, ); - for (index, texture) in material.textures.iter().enumerate() { - let gl_texture = gl_textures - .entry(texture.id()) - .or_insert_with(|| create_gl_texture(texture)); + 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 gl_texture = default_1x1_gl_texture.get_or_insert_with(|| { + create_gl_texture( + &Image::from_color(1, Color::WHITE_U8), + &TextureProperties::default(), + ) + }); + + gl_texture.bind_to_texture_unit(texture_unit); - let texture_unit = TextureUnit::from_num(index).expect("Too many textures"); + continue; + }; - set_active_texture_unit(texture_unit); + let texture_image_asset_id = texture.asset_handle.id(); + + let gl_texture = match gl_textures.get(&texture_image_asset_id) { + Some(gl_texture) => gl_texture, + None => { + let Some(image) = assets.get::<Image>(&texture.asset_handle) else { + tracing::trace!("Missing texture asset"); + continue 'subject_loop; + }; + + gl_textures.insert( + texture_image_asset_id, + create_gl_texture(image, &texture.properties), + ); + + gl_textures + .get(&texture.asset_handle.id()) + .expect("Not possible") + } + }; - gl_texture.bind(); + gl_texture.bind_to_texture_unit(texture_unit); } shader_program.activate(); @@ -248,7 +313,8 @@ fn render( struct GlobalGlObjects { shader_program: Option<GlShaderProgram>, - textures: HashMap<TextureId, GlTexture>, + textures: HashMap<AssetId, GlTexture>, + default_1x1_texture: Option<GlTexture>, } fn set_viewport(position: Vec2<u32>, size: Dimens<u32>) @@ -275,17 +341,31 @@ fn draw_mesh(gl_objects: &GlObjects) } } -fn create_gl_texture(texture: &Texture) -> GlTexture +fn create_gl_texture(image: &Image, texture_properties: &TextureProperties) -> GlTexture { let mut gl_texture = GlTexture::new(); gl_texture.generate( - *texture.dimensions(), - texture.image().as_bytes(), - texture.pixel_data_format(), + image.dimensions(), + image.as_bytes(), + match image.color_type() { + ImageColorType::Rgb8 => GlTexturePixelDataFormat::Rgb8, + ImageColorType::Rgba8 => GlTexturePixelDataFormat::Rgba8, + _ => { + unimplemented!(); + } + }, ); - gl_texture.apply_properties(texture.properties()); + gl_texture.set_wrap(texture_wrapping_to_gl(texture_properties.wrap)); + + gl_texture.set_magnifying_filter(texture_filtering_to_gl( + texture_properties.magnifying_filter, + )); + + gl_texture.set_minifying_filter(texture_filtering_to_gl( + texture_properties.minifying_filter, + )); gl_texture } @@ -380,7 +460,13 @@ impl GlObjects let mut vertex_arr = VertexArray::new(); let mut vertex_buffer = Buffer::new(); - vertex_buffer.store(mesh.vertices(), BufferUsage::Static); + vertex_buffer.store_mapped(mesh.vertices(), BufferUsage::Static, |vertex| { + Vertex { + pos: vertex.pos, + texture_coords: vertex.texture_coords, + normal: vertex.normal, + } + }); vertex_arr.bind_vertex_buffer(0, &vertex_buffer, 0); @@ -438,16 +524,16 @@ fn apply_transformation_matrices( transformation: Transformation, gl_shader_program: &mut GlShaderProgram, camera: &Camera, - camera_pos: &Position, + camera_world_pos: &WorldPosition, window_size: Dimens<u32>, ) { gl_shader_program - .set_uniform_matrix_4fv(c"model", &create_transformation_matrix(transformation)); + .set_uniform(c"model", &create_transformation_matrix(transformation)); - let view_matrix = create_view_matrix(camera, &camera_pos.position); + let view_matrix = create_view_matrix(camera, &camera_world_pos.position); - gl_shader_program.set_uniform_matrix_4fv(c"view", &view_matrix); + gl_shader_program.set_uniform(c"view", &view_matrix); #[allow(clippy::cast_precision_loss)] let proj_matrix = match &camera.projection { @@ -455,27 +541,33 @@ fn apply_transformation_matrices( window_size.width as f32 / window_size.height as f32, ClipVolume::NegOneToOne, ), - Projection::Orthographic(orthographic_proj) => { - orthographic_proj.to_matrix_rh(&camera_pos.position, ClipVolume::NegOneToOne) - } + Projection::Orthographic(orthographic_proj) => orthographic_proj + .to_matrix_rh(&camera_world_pos.position, ClipVolume::NegOneToOne), }; - gl_shader_program.set_uniform_matrix_4fv(c"projection", &proj_matrix); + gl_shader_program.set_uniform(c"projection", &proj_matrix); } -fn apply_light<PointLightHolder>( +fn apply_light<'point_light>( material: &Material, material_flags: &MaterialFlags, global_light: &GlobalLight, gl_shader_program: &mut GlShaderProgram, - point_lights: &[PointLightHolder], + (point_light_iter, point_light_cnt): ( + impl Iterator< + Item = ( + ComponentHandle<'point_light, PointLight>, + ComponentHandle<'point_light, WorldPosition>, + ), + >, + usize, + ), directional_lights: &[&DirectionalLight], - camera_pos: &Position, -) where - PointLightHolder: Deref<Target = PointLight>, + camera_world_pos: &WorldPosition, +) { debug_assert!( - point_lights.len() < 64, + point_light_cnt < 64, "Shader cannot handle more than 64 point lights" ); @@ -485,7 +577,7 @@ fn apply_light<PointLightHolder>( ); for (dir_light_index, dir_light) in directional_lights.iter().enumerate() { - gl_shader_program.set_uniform_vec_3fv( + gl_shader_program.set_uniform( &create_light_uniform_name( "directional_lights", dir_light_index, @@ -505,78 +597,68 @@ fn apply_light<PointLightHolder>( // There probably won't be more than 2147483648 directional lights #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] gl_shader_program - .set_uniform_1i(c"directional_light_cnt", directional_lights.len() as i32); + .set_uniform(c"directional_light_cnt", &(directional_lights.len() as i32)); - for (point_light_index, point_light) in point_lights.iter().enumerate() { - gl_shader_program.set_uniform_vec_3fv( + for (point_light_index, (point_light, point_light_world_pos)) in + point_light_iter.enumerate() + { + gl_shader_program.set_uniform( &create_light_uniform_name("point_lights", point_light_index, "position"), - &point_light.position, + &(point_light_world_pos.position + point_light.local_position), ); set_light_phong_uniforms( gl_shader_program, "point_lights", point_light_index, - &**point_light, + &*point_light, ); set_light_attenuation_uniforms( gl_shader_program, "point_lights", point_light_index, - point_light, + &*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_1i(c"point_light_cnt", point_lights.len() as i32); + gl_shader_program.set_uniform(c"point_light_cnt", &(point_light_cnt as i32)); - gl_shader_program.set_uniform_vec_3fv( + gl_shader_program.set_uniform( c"material.ambient", - &if material_flags.use_ambient_color { + &Vec3::from(if material_flags.use_ambient_color { material.ambient.clone() } else { global_light.ambient.clone() - } - .into(), + }), ); gl_shader_program - .set_uniform_vec_3fv(c"material.diffuse", &material.diffuse.clone().into()); + .set_uniform(c"material.diffuse", &Vec3::from(material.diffuse.clone())); #[allow(clippy::cast_possible_wrap)] gl_shader_program - .set_uniform_vec_3fv(c"material.specular", &material.specular.clone().into()); - - let texture_map = material - .textures - .iter() - .enumerate() - .map(|(index, texture)| (texture.id(), index)) - .collect::<HashMap<_, _>>(); + .set_uniform(c"material.specular", &Vec3::from(material.specular.clone())); #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform_1i( - c"material.ambient_map", - *texture_map.get(&material.ambient_map).unwrap() as i32, - ); + gl_shader_program + .set_uniform(c"material.ambient_map", &(AMBIENT_MAP_TEXTURE_UNIT as i32)); #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform_1i( - c"material.diffuse_map", - *texture_map.get(&material.diffuse_map).unwrap() as i32, - ); + gl_shader_program + .set_uniform(c"material.diffuse_map", &(DIFFUSE_MAP_TEXTURE_UNIT as i32)); #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform_1i( + gl_shader_program.set_uniform( c"material.specular_map", - *texture_map.get(&material.specular_map).unwrap() as i32, + &(SPECULAR_MAP_TEXTURE_UNIT as i32), ); - gl_shader_program.set_uniform_1fv(c"material.shininess", material.shininess); + gl_shader_program.set_uniform(c"material.shininess", &material.shininess); - gl_shader_program.set_uniform_vec_3fv(c"view_pos", &camera_pos.position); + gl_shader_program.set_uniform(c"view_pos", &camera_world_pos.position); } fn set_light_attenuation_uniforms( @@ -586,27 +668,27 @@ fn set_light_attenuation_uniforms( light: &PointLight, ) { - gl_shader_program.set_uniform_1fv( + gl_shader_program.set_uniform( &create_light_uniform_name( light_array, light_index, "attenuation_props.constant", ), - light.attenuation_params.constant, + &light.attenuation_params.constant, ); - gl_shader_program.set_uniform_1fv( + gl_shader_program.set_uniform( &create_light_uniform_name(light_array, light_index, "attenuation_props.linear"), - light.attenuation_params.linear, + &light.attenuation_params.linear, ); - gl_shader_program.set_uniform_1fv( + gl_shader_program.set_uniform( &create_light_uniform_name( light_array, light_index, "attenuation_props.quadratic", ), - light.attenuation_params.quadratic, + &light.attenuation_params.quadratic, ); } @@ -617,14 +699,14 @@ fn set_light_phong_uniforms( light: &impl Light, ) { - gl_shader_program.set_uniform_vec_3fv( + gl_shader_program.set_uniform( &create_light_uniform_name(light_array, light_index, "phong.diffuse"), - &light.diffuse().clone().into(), + &Vec3::from(light.diffuse().clone()), ); - gl_shader_program.set_uniform_vec_3fv( + gl_shader_program.set_uniform( &create_light_uniform_name(light_array, light_index, "phong.specular"), - &light.specular().clone().into(), + &Vec3::from(light.specular().clone()), ); } @@ -740,3 +822,23 @@ fn create_transformation_matrix(transformation: Transformation) -> Matrix<f32, 4 matrix } + +#[inline] +fn texture_wrapping_to_gl(texture_wrapping: TextureWrapping) -> GlTextureWrapping +{ + match texture_wrapping { + TextureWrapping::Repeat => GlTextureWrapping::Repeat, + TextureWrapping::MirroredRepeat => GlTextureWrapping::MirroredRepeat, + TextureWrapping::ClampToEdge => GlTextureWrapping::ClampToEdge, + TextureWrapping::ClampToBorder => GlTextureWrapping::ClampToBorder, + } +} + +#[inline] +fn texture_filtering_to_gl(texture_filtering: TextureFiltering) -> GlTextureFiltering +{ + match texture_filtering { + TextureFiltering::Linear => GlTextureFiltering::Linear, + TextureFiltering::Nearest => GlTextureFiltering::Nearest, + } +} diff --git a/engine/src/vertex.rs b/engine/src/renderer/opengl/vertex.rs index 30640c4..499b94b 100644 --- a/engine/src/vertex.rs +++ b/engine/src/renderer/opengl/vertex.rs @@ -1,24 +1,17 @@ -use std::mem::size_of; - -use crate::util::builder; use crate::vector::{Vec2, Vec3}; -builder! { -#[builder(name = Builder, derives = (Debug, Default))] -#[derive(Debug, Clone, Default, PartialEq)] +#[derive(Debug, Clone)] #[repr(C)] -#[non_exhaustive] pub struct Vertex { pub pos: Vec3<f32>, pub texture_coords: Vec2<f32>, pub normal: Vec3<f32>, } -} impl Vertex { - pub(crate) fn attrs() -> &'static [Attribute] + pub fn attrs() -> &'static [Attribute] { #[allow(clippy::cast_possible_truncation)] &[ @@ -44,15 +37,17 @@ impl Vertex } } -pub(crate) struct Attribute +#[derive(Debug)] +pub struct Attribute { - pub(crate) index: u32, - pub(crate) component_type: AttributeComponentType, - pub(crate) component_cnt: AttributeComponentCnt, - pub(crate) component_size: u32, + pub index: u32, + pub component_type: AttributeComponentType, + pub component_cnt: AttributeComponentCnt, + pub component_size: u32, } -pub(crate) enum AttributeComponentType +#[derive(Debug)] +pub enum AttributeComponentType { Float, } @@ -60,7 +55,7 @@ pub(crate) enum AttributeComponentType #[derive(Debug, Clone, Copy)] #[repr(u32)] #[allow(dead_code)] -pub(crate) enum AttributeComponentCnt +pub enum AttributeComponentCnt { One = 1, Two = 2, diff --git a/engine/src/texture.rs b/engine/src/texture.rs index 4a4fe86..d02b9ff 100644 --- a/engine/src/texture.rs +++ b/engine/src/texture.rs @@ -1,194 +1,34 @@ -use std::fmt::Display; -use std::path::Path; -use std::sync::atomic::{AtomicU32, Ordering}; - -use image::io::Reader as ImageReader; -use image::{DynamicImage, ImageError, Rgb, RgbImage}; - -use crate::color::Color; -use crate::data_types::dimens::Dimens; -use crate::opengl::texture::PixelDataFormat; -use crate::util::builder; - -static NEXT_ID: AtomicU32 = AtomicU32::new(0); - -mod reexports -{ - pub use crate::opengl::texture::{Filtering, Wrapping}; -} - -pub use reexports::*; +use crate::asset::Handle as AssetHandle; +use crate::image::Image; +use crate::builder; #[derive(Debug, Clone)] +#[non_exhaustive] pub struct Texture { - id: Id, - image: DynamicImage, - pixel_data_format: PixelDataFormat, - dimensions: Dimens<u32>, - properties: Properties, + pub asset_handle: AssetHandle<Image>, + pub properties: Properties, } impl Texture { - pub fn builder() -> Builder - { - Builder::default() - } - - /// Opens a texture image. - /// - /// # Errors - /// Will return `Err` if: - /// - Opening the image fails - /// - The image data is not 8-bit/color RGB - pub fn open(path: &Path) -> Result<Self, Error> - { - Self::builder().open(path) - } - - #[must_use] - pub fn new_from_color(dimensions: &Dimens<u32>, color: &Color<u8>) -> Self - { - Self::builder().build_with_single_color(dimensions, color) - } - - #[must_use] - pub fn id(&self) -> Id - { - self.id - } - - #[must_use] - pub fn properties(&self) -> &Properties - { - &self.properties - } - - pub fn properties_mut(&mut self) -> &mut Properties - { - &mut self.properties - } - - #[must_use] - pub fn dimensions(&self) -> &Dimens<u32> - { - &self.dimensions - } - - #[must_use] - pub fn pixel_data_format(&self) -> PixelDataFormat - { - self.pixel_data_format - } - - #[must_use] - pub fn image(&self) -> &DynamicImage - { - &self.image - } -} - -impl Drop for Texture -{ - fn drop(&mut self) - { - NEXT_ID.fetch_sub(1, Ordering::Relaxed); - } -} - -/// Texture builder. -#[derive(Debug, Default, Clone)] -pub struct Builder -{ - properties: Properties, -} - -impl Builder -{ - pub fn properties(mut self, properties: Properties) -> Self + pub fn new(asset_handle: AssetHandle<Image>) -> Self { - self.properties = properties; - self - } - - /// Opens a image as a texture. - /// - /// # Errors - /// Will return `Err` if: - /// - Opening the image fails - /// - Decoding the image fails - /// - The image data is in a unsupported format - pub fn open(&self, path: &(impl AsRef<Path> + ?Sized)) -> Result<Texture, Error> - { - let image = ImageReader::open(path) - .map_err(Error::OpenImageFailed)? - .decode() - .map_err(Error::DecodeImageFailed)?; - - let pixel_data_format = match &image { - DynamicImage::ImageRgb8(_) => PixelDataFormat::Rgb8, - DynamicImage::ImageRgba8(_) => PixelDataFormat::Rgba8, - _ => { - return Err(Error::UnsupportedImageDataFormat); - } - }; - - let dimensions = Dimens { - width: image.width(), - height: image.height(), - }; - - let id = NEXT_ID.fetch_add(1, Ordering::Relaxed); - - Ok(Texture { - id: Id::new(id), - image, - pixel_data_format, - dimensions, - properties: self.properties.clone(), - }) + Self { + asset_handle, + properties: Properties::default(), + } } - #[must_use] - pub fn build_with_single_color( - &self, - dimensions: &Dimens<u32>, - color: &Color<u8>, - ) -> Texture + pub fn with_properties( + asset_handle: AssetHandle<Image>, + properties: Properties, + ) -> Self { - let image = RgbImage::from_pixel( - dimensions.width, - dimensions.height, - Rgb([color.red, color.green, color.blue]), - ); - - let id = NEXT_ID.fetch_add(1, Ordering::Relaxed); - - Texture { - id: Id::new(id), - image: image.into(), - pixel_data_format: PixelDataFormat::Rgb8, - dimensions: *dimensions, - properties: self.properties.clone(), - } + Self { asset_handle, properties } } } -/// Texture error. -#[derive(Debug, thiserror::Error)] -pub enum Error -{ - #[error("Failed to open texture image")] - OpenImageFailed(#[source] std::io::Error), - - #[error("Failed to decode texture image")] - DecodeImageFailed(#[source] ImageError), - - #[error("Unsupported image data format")] - UnsupportedImageDataFormat, -} - builder! { /// Texture properties #[builder(name = PropertiesBuilder, derives=(Debug, Clone))] @@ -230,25 +70,21 @@ impl Default for PropertiesBuilder } } -/// Texture ID. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Id -{ - id: u32, -} - -impl Id +#[non_exhaustive] +pub enum Filtering { - fn new(id: u32) -> Self - { - Self { id } - } + Nearest, + Linear, } -impl Display for Id +/// Texture wrapping. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum Wrapping { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result - { - self.id.fmt(formatter) - } + Repeat, + MirroredRepeat, + ClampToEdge, + ClampToBorder, } diff --git a/engine/src/transform.rs b/engine/src/transform.rs index 5e5e296..7c0c941 100644 --- a/engine/src/transform.rs +++ b/engine/src/transform.rs @@ -2,14 +2,14 @@ use ecs::Component; use crate::vector::Vec3; -/// A position in 3D space. +/// A position in world space. #[derive(Debug, Default, Clone, Copy, Component)] -pub struct Position +pub struct WorldPosition { pub position: Vec3<f32>, } -impl From<Vec3<f32>> for Position +impl From<Vec3<f32>> for WorldPosition { fn from(position: Vec3<f32>) -> Self { diff --git a/engine/src/util.rs b/engine/src/util.rs index 0f6c78c..cc4677d 100644 --- a/engine/src/util.rs +++ b/engine/src/util.rs @@ -25,6 +25,18 @@ macro_rules! or { pub(crate) use or; +#[macro_export] +macro_rules! expand_map_opt { + ($in: tt, no_occurance=($($no_occurance: tt)*), occurance=($($occurance: tt)*)) => { + $($occurance)* + }; + + (, no_occurance=($($no_occurance: tt)*), occurance=($($occurance: tt)*)) => { + $($no_occurance)* + }; +} + +#[macro_export] macro_rules! builder { ( $(#[doc = $doc: literal])* @@ -36,7 +48,8 @@ macro_rules! builder { $visibility: vis struct $name: ident { $( - $(#[$field_attr: meta])* + $(#[doc = $field_doc: literal])* + $(#[builder(skip_generate_fn$($field_skip_generate_fn: tt)?)])? $field_visibility: vis $field: ident: $field_type: ty, )* } @@ -46,7 +59,7 @@ macro_rules! builder { $visibility struct $name { $( - $(#[$field_attr])* + $(#[doc = $field_doc])* $field_visibility $field: $field_type, )* } @@ -62,12 +75,18 @@ macro_rules! builder { impl $builder_name { $( - #[must_use] - $visibility fn $field(mut self, $field: $field_type) -> Self - { - self.$field = $field; - self - } + $crate::expand_map_opt!( + $(true $($field_skip_generate_fn)?)?, + no_occurance=( + #[must_use] + $visibility fn $field(mut self, $field: $field_type) -> Self + { + self.$field = $field; + self + } + ), + occurance=() + ); )* #[must_use] @@ -82,6 +101,7 @@ macro_rules! builder { impl From<$name> for $builder_name { + #[allow(unused_variables)] fn from(built: $name) -> Self { Self { @@ -94,8 +114,6 @@ macro_rules! builder { }; } -pub(crate) use builder; - pub enum RefOrValue<'a, T> { Ref(&'a T), diff --git a/engine/src/window.rs b/engine/src/window.rs index 00c360e..d342341 100644 --- a/engine/src/window.rs +++ b/engine/src/window.rs @@ -4,8 +4,8 @@ use std::ffi::{CStr, CString}; use bitflags::bitflags; use ecs::actions::Actions; use ecs::extension::Collector as ExtensionCollector; -use ecs::phase::{Phase, PRESENT as PRESENT_PHASE, START as START_PHASE}; -use ecs::relationship::{ChildOf, Relationship}; +use ecs::pair::{ChildOf, Pair}; +use ecs::phase::{Phase, START as START_PHASE}; use ecs::sole::Single; use ecs::{static_entity, Sole}; use glfw::window::{Hint as WindowCreationHint, HintValue as WindowCreationHintValue}; @@ -13,11 +13,12 @@ use glfw::WindowSize; use util_macros::VariantArr; use crate::data_types::dimens::Dimens; +use crate::renderer::RENDER_PHASE; use crate::vector::Vec2; static_entity!( pub UPDATE_PHASE, - (Phase, <Relationship<ChildOf, Phase>>::new(*PRESENT_PHASE)) + (Phase, Pair::new::<ChildOf>(*RENDER_PHASE)) ); #[derive(Debug, Sole)] diff --git a/engine/src/work_queue.rs b/engine/src/work_queue.rs new file mode 100644 index 0000000..7226c7d --- /dev/null +++ b/engine/src/work_queue.rs @@ -0,0 +1,44 @@ +use std::marker::PhantomData; +use std::sync::mpsc::{channel as mpsc_channel, Sender as MpscSender}; +use std::thread::JoinHandle as ThreadHandle; + +pub struct Work<UserData: Send + Sync + 'static> +{ + pub func: fn(UserData), + pub user_data: UserData, +} + +#[derive(Debug)] +pub struct WorkQueue<UserData: Send + Sync + 'static> +{ + work_sender: MpscSender<Work<UserData>>, + _thread: ThreadHandle<()>, + _pd: PhantomData<UserData>, +} + +impl<UserData: Send + Sync + 'static> WorkQueue<UserData> +{ + pub fn new() -> Self + { + let (work_sender, work_receiver) = mpsc_channel::<Work<UserData>>(); + + Self { + work_sender, + _thread: std::thread::spawn(move || { + let work_receiver = work_receiver; + + while let Ok(work) = work_receiver.recv() { + (work.func)(work.user_data); + } + }), + _pd: PhantomData, + } + } + + pub fn add_work(&self, work: Work<UserData>) + { + if self.work_sender.send(work).is_err() { + tracing::error!("Cannot add work to work queue. Work queue thread is dead"); + } + } +} |