summaryrefslogtreecommitdiff
path: root/engine/src
diff options
context:
space:
mode:
Diffstat (limited to 'engine/src')
-rw-r--r--engine/src/asset.rs777
-rw-r--r--engine/src/camera/fly.rs80
-rw-r--r--engine/src/collision.rs142
-rw-r--r--engine/src/data_types/color.rs1
-rw-r--r--engine/src/data_types/dimens.rs65
-rw-r--r--engine/src/data_types/matrix.rs2
-rw-r--r--engine/src/data_types/vector.rs49
-rw-r--r--engine/src/draw_flags.rs2
-rw-r--r--engine/src/event.rs27
-rw-r--r--engine/src/file_format/wavefront/mtl.rs211
-rw-r--r--engine/src/file_format/wavefront/obj.rs54
-rw-r--r--engine/src/image.rs184
-rw-r--r--engine/src/input.rs246
-rw-r--r--engine/src/input/keyboard.rs6
-rw-r--r--engine/src/input/mouse.rs6
-rw-r--r--engine/src/lib.rs63
-rw-r--r--engine/src/lighting.rs8
-rw-r--r--engine/src/material.rs125
-rw-r--r--engine/src/math.rs2
-rw-r--r--engine/src/mesh.rs139
-rw-r--r--engine/src/mesh/cube.rs1007
-rw-r--r--engine/src/model.rs176
-rw-r--r--engine/src/opengl/buffer.rs92
-rw-r--r--engine/src/opengl/debug.rs145
-rw-r--r--engine/src/opengl/mod.rs107
-rw-r--r--engine/src/opengl/shader.rs247
-rw-r--r--engine/src/opengl/texture.rs240
-rw-r--r--engine/src/opengl/util.rs30
-rw-r--r--engine/src/opengl/vertex_array.rs183
-rw-r--r--engine/src/performance.rs59
-rw-r--r--engine/src/projection.rs118
-rw-r--r--engine/src/renderer.rs79
-rw-r--r--engine/src/renderer/opengl.rs1540
-rw-r--r--engine/src/renderer/opengl/glsl/light.glsl4
-rw-r--r--engine/src/renderer/opengl/glutin_compat.rs268
-rw-r--r--engine/src/renderer/opengl/graphics_mesh.rs120
-rw-r--r--engine/src/renderer/opengl/vertex.rs (renamed from engine/src/vertex.rs)35
-rw-r--r--engine/src/texture.rs191
-rw-r--r--engine/src/transform.rs6
-rw-r--r--engine/src/util.rs145
-rw-r--r--engine/src/window.rs318
-rw-r--r--engine/src/windowing.rs669
-rw-r--r--engine/src/windowing/keyboard.rs763
-rw-r--r--engine/src/windowing/mouse.rs136
-rw-r--r--engine/src/windowing/window.rs171
-rw-r--r--engine/src/windowing/window/platform.rs12
-rw-r--r--engine/src/work_queue.rs44
47 files changed, 5927 insertions, 3167 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 b6ba7aa..a034851 100644
--- a/engine/src/camera/fly.rs
+++ b/engine/src/camera/fly.rs
@@ -1,15 +1,16 @@
use ecs::component::local::Local;
+use ecs::phase::UPDATE as UPDATE_PHASE;
use ecs::sole::Single;
-use ecs::system::{Into, System};
+use ecs::system::initializable::Initializable;
+use ecs::system::Into;
use ecs::{Component, Query};
-use glfw::window::{Key, KeyState};
+use crate::builder;
use crate::camera::{Active as ActiveCamera, Camera};
use crate::delta_time::DeltaTime;
-use crate::event::Update as UpdateEvent;
-use crate::input::{Cursor, CursorFlags, Keys};
-use crate::transform::Position;
-use crate::util::builder;
+use crate::input::keyboard::{Key, KeyState, Keyboard};
+use crate::input::mouse::Motion as MouseMotion;
+use crate::transform::WorldPosition;
use crate::vector::{Vec2, Vec3};
builder! {
@@ -60,12 +61,7 @@ impl ecs::extension::Extension for Extension
{
fn collect(self, mut collector: ecs::extension::Collector<'_>)
{
- collector.add_system(
- UpdateEvent,
- update
- .into_system()
- .initialize((CursorState::default(), self.0)),
- );
+ collector.add_system(*UPDATE_PHASE, update.into_system().initialize((self.0,)));
}
}
@@ -76,37 +72,30 @@ pub struct Options
}
fn update(
- camera_query: Query<(Camera, Position, Fly, ActiveCamera)>,
- keys: Single<Keys>,
- cursor: Single<Cursor>,
- cursor_flags: Single<CursorFlags>,
+ camera_query: Query<(&mut Camera, &mut WorldPosition, &mut Fly, &ActiveCamera)>,
+ keyboard: Single<Keyboard>,
+ mouse_motion: Single<MouseMotion>,
delta_time: Single<DeltaTime>,
- mut cursor_state: Local<CursorState>,
options: Local<Options>,
)
{
- for (mut camera, mut camera_pos, mut fly_camera, _) in &camera_query {
- if cursor.has_moved && cursor_flags.is_first_move.flag {
- #[cfg(feature = "debug")]
- tracing::debug!("First cursor move");
-
- cursor_state.last_pos = cursor.position;
- }
-
+ for (mut camera, mut camera_world_pos, mut fly_camera, _) in &camera_query {
let delta_time = delta_time.duration;
- let mut x_offset = cursor.position.x - cursor_state.last_pos.x;
- let mut y_offset = cursor_state.last_pos.y - cursor.position.y;
+ // tracing::info!("Mouse motion: {:?}", mouse_motion.position_delta);
- cursor_state.last_pos = cursor.position;
+ if mouse_motion.position_delta != (Vec2 { x: 0.0, y: 0.0 }) {
+ let x_offset =
+ mouse_motion.position_delta.x * f64::from(options.mouse_sensitivity);
- x_offset *= f64::from(options.mouse_sensitivity);
- y_offset *= f64::from(options.mouse_sensitivity);
+ let y_offset =
+ (-mouse_motion.position_delta.y) * f64::from(options.mouse_sensitivity);
- fly_camera.current_yaw += x_offset;
- fly_camera.current_pitch += y_offset;
+ fly_camera.current_yaw += x_offset;
+ fly_camera.current_pitch += y_offset;
- fly_camera.current_pitch = fly_camera.current_pitch.clamp(-89.0, 89.0);
+ fly_camera.current_pitch = fly_camera.current_pitch.clamp(-89.0, 89.0);
+ }
// TODO: This casting to a f32 from a f64 is horrible. fix it
#[allow(clippy::cast_possible_truncation)]
@@ -123,35 +112,30 @@ fn update(
camera.global_up = cam_right.cross(&direction).normalize();
- if matches!(keys.get_key_state(Key::W), KeyState::Pressed) {
- camera_pos.position +=
+ if keyboard.get_key_state(Key::W) == KeyState::Pressed {
+ camera_world_pos.position +=
direction * fly_camera.speed * delta_time.as_secs_f32();
}
- if matches!(keys.get_key_state(Key::S), KeyState::Pressed) {
- camera_pos.position -=
+ if keyboard.get_key_state(Key::S) == KeyState::Pressed {
+ camera_world_pos.position -=
direction * fly_camera.speed * delta_time.as_secs_f32();
}
- if matches!(keys.get_key_state(Key::A), KeyState::Pressed) {
+ if keyboard.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 matches!(keys.get_key_state(Key::D), KeyState::Pressed) {
+ if keyboard.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;
}
}
-
-#[derive(Debug, Default, Component)]
-struct CursorState
-{
- last_pos: Vec2<f64>,
-}
diff --git a/engine/src/collision.rs b/engine/src/collision.rs
new file mode 100644
index 0000000..aefd9b6
--- /dev/null
+++ b/engine/src/collision.rs
@@ -0,0 +1,142 @@
+use ecs::Component;
+
+use crate::mesh::Mesh;
+use crate::vector::Vec3;
+
+pub trait Collider<Other>
+{
+ fn intersects(&self, other: &Other) -> bool;
+}
+
+#[derive(Debug, Default, Clone, Component)]
+#[non_exhaustive]
+pub struct BoxCollider
+{
+ pub min: Vec3<f32>,
+ pub max: Vec3<f32>,
+}
+
+impl BoxCollider
+{
+ pub fn for_mesh(mesh: &Mesh) -> Self
+ {
+ let furthest_dir_points = mesh.find_furthest_vertex_positions();
+
+ Self {
+ min: Vec3 {
+ x: furthest_dir_points.left.x,
+ y: furthest_dir_points.down.y,
+ z: furthest_dir_points.back.z,
+ },
+ max: Vec3 {
+ x: furthest_dir_points.right.x,
+ y: furthest_dir_points.up.y,
+ z: furthest_dir_points.front.z,
+ },
+ }
+ }
+
+ pub fn offset(self, offset: Vec3<f32>) -> Self
+ {
+ Self {
+ min: self.min + offset,
+ max: self.max + offset,
+ }
+ }
+}
+
+impl Collider<BoxCollider> for BoxCollider
+{
+ fn intersects(&self, other: &BoxCollider) -> bool
+ {
+ self.min.x <= other.max.x
+ && self.max.x >= other.min.x
+ && self.min.y <= other.max.y
+ && self.max.y >= other.min.y
+ && self.min.z <= other.max.z
+ && self.max.z >= other.min.z
+ }
+}
+
+impl Collider<SphereCollider> for BoxCollider
+{
+ fn intersects(&self, other: &SphereCollider) -> bool
+ {
+ other.intersects(self)
+ }
+}
+
+impl Collider<Vec3<f32>> for BoxCollider
+{
+ fn intersects(&self, other: &Vec3<f32>) -> bool
+ {
+ other.x >= self.min.x
+ && other.y >= self.min.y
+ && other.z >= self.min.z
+ && other.x <= self.max.x
+ && other.y <= self.max.y
+ && other.z <= self.max.z
+ }
+}
+
+#[derive(Debug, Default, Clone, Component)]
+pub struct SphereCollider
+{
+ pub center: Vec3<f32>,
+ pub radius: f32,
+}
+
+impl SphereCollider
+{
+ pub fn offset(self, offset: Vec3<f32>) -> Self
+ {
+ Self {
+ center: self.center + offset,
+ radius: self.radius,
+ }
+ }
+}
+
+impl Collider<SphereCollider> for SphereCollider
+{
+ fn intersects(&self, other: &SphereCollider) -> bool
+ {
+ (&self.center - &other.center).length() <= self.radius + other.radius
+ }
+}
+
+impl Collider<BoxCollider> for SphereCollider
+{
+ fn intersects(&self, other: &BoxCollider) -> bool
+ {
+ let mut min_distance = 0.0;
+
+ if self.center.x < other.min.x {
+ min_distance += (self.center.x - other.min.x).powf(2.0);
+ } else if self.center.x > other.max.x {
+ min_distance += (self.center.x - other.max.x).powf(2.0);
+ }
+
+ if self.center.y < other.min.y {
+ min_distance += (self.center.y - other.min.y).powf(2.0);
+ } else if self.center.y > other.max.y {
+ min_distance += (self.center.y - other.max.y).powf(2.0);
+ }
+
+ if self.center.z < other.min.z {
+ min_distance += (self.center.z - other.min.z).powf(2.0);
+ } else if self.center.z > other.max.z {
+ min_distance += (self.center.z - other.max.z).powf(2.0);
+ }
+
+ min_distance <= self.radius.powf(2.0)
+ }
+}
+
+impl Collider<Vec3<f32>> for SphereCollider
+{
+ fn intersects(&self, other: &Vec3<f32>) -> bool
+ {
+ (&self.center - other).length() <= self.radius
+ }
+}
diff --git a/engine/src/data_types/color.rs b/engine/src/data_types/color.rs
index cef3b92..c5316e6 100644
--- a/engine/src/data_types/color.rs
+++ b/engine/src/data_types/color.rs
@@ -1,7 +1,6 @@
use std::ops::{Add, Div, Mul, Neg, Sub};
#[derive(Debug, Clone, Default)]
-#[repr(C)]
pub struct Color<Value>
{
pub red: Value,
diff --git a/engine/src/data_types/dimens.rs b/engine/src/data_types/dimens.rs
index b395627..8bf239f 100644
--- a/engine/src/data_types/dimens.rs
+++ b/engine/src/data_types/dimens.rs
@@ -1,7 +1,70 @@
-/// Dimensions.
+use std::num::NonZeroU32;
+
+/// 2D dimensions.
#[derive(Debug, Clone, Copy)]
pub struct Dimens<Value>
{
pub width: 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 }
+ }
+}
+
+impl Dimens<u32>
+{
+ #[must_use]
+ pub fn try_into_nonzero(self) -> Option<Dimens<NonZeroU32>>
+ {
+ Some(Dimens {
+ width: NonZeroU32::new(self.width)?,
+ height: NonZeroU32::new(self.height)?,
+ })
+ }
+}
+
+/// 3D dimensions.
+#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
+pub struct Dimens3<Value>
+{
+ pub width: 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/matrix.rs b/engine/src/data_types/matrix.rs
index 3a29ae2..b754b62 100644
--- a/engine/src/data_types/matrix.rs
+++ b/engine/src/data_types/matrix.rs
@@ -4,7 +4,7 @@ use crate::vector::Vec3;
pub struct Matrix<Value, const ROWS: usize, const COLUMNS: usize>
{
/// Items must be layed out this way for it to work with OpenGL shaders.
- items: [[Value; ROWS]; COLUMNS],
+ pub items: [[Value; ROWS]; COLUMNS],
}
impl<Value, const ROWS: usize, const COLUMNS: usize> Matrix<Value, ROWS, COLUMNS>
diff --git a/engine/src/data_types/vector.rs b/engine/src/data_types/vector.rs
index 17953f4..dc6df30 100644
--- a/engine/src/data_types/vector.rs
+++ b/engine/src/data_types/vector.rs
@@ -2,7 +2,7 @@ use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign};
use crate::color::Color;
-#[derive(Debug, Default, Clone, Copy)]
+#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct Vec2<Value>
{
pub x: Value,
@@ -14,6 +14,29 @@ impl Vec2<u32>
pub const ZERO: Self = Self { x: 0, y: 0 };
}
+impl<Value> Add for Vec2<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 }
+ }
+}
+
+impl<Value> AddAssign for Vec2<Value>
+where
+ Value: Add<Value, Output = Value> + Clone,
+{
+ fn add_assign(&mut self, rhs: Self)
+ {
+ self.x = self.x.clone() + rhs.x;
+ self.y = self.y.clone() + rhs.y;
+ }
+}
+
impl<Value> Add<Value> for Vec2<Value>
where
Value: Add<Output = Value> + Clone,
@@ -74,8 +97,7 @@ where
}
}
-#[derive(Debug, Default, Clone, Copy)]
-#[repr(C)]
+#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct Vec3<Value>
{
pub x: Value,
@@ -85,6 +107,11 @@ pub struct Vec3<Value>
impl Vec3<f32>
{
+ pub const BACK: Self = Self { x: 0.0, y: 0.0, z: -1.0 };
+ pub const DOWN: Self = Self { x: 0.0, y: -1.0, z: 0.0 };
+ pub const FRONT: Self = Self { x: 0.0, y: 0.0, z: 1.0 };
+ pub const LEFT: Self = Self { x: -1.0, y: 0.0, z: 0.0 };
+ pub const RIGHT: Self = Self { x: 1.0, y: 0.0, z: 0.0 };
pub const UP: Self = Self { x: 0.0, y: 1.0, z: 0.0 };
/// Returns the length of the vector.
@@ -209,6 +236,22 @@ where
}
}
+impl<Value> Mul for Vec3<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,
+ }
+ }
+}
+
impl<Value> Neg for Vec3<Value>
where
Value: Neg<Output = 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/event.rs b/engine/src/event.rs
deleted file mode 100644
index e5ae486..0000000
--- a/engine/src/event.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-pub use ecs::event::start::Start;
-use ecs::event::Event;
-
-#[derive(Debug)]
-pub struct Update;
-
-impl Event for Update {}
-
-#[derive(Debug)]
-pub struct PreUpdate;
-
-impl Event for PreUpdate {}
-
-#[derive(Debug)]
-pub struct Present;
-
-impl Event for Present {}
-
-#[derive(Debug)]
-pub struct PostPresent;
-
-impl Event for PostPresent {}
-
-#[derive(Debug)]
-pub struct Conclude;
-
-impl Event for Conclude {}
diff --git a/engine/src/file_format/wavefront/mtl.rs b/engine/src/file_format/wavefront/mtl.rs
index ef6e894..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`.
///
@@ -44,25 +42,47 @@ pub fn parse(obj_content: &str) -> Result<Vec<NamedMaterial>, Error>
.filter(|(_, statement)| matches!(statement.keyword, Keyword::Newmtl))
.count();
- #[cfg(feature = "debug")]
tracing::debug!("Material count: {material_cnt}");
statements_to_materials(statements, material_cnt)
}
#[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)]
@@ -71,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}"
@@ -93,7 +119,7 @@ pub enum Error
},
}
-#[cfg_attr(feature = "debug", tracing::instrument(skip_all))]
+#[tracing::instrument(skip_all)]
fn statements_to_materials(
statements: impl IntoIterator<Item = (usize, Statement<Keyword>)>,
material_cnt: usize,
@@ -101,63 +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 {
- #[cfg(feature = "debug")]
- 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)?;
- #[cfg(feature = "debug")]
- 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)?;
- #[cfg(feature = "debug")]
- 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)?;
- #[cfg(feature = "debug")]
- 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 {
@@ -170,55 +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))?;
-
- #[cfg(feature = "debug")]
- 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,
+ });
+ }
- #[cfg(feature = "debug")]
- 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,
+ });
+ }
- #[cfg(feature = "debug")]
- 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 {
- #[cfg(feature = "debug")]
- 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)
@@ -244,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 {
@@ -280,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 88e6580..88d566c 100644
--- a/engine/src/file_format/wavefront/obj.rs
+++ b/engine/src/file_format/wavefront/obj.rs
@@ -2,6 +2,7 @@
//!
//! File format documentation: <https://paulbourke.net/dataformats/obj>
+use std::collections::HashMap;
use std::fs::read_to_string;
use std::path::PathBuf;
@@ -12,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`.
///
@@ -82,26 +82,32 @@ impl Obj
/// - A face index does not fit in a [`u32`]
pub fn to_mesh(&self) -> Result<Mesh, Error>
{
- let vertices = self
- .faces
- .iter()
- .flat_map(|face| face.vertices.clone())
- .map(|face_vertex| face_vertex.to_vertex(self))
- .collect::<Result<Vec<_>, Error>>()?;
-
- Ok(Mesh::new(
- vertices,
- Some(
- self.faces
- .iter()
- .flat_map(|face| face.vertices.clone())
- .enumerate()
- .map(|(index, _)| {
- u32::try_from(index).map_err(|_| Error::FaceIndexTooBig(index))
- })
- .collect::<Result<Vec<_>, _>>()?,
- ),
- ))
+ let mut vertices = Vec::<Vertex>::with_capacity(self.faces.len() * 3);
+ let mut indices = Vec::<u32>::with_capacity(self.faces.len() * 3);
+
+ let mut added_face_vertices =
+ HashMap::<FaceVertex, u32>::with_capacity(self.faces.len() * 3);
+
+ for face in &self.faces {
+ for face_vertex in &face.vertices {
+ if let Some(index) = added_face_vertices.get(&face_vertex) {
+ indices.push(*index);
+
+ continue;
+ }
+
+ vertices.push(face_vertex.to_vertex(self)?);
+
+ let vertex_index = u32::try_from(vertices.len() - 1)
+ .map_err(|_| Error::FaceIndexTooBig(vertices.len() - 1))?;
+
+ indices.push(vertex_index);
+
+ added_face_vertices.insert(face_vertex.clone(), vertex_index);
+ }
+ }
+
+ Ok(Mesh::new(vertices, Some(indices)))
}
/// Reads and parses the material libraries of this `Obj`.
@@ -142,7 +148,7 @@ pub struct Face
pub material_name: Option<String>,
}
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FaceVertex
{
pub position: u32,
@@ -161,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 f4166f6..f8c9dfd 100644
--- a/engine/src/input.rs
+++ b/engine/src/input.rs
@@ -1,145 +1,23 @@
-use std::collections::HashMap;
-
+use ecs::declare_entity;
use ecs::extension::Collector as ExtensionCollector;
-use ecs::sole::Single;
-use ecs::Sole;
-
-use crate::event::{
- PostPresent as PostPresentEvent,
- PreUpdate as PreUpdateEvent,
- Start as StartEvent,
-};
-use crate::vector::Vec2;
-use crate::window::Window;
-
-mod reexports
-{
- pub use crate::window::{Key, KeyState};
-}
-
-pub use reexports::*;
-
-#[derive(Debug, Sole)]
-pub struct Keys
-{
- map: HashMap<Key, KeyData>,
-}
-
-impl Keys
-{
- #[must_use]
- pub fn new() -> Self
- {
- Self {
- map: Key::KEYS
- .iter()
- .map(|key| {
- (
- *key,
- KeyData {
- state: KeyState::Released,
- prev_tick_state: KeyState::Released,
- },
- )
- })
- .collect(),
- }
- }
-
- #[must_use]
- pub fn get_key_state(&self, key: Key) -> KeyState
- {
- let Some(key_data) = self.map.get(&key) else {
- unreachable!();
- };
-
- key_data.state
- }
-
- #[must_use]
- pub fn get_prev_key_state(&self, key: Key) -> KeyState
- {
- let Some(key_data) = self.map.get(&key) else {
- unreachable!();
- };
-
- key_data.prev_tick_state
- }
+use ecs::pair::{DependsOn, Pair};
+use ecs::phase::Phase;
- pub fn set_key_state(&mut self, key: Key, new_key_state: KeyState)
- {
- if matches!(new_key_state, KeyState::Repeat) {
- return;
- }
-
- let Some(key_data) = self.map.get_mut(&key) else {
- unreachable!();
- };
+use crate::windowing::PHASE as WINDOWING_PHASE;
- key_data.state = new_key_state;
- }
+pub mod keyboard;
+pub mod mouse;
- #[must_use]
- pub fn is_anything_pressed(&self) -> bool
- {
- self.map
- .values()
- .any(|key_data| matches!(key_data.state, KeyState::Pressed))
- }
-}
-
-impl Default for Keys
-{
- fn default() -> Self
- {
- Self::new()
- }
-}
-
-#[derive(Debug, Default, Clone, Sole)]
-pub struct Cursor
-{
- pub position: Vec2<f64>,
- pub has_moved: bool,
-}
-
-#[derive(Debug, Clone, Sole)]
-pub struct CursorFlags
-{
- /// This flag is set in two situations:
- /// A: The window has just started
- /// B: The window has gained focus again after losing focus.
- ///
- /// This flag only lasts a single tick then it is cleared (at the beginning of the
- /// next tick).
- pub is_first_move: CursorFlag,
-}
-
-impl Default for CursorFlags
-{
- fn default() -> Self
- {
- Self {
- is_first_move: CursorFlag { flag: true, ..Default::default() },
- }
- }
-}
-
-#[derive(Debug, Default, Clone)]
-pub struct CursorFlag
-{
- pub flag: bool,
- pub pending_clear: bool,
-}
-
-impl CursorFlag
-{
- pub fn clear(&mut self)
- {
- self.flag = false;
- self.pending_clear = false;
- }
-}
+declare_entity!(
+ pub PHASE,
+ (
+ Phase,
+ Pair::builder()
+ .relation::<DependsOn>()
+ .target_id(*WINDOWING_PHASE)
+ .build()
+ )
+);
/// Input extension.
#[derive(Debug, Default)]
@@ -147,96 +25,8 @@ pub struct Extension {}
impl ecs::extension::Extension for Extension
{
- fn collect(self, mut collector: ExtensionCollector<'_>)
+ fn collect(self, _collector: ExtensionCollector<'_>)
{
- collector.add_system(StartEvent, initialize);
- collector.add_system(PreUpdateEvent, maybe_clear_cursor_is_first_move);
- collector.add_system(PostPresentEvent, set_keys_prev_tick_state);
-
- collector.add_sole(Keys::default()).ok();
- collector.add_sole(Cursor::default()).ok();
- collector.add_sole(CursorFlags::default()).ok();
+ // TODO: Add input mapping
}
}
-
-fn initialize(
- keys: Single<Keys>,
- cursor: Single<Cursor>,
- cursor_flags: Single<CursorFlags>,
- window: Single<Window>,
-)
-{
- let keys_weak_ref = keys.to_weak_ref();
-
- window.set_key_callback(move |key, _scancode, key_state, _modifiers| {
- let keys_ref = keys_weak_ref.access().expect("No world");
-
- let mut keys = keys_ref.to_single();
-
- keys.set_key_state(key, key_state);
- });
-
- let cursor_weak_ref = cursor.to_weak_ref();
-
- window.set_cursor_pos_callback(move |cursor_position| {
- let cursor_ref = cursor_weak_ref.access().expect("No world");
-
- let mut cursor = cursor_ref.to_single();
-
- cursor.position = Vec2 {
- x: cursor_position.x,
- y: cursor_position.y,
- };
-
- cursor.has_moved = true;
- });
-
- let cursor_flags_weak_ref = cursor_flags.to_weak_ref();
-
- window.set_focus_callback(move |is_focused| {
- #[cfg(feature = "debug")]
- tracing::trace!("Window is focused: {is_focused}");
-
- let cursor_flags_ref = cursor_flags_weak_ref.access().expect("No world");
-
- cursor_flags_ref.to_single().is_first_move.flag = is_focused;
- });
-}
-
-fn maybe_clear_cursor_is_first_move(
- cursor: Single<Cursor>,
- mut cursor_flags: Single<CursorFlags>,
-)
-{
- if cursor_flags.is_first_move.pending_clear {
- #[cfg(feature = "debug")]
- tracing::trace!("Clearing is_first_move");
-
- // This flag was set for the whole previous tick so it can be cleared now
- cursor_flags.is_first_move.clear();
-
- return;
- }
-
- if cursor.has_moved && cursor_flags.is_first_move.flag {
- #[cfg(feature = "debug")]
- tracing::trace!("Setting flag to clear is_first_move next tick");
-
- // Make this system clear is_first_move the next time it runs
- cursor_flags.is_first_move.pending_clear = true;
- }
-}
-
-fn set_keys_prev_tick_state(mut keys: Single<Keys>)
-{
- for key_data in keys.map.values_mut() {
- key_data.prev_tick_state = key_data.state;
- }
-}
-
-#[derive(Debug)]
-struct KeyData
-{
- state: KeyState,
- prev_tick_state: KeyState,
-}
diff --git a/engine/src/input/keyboard.rs b/engine/src/input/keyboard.rs
new file mode 100644
index 0000000..d226df0
--- /dev/null
+++ b/engine/src/input/keyboard.rs
@@ -0,0 +1,6 @@
+mod reexports
+{
+ pub use crate::windowing::keyboard::{Key, KeyState, Keyboard};
+}
+
+pub use reexports::*;
diff --git a/engine/src/input/mouse.rs b/engine/src/input/mouse.rs
new file mode 100644
index 0000000..90091f3
--- /dev/null
+++ b/engine/src/input/mouse.rs
@@ -0,0 +1,6 @@
+mod reexports
+{
+ pub use crate::windowing::mouse::{Button, ButtonState, Buttons, Motion};
+}
+
+pub use reexports::*;
diff --git a/engine/src/lib.rs b/engine/src/lib.rs
index 07b2f8d..d5531c1 100644
--- a/engine/src/lib.rs
+++ b/engine/src/lib.rs
@@ -2,58 +2,48 @@
#![allow(clippy::needless_pass_by_value)]
use ecs::component::Sequence as ComponentSequence;
-use ecs::event::component::TypeTransformComponentsToAddedEvents;
-use ecs::event::{Event, Sequence as EventSequence};
use ecs::extension::Extension;
+use ecs::phase::PRE_UPDATE as PRE_UPDATE_PHASE;
use ecs::sole::Sole;
+use ecs::system::initializable::Initializable;
+use ecs::system::observer::Observer;
use ecs::system::{Into, System};
-use ecs::tuple::Reduce as TupleReduce;
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};
-use crate::event::{
- Conclude as ConcludeEvent,
- PostPresent as PostPresentEvent,
- PreUpdate as PreUpdateEvent,
- Present as PresentEvent,
- Update as UpdateEvent,
-};
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 event;
pub mod file_format;
+pub mod image;
pub mod input;
pub mod lighting;
pub mod material;
pub mod math;
pub mod mesh;
-pub mod performance;
+pub mod model;
pub mod projection;
pub mod renderer;
pub mod texture;
pub mod transform;
-pub mod vertex;
-pub mod window;
+pub mod windowing;
pub extern crate ecs;
pub(crate) use crate::data_types::matrix;
pub use crate::data_types::{color, vector};
-type EventOrder = (
- PreUpdateEvent,
- UpdateEvent,
- PresentEvent,
- PostPresentEvent,
- ConcludeEvent,
-);
+const INITIAL_ASSET_CAPACITY: usize = 128;
#[derive(Debug)]
pub struct Engine
@@ -67,35 +57,52 @@ impl Engine
#[must_use]
pub fn new() -> Self
{
+ #[cfg(windows)]
+ nu_ansi_term::enable_ansi_support().unwrap();
+
let mut world = World::new();
world.add_sole(DeltaTime::default()).ok();
world.register_system(
- PreUpdateEvent,
+ *PRE_UPDATE_PHASE,
update_delta_time
.into_system()
.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 }
}
pub fn spawn<Comps>(&mut self, components: Comps) -> Uid
where
- Comps: ComponentSequence + TupleReduce<TypeTransformComponentsToAddedEvents>,
- Comps::Out: EventSequence,
+ Comps: ComponentSequence,
{
self.world.create_entity(components)
}
pub fn register_system<'this, SystemImpl>(
&'this mut self,
- event: impl Event,
+ phase_euid: Uid,
system: impl System<'this, SystemImpl>,
)
{
- self.world.register_system(event, system);
+ self.world.register_system(phase_euid, system);
+ }
+
+ pub fn register_observer<'this, SystemImpl>(
+ &'this mut self,
+ observer: impl Observer<'this, SystemImpl>,
+ )
+ {
+ self.world.register_observer(observer);
}
/// Adds a globally shared singleton value.
@@ -113,9 +120,9 @@ impl Engine
}
/// Runs the event loop.
- pub fn start(&self)
+ pub fn start(&mut self)
{
- self.world.event_loop::<EventOrder>();
+ self.world.start_loop();
}
}
diff --git a/engine/src/lighting.rs b/engine/src/lighting.rs
index 48adb0e..9ab2ca8 100644
--- a/engine/src/lighting.rs
+++ b/engine/src/lighting.rs
@@ -1,8 +1,8 @@
use ecs::{Component, Sole};
+use crate::builder;
use crate::color::Color;
use crate::data_types::vector::Vec3;
-use crate::util::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(),
@@ -58,7 +59,6 @@ pub struct AttenuationParams
impl Default for AttenuationParams
{
- #[must_use]
fn default() -> Self
{
Self {
diff --git a/engine/src/material.rs b/engine/src/material.rs
index aae6003..56ff15f 100644
--- a/engine/src/material.rs
+++ b/engine/src/material.rs
@@ -1,35 +1,48 @@
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,
}
+impl Material
+{
+ pub fn builder() -> Builder
+ {
+ Builder::default()
+ }
+}
+
+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,
}
@@ -39,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,
}
}
@@ -53,7 +65,7 @@ impl Builder
#[must_use]
pub fn ambient(mut self, ambient: Color<f32>) -> Self
{
- self.ambient = Some(ambient);
+ self.ambient = ambient;
self
}
@@ -61,7 +73,7 @@ impl Builder
#[must_use]
pub fn diffuse(mut self, diffuse: Color<f32>) -> Self
{
- self.diffuse = Some(diffuse);
+ self.diffuse = diffuse;
self
}
@@ -69,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);
@@ -83,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);
@@ -91,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);
@@ -99,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;
@@ -127,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,
}
}
@@ -198,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/math.rs b/engine/src/math.rs
index b86e760..0340de8 100644
--- a/engine/src/math.rs
+++ b/engine/src/math.rs
@@ -13,5 +13,5 @@ pub fn calc_triangle_surface_normal(
let v1 = edge_b - egde_a;
let v2 = edge_c - egde_a;
- v1.cross(&v2)
+ v1.cross(&v2).normalize()
}
diff --git a/engine/src/mesh.rs b/engine/src/mesh.rs
index de0af70..fb977af 100644
--- a/engine/src/mesh.rs
+++ b/engine/src/mesh.rs
@@ -1,10 +1,9 @@
-use ecs::Component;
-
-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>,
@@ -26,8 +25,140 @@ impl Mesh
}
#[must_use]
+ pub fn vertices_mut(&mut self) -> &mut [Vertex]
+ {
+ &mut self.vertices
+ }
+
+ #[must_use]
pub fn indices(&self) -> Option<&[u32]>
{
self.indices.as_deref()
}
+
+ #[must_use]
+ pub fn indices_mut(&mut self) -> Option<&mut [u32]>
+ {
+ self.indices.as_deref_mut()
+ }
+
+ /// Finds the vertex positions that are furthest in every 3D direction. Keep in mind
+ /// that this can be quite time-expensive if the mesh has many vertices.
+ pub fn find_furthest_vertex_positions(&self) -> DirectionPositions<'_>
+ {
+ let mut point_iter = self.vertices().iter().map(|vertex| &vertex.pos).into_iter();
+
+ let first_point = point_iter.next().unwrap();
+
+ point_iter
+ .fold(
+ FurthestPosAcc {
+ up: FurthestPos::new(&first_point, &Vec3::UP),
+ down: FurthestPos::new(&first_point, &Vec3::DOWN),
+ left: FurthestPos::new(&first_point, &Vec3::LEFT),
+ right: FurthestPos::new(&first_point, &Vec3::RIGHT),
+ back: FurthestPos::new(&first_point, &Vec3::BACK),
+ front: FurthestPos::new(&first_point, &Vec3::FRONT),
+ },
+ |mut furthest_pos_acc, pos| {
+ furthest_pos_acc.up.update_if_further(pos);
+ furthest_pos_acc.down.update_if_further(pos);
+ furthest_pos_acc.left.update_if_further(pos);
+ furthest_pos_acc.right.update_if_further(pos);
+ furthest_pos_acc.back.update_if_further(pos);
+ furthest_pos_acc.front.update_if_further(pos);
+
+ furthest_pos_acc
+ },
+ )
+ .into()
+ }
+}
+
+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>
+{
+ pub up: &'mesh Vec3<f32>,
+ pub down: &'mesh Vec3<f32>,
+ pub left: &'mesh Vec3<f32>,
+ pub right: &'mesh Vec3<f32>,
+ pub back: &'mesh Vec3<f32>,
+ pub front: &'mesh Vec3<f32>,
+}
+
+impl<'mesh> From<FurthestPosAcc<'mesh>> for DirectionPositions<'mesh>
+{
+ fn from(acc: FurthestPosAcc<'mesh>) -> Self
+ {
+ Self {
+ up: acc.up.pos,
+ down: acc.down.pos,
+ left: acc.left.pos,
+ right: acc.right.pos,
+ back: acc.back.pos,
+ front: acc.front.pos,
+ }
+ }
+}
+
+#[derive(Debug)]
+struct FurthestPosAcc<'mesh>
+{
+ up: FurthestPos<'mesh>,
+ down: FurthestPos<'mesh>,
+ left: FurthestPos<'mesh>,
+ right: FurthestPos<'mesh>,
+ back: FurthestPos<'mesh>,
+ front: FurthestPos<'mesh>,
+}
+
+#[derive(Debug)]
+struct FurthestPos<'mesh>
+{
+ pos: &'mesh Vec3<f32>,
+ dot_prod: f32,
+ direction: &'mesh Vec3<f32>,
+}
+
+impl<'mesh> FurthestPos<'mesh>
+{
+ fn new(pos: &'mesh Vec3<f32>, direction: &'mesh Vec3<f32>) -> Self
+ {
+ Self {
+ pos,
+ dot_prod: direction.dot(&pos),
+ direction,
+ }
+ }
+
+ fn update_if_further(&mut self, point: &'mesh Vec3<f32>)
+ {
+ let point_dot_prod = self.direction.dot(point);
+
+ if point_dot_prod > self.dot_prod {
+ self.pos = point;
+ self.dot_prod = point_dot_prod;
+ }
+ }
}
diff --git a/engine/src/mesh/cube.rs b/engine/src/mesh/cube.rs
index 7cdf885..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::vector::Vec3;
-use crate::vertex::{Builder as VertexBuilder, Vertex};
+use crate::mesh::{Mesh, Vertex};
+use crate::builder;
+use crate::vector::{Vec2, Vec3};
builder! {
/// Cube mesh creation specification.
@@ -27,676 +27,467 @@ impl CreationSpec
}
}
-#[derive(Debug)]
+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
{
+ /// +Z
Front,
+
+ /// -Z
Back,
+
+ /// -X
Left,
+
+ /// +X
Right,
+
+ /// +Y
Top,
+
+ /// -Y
Bottom,
}
-#[derive(Debug)]
-pub enum Corner
+/// Describes what location on a side of a cube a face is.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub enum FaceLocation
{
- TopRight,
- TopLeft,
- BottomRight,
- BottomLeft,
+ /// 🮝
+ RightUp,
+
+ /// 🮟
+ LeftDown,
}
/// Creates a cube mesh.
+///
+/// By default, the texture coordinates are arranged so that the full texture is visible
+/// on every side. This can be changed inside of the `face_cb` function.
pub fn create(
creation_spec: CreationSpec,
- vertex_builder_cb: impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder,
+ face_cb: impl FnMut(FaceVertices, Side, FaceLocation) -> FaceVertices,
) -> Mesh
{
- let mut vertices = [const { None }; VertexIndex::VARIANT_CNT];
-
- create_front(&creation_spec, &mut vertices, &vertex_builder_cb);
- create_back(&creation_spec, &mut vertices, &vertex_builder_cb);
- create_right(&creation_spec, &mut vertices, &vertex_builder_cb);
- create_left(&creation_spec, &mut vertices, &vertex_builder_cb);
- create_top(&creation_spec, &mut vertices, &vertex_builder_cb);
- create_bottom(&creation_spec, &mut vertices, &vertex_builder_cb);
-
- Mesh::new(
- vertices.map(Option::unwrap).to_vec(),
- Some(
- VERTEX_INDICES
- .into_iter()
- .flatten()
- .map(|index| index as u32)
- .collect(),
- ),
- )
-}
+ let mut data = Data::default();
-macro_rules! one {
- ($tt: tt) => {
- 1
- };
-}
+ create_side(&SidePositions::new_top(&creation_spec), &mut data);
+ create_side(&SidePositions::new_bottom(&creation_spec), &mut data);
+ create_side(&SidePositions::new_left(&creation_spec), &mut data);
+ create_side(&SidePositions::new_right(&creation_spec), &mut data);
+ create_side(&SidePositions::new_back(&creation_spec), &mut data);
+ create_side(&SidePositions::new_front(&creation_spec), &mut data);
-macro_rules! enum_with_variant_cnt {
- (
- $(#[$attr: meta])*
- enum $name: ident {
- $($variant: ident,)*
- }
- ) => {
- $(#[$attr])*
- enum $name {
- $($variant,)*
- }
-
- impl $name {
- const VARIANT_CNT: usize = 0 $(+ one!($variant))*;
- }
- };
+ data.into_mesh(face_cb)
}
-enum_with_variant_cnt! {
-#[repr(u32)]
-enum VertexIndex
+#[derive(Debug, Default)]
+struct Data
{
- FrontTopRight,
- FrontBottomRight,
- FrontBottomLeft,
- FrontTopLeft,
-
- BackTopRight,
- BackBottomRight,
- BackBottomLeft,
- BackTopLeft,
-
- RightBackTop,
- RightBackBottom,
- RightFrontTop,
- RightFrontBottom,
-
- LeftBackTop,
- LeftBackBottom,
- LeftFrontTop,
- LeftFrontBottom,
-
- TopBackRight,
- TopBackLeft,
- TopFrontRight,
- TopFrontLeft,
-
- BottomBackRight,
- BottomBackLeft,
- BottomFrontRight,
- BottomFrontLeft,
-}
+ faces: Vec<Face>,
+ vertex_data: VertexData,
}
-fn create_front(
- creation_spec: &CreationSpec,
- vertices: &mut [Option<Vertex>],
- vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder,
-)
+#[derive(Debug, Default)]
+struct VertexData
{
- let front_top_right_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: creation_spec.height / 2.0,
- z: -(creation_spec.depth / 2.0),
- };
-
- let front_bottom_right_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: -(creation_spec.height / 2.0),
- z: -(creation_spec.depth / 2.0),
- };
+ vertex_positions: Vec<Vec3<f32>>,
+ vertex_normals: Vec<Vec3<f32>>,
+}
- let front_bottom_left_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: -(creation_spec.height / 2.0),
- z: -(creation_spec.depth / 2.0),
- };
+impl Data
+{
+ fn into_mesh(
+ self,
+ mut face_cb: impl FnMut(FaceVertices, Side, FaceLocation) -> FaceVertices,
+ ) -> Mesh
+ {
+ let mut vertices = Vec::<Vertex>::with_capacity(self.faces.len() * 3);
+ let mut indices = Vec::<u32>::with_capacity(self.faces.len() * 3);
- let front_top_left_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: creation_spec.height / 2.0,
- z: -(creation_spec.depth / 2.0),
- };
+ let mut face_location = FaceLocation::RightUp;
- let front_normal = calc_triangle_surface_normal(
- &front_top_right_pos,
- &front_bottom_right_pos,
- &front_top_left_pos,
- );
-
- vertices[VertexIndex::FrontTopRight as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(front_top_right_pos)
- .normal(front_normal),
- Side::Front,
- Corner::TopRight,
- )
- .build(),
- );
-
- vertices[VertexIndex::FrontBottomRight as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(front_bottom_right_pos)
- .normal(front_normal),
- Side::Front,
- Corner::BottomRight,
- )
- .build(),
- );
-
- vertices[VertexIndex::FrontBottomLeft as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(front_bottom_left_pos)
- .normal(front_normal),
- Side::Front,
- Corner::BottomLeft,
- )
- .build(),
- );
-
- vertices[VertexIndex::FrontTopLeft as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(front_top_left_pos)
- .normal(front_normal),
- Side::Front,
- Corner::TopLeft,
- )
- .build(),
- );
-}
+ let Self { faces, vertex_data } = self;
-fn create_back(
- creation_spec: &CreationSpec,
- vertices: &mut [Option<Vertex>],
- vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder,
-)
-{
- let back_top_right_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: creation_spec.height / 2.0,
- z: creation_spec.depth / 2.0,
- };
+ for face in faces {
+ let side = face.side;
- let back_bottom_right_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: -(creation_spec.height / 2.0),
- z: creation_spec.depth / 2.0,
- };
+ let face_vertices = face_cb(
+ FaceVertices::new(face, &vertex_data)
+ .with_full_per_side_tex_coords(face_location),
+ side,
+ face_location,
+ );
- let back_bottom_left_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: -(creation_spec.height / 2.0),
- z: creation_spec.depth / 2.0,
- };
+ for vertex in face_vertices.vertices {
+ if let Some((prev_vertex_index, _)) = vertices
+ .iter()
+ .enumerate()
+ .find(|(_, prev_vertex)| *prev_vertex == &vertex)
+ {
+ indices
+ .push(u32::try_from(prev_vertex_index).expect(
+ "Vertex index does not fit into 32-bit unsigned int",
+ ));
- let back_top_left_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: creation_spec.height / 2.0,
- z: creation_spec.depth / 2.0,
- };
+ continue;
+ }
- let back_normal = -calc_triangle_surface_normal(
- &back_top_right_pos,
- &back_bottom_right_pos,
- &back_top_left_pos,
- );
-
- vertices[VertexIndex::BackTopRight as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(back_top_right_pos)
- .normal(back_normal),
- Side::Back,
- Corner::TopRight,
- )
- .build(),
- );
-
- vertices[VertexIndex::BackBottomRight as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(back_bottom_right_pos)
- .normal(back_normal),
- Side::Back,
- Corner::BottomRight,
- )
- .build(),
- );
-
- vertices[VertexIndex::BackBottomLeft as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(back_bottom_left_pos)
- .normal(back_normal),
- Side::Back,
- Corner::BottomLeft,
- )
- .build(),
- );
-
- vertices[VertexIndex::BackTopLeft as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(back_top_left_pos)
- .normal(back_normal),
- Side::Back,
- Corner::TopLeft,
- )
- .build(),
- );
-}
+ vertices.push(vertex);
-fn create_right(
- creation_spec: &CreationSpec,
- vertices: &mut [Option<Vertex>],
- vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder,
-)
-{
- let right_back_top_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: creation_spec.height / 2.0,
- z: creation_spec.depth / 2.0,
- };
+ let vertex_index = u32::try_from(vertices.len() - 1)
+ .expect("Vertex index does not fit into 32-bit unsigned int");
- let right_back_bottom_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: -(creation_spec.height / 2.0),
- z: creation_spec.depth / 2.0,
- };
+ indices.push(vertex_index);
+ }
- let right_front_top_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: creation_spec.height / 2.0,
- z: -(creation_spec.depth / 2.0),
- };
+ match face_location {
+ FaceLocation::RightUp => face_location = FaceLocation::LeftDown,
+ FaceLocation::LeftDown => face_location = FaceLocation::RightUp,
+ }
+ }
- let right_front_bottom_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: -(creation_spec.height / 2.0),
- z: -(creation_spec.depth / 2.0),
- };
+ Mesh::new(vertices, Some(indices))
+ }
+}
- let right_normal = calc_triangle_surface_normal(
- &right_back_top_pos,
- &right_back_bottom_pos,
- &right_front_top_pos,
- );
-
- vertices[VertexIndex::RightBackTop as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(right_back_top_pos)
- .normal(right_normal),
- Side::Right,
- Corner::TopLeft,
- )
- .build(),
- );
-
- vertices[VertexIndex::RightBackBottom as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(right_back_bottom_pos)
- .normal(right_normal),
- Side::Right,
- Corner::BottomLeft,
- )
- .build(),
- );
-
- vertices[VertexIndex::RightFrontTop as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(right_front_top_pos)
- .normal(right_normal),
- Side::Right,
- Corner::TopRight,
- )
- .build(),
- );
-
- vertices[VertexIndex::RightFrontBottom as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(right_front_bottom_pos)
- .normal(right_normal),
- Side::Right,
- Corner::BottomRight,
- )
- .build(),
- );
+/// The vertices of a single face of a cube.
+#[derive(Debug, Default, Clone)]
+pub struct FaceVertices
+{
+ /// The three vertices of a face in counter-clockwise order.
+ ///
+ /// Order when [`FaceLocation::RightUp`]:
+ /// ```text
+ /// ₂ ₁
+ /// 🮝
+ /// ³
+ /// ```
+ ///
+ /// Order when [`FaceLocation::LeftDown`]:
+ /// ```text
+ /// ₁
+ /// 🮟
+ /// ² ³
+ /// ```
+ pub vertices: [Vertex; 3],
}
-fn create_left(
- creation_spec: &CreationSpec,
- vertices: &mut [Option<Vertex>],
- vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder,
-)
+impl FaceVertices
{
- let left_back_top_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: creation_spec.height / 2.0,
- z: creation_spec.depth / 2.0,
- };
+ fn new(face: Face, vertex_data: &VertexData) -> Self
+ {
+ Self {
+ vertices: face.vertices.map(|face_vertex| {
+ let vertex_pos = vertex_data
+ .vertex_positions
+ .get(face_vertex.pos_index as usize)
+ .expect("Vertex position index is out of bounds")
+ .clone();
+
+ let vertex_normal = vertex_data
+ .vertex_normals
+ .get(face_vertex.normal_index as usize)
+ .expect("Vertex normal index is out of bounds")
+ .clone();
+
+ Vertex::builder()
+ .pos(vertex_pos)
+ .normal(vertex_normal)
+ .build()
+ }),
+ }
+ }
- let left_back_bottom_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: -(creation_spec.height / 2.0),
- z: creation_spec.depth / 2.0,
- };
+ fn with_full_per_side_tex_coords(mut self, face_location: FaceLocation) -> Self
+ {
+ match face_location {
+ FaceLocation::RightUp => {
+ self.vertices[0].texture_coords = Vec2 { x: 1.0, y: 1.0 };
+ self.vertices[1].texture_coords = Vec2 { x: 0.0, y: 1.0 };
+ self.vertices[2].texture_coords = Vec2 { x: 1.0, y: 0.0 };
+ }
+ FaceLocation::LeftDown => {
+ self.vertices[0].texture_coords = Vec2 { x: 0.0, y: 1.0 };
+ self.vertices[1].texture_coords = Vec2 { x: 0.0, y: 0.0 };
+ self.vertices[2].texture_coords = Vec2 { x: 1.0, y: 0.0 };
+ }
+ };
+
+ self
+ }
+}
- let left_front_top_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: creation_spec.height / 2.0,
- z: -(creation_spec.depth / 2.0),
- };
+#[derive(Debug)]
+struct Face
+{
+ vertices: [FaceVertex; 3],
+ side: Side,
+}
- let left_front_bottom_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: -(creation_spec.height / 2.0),
- z: -(creation_spec.depth / 2.0),
- };
+#[derive(Debug, PartialEq, Eq, Hash, Clone)]
+struct FaceVertex
+{
+ pos_index: u32,
+ normal_index: u32,
+}
- let left_normal = -calc_triangle_surface_normal(
- &left_back_top_pos,
- &left_back_bottom_pos,
- &left_front_top_pos,
- );
-
- vertices[VertexIndex::LeftBackTop as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(left_back_top_pos)
- .normal(left_normal),
- Side::Left,
- Corner::TopRight,
- )
- .build(),
- );
-
- vertices[VertexIndex::LeftBackBottom as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(left_back_bottom_pos)
- .normal(left_normal),
- Side::Left,
- Corner::BottomRight,
- )
- .build(),
- );
-
- vertices[VertexIndex::LeftFrontTop as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(left_front_top_pos)
- .normal(left_normal),
- Side::Left,
- Corner::TopLeft,
- )
- .build(),
- );
-
- vertices[VertexIndex::LeftFrontBottom as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(left_front_bottom_pos)
- .normal(left_normal),
- Side::Left,
- Corner::BottomLeft,
- )
- .build(),
- );
+#[derive(Debug)]
+struct SidePositions
+{
+ up_left: Vec3<f32>,
+ up_right: Vec3<f32>,
+ down_left: Vec3<f32>,
+ down_right: Vec3<f32>,
+ normal_calc_order: NormalCalcOrder,
+ side: Side,
}
-fn create_top(
- creation_spec: &CreationSpec,
- vertices: &mut [Option<Vertex>],
- vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder,
-)
+impl SidePositions
{
- let top_back_right_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: creation_spec.height / 2.0,
- z: creation_spec.depth / 2.0,
- };
+ fn new_top(creation_spec: &CreationSpec) -> Self
+ {
+ let up_left = Vec3 {
+ x: -(creation_spec.width / 2.0),
+ y: creation_spec.height / 2.0,
+ z: creation_spec.depth / 2.0,
+ };
+
+ let down_right = Vec3 {
+ x: creation_spec.width / 2.0,
+ y: creation_spec.height / 2.0,
+ z: -(creation_spec.depth / 2.0),
+ };
+
+ Self {
+ up_left,
+ up_right: Vec3 { x: down_right.x, ..up_left.clone() },
+ down_left: Vec3 { x: up_left.x, ..down_right.clone() },
+ down_right,
+ normal_calc_order: NormalCalcOrder::Clockwise,
+ side: Side::Top,
+ }
+ }
- let top_back_left_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: creation_spec.height / 2.0,
- z: creation_spec.depth / 2.0,
- };
+ fn new_bottom(creation_spec: &CreationSpec) -> Self
+ {
+ let up_left = Vec3 {
+ x: -(creation_spec.width / 2.0),
+ y: -creation_spec.height / 2.0,
+ z: creation_spec.depth / 2.0,
+ };
+
+ let down_right = Vec3 {
+ x: creation_spec.width / 2.0,
+ y: -creation_spec.height / 2.0,
+ z: -(creation_spec.depth / 2.0),
+ };
+
+ Self {
+ up_left,
+ up_right: Vec3 { x: down_right.x, ..up_left.clone() },
+ down_left: Vec3 { x: up_left.x, ..down_right.clone() },
+ down_right,
+ normal_calc_order: NormalCalcOrder::CounterClockwise,
+ side: Side::Bottom,
+ }
+ }
- let top_front_left_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: creation_spec.height / 2.0,
- z: -(creation_spec.depth / 2.0),
- };
+ fn new_left(creation_spec: &CreationSpec) -> Self
+ {
+ let up_left = Vec3 {
+ x: -(creation_spec.width / 2.0),
+ y: creation_spec.height / 2.0,
+ z: -(creation_spec.depth / 2.0),
+ };
+
+ let down_right = Vec3 {
+ x: -(creation_spec.width / 2.0),
+ y: -(creation_spec.height / 2.0),
+ z: creation_spec.depth / 2.0,
+ };
+
+ Self {
+ up_left,
+ up_right: Vec3 { z: down_right.z, ..up_left.clone() },
+ down_left: Vec3 { z: up_left.z, ..down_right.clone() },
+ down_right,
+ normal_calc_order: NormalCalcOrder::CounterClockwise,
+ side: Side::Left,
+ }
+ }
- let top_front_right_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: creation_spec.height / 2.0,
- z: -(creation_spec.depth / 2.0),
- };
+ fn new_right(creation_spec: &CreationSpec) -> Self
+ {
+ let up_left = Vec3 {
+ x: (creation_spec.width / 2.0),
+ y: creation_spec.height / 2.0,
+ z: -(creation_spec.depth / 2.0),
+ };
+
+ let down_right = Vec3 {
+ x: (creation_spec.width / 2.0),
+ y: -(creation_spec.height / 2.0),
+ z: creation_spec.depth / 2.0,
+ };
+
+ Self {
+ up_left,
+ up_right: Vec3 { z: down_right.z, ..up_left.clone() },
+ down_left: Vec3 { z: up_left.z, ..down_right.clone() },
+ down_right,
+ normal_calc_order: NormalCalcOrder::Clockwise,
+ side: Side::Right,
+ }
+ }
+
+ fn new_back(creation_spec: &CreationSpec) -> Self
+ {
+ let up_left = Vec3 {
+ x: -(creation_spec.width / 2.0),
+ y: creation_spec.height / 2.0,
+ z: -creation_spec.depth / 2.0,
+ };
+
+ let down_right = Vec3 {
+ x: creation_spec.width / 2.0,
+ y: -(creation_spec.height / 2.0),
+ z: -creation_spec.depth / 2.0,
+ };
+
+ Self {
+ up_left,
+ up_right: Vec3 { x: down_right.x, ..up_left.clone() },
+ down_left: Vec3 { x: up_left.x, ..down_right.clone() },
+ down_right,
+ normal_calc_order: NormalCalcOrder::Clockwise,
+ side: Side::Back,
+ }
+ }
- let top_normal = -calc_triangle_surface_normal(
- &top_back_right_pos,
- &top_back_left_pos,
- &top_front_right_pos,
- );
-
- vertices[VertexIndex::TopBackRight as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(top_back_right_pos)
- .normal(top_normal),
- Side::Top,
- Corner::TopRight,
- )
- .build(),
- );
-
- vertices[VertexIndex::TopBackLeft as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(top_back_left_pos)
- .normal(top_normal),
- Side::Top,
- Corner::TopLeft,
- )
- .build(),
- );
-
- vertices[VertexIndex::TopFrontLeft as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(top_front_left_pos)
- .normal(top_normal),
- Side::Top,
- Corner::BottomLeft,
- )
- .build(),
- );
-
- vertices[VertexIndex::TopFrontRight as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(top_front_right_pos)
- .normal(top_normal),
- Side::Top,
- Corner::BottomRight,
- )
- .build(),
- );
+ fn new_front(creation_spec: &CreationSpec) -> Self
+ {
+ let up_left = Vec3 {
+ x: -(creation_spec.width / 2.0),
+ y: creation_spec.height / 2.0,
+ z: creation_spec.depth / 2.0,
+ };
+
+ let down_right = Vec3 {
+ x: creation_spec.width / 2.0,
+ y: -(creation_spec.height / 2.0),
+ z: creation_spec.depth / 2.0,
+ };
+
+ Self {
+ up_left,
+ up_right: Vec3 { x: down_right.x, ..up_left.clone() },
+ down_left: Vec3 { x: up_left.x, ..down_right.clone() },
+ down_right,
+ normal_calc_order: NormalCalcOrder::CounterClockwise,
+ side: Side::Front,
+ }
+ }
}
-fn create_bottom(
- creation_spec: &CreationSpec,
- vertices: &mut [Option<Vertex>],
- vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder,
-)
+#[derive(Debug)]
+enum NormalCalcOrder
{
- let bottom_back_right_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: -(creation_spec.height / 2.0),
- z: (creation_spec.depth / 2.0),
- };
+ Clockwise,
+ CounterClockwise,
+}
- let bottom_back_left_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: -(creation_spec.height / 2.0),
- z: creation_spec.depth / 2.0,
+fn create_side(side_positions: &SidePositions, data: &mut Data)
+{
+ let normal = match side_positions.normal_calc_order {
+ NormalCalcOrder::Clockwise => calc_triangle_surface_normal(
+ &side_positions.up_left,
+ &side_positions.up_right,
+ &side_positions.down_left,
+ ),
+ NormalCalcOrder::CounterClockwise => calc_triangle_surface_normal(
+ &side_positions.up_left,
+ &side_positions.down_left,
+ &side_positions.up_right,
+ ),
};
- let bottom_front_right_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: -(creation_spec.height / 2.0),
- z: -(creation_spec.depth / 2.0),
- };
+ data.vertex_data.vertex_normals.push(normal);
- let bottom_front_left_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: -(creation_spec.height / 2.0),
- z: -(creation_spec.depth / 2.0),
- };
+ let top_normal_index = data.vertex_data.vertex_normals.len() - 1;
- let bottom_normal = calc_triangle_surface_normal(
- &bottom_back_right_pos,
- &bottom_back_left_pos,
- &bottom_front_right_pos,
- );
-
- vertices[VertexIndex::BottomBackRight as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(bottom_back_right_pos)
- .normal(bottom_normal),
- Side::Bottom,
- Corner::BottomRight,
- )
- .build(),
- );
-
- vertices[VertexIndex::BottomBackLeft as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(bottom_back_left_pos)
- .normal(bottom_normal),
- Side::Bottom,
- Corner::BottomLeft,
- )
- .build(),
- );
-
- vertices[VertexIndex::BottomFrontRight as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(bottom_front_right_pos)
- .normal(bottom_normal),
- Side::Bottom,
- Corner::TopRight,
- )
- .build(),
- );
-
- vertices[VertexIndex::BottomFrontLeft as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(bottom_front_left_pos)
- .normal(bottom_normal),
- Side::Bottom,
- Corner::TopLeft,
- )
- .build(),
- );
-}
+ data.vertex_data
+ .vertex_positions
+ .push(side_positions.up_right);
-const VERTEX_INDICES_FRONT: [VertexIndex; 6] = [
- // 🮝
- VertexIndex::FrontTopRight,
- VertexIndex::FrontBottomRight,
- VertexIndex::FrontTopLeft,
- //
- // 🮟
- VertexIndex::FrontBottomRight,
- VertexIndex::FrontBottomLeft,
- VertexIndex::FrontTopLeft,
-];
+ let up_right_pos_index = data.vertex_data.vertex_positions.len() - 1;
-const VERTEX_INDICES_BACK: [VertexIndex; 6] = [
- // 🮝
- VertexIndex::BackTopRight,
- VertexIndex::BackBottomRight,
- VertexIndex::BackTopLeft,
- //
- // 🮟
- VertexIndex::BackBottomRight,
- VertexIndex::BackBottomLeft,
- VertexIndex::BackTopLeft,
-];
+ data.vertex_data
+ .vertex_positions
+ .push(side_positions.up_left);
-const VERTEX_INDICES_RIGHT: [VertexIndex; 6] = [
- // 🮝
- VertexIndex::RightBackTop,
- VertexIndex::RightBackBottom,
- VertexIndex::RightFrontTop,
- //
- // 🮟
- VertexIndex::RightBackBottom,
- VertexIndex::RightFrontBottom,
- VertexIndex::RightFrontTop,
-];
+ let up_left_pos_index = data.vertex_data.vertex_positions.len() - 1;
-const VERTEX_INDICES_LEFT: [VertexIndex; 6] = [
- // 🮝
- VertexIndex::LeftBackTop,
- VertexIndex::LeftBackBottom,
- VertexIndex::LeftFrontTop,
- //
- // 🮟
- VertexIndex::LeftBackBottom,
- VertexIndex::LeftFrontBottom,
- VertexIndex::LeftFrontTop,
-];
+ data.vertex_data
+ .vertex_positions
+ .push(side_positions.down_left);
-const VERTEX_INDICES_TOP: [VertexIndex; 6] = [
- // 🮝
- VertexIndex::TopBackRight,
- VertexIndex::TopBackLeft,
- VertexIndex::TopFrontRight,
- //
- // 🮟
- VertexIndex::TopBackLeft,
- VertexIndex::TopFrontLeft,
- VertexIndex::TopFrontRight,
-];
+ let down_left_pos_index = data.vertex_data.vertex_positions.len() - 1;
+
+ data.vertex_data
+ .vertex_positions
+ .push(side_positions.down_right);
+
+ let down_right_pos_index = data.vertex_data.vertex_positions.len() - 1;
-const VERTEX_INDICES_BOTTOM: [VertexIndex; 6] = [
// 🮝
- VertexIndex::BottomBackRight,
- VertexIndex::BottomBackLeft,
- VertexIndex::BottomFrontRight,
- //
+ data.faces.push(Face {
+ vertices: [
+ FaceVertex {
+ pos_index: up_right_pos_index as u32,
+ normal_index: top_normal_index as u32,
+ },
+ FaceVertex {
+ pos_index: up_left_pos_index as u32,
+ normal_index: top_normal_index as u32,
+ },
+ FaceVertex {
+ pos_index: down_right_pos_index as u32,
+ normal_index: top_normal_index as u32,
+ },
+ ],
+ side: side_positions.side,
+ });
+
// 🮟
- VertexIndex::BottomBackLeft,
- VertexIndex::BottomFrontLeft,
- VertexIndex::BottomFrontRight,
-];
-
-const VERTEX_INDICES: [[VertexIndex; 6]; 6] = [
- VERTEX_INDICES_FRONT,
- VERTEX_INDICES_BACK,
- VERTEX_INDICES_RIGHT,
- VERTEX_INDICES_LEFT,
- VERTEX_INDICES_TOP,
- VERTEX_INDICES_BOTTOM,
-];
+ data.faces.push(Face {
+ vertices: [
+ FaceVertex {
+ pos_index: up_left_pos_index as u32,
+ normal_index: top_normal_index as u32,
+ },
+ FaceVertex {
+ pos_index: down_left_pos_index as u32,
+ normal_index: top_normal_index as u32,
+ },
+ FaceVertex {
+ pos_index: down_right_pos_index as u32,
+ normal_index: top_normal_index as u32,
+ },
+ ],
+ side: side_positions.side,
+ });
+}
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
deleted file mode 100644
index 2be7f12..0000000
--- a/engine/src/opengl/buffer.rs
+++ /dev/null
@@ -1,92 +0,0 @@
-use std::marker::PhantomData;
-use std::mem::size_of_val;
-
-#[derive(Debug)]
-pub struct Buffer<Item>
-{
- buf: gl::types::GLuint,
- _pd: PhantomData<Item>,
-}
-
-impl<Item> Buffer<Item>
-{
- pub fn new() -> Self
- {
- let mut buffer = gl::types::GLuint::default();
-
- unsafe {
- gl::CreateBuffers(1, &mut buffer);
- };
-
- Self { buf: buffer, _pd: PhantomData }
- }
-
- /// Stores items in the currently bound buffer.
- pub fn store(&mut self, items: &[Item], usage: Usage)
- {
- unsafe {
- #[allow(clippy::cast_possible_wrap)]
- gl::NamedBufferData(
- self.buf,
- size_of_val(items) as gl::types::GLsizeiptr,
- items.as_ptr().cast(),
- usage.into_gl(),
- );
- }
- }
-
- pub fn object(&self) -> gl::types::GLuint
- {
- self.buf
- }
-
- /// Does a weak clone of this buffer. The buffer itself is NOT copied in any way this
- /// function only copies the internal buffer ID.
- ///
- /// # Safety
- /// The returned `Buffer` must not be dropped if another `Buffer` referencing the
- /// same buffer ID is used later or if a [`VertexArray`] is used later.
- ///
- /// [`VertexArray`]: crate::opengl::vertex_array::VertexArray
- pub unsafe fn clone_weak(&self) -> Self
- {
- Self { buf: self.buf, _pd: PhantomData }
- }
-}
-
-impl<Item> Drop for Buffer<Item>
-{
- fn drop(&mut self)
- {
- unsafe {
- gl::DeleteBuffers(1, &self.buf);
- }
- }
-}
-
-/// Buffer usage.
-#[derive(Debug)]
-#[allow(dead_code)]
-pub enum Usage
-{
- /// The buffer data is set only once and used by the GPU at most a few times.
- Stream,
-
- /// The buffer data is set only once and used many times.
- Static,
-
- /// The buffer data is changed a lot and used many times.
- Dynamic,
-}
-
-impl Usage
-{
- fn into_gl(self) -> gl::types::GLenum
- {
- match self {
- Self::Stream => gl::STREAM_DRAW,
- Self::Static => gl::STATIC_DRAW,
- Self::Dynamic => gl::DYNAMIC_DRAW,
- }
- }
-}
diff --git a/engine/src/opengl/debug.rs b/engine/src/opengl/debug.rs
deleted file mode 100644
index 203590a..0000000
--- a/engine/src/opengl/debug.rs
+++ /dev/null
@@ -1,145 +0,0 @@
-use std::ffi::c_void;
-use std::io::{stderr, Write};
-use std::panic::catch_unwind;
-use std::ptr::null_mut;
-use std::sync::Mutex;
-
-use crate::opengl::util::gl_enum;
-
-pub type MessageCallback = fn(
- source: MessageSource,
- ty: MessageType,
- id: u32,
- severity: MessageSeverity,
- message: &str,
-);
-
-pub fn enable_debug_output()
-{
- unsafe {
- gl::Enable(gl::DEBUG_OUTPUT);
- gl::Enable(gl::DEBUG_OUTPUT_SYNCHRONOUS);
- }
-}
-
-pub fn set_debug_message_callback(cb: MessageCallback)
-{
- *DEBUG_MESSAGE_CB.lock().unwrap() = Some(cb);
-
- unsafe {
- gl::DebugMessageCallback(Some(debug_message_cb), null_mut());
- }
-}
-
-pub fn set_debug_message_control(
- source: Option<MessageSource>,
- ty: Option<MessageType>,
- severity: Option<MessageSeverity>,
- ids: &[u32],
- ids_action: MessageIdsAction,
-)
-{
- // Ids shouldn't realistically be large enough to cause a panic here
- let ids_len: i32 = ids.len().try_into().unwrap();
-
- unsafe {
- gl::DebugMessageControl(
- source.map_or(gl::DONT_CARE, |source| source as u32),
- ty.map_or(gl::DONT_CARE, |ty| ty as u32),
- severity.map_or(gl::DONT_CARE, |severity| severity as u32),
- ids_len,
- ids.as_ptr(),
- ids_action as u8,
- );
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-#[allow(dead_code)]
-pub enum MessageIdsAction
-{
- Enable = 1,
- Disable = 0,
-}
-
-gl_enum! {
-pub enum MessageSource
-{
- Api = gl::DEBUG_SOURCE_API,
- WindowSystem = gl::DEBUG_SOURCE_WINDOW_SYSTEM,
- ShaderCompiler = gl::DEBUG_SOURCE_SHADER_COMPILER,
- ThirdParty = gl::DEBUG_SOURCE_THIRD_PARTY,
- Application = gl::DEBUG_SOURCE_APPLICATION,
- Other = gl::DEBUG_SOURCE_OTHER,
-}
-}
-
-gl_enum! {
-pub enum MessageType
-{
- DeprecatedBehavior = gl::DEBUG_TYPE_DEPRECATED_BEHAVIOR,
- Error = gl::DEBUG_TYPE_ERROR,
- Marker = gl::DEBUG_TYPE_MARKER,
- Other = gl::DEBUG_TYPE_OTHER,
- Performance = gl::DEBUG_TYPE_PERFORMANCE,
- PopGroup = gl::DEBUG_TYPE_POP_GROUP,
- PushGroup = gl::DEBUG_TYPE_PUSH_GROUP,
- Portability = gl::DEBUG_TYPE_PORTABILITY,
- UndefinedBehavior = gl::DEBUG_TYPE_UNDEFINED_BEHAVIOR,
-}
-}
-
-gl_enum! {
-pub enum MessageSeverity
-{
- High = gl::DEBUG_SEVERITY_HIGH,
- Medium = gl::DEBUG_SEVERITY_MEDIUM,
- Low = gl::DEBUG_SEVERITY_LOW,
- Notification = gl::DEBUG_SEVERITY_NOTIFICATION,
-}
-}
-
-static DEBUG_MESSAGE_CB: Mutex<Option<MessageCallback>> = Mutex::new(None);
-
-extern "system" fn debug_message_cb(
- source: gl::types::GLenum,
- ty: gl::types::GLenum,
- id: gl::types::GLuint,
- severity: gl::types::GLenum,
- message_length: gl::types::GLsizei,
- message: *const gl::types::GLchar,
- _user_param: *mut c_void,
-)
-{
- // Unwinds are catched because unwinding from Rust code into foreign code is UB.
- let res = catch_unwind(|| {
- let cb_lock = DEBUG_MESSAGE_CB.lock().unwrap();
-
- if let Some(cb) = *cb_lock {
- let msg_source = MessageSource::from_gl(source).unwrap();
- let msg_type = MessageType::from_gl(ty).unwrap();
- let msg_severity = MessageSeverity::from_gl(severity).unwrap();
-
- let msg_length = usize::try_from(message_length).unwrap();
-
- // SAFETY: The received message should be a valid ASCII string
- let message = unsafe {
- std::str::from_utf8_unchecked(std::slice::from_raw_parts(
- message.cast(),
- msg_length,
- ))
- };
-
- cb(msg_source, msg_type, id, msg_severity, message);
- }
- });
-
- if res.is_err() {
- // eprintln is not used since it can panic and unwinds are unwanted because
- // unwinding from Rust code into foreign code is UB.
- stderr()
- .write_all(b"ERROR: Panic in debug message callback")
- .ok();
- println!();
- }
-}
diff --git a/engine/src/opengl/mod.rs b/engine/src/opengl/mod.rs
index a4d3959..2208ac6 100644
--- a/engine/src/opengl/mod.rs
+++ b/engine/src/opengl/mod.rs
@@ -1,108 +1 @@
-use bitflags::bitflags;
-
-use crate::data_types::dimens::Dimens;
-use crate::vector::Vec2;
-
-pub mod buffer;
pub mod glsl;
-pub mod shader;
-pub mod texture;
-pub mod vertex_array;
-
-mod util;
-
-#[cfg(feature = "debug")]
-pub mod debug;
-
-pub fn set_viewport(position: Vec2<u32>, size: Dimens<u32>)
-{
- unsafe {
- #[allow(clippy::cast_possible_wrap)]
- gl::Viewport(
- position.x as i32,
- position.y as i32,
- size.width as i32,
- size.height as i32,
- );
- }
-}
-
-pub fn clear_buffers(mask: BufferClearMask)
-{
- unsafe {
- gl::Clear(mask.bits());
- }
-}
-
-pub fn set_polygon_mode(face: impl Into<PolygonModeFace>, mode: impl Into<PolygonMode>)
-{
- unsafe {
- gl::PolygonMode(face.into() as u32, mode.into() as u32);
- }
-}
-
-pub fn enable(capacity: Capability)
-{
- unsafe {
- gl::Enable(capacity as u32);
- }
-}
-
-bitflags! {
- #[derive(Debug, Clone, Copy)]
- pub struct BufferClearMask: u32 {
- const COLOR = gl::COLOR_BUFFER_BIT;
- const DEPTH = gl::DEPTH_BUFFER_BIT;
- const STENCIL = gl::STENCIL_BUFFER_BIT;
- }
-}
-
-#[derive(Debug)]
-#[repr(u32)]
-pub enum Capability
-{
- DepthTest = gl::DEPTH_TEST,
- MultiSample = gl::MULTISAMPLE,
-}
-
-#[derive(Debug)]
-#[repr(u32)]
-pub enum PolygonMode
-{
- Point = gl::POINT,
- Line = gl::LINE,
- Fill = gl::FILL,
-}
-
-impl From<crate::draw_flags::PolygonMode> for PolygonMode
-{
- fn from(mode: crate::draw_flags::PolygonMode) -> Self
- {
- match mode {
- crate::draw_flags::PolygonMode::Point => Self::Point,
- crate::draw_flags::PolygonMode::Fill => Self::Fill,
- crate::draw_flags::PolygonMode::Line => Self::Line,
- }
- }
-}
-
-#[derive(Debug)]
-#[repr(u32)]
-pub enum PolygonModeFace
-{
- Front = gl::FRONT,
- Back = gl::BACK,
- FrontAndBack = gl::FRONT_AND_BACK,
-}
-
-impl From<crate::draw_flags::PolygonModeFace> for PolygonModeFace
-{
- fn from(face: crate::draw_flags::PolygonModeFace) -> Self
- {
- match face {
- crate::draw_flags::PolygonModeFace::Front => Self::Front,
- crate::draw_flags::PolygonModeFace::Back => Self::Back,
- crate::draw_flags::PolygonModeFace::FrontAndBack => Self::FrontAndBack,
- }
- }
-}
diff --git a/engine/src/opengl/shader.rs b/engine/src/opengl/shader.rs
deleted file mode 100644
index 36dc1a4..0000000
--- a/engine/src/opengl/shader.rs
+++ /dev/null
@@ -1,247 +0,0 @@
-use std::ffi::CStr;
-use std::ptr::null_mut;
-
-use crate::matrix::Matrix;
-use crate::vector::Vec3;
-
-#[derive(Debug)]
-pub struct Shader
-{
- shader: gl::types::GLuint,
-}
-
-impl Shader
-{
- pub fn new(kind: Kind) -> Self
- {
- let shader = unsafe { gl::CreateShader(kind.into_gl()) };
-
- Self { shader }
- }
-
- pub fn set_source(&mut self, source: &str) -> Result<(), Error>
- {
- if !source.is_ascii() {
- return Err(Error::SourceNotAscii);
- }
-
- unsafe {
- #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
- gl::ShaderSource(
- self.shader,
- 1,
- &source.as_ptr().cast(),
- &(source.len() as gl::types::GLint),
- );
- }
-
- Ok(())
- }
-
- pub fn compile(&mut self) -> Result<(), Error>
- {
- unsafe {
- gl::CompileShader(self.shader);
- }
-
- let mut compile_success = gl::types::GLint::default();
-
- unsafe {
- gl::GetShaderiv(self.shader, gl::COMPILE_STATUS, &mut compile_success);
- }
-
- if compile_success == 0 {
- let info_log = self.get_info_log();
-
- return Err(Error::CompileFailed(info_log));
- }
-
- Ok(())
- }
-
- fn get_info_log(&self) -> String
- {
- let mut buf = vec![gl::types::GLchar::default(); 512];
-
- unsafe {
- #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
- gl::GetShaderInfoLog(
- self.shader,
- 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()) }
- }
-}
-
-impl Drop for Shader
-{
- fn drop(&mut self)
- {
- unsafe {
- gl::DeleteShader(self.shader);
- }
- }
-}
-
-/// Shader kind.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub enum Kind
-{
- Vertex,
- Fragment,
-}
-
-impl Kind
-{
- fn into_gl(self) -> gl::types::GLenum
- {
- match self {
- Self::Vertex => gl::VERTEX_SHADER,
- Self::Fragment => gl::FRAGMENT_SHADER,
- }
- }
-}
-
-/// Shader program
-#[derive(Debug, PartialEq, Eq, Hash)]
-pub struct Program
-{
- program: gl::types::GLuint,
-}
-
-impl Program
-{
- pub fn new() -> Self
- {
- let program = unsafe { gl::CreateProgram() };
-
- Self { program }
- }
-
- pub fn attach(&mut self, shader: &Shader)
- {
- unsafe {
- gl::AttachShader(self.program, shader.shader);
- }
- }
-
- pub fn link(&mut self) -> Result<(), Error>
- {
- unsafe {
- gl::LinkProgram(self.program);
- }
-
- let mut link_success = gl::types::GLint::default();
-
- unsafe {
- gl::GetProgramiv(self.program, gl::LINK_STATUS, &mut link_success);
- }
-
- if link_success == 0 {
- let info_log = self.get_info_log();
-
- return Err(Error::CompileFailed(info_log));
- }
-
- Ok(())
- }
-
- pub fn activate(&self)
- {
- unsafe {
- gl::UseProgram(self.program);
- }
- }
-
- pub fn set_uniform_matrix_4fv(&mut self, name: &CStr, matrix: &Matrix<f32, 4, 4>)
- {
- let uniform_location =
- unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) };
-
- unsafe {
- gl::ProgramUniformMatrix4fv(
- self.program,
- uniform_location,
- 1,
- gl::FALSE,
- matrix.as_ptr(),
- );
- }
- }
-
- pub fn set_uniform_vec_3fv(&mut self, name: &CStr, vec: &Vec3<f32>)
- {
- let uniform_location =
- unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) };
-
- unsafe {
- gl::ProgramUniform3fv(self.program, uniform_location, 1, vec.as_ptr());
- }
- }
-
- pub fn set_uniform_1fv(&mut self, name: &CStr, num: f32)
- {
- let uniform_location =
- unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) };
-
- unsafe {
- gl::ProgramUniform1fv(self.program, uniform_location, 1, &num);
- }
- }
-
- pub fn set_uniform_1i(&mut self, name: &CStr, num: i32)
- {
- let uniform_location =
- unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) };
-
- unsafe {
- gl::ProgramUniform1i(self.program, uniform_location, num);
- }
- }
-
- fn get_info_log(&self) -> String
- {
- 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(),
- );
- }
-
- 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
-{
- fn drop(&mut self)
- {
- unsafe {
- gl::DeleteProgram(self.program);
- }
- }
-}
-
-/// Shader error.
-#[derive(Debug, thiserror::Error)]
-pub enum Error
-{
- #[error("All characters in source are not within the ASCII range")]
- SourceNotAscii,
-
- #[error("Failed to compile: {0}")]
- CompileFailed(String),
-}
diff --git a/engine/src/opengl/texture.rs b/engine/src/opengl/texture.rs
deleted file mode 100644
index 074ade7..0000000
--- a/engine/src/opengl/texture.rs
+++ /dev/null
@@ -1,240 +0,0 @@
-use crate::data_types::dimens::Dimens;
-use crate::texture::{Id, Properties};
-
-#[derive(Debug)]
-pub struct Texture
-{
- texture: gl::types::GLuint,
-}
-
-impl Texture
-{
- pub fn new() -> Self
- {
- let mut texture = gl::types::GLuint::default();
-
- unsafe {
- gl::CreateTextures(gl::TEXTURE_2D, 1, &mut texture);
- };
-
- Self { texture }
- }
-
- pub fn bind(&self)
- {
- unsafe {
- gl::BindTexture(gl::TEXTURE_2D, self.texture);
- }
- }
-
- pub fn generate(
- &mut self,
- dimens: Dimens<u32>,
- data: &[u8],
- pixel_data_format: PixelDataFormat,
- )
- {
- self.alloc_image(pixel_data_format, dimens, data);
-
- unsafe {
- gl::GenerateTextureMipmap(self.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();
-
- #[allow(clippy::cast_possible_wrap)]
- unsafe {
- gl::TextureParameteri(self.texture, gl::TEXTURE_WRAP_S, wrapping_gl as i32);
- gl::TextureParameteri(self.texture, gl::TEXTURE_WRAP_T, wrapping_gl as i32);
- }
- }
-
- pub fn set_magnifying_filter(&mut self, filtering: Filtering)
- {
- let filtering_gl = filtering.to_gl();
-
- #[allow(clippy::cast_possible_wrap)]
- unsafe {
- gl::TextureParameteri(
- self.texture,
- gl::TEXTURE_MAG_FILTER,
- filtering_gl as i32,
- );
- }
- }
-
- pub fn set_minifying_filter(&mut self, filtering: Filtering)
- {
- let filtering_gl = filtering.to_gl();
-
- #[allow(clippy::cast_possible_wrap)]
- unsafe {
- gl::TextureParameteri(
- self.texture,
- gl::TEXTURE_MIN_FILTER,
- filtering_gl as i32,
- );
- }
- }
-
- fn alloc_image(
- &mut self,
- pixel_data_format: PixelDataFormat,
- dimens: Dimens<u32>,
- data: &[u8],
- )
- {
- unsafe {
- #[allow(clippy::cast_possible_wrap)]
- gl::TextureStorage2D(
- self.texture,
- 1,
- pixel_data_format.to_sized_internal_format(),
- dimens.width as i32,
- dimens.height as i32,
- );
-
- #[allow(clippy::cast_possible_wrap)]
- gl::TextureSubImage2D(
- self.texture,
- 0,
- 0,
- 0,
- dimens.width as i32,
- dimens.height as i32,
- pixel_data_format.to_format(),
- gl::UNSIGNED_BYTE,
- data.as_ptr().cast(),
- );
- }
- }
-}
-
-impl Drop for Texture
-{
- fn drop(&mut self)
- {
- unsafe {
- gl::DeleteTextures(1, &self.texture);
- }
- }
-}
-
-/// Texture wrapping.
-#[derive(Debug, Clone, Copy)]
-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,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub enum Filtering
-{
- Nearest,
- Linear,
-}
-
-impl Filtering
-{
- fn to_gl(self) -> gl::types::GLenum
- {
- match self {
- Self::Linear => gl::LINEAR,
- Self::Nearest => gl::NEAREST,
- }
- }
-}
-
-/// Texture pixel data format.
-#[derive(Debug, Clone, Copy)]
-pub enum PixelDataFormat
-{
- Rgb8,
- Rgba8,
-}
-
-impl PixelDataFormat
-{
- fn to_sized_internal_format(self) -> gl::types::GLenum
- {
- match self {
- Self::Rgb8 => gl::RGB8,
- Self::Rgba8 => gl::RGBA8,
- }
- }
-
- fn to_format(self) -> gl::types::GLenum
- {
- match self {
- Self::Rgb8 => gl::RGB,
- Self::Rgba8 => gl::RGBA,
- }
- }
-}
-
-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_texture_id(texture_id: Id) -> Option<Self> {
- match texture_id.into_inner() {
- #(
- N => Some(Self::No~N),
- )*
- _ => None
- }
- }
- }
- });
- };
-}
-
-texture_unit_enum!(cnt = 31);
diff --git a/engine/src/opengl/util.rs b/engine/src/opengl/util.rs
deleted file mode 100644
index e60778f..0000000
--- a/engine/src/opengl/util.rs
+++ /dev/null
@@ -1,30 +0,0 @@
-// May only be used when certain crate features are enabled
-#![allow(unused_macros, unused_imports)]
-
-macro_rules! gl_enum {
- (
- $visibility: vis enum $name: ident
- {$(
- $variant: ident = gl::$gl_enum: ident,
- )+}
- ) => {
- #[derive(Debug, Clone, Copy)]
- #[repr(u32)]
- $visibility enum $name
- {$(
- $variant = gl::$gl_enum,
- )+}
-
- impl $name {
- fn from_gl(num: gl::types::GLenum) -> Option<Self>
- {
- match num {
- $(gl::$gl_enum => Some(Self::$variant),)+
- _ => None
- }
- }
- }
- };
-}
-
-pub(crate) use gl_enum;
diff --git a/engine/src/opengl/vertex_array.rs b/engine/src/opengl/vertex_array.rs
deleted file mode 100644
index da5d91e..0000000
--- a/engine/src/opengl/vertex_array.rs
+++ /dev/null
@@ -1,183 +0,0 @@
-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
-{
- array: gl::types::GLuint,
-}
-
-impl VertexArray
-{
- pub fn new() -> Self
- {
- let mut array = 0;
-
- unsafe {
- gl::CreateVertexArrays(1, &mut array);
- }
-
- Self { array }
- }
-
- /// Draws the currently bound vertex array.
- pub fn draw_arrays(primitive_kind: PrimitiveKind, start_index: u32, cnt: u32)
- {
- unsafe {
- #[allow(clippy::cast_possible_wrap)]
- gl::DrawArrays(
- primitive_kind.into_gl(),
- start_index as gl::types::GLint,
- cnt as gl::types::GLsizei,
- );
- }
- }
-
- /// Draws the currently bound vertex array.
- pub fn draw_elements(primitive_kind: PrimitiveKind, offset: u32, cnt: u32)
- {
- unsafe {
- #[allow(clippy::cast_possible_wrap)]
- gl::DrawElements(
- primitive_kind.into_gl(),
- cnt as gl::types::GLsizei,
- gl::UNSIGNED_INT,
- (offset as gl::types::GLint) as *const _,
- );
- }
- }
-
- pub fn bind_element_buffer(&mut self, element_buffer: &Buffer<u32>)
- {
- unsafe {
- gl::VertexArrayElementBuffer(self.array, element_buffer.object());
- }
- }
-
- pub fn bind_vertex_buffer(
- &mut self,
- binding_index: u32,
- vertex_buffer: &Buffer<Vertex>,
- offset: isize,
- )
- {
- unsafe {
- gl::VertexArrayVertexBuffer(
- self.array,
- binding_index,
- vertex_buffer.object(),
- offset,
- VERTEX_STRIDE,
- );
- }
- }
-
- pub fn enable_attrib(&mut self, attrib_index: u32)
- {
- unsafe {
- gl::EnableVertexArrayAttrib(self.array, attrib_index as gl::types::GLuint);
- }
- }
-
- pub fn set_attrib_format(
- &mut self,
- attrib_index: u32,
- data_type: DataType,
- normalized: bool,
- offset: u32,
- )
- {
- unsafe {
- #[allow(clippy::cast_possible_wrap)]
- gl::VertexArrayAttribFormat(
- self.array,
- attrib_index,
- data_type.size() as gl::types::GLint,
- data_type as u32,
- if normalized { gl::TRUE } else { gl::FALSE },
- offset,
- );
- }
- }
-
- /// Associate a vertex attribute and a vertex buffer binding.
- pub fn set_attrib_vertex_buf_binding(
- &mut self,
- attrib_index: u32,
- vertex_buf_binding_index: u32,
- )
- {
- unsafe {
- gl::VertexArrayAttribBinding(
- self.array,
- attrib_index,
- vertex_buf_binding_index,
- );
- }
- }
-
- pub fn bind(&self)
- {
- unsafe { gl::BindVertexArray(self.array) }
- }
-
- /// Does a weak clone of this vertex array. The vertex array itself is NOT copied in
- /// any way this function only copies the internal vertex array ID.
- ///
- /// # Safety
- /// The returned `VertexArray` must not be dropped if another `VertexArray`
- /// referencing the same vertex array ID is used later.
- pub unsafe fn clone_unsafe(&self) -> Self
- {
- Self { array: self.array }
- }
-}
-
-impl Drop for VertexArray
-{
- fn drop(&mut self)
- {
- unsafe {
- gl::DeleteVertexArrays(1, &self.array);
- }
- }
-}
-
-#[derive(Debug)]
-pub enum PrimitiveKind
-{
- Triangles,
-}
-
-impl PrimitiveKind
-{
- fn into_gl(self) -> gl::types::GLenum
- {
- match self {
- Self::Triangles => gl::TRIANGLES,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-#[repr(u32)]
-pub enum DataType
-{
- Float = gl::FLOAT,
-}
-
-impl DataType
-{
- pub fn size(self) -> u32
- {
- #[allow(clippy::cast_possible_truncation)]
- match self {
- Self::Float => size_of::<gl::types::GLfloat>() as u32,
- }
- }
-}
diff --git a/engine/src/performance.rs b/engine/src/performance.rs
deleted file mode 100644
index ffc5c27..0000000
--- a/engine/src/performance.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-use std::time::Instant;
-
-use ecs::component::local::Local;
-use ecs::system::{Into, System};
-use ecs::Component;
-
-use crate::event::PostPresent as PostPresentEvent;
-
-#[derive(Debug, Default)]
-#[non_exhaustive]
-pub struct Extension {}
-
-impl ecs::extension::Extension for Extension
-{
- fn collect(self, mut collector: ecs::extension::Collector<'_>)
- {
- collector.add_system(
- PostPresentEvent,
- log_perf.into_system().initialize((State::default(),)),
- );
- }
-}
-
-#[cfg(feature = "debug")]
-macro_rules! log_perf {
- ($($tt: tt)*) => {
- tracing::info!($($tt)*);
- };
-}
-
-#[cfg(not(feature = "debug"))]
-macro_rules! log_perf {
- ($($tt: tt)*) => {
- println!($($tt)*);
- };
-}
-
-fn log_perf(mut state: Local<State>)
-{
- let Some(last_time) = state.last_time else {
- state.last_time = Some(Instant::now());
- return;
- };
-
- let time_now = Instant::now();
-
- state.last_time = Some(time_now);
-
- log_perf!(
- "Frame time: {}us",
- time_now.duration_since(last_time).as_micros()
- );
-}
-
-#[derive(Debug, Default, Component)]
-struct State
-{
- last_time: Option<Instant>,
-}
diff --git a/engine/src/projection.rs b/engine/src/projection.rs
index aa84a9f..115ca39 100644
--- a/engine/src/projection.rs
+++ b/engine/src/projection.rs
@@ -1,10 +1,14 @@
+use crate::data_types::dimens::Dimens3;
use crate::matrix::Matrix;
+use crate::builder;
+use crate::vector::Vec3;
#[derive(Debug)]
#[non_exhaustive]
pub enum Projection
{
Perspective(Perspective),
+ Orthographic(Orthographic),
}
/// Perspective projection parameters.
@@ -16,6 +20,29 @@ pub struct Perspective
pub near: f32,
}
+impl Perspective
+{
+ /// Creates a perspective projection matrix using right-handed coordinates.
+ #[inline]
+ pub fn to_matrix_rh(&self, aspect: f32, clip_volume: ClipVolume)
+ -> Matrix<f32, 4, 4>
+ {
+ let mut out = Matrix::new();
+
+ match clip_volume {
+ ClipVolume::NegOneToOne => {
+ out.set_cell(0, 0, (1.0 / (self.fov_radians / 2.0).tan()) / aspect);
+ out.set_cell(1, 1, 1.0 / (self.fov_radians / 2.0).tan());
+ out.set_cell(2, 2, (self.near + self.far) / (self.near - self.far));
+ out.set_cell(2, 3, (2.0 * self.near * self.far) / (self.near - self.far));
+ out.set_cell(3, 2, -1.0);
+ }
+ }
+
+ out
+ }
+}
+
impl Default for Perspective
{
fn default() -> Self
@@ -28,30 +55,83 @@ impl Default for Perspective
}
}
-pub(crate) fn new_perspective_matrix(
- perspective: &Perspective,
- aspect: f32,
-) -> Matrix<f32, 4, 4>
+builder! {
+#[builder(name = OrthographicBuilder, derives=(Debug, Clone))]
+#[derive(Debug, Clone, PartialEq, PartialOrd)]
+#[non_exhaustive]
+pub struct Orthographic
{
- let mut out = Matrix::new();
+ pub size: Dimens3<f32>,
+}
+}
- out.set_cell(0, 0, (1.0 / (perspective.fov_radians / 2.0).tan()) / aspect);
+impl Orthographic
+{
+ pub fn builder() -> OrthographicBuilder
+ {
+ OrthographicBuilder::default()
+ }
- out.set_cell(1, 1, 1.0 / (perspective.fov_radians / 2.0).tan());
+ /// Creates a orthographic projection matrix using right-handed coordinates.
+ pub fn to_matrix_rh(
+ &self,
+ center_pos: &Vec3<f32>,
+ clip_volume: ClipVolume,
+ ) -> Matrix<f32, 4, 4>
+ {
+ let mut result = Matrix::<f32, 4, 4>::new();
- out.set_cell(
- 2,
- 2,
- (perspective.near + perspective.far) / (perspective.near - perspective.far),
- );
+ let left = center_pos.x - (self.size.width / 2.0);
+ let right = center_pos.x + (self.size.width / 2.0);
+ let bottom = center_pos.y - (self.size.height / 2.0);
+ let top = center_pos.y + (self.size.height / 2.0);
+ let near = center_pos.z - (self.size.depth / 2.0);
+ let far = center_pos.z + (self.size.depth / 2.0);
- out.set_cell(
- 2,
- 3,
- (2.0 * perspective.near * perspective.far) / (perspective.near - perspective.far),
- );
+ match clip_volume {
+ ClipVolume::NegOneToOne => {
+ result.set_cell(0, 0, 2.0 / (right - left));
+ result.set_cell(1, 1, 2.0 / (top - bottom));
+ result.set_cell(2, 2, -2.0 / (far - near));
+ result.set_cell(0, 3, -(right + left) / (right - left));
+ result.set_cell(1, 3, -(top + bottom) / (top - bottom));
+ result.set_cell(2, 3, -(far + near) / (far - near));
+ result.set_cell(3, 3, 1.0);
+ }
+ }
- out.set_cell(3, 2, -1.0);
+ result
+ }
+}
- out
+impl Default for Orthographic
+{
+ fn default() -> Self
+ {
+ Self {
+ size: Dimens3 {
+ width: 10.0,
+ height: 7.0,
+ depth: 10.0,
+ },
+ }
+ }
+}
+
+impl Default for OrthographicBuilder
+{
+ fn default() -> Self
+ {
+ let orthographic = Orthographic::default();
+
+ OrthographicBuilder { size: orthographic.size }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[non_exhaustive]
+pub enum ClipVolume
+{
+ /// -1 to +1. This is the OpenGL clip volume definition.
+ NegOneToOne,
}
diff --git a/engine/src/renderer.rs b/engine/src/renderer.rs
index 2544919..6d25f6b 100644
--- a/engine/src/renderer.rs
+++ b/engine/src/renderer.rs
@@ -1 +1,80 @@
+use ecs::pair::{ChildOf, Pair};
+use ecs::phase::{Phase, POST_UPDATE as POST_UPDATE_PHASE};
+use ecs::{declare_entity, Component};
+
+use crate::builder;
+
pub mod opengl;
+
+declare_entity!(
+ pub RENDER_PHASE,
+ (
+ Phase,
+ Pair::builder()
+ .relation::<ChildOf>()
+ .target_id(*POST_UPDATE_PHASE)
+ .build()
+ )
+);
+
+builder! {
+/// Window graphics properties.
+#[builder(name=GraphicsPropertiesBuilder, derives=(Debug, Clone))]
+#[derive(Debug, Clone, Component)]
+#[non_exhaustive]
+pub struct GraphicsProperties
+{
+ /// Number of samples for multisampling. `None` means no multisampling.
+ #[builder(skip_generate_fn)]
+ pub multisampling_sample_cnt: Option<u8>,
+
+ /// Whether graphics API debugging is enabled.
+ pub debug: bool,
+
+ /// Whether depth testing is enabled
+ pub depth_test: bool,
+}
+}
+
+impl GraphicsProperties
+{
+ pub fn builder() -> GraphicsPropertiesBuilder
+ {
+ GraphicsPropertiesBuilder::default()
+ }
+}
+
+impl Default for GraphicsProperties
+{
+ fn default() -> Self
+ {
+ Self::builder().build()
+ }
+}
+
+impl GraphicsPropertiesBuilder
+{
+ pub fn multisampling_sample_cnt(mut self, multisampling_sample_cnt: u8) -> Self
+ {
+ self.multisampling_sample_cnt = Some(multisampling_sample_cnt);
+ self
+ }
+
+ pub fn no_multisampling(mut self) -> Self
+ {
+ self.multisampling_sample_cnt = None;
+ self
+ }
+}
+
+impl Default for GraphicsPropertiesBuilder
+{
+ fn default() -> Self
+ {
+ Self {
+ multisampling_sample_cnt: Some(8),
+ debug: false,
+ depth_test: true,
+ }
+ }
+}
diff --git a/engine/src/renderer/opengl.rs b/engine/src/renderer/opengl.rs
index deb26a8..fb7dfbe 100644
--- a/engine/src/renderer/opengl.rs
+++ b/engine/src/renderer/opengl.rs
@@ -1,70 +1,173 @@
//! OpenGL renderer.
+use std::any::type_name;
use std::collections::HashMap;
-use std::ffi::{c_void, CString};
+use std::ffi::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::query::options::{Not, With};
+use ecs::component::Handle as ComponentHandle;
+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::query::term::Without;
use ecs::sole::Single;
-use ecs::system::{Into as _, System};
-use ecs::{Component, Query};
+use ecs::system::observer::Observe;
+use ecs::{declare_entity, Component, Query};
+use glutin::display::GetGlDisplay;
+use glutin::prelude::GlDisplay;
+use glutin::surface::GlSurface;
+use opengl_bindings::debug::{
+ set_debug_message_callback,
+ set_debug_message_control,
+ MessageIdsAction,
+ MessageSeverity,
+ MessageSource,
+ MessageType,
+ SetDebugMessageControlError as GlSetDebugMessageControlError,
+};
+use opengl_bindings::misc::{
+ clear_buffers,
+ enable,
+ set_enabled,
+ BufferClearMask,
+ Capability,
+ SetViewportError as GlSetViewportError,
+};
+use opengl_bindings::shader::{
+ Error as GlShaderError,
+ Kind as ShaderKind,
+ Program as GlShaderProgram,
+ Shader as GlShader,
+};
+use opengl_bindings::texture::{
+ Filtering as GlTextureFiltering,
+ GenerateError as GlTextureGenerateError,
+ PixelDataFormat as GlTexturePixelDataFormat,
+ Texture as GlTexture,
+ Wrapping as GlTextureWrapping,
+};
+use opengl_bindings::vertex_array::{
+ DrawError as GlDrawError,
+ PrimitiveKind,
+ VertexArray,
+};
+use opengl_bindings::{ContextWithFns, CurrentContextWithFns};
+use safer_ffi::layout::ReprC;
+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::event::{Present as PresentEvent, Start as StartEvent};
+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::opengl::buffer::{Buffer, Usage as BufferUsage};
-#[cfg(feature = "debug")]
-use crate::opengl::debug::{MessageSeverity, MessageSource, MessageType};
+use crate::model::Model;
use crate::opengl::glsl::{
preprocess as glsl_preprocess,
PreprocessingError as GlslPreprocessingError,
};
-use crate::opengl::shader::{
- Error as GlShaderError,
- Kind as ShaderKind,
- Program as GlShaderProgram,
- Shader as GlShader,
-};
-use crate::opengl::texture::{
- set_active_texture_unit,
- Texture as GlTexture,
- TextureUnit,
+use crate::projection::{ClipVolume, Projection};
+use crate::renderer::opengl::glutin_compat::{
+ DisplayBuilder,
+ Error as GlutinCompatError,
};
-use crate::opengl::vertex_array::{
- DataType as VertexArrayDataType,
- PrimitiveKind,
- VertexArray,
+use crate::renderer::opengl::graphics_mesh::GraphicsMesh;
+use crate::renderer::{GraphicsProperties, RENDER_PHASE};
+use crate::texture::{
+ Filtering as TextureFiltering,
+ Properties as TextureProperties,
+ Wrapping as TextureWrapping,
};
-use crate::opengl::{clear_buffers, enable, BufferClearMask, Capability};
-use crate::projection::{new_perspective_matrix, Projection};
-use crate::texture::{Id as TextureId, Texture};
-use crate::transform::{Position, Scale};
-use crate::util::NeverDrop;
+use crate::transform::{Scale, WorldPosition};
+use crate::util::MapVec;
use crate::vector::{Vec2, Vec3};
-use crate::vertex::{AttributeComponentType, Vertex};
-use crate::window::Window;
-
-type RenderableEntity = (
- Mesh,
- Material,
- Option<MaterialFlags>,
- Option<Position>,
- Option<Scale>,
- Option<DrawFlags>,
- Option<GlObjects>,
+use crate::windowing::window::{
+ Closed as WindowClosed,
+ CreationAttributes as WindowCreationAttributes,
+ CreationReady,
+ Window,
+};
+use crate::windowing::Context as WindowingContext;
+
+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;
+
+type RenderableEntity<'a> = (
+ &'a Model,
+ Option<&'a MaterialFlags>,
+ Option<&'a WorldPosition>,
+ Option<&'a Scale>,
+ Option<&'a DrawFlags>,
+ &'a [Pair<DataInGraphicsContext, Wildcard>],
+);
+
+declare_entity!(
+ pub POST_RENDER_PHASE,
+ (Phase, Pair::builder().relation::<ChildOf>().target_id(*RENDER_PHASE).build())
);
+#[derive(Debug, Component)]
+struct WithGraphicsContext;
+
+#[derive(Debug, Component)]
+struct WindowGlConfig
+{
+ gl_config: glutin::config::Config,
+}
+
+#[derive(Debug, Component)]
+struct WindowGraphicsSurface
+{
+ surface: glutin::surface::Surface<glutin::surface::WindowSurface>,
+}
+
+#[derive(Component)]
+struct GraphicsContext
+{
+ context: ContextWithFns,
+ shader_program: Option<GlShaderProgram>,
+ textures_objs: HashMap<AssetId, GlTexture>,
+ default_1x1_texture_obj: Option<GlTexture>,
+ graphics_mesh_store: GraphicsMeshStore,
+}
+
+#[derive(Debug, Default)]
+struct GraphicsMeshStore
+{
+ graphics_meshes: MapVec<GraphicsMeshId, GraphicsMesh>,
+ next_id: GraphicsMeshId,
+}
+
+impl GraphicsMeshStore
+{
+ fn insert(&mut self, graphics_mesh: GraphicsMesh) -> GraphicsMeshId
+ {
+ let id = self.next_id;
+
+ self.graphics_meshes.insert(id, graphics_mesh);
+
+ self.next_id.inner += 1;
+
+ id
+ }
+}
+
+#[derive(Debug, Component)]
+struct DataInGraphicsContext
+{
+ graphics_mesh_id: GraphicsMeshId,
+}
+
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct Extension {}
@@ -73,226 +176,904 @@ impl ecs::extension::Extension for Extension
{
fn collect(self, mut collector: ecs::extension::Collector<'_>)
{
- collector.add_system(StartEvent, initialize);
+ collector.add_declared_entity(&RENDER_PHASE);
+ collector.add_declared_entity(&POST_RENDER_PHASE);
- collector.add_system(
- PresentEvent,
- render
- .into_system()
- .initialize((GlobalGlObjects::default(),)),
- );
+ collector.add_system(*RENDER_PHASE, render);
+
+ collector.add_system(*POST_RENDER_PHASE, prepare_windows);
+ collector.add_system(*POST_RENDER_PHASE, init_window_graphics);
+
+ collector.add_observer(handle_model_removed);
+
+ collector.add_observer(handle_window_changed);
+ collector.add_observer(handle_window_removed);
+ }
+}
+
+#[tracing::instrument(skip_all)]
+fn handle_model_removed(observe: Observe<Pair<Removed, Model>>, mut actions: Actions)
+{
+ for evt_match in &observe {
+ let ent_id = evt_match.id();
+
+ tracing::debug!(entity_id=%ent_id, "Cleaning up after model");
+
+ let ent = evt_match.get_ent_infallible();
+
+ for data_in_graphics_ctx_pair in
+ ent.get_wildcard_pair_matches::<DataInGraphicsContext, Wildcard>()
+ {
+ actions.remove_components(ent_id, [data_in_graphics_ctx_pair.id()]);
+
+ let Some(graphics_context_ent) = data_in_graphics_ctx_pair.get_target_ent()
+ else {
+ tracing::trace!(
+ concat!(
+ "Graphics context referenced by pair ({}, {}) does not exist. ",
+ "Skipping cleanup of this model"
+ ),
+ type_name::<DataInGraphicsContext>(),
+ data_in_graphics_ctx_pair.id().target_entity()
+ );
+
+ continue;
+ };
+
+ let Some(data_in_graphics_ctx) =
+ data_in_graphics_ctx_pair.get_data_as_relation()
+ else {
+ unreachable!();
+ };
+
+ let Some(mut graphics_context) =
+ graphics_context_ent.get_mut::<GraphicsContext>()
+ else {
+ tracing::trace!(
+ "Graphics context entity {} does not have a {} component",
+ graphics_context_ent.uid(),
+ type_name::<GraphicsContext>()
+ );
+ continue;
+ };
+
+ graphics_context
+ .graphics_mesh_store
+ .graphics_meshes
+ .remove(data_in_graphics_ctx.graphics_mesh_id);
+ }
}
}
-fn initialize(window: Single<Window>)
+#[tracing::instrument(skip_all)]
+fn handle_window_changed(
+ observe: Observe<Pair<Changed, Window>>,
+ entity_obtainer: EntityObtainer,
+)
{
- window
- .make_context_current()
- .expect("Failed to make window context current");
+ for evt_match in &observe {
+ let window_ent = evt_match.get_ent_infallible();
- gl::load_with(|symbol| match window.get_proc_address(symbol) {
- Ok(addr) => addr as *const c_void,
- Err(err) => {
- println!(
- "FATAL ERROR: Failed to get adress of OpenGL function {symbol}: {err}",
+ tracing::trace!(
+ new_state = ?evt_match.get_changed_comp(),
+ "Handling window change"
+ );
+
+ let Some(window_graphics_surface) = window_ent.get::<WindowGraphicsSurface>()
+ else {
+ continue;
+ };
+
+ let Some(graphics_context_ent_id) = window_ent
+ .get_matching_components(
+ Pair::builder()
+ .relation::<WithGraphicsContext>()
+ .target_id(Wildcard::uid())
+ .build()
+ .id(),
+ )
+ .next()
+ .map(|comp_ref| comp_ref.id().target_entity())
+ else {
+ continue;
+ };
+
+ let Some(graphics_context_ent) =
+ entity_obtainer.get_entity(graphics_context_ent_id)
+ else {
+ tracing::error!("Graphics context entity does not exist");
+ continue;
+ };
+
+ let Some(graphics_context) = graphics_context_ent.get::<GraphicsContext>() else {
+ tracing::error!(
+ "Graphics context entity does not have a GraphicsContext component"
);
+ continue;
+ };
+
+ let Ok(current_graphics_context) = graphics_context
+ .context
+ .make_current(&window_graphics_surface.surface)
+ else {
+ tracing::error!("Failed to make graphics context current");
+ continue;
+ };
- abort();
+ if let Err(err) = set_viewport(
+ &current_graphics_context,
+ Vec2::default(),
+ evt_match.get_changed_comp().inner_size(),
+ ) {
+ tracing::error!("Failed to set viewport: {err}");
}
- });
+ }
+}
- #[cfg(feature = "debug")]
- initialize_debug();
+#[tracing::instrument(skip_all)]
+fn handle_window_removed(observe: Observe<Pair<Removed, Window>>, mut actions: Actions)
+{
+ for evt_match in &observe {
+ let window_ent_id = evt_match.id();
- let window_size = window.size().expect("Failed to get window size");
+ let window_ent = evt_match.get_ent_infallible();
- set_viewport(Vec2 { x: 0, y: 0 }, window_size);
+ tracing::debug!(
+ entity_id = %window_ent_id,
+ title = %evt_match.get_removed_comp().title,
+ "Handling removal of window"
+ );
+
+ actions.remove_comps::<(WindowGraphicsSurface, WindowGlConfig)>(window_ent_id);
+
+ let Some(with_graphics_ctx_pair_handle) =
+ window_ent.get_first_wildcard_pair_match::<WithGraphicsContext, Wildcard>()
+ else {
+ tracing::warn!("Window entity is missing a (WithGraphicsContext, *) pair");
+ continue;
+ };
+
+ let graphics_context_ent_id = with_graphics_ctx_pair_handle.id().target_entity();
- window.set_framebuffer_size_callback(|new_window_size| {
- set_viewport(Vec2::ZERO, new_window_size);
- });
+ actions.remove_comps::<(GraphicsContext,)>(graphics_context_ent_id);
- enable(Capability::DepthTest);
- enable(Capability::MultiSample);
+ actions.remove_components(window_ent_id, [with_graphics_ctx_pair_handle.id()]);
+ }
}
-#[allow(clippy::too_many_arguments)]
-fn render(
- query: Query<RenderableEntity, Not<With<NoDraw>>>,
- point_light_query: Query<(PointLight,)>,
- directional_lights: Query<(DirectionalLight,)>,
- camera_query: Query<(Camera, Position, ActiveCamera)>,
- window: Single<Window>,
- global_light: Single<GlobalLight>,
- mut gl_objects: Local<GlobalGlObjects>,
+#[derive(Debug, Component)]
+struct SetupFailed;
+
+// fn on_window_creation_attrs_added(
+// observe: Observe<Pair<Added, WindowCreationAttributes>>,
+// windowing: Single<Windowing>,
+// window_store: Single<WindowStore>,
+// mut actions: Actions,
+// )
+// {
+// for evt_match in &observe {
+// let Some(ent) = evt_match.get_entity() else {
+// unreachable!();
+// };
+//
+// if ent.has_component(WindowGlConfig::id()) ||
+// ent.has_component(WindowClosed::id()) || ent.has_component() {} }
+// }
+
+fn prepare_windows(
+ window_query: Query<
+ (
+ Option<&Window>,
+ &mut WindowCreationAttributes,
+ Option<&GraphicsProperties>,
+ ),
+ (
+ Without<CreationReady>,
+ Without<WindowGlConfig>,
+ Without<WindowClosed>,
+ Without<SetupFailed>,
+ ),
+ >,
+ windowing_context: Single<WindowingContext>,
mut actions: Actions,
)
{
- let Some((camera, camera_pos, _)) = camera_query.iter().next() else {
- #[cfg(feature = "debug")]
- tracing::warn!("No current camera. Nothing will be rendered");
+ let Some(display_handle) = windowing_context.display_handle() else {
return;
};
- let point_lights = point_light_query
- .iter()
- .map(|(point_light,)| point_light)
- .collect::<Vec<_>>();
+ for (window_ent_id, (window, mut window_creation_attrs, graphics_props)) in
+ window_query.iter_with_euids()
+ {
+ tracing::debug!("Preparing window entity {window_ent_id} for use in rendering");
- let directional_lights = directional_lights.iter().collect::<Vec<_>>();
+ let mut glutin_config_template_builder =
+ glutin::config::ConfigTemplateBuilder::new();
- let GlobalGlObjects {
- shader_program,
- textures: gl_textures,
- } = &mut *gl_objects;
+ let graphics_props = match graphics_props.as_ref() {
+ Some(graphics_props) => &*graphics_props,
+ None => {
+ actions.add_components(window_ent_id, (GraphicsProperties::default(),));
- let shader_program =
- shader_program.get_or_insert_with(|| create_default_shader_program().unwrap());
+ &GraphicsProperties::default()
+ }
+ };
- clear_buffers(BufferClearMask::COLOR | BufferClearMask::DEPTH);
+ if let Some(multisampling_sample_cnt) = graphics_props.multisampling_sample_cnt {
+ glutin_config_template_builder = glutin_config_template_builder
+ .with_multisampling(multisampling_sample_cnt);
+ }
- for (
- entity_index,
- (mesh, material, material_flags, position, scale, draw_flags, gl_objects),
- ) in query.iter().enumerate()
- {
- let material_flags = material_flags
- .map(|material_flags| material_flags.clone())
- .unwrap_or_default();
+ let window_handle = match window
+ .as_ref()
+ .map(|window| unsafe {
+ windowing_context.get_window_as_handle(&window.wid())
+ })
+ .flatten()
+ .transpose()
+ {
+ Ok(window_handle) => window_handle,
+ Err(err) => {
+ tracing::error!("Failed to get window handle: {err}");
+ actions.add_components(window_ent_id, (SetupFailed,));
+ continue;
+ }
+ };
- let new_gl_objects;
+ let (new_window_creation_attrs, gl_config) = match DisplayBuilder::new()
+ .with_window_attributes(window_creation_attrs.clone())
+ .build(
+ window_handle,
+ &display_handle,
+ glutin_config_template_builder,
+ |mut cfgs| cfgs.next(),
+ ) {
+ Ok((new_window_creation_attrs, gl_config)) => {
+ (new_window_creation_attrs, gl_config)
+ }
+ Err(GlutinCompatError::WindowRequired) => {
+ actions.add_components(window_ent_id, (CreationReady,));
+ continue;
+ }
+ Err(err) => {
+ tracing::error!("Failed to create platform graphics display: {err}");
+ actions.add_components(window_ent_id, (SetupFailed,));
+ continue;
+ }
+ };
- let gl_objects = if let Some(gl_objects) = gl_objects.as_deref() {
- gl_objects
- } else {
- // TODO: Account for when meshes are changed
- let gl_objects = GlObjects::new(&mesh);
+ *window_creation_attrs = new_window_creation_attrs;
+
+ // let gl_config_template = glutin_config_template_builder.build();
+ //
+ // let display = match glutin_winit_compat::create_display(
+ // unsafe { engine_display.as_display_handle() },
+ // glutin_winit_compat::ApiPreference::default(),
+ // None,
+ // ) {
+ // Ok(gl_display) => gl_display,
+ // Err(err) => {
+ // tracing::error!("Failed to create graphics platform display: {err}");
+ // continue;
+ // }
+ // };
+ //
+ // let mut gl_configs = match unsafe { display.find_configs(gl_config_template) }
+ // { Ok(gl_configs) => gl_configs,
+ // Err(err) => {
+ // tracing::error!("Failed to find GL configs: {err:?}");
+ // continue;
+ // }
+ // };
+ //
+ // let Some(first_gl_config) = gl_configs.next() else {
+ // tracing::error!("No matching GL configuration exists");
+ // continue;
+ // };
+ //
+ // *window_creation_attrs = finalize_window_creation_attrs(
+ // window_creation_attrs.clone(),
+ // &first_gl_config,
+ // );
+
+ actions.add_components(window_ent_id, (WindowGlConfig { gl_config },));
+
+ if window.is_none() {
+ actions.add_components(window_ent_id, (CreationReady,));
+ }
+ }
+}
- new_gl_objects = Some(gl_objects.clone());
+#[tracing::instrument(skip_all)]
+fn init_window_graphics(
+ window_query: Query<
+ (&Window, &WindowGlConfig, &GraphicsProperties),
+ (Without<WindowGraphicsSurface>, Without<SetupFailed>),
+ >,
+ mut actions: Actions,
+ windowing_context: Single<WindowingContext>,
+)
+{
+ for (window_ent_id, (window, window_gl_config, graphics_props)) in
+ window_query.iter_with_euids()
+ {
+ tracing::info!("Initializing graphics for window {window_ent_id}");
+
+ let display = window_gl_config.gl_config.display();
+
+ let window_handle =
+ match unsafe { windowing_context.get_window_as_handle(&window.wid()) }
+ .transpose()
+ {
+ Ok(Some(window_handle)) => window_handle,
+ Ok(None) => {
+ tracing::error!(
+ wid = ?window.wid(),
+ entity_id = %window_ent_id,
+ "Windowing context does not contain window"
+ );
+ actions.add_components(window_ent_id, (SetupFailed,));
+ continue;
+ }
+ Err(err) => {
+ tracing::error!("Failed to get window handle: {err}");
+ actions.add_components(window_ent_id, (SetupFailed,));
+ continue;
+ }
+ };
- actions.add_components(
- query.get_entity_uid(entity_index).unwrap(),
- (gl_objects,),
+ let Some(window_inner_size) = window.inner_size().try_into_nonzero() else {
+ tracing::error!(
+ "Cannot create a surface for a window with a width/height of 0",
);
+ continue;
+ };
- &*new_gl_objects.unwrap()
+ let surface = match unsafe {
+ display.create_window_surface(
+ &window_gl_config.gl_config,
+ &glutin::surface::SurfaceAttributesBuilder::<
+ glutin::surface::WindowSurface,
+ >::new()
+ .build(
+ window_handle.as_raw(),
+ window_inner_size.width,
+ window_inner_size.height,
+ ),
+ )
+ } {
+ Ok(surface) => surface,
+ Err(err) => {
+ tracing::error!("Failed to create window surface: {err}");
+ continue;
+ }
};
- apply_transformation_matrices(
- Transformation {
- position: position.map(|pos| *pos).unwrap_or_default().position,
- scale: scale.map(|scale| *scale).unwrap_or_default().scale,
- },
- shader_program,
- &camera,
- &camera_pos,
- window.size().expect("Failed to get window size"),
- );
+ let context = match unsafe {
+ display.create_context(
+ &window_gl_config.gl_config,
+ &glutin::context::ContextAttributesBuilder::new()
+ .with_debug(graphics_props.debug)
+ .build(Some(window_handle.as_raw())),
+ )
+ } {
+ Ok(context) => context,
+ Err(err) => {
+ tracing::error!("Failed to create graphics context: {err}");
+ continue;
+ }
+ };
- apply_light(
- &material,
- &material_flags,
- &global_light,
- shader_program,
- point_lights.as_slice(),
- directional_lights
- .iter()
- .map(|(dir_light,)| &**dir_light)
- .collect::<Vec<_>>()
- .as_slice(),
- &camera_pos,
+ let context = match ContextWithFns::new(context, &surface) {
+ Ok(context) => context,
+ Err(err) => {
+ tracing::error!("Failed to create graphics context: {err}");
+ continue;
+ }
+ };
+
+ let Ok(current_graphics_context) = context.make_current(&surface) else {
+ tracing::error!("Failed to make graphics context current");
+ continue;
+ };
+
+ if let Err(err) = set_viewport(
+ &current_graphics_context,
+ Vec2 { x: 0, y: 0 },
+ window.inner_size(),
+ ) {
+ tracing::error!("Failed to set viewport: {err}");
+ }
+
+ set_enabled(
+ &current_graphics_context,
+ Capability::DepthTest,
+ graphics_props.depth_test,
);
- for texture in &material.textures {
- let gl_texture = gl_textures
- .entry(texture.id())
- .or_insert_with(|| create_gl_texture(texture));
+ set_enabled(
+ &current_graphics_context,
+ Capability::MultiSample,
+ graphics_props.multisampling_sample_cnt.is_some(),
+ );
- let texture_unit =
- TextureUnit::from_texture_id(texture.id()).unwrap_or_else(|| {
- panic!("Texture id {} is a invalid texture unit", texture.id());
- });
+ if graphics_props.debug {
+ enable(&current_graphics_context, Capability::DebugOutput);
+ enable(
+ &current_graphics_context,
+ Capability::DebugOutputSynchronous,
+ );
- set_active_texture_unit(texture_unit);
+ set_debug_message_callback(
+ &current_graphics_context,
+ opengl_debug_message_cb,
+ );
- gl_texture.bind();
+ match set_debug_message_control(
+ &current_graphics_context,
+ None,
+ None,
+ None,
+ &[],
+ MessageIdsAction::Disable,
+ ) {
+ Ok(()) => {}
+ Err(GlSetDebugMessageControlError::TooManyIds {
+ id_cnt: _,
+ max_id_cnt: _,
+ }) => {
+ unreachable!() // No ids are given
+ }
+ }
}
- shader_program.activate();
+ let graphics_context_ent_id = actions.spawn((GraphicsContext {
+ context,
+ shader_program: None,
+ textures_objs: HashMap::new(),
+ default_1x1_texture_obj: None,
+ graphics_mesh_store: GraphicsMeshStore::default(),
+ },));
+
+ actions.add_components(
+ window_ent_id,
+ (
+ WindowGraphicsSurface { surface },
+ Pair::builder()
+ .relation::<WithGraphicsContext>()
+ .target_id(graphics_context_ent_id)
+ .build(),
+ ),
+ );
+ }
+}
- if let Some(draw_flags) = &draw_flags {
- crate::opengl::set_polygon_mode(
- draw_flags.polygon_mode_config.face,
- draw_flags.polygon_mode_config.mode,
+#[tracing::instrument(skip_all)]
+#[allow(clippy::too_many_arguments)]
+fn render(
+ query: Query<RenderableEntity<'_>, (Without<NoDraw>,)>,
+ point_light_query: Query<(&PointLight, &WorldPosition)>,
+ directional_lights: Query<(&DirectionalLight,)>,
+ camera_query: Query<(&Camera, &WorldPosition, &ActiveCamera)>,
+ window_query: Query<(
+ &Window,
+ &WindowGraphicsSurface,
+ &GraphicsProperties,
+ Pair<WithGraphicsContext, Wildcard>,
+ )>,
+ global_light: Single<GlobalLight>,
+ assets: Single<Assets>,
+ mut actions: Actions,
+)
+{
+ for (
+ window_ent_id,
+ (window, window_graphics_surface, window_graphics_props, graphics_context_pair),
+ ) in window_query.iter_with_euids()
+ {
+ let Some(graphics_context_ent) = graphics_context_pair.get_target_ent() else {
+ tracing::error!("Window's associated graphics context entity does not exist");
+ actions.remove_components(window_ent_id, [graphics_context_pair.id()]);
+ continue;
+ };
+
+ let Some(mut graphics_context) =
+ graphics_context_ent.get_mut::<GraphicsContext>()
+ else {
+ tracing::error!(
+ "Graphics context entity does not have a GraphicsContext component"
);
- }
+ return;
+ };
+
+ let GraphicsContext {
+ ref context,
+ ref mut shader_program,
+ ref mut textures_objs,
+ ref mut default_1x1_texture_obj,
+ ref mut graphics_mesh_store,
+ } = *graphics_context;
+
+ let Some((camera, camera_world_pos, _)) = camera_query.iter().next() else {
+ tracing::warn!("No current camera. Nothing will be rendered");
+ return;
+ };
+
+ let Ok(current_graphics_context) =
+ context.make_current(&window_graphics_surface.surface)
+ else {
+ tracing::error!("Failed to make graphics context current");
+ continue;
+ };
+
+ let directional_lights = directional_lights.iter().collect::<Vec<_>>();
+
+ let shader_program = shader_program.get_or_insert_with(|| {
+ create_default_shader_program(&current_graphics_context).unwrap()
+ });
+
+ let mut clear_mask = BufferClearMask::COLOR;
+
+ clear_mask.set(BufferClearMask::DEPTH, window_graphics_props.depth_test);
+
+ clear_buffers(&current_graphics_context, clear_mask);
- draw_mesh(gl_objects);
+ for (
+ euid,
+ (
+ model,
+ material_flags,
+ position,
+ scale,
+ draw_flags,
+ data_in_graphics_ctx_pairs,
+ ),
+ ) in query.iter_with_euids()
+ {
+ let Some(model_data) = assets.get(&model.asset_handle) else {
+ tracing::trace!("Missing model asset");
+ continue;
+ };
- if draw_flags.is_some() {
- let default_polygon_mode_config = PolygonModeConfig::default();
+ let material_flags = material_flags
+ .map(|material_flags| material_flags.clone())
+ .unwrap_or_default();
+
+ let graphics_mesh_id = match data_in_graphics_ctx_pairs
+ .get_with_target_id(graphics_context_ent.uid())
+ {
+ Some(data_in_graphics_ctx_pair) => {
+ let Some(data_in_graphics_ctx) =
+ data_in_graphics_ctx_pair.get_data::<DataInGraphicsContext>()
+ else {
+ tracing::warn!(
+ concat!(
+ "Pair with relation {} ({}) has no data or data with a ",
+ "wrong type. This pair will be removed"
+ ),
+ type_name::<DataInGraphicsContext>(),
+ data_in_graphics_ctx_pair.id()
+ );
+
+ actions.remove_components(euid, [data_in_graphics_ctx_pair.id()]);
+ continue;
+ };
+
+ data_in_graphics_ctx.graphics_mesh_id
+ }
+ None => {
+ let graphics_mesh = match GraphicsMesh::new(
+ &current_graphics_context,
+ &model_data.mesh,
+ ) {
+ Ok(graphics_mesh) => graphics_mesh,
+ Err(err) => {
+ tracing::error!(
+ "Failed to create {}: {err}",
+ type_name::<GraphicsMesh>()
+ );
+
+ // This system should not try again
+ actions.add_components(euid, (NoDraw,));
+
+ continue;
+ }
+ };
+
+ let graphics_mesh_id = graphics_mesh_store.insert(graphics_mesh);
+
+ actions.add_components(
+ euid,
+ (Pair::builder()
+ .relation_as_data(DataInGraphicsContext { graphics_mesh_id })
+ .target_id(graphics_context_ent.uid())
+ .build(),),
+ );
+
+ graphics_mesh_id
+ }
+ };
+
+ let Some(graphics_mesh) =
+ graphics_mesh_store.graphics_meshes.get(&graphics_mesh_id)
+ else {
+ tracing::error!("Graphics mesh with ID: {graphics_mesh_id:?} not found");
+ continue;
+ };
- crate::opengl::set_polygon_mode(
- default_polygon_mode_config.face,
- default_polygon_mode_config.mode,
+ apply_transformation_matrices(
+ &current_graphics_context,
+ Transformation {
+ position: position.map(|pos| *pos).unwrap_or_default().position,
+ scale: scale.map(|scale| *scale).unwrap_or_default().scale,
+ },
+ shader_program,
+ &camera,
+ &camera_world_pos,
+ window.inner_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(
+ &current_graphics_context,
+ &material,
+ &material_flags,
+ &global_light,
+ shader_program,
+ (point_light_query.iter(), point_light_query.iter().count()),
+ directional_lights
+ .iter()
+ .map(|(dir_light,)| &**dir_light)
+ .collect::<Vec<_>>()
+ .as_slice(),
+ &camera_world_pos,
+ );
+
+ match create_bind_material_textures(
+ &current_graphics_context,
+ &material,
+ &assets,
+ textures_objs,
+ default_1x1_texture_obj,
+ ) {
+ Ok(()) => {}
+ Err(CreateBindMaterialTexturesError::MissingTextureAsset) => {
+ continue;
+ }
+ Err(
+ err @ CreateBindMaterialTexturesError::CreateTextureFailed { .. },
+ ) => {
+ tracing::error!(
+ "Creating &/ binding material textures failed: {err}"
+ );
+
+ // This system should not try again
+ actions.add_components(euid, (NoDraw,));
+
+ continue;
+ }
+ }
+
+ shader_program.activate(&current_graphics_context);
+
+ if let Some(draw_flags) = &draw_flags {
+ opengl_bindings::misc::set_polygon_mode(
+ &current_graphics_context,
+ draw_flags.polygon_mode_config.face,
+ draw_flags.polygon_mode_config.mode,
+ );
+ }
+
+ if let Err(err) = draw_mesh(&current_graphics_context, &graphics_mesh) {
+ tracing::error!(
+ entity_id = %euid,
+ graphics_context_entity_id = %graphics_context_ent.uid(),
+ "Failed to draw mesh: {err}",
+ );
+
+ // This system should not try again
+ actions.add_components(euid, (NoDraw,));
+
+ continue;
+ };
+
+ if draw_flags.is_some() {
+ let default_polygon_mode_config = PolygonModeConfig::default();
+
+ opengl_bindings::misc::set_polygon_mode(
+ &current_graphics_context,
+ default_polygon_mode_config.face,
+ default_polygon_mode_config.mode,
+ );
+ }
+ }
+
+ if let Err(err) = window_graphics_surface
+ .surface
+ .swap_buffers(context.context())
+ {
+ tracing::error!("Failed to swap buffers: {err}");
}
}
}
-#[derive(Debug, Default, Component)]
-struct GlobalGlObjects
+fn create_default_texture(current_context: &CurrentContextWithFns<'_>) -> GlTexture
{
- shader_program: Option<GlShaderProgram>,
- textures: HashMap<TextureId, 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!(),
+ }
}
-fn set_viewport(position: Vec2<u32>, size: Dimens<u32>)
+fn create_bind_material_textures(
+ current_context: &CurrentContextWithFns<'_>,
+ material: &Material,
+ assets: &Assets,
+ texture_objs: &mut HashMap<AssetId, GlTexture>,
+ default_1x1_texture_obj: &mut Option<GlTexture>,
+) -> Result<(), CreateBindMaterialTexturesError>
{
- crate::opengl::set_viewport(position, size);
+ 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_texture_obj
+ .get_or_insert_with(|| create_default_texture(current_context));
+
+ gl_texture.bind_to_texture_unit(current_context, texture_unit);
+
+ continue;
+ };
+
+ let texture_image_asset_id = texture.asset_handle.id();
+
+ let gl_texture = match texture_objs.get(&texture_image_asset_id) {
+ Some(gl_texture) => gl_texture,
+ None => {
+ let Some(image) = assets.get::<Image>(&texture.asset_handle) else {
+ tracing::trace!(handle=?texture.asset_handle, "Missing texture asset");
+ return Err(CreateBindMaterialTexturesError::MissingTextureAsset);
+ };
+
+ texture_objs.entry(texture_image_asset_id).or_insert(
+ create_gl_texture(current_context, image, &texture.properties)
+ .map_err(|err| {
+ CreateBindMaterialTexturesError::CreateTextureFailed {
+ err,
+ image_asset_id: texture_image_asset_id,
+ }
+ })?,
+ )
+ }
+ };
+
+ gl_texture.bind_to_texture_unit(current_context, texture_unit);
+ }
+
+ Ok(())
}
-#[cfg(feature = "debug")]
-fn initialize_debug()
+#[derive(Debug, thiserror::Error)]
+enum CreateBindMaterialTexturesError
{
- use crate::opengl::debug::{
- enable_debug_output,
- set_debug_message_callback,
- set_debug_message_control,
- MessageIdsAction,
- };
+ #[error("Missing texture asset")]
+ MissingTextureAsset,
- enable_debug_output();
+ #[error("Failed to create texture from image asset with ID {image_asset_id:?}")]
+ CreateTextureFailed
+ {
+ #[source]
+ err: GlTextureGenerateError,
+ image_asset_id: AssetId,
+ },
+}
- set_debug_message_callback(opengl_debug_message_cb);
+fn set_viewport(
+ current_context: &CurrentContextWithFns<'_>,
+ position: Vec2<u32>,
+ size: &Dimens<u32>,
+) -> Result<(), GlSetViewportError>
+{
+ let position =
+ opengl_bindings::data_types::Vec2::<u32> { x: position.x, y: position.y };
+
+ let size = opengl_bindings::data_types::Dimens::<u32> {
+ width: size.width,
+ height: size.height,
+ };
- set_debug_message_control(None, None, None, &[], MessageIdsAction::Disable);
+ opengl_bindings::misc::set_viewport(current_context, &position, &size)
}
-fn draw_mesh(gl_objects: &GlObjects)
+fn draw_mesh(
+ current_context: &CurrentContextWithFns<'_>,
+ graphics_mesh: &GraphicsMesh,
+) -> Result<(), GlDrawError>
{
- gl_objects.vertex_arr.bind();
-
- if gl_objects.index_buffer.is_some() {
- VertexArray::draw_elements(PrimitiveKind::Triangles, 0, gl_objects.index_cnt);
+ graphics_mesh.vertex_arr.bind(current_context);
+
+ if graphics_mesh.index_buffer.is_some() {
+ VertexArray::draw_elements(
+ current_context,
+ PrimitiveKind::Triangles,
+ 0,
+ graphics_mesh.element_cnt,
+ )?;
} else {
- VertexArray::draw_arrays(PrimitiveKind::Triangles, 0, 3);
+ VertexArray::draw_arrays(
+ current_context,
+ PrimitiveKind::Triangles,
+ 0,
+ graphics_mesh.element_cnt,
+ )?;
}
+
+ Ok(())
}
-fn create_gl_texture(texture: &Texture) -> GlTexture
+fn create_gl_texture(
+ current_context: &CurrentContextWithFns<'_>,
+ image: &Image,
+ texture_properties: &TextureProperties,
+) -> Result<GlTexture, GlTextureGenerateError>
{
- let mut gl_texture = GlTexture::new();
+ let gl_texture = GlTexture::new(current_context);
gl_texture.generate(
- *texture.dimensions(),
- texture.image().as_bytes(),
- texture.pixel_data_format(),
+ current_context,
+ &image.dimensions().into(),
+ image.as_bytes(),
+ match image.color_type() {
+ ImageColorType::Rgb8 => GlTexturePixelDataFormat::Rgb8,
+ ImageColorType::Rgba8 => GlTexturePixelDataFormat::Rgba8,
+ _ => {
+ unimplemented!();
+ }
+ },
+ )?;
+
+ gl_texture.set_wrap(
+ current_context,
+ texture_wrapping_to_gl(texture_properties.wrap),
+ );
+
+ gl_texture.set_magnifying_filter(
+ current_context,
+ texture_filtering_to_gl(texture_properties.magnifying_filter),
);
- gl_texture.apply_properties(texture.properties());
+ gl_texture.set_minifying_filter(
+ current_context,
+ texture_filtering_to_gl(texture_properties.minifying_filter),
+ );
- gl_texture
+ Ok(gl_texture)
}
const VERTEX_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/vertex.glsl");
@@ -301,32 +1082,34 @@ 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() -> Result<GlShaderProgram, CreateShaderError>
+fn create_default_shader_program(
+ current_context: &CurrentContextWithFns<'_>,
+) -> Result<GlShaderProgram, CreateShaderError>
{
- let mut vertex_shader = GlShader::new(ShaderKind::Vertex);
+ let vertex_shader = GlShader::new(current_context, ShaderKind::Vertex);
- vertex_shader.set_source(&*glsl_preprocess(
- VERTEX_GLSL_SHADER_SRC,
- &get_glsl_shader_content,
- )?)?;
+ vertex_shader.set_source(
+ current_context,
+ &*glsl_preprocess(VERTEX_GLSL_SHADER_SRC, &get_glsl_shader_content)?,
+ )?;
- vertex_shader.compile()?;
+ vertex_shader.compile(current_context)?;
- let mut fragment_shader = GlShader::new(ShaderKind::Fragment);
+ let fragment_shader = GlShader::new(current_context, ShaderKind::Fragment);
- fragment_shader.set_source(&*glsl_preprocess(
- FRAGMENT_GLSL_SHADER_SRC,
- &get_glsl_shader_content,
- )?)?;
+ fragment_shader.set_source(
+ current_context,
+ &*glsl_preprocess(FRAGMENT_GLSL_SHADER_SRC, &get_glsl_shader_content)?,
+ )?;
- fragment_shader.compile()?;
+ fragment_shader.compile(current_context)?;
- let mut gl_shader_program = GlShaderProgram::new();
+ let gl_shader_program = GlShaderProgram::new(current_context);
- gl_shader_program.attach(&vertex_shader);
- gl_shader_program.attach(&fragment_shader);
+ gl_shader_program.attach(current_context, &vertex_shader);
+ gl_shader_program.attach(current_context, &fragment_shader);
- gl_shader_program.link()?;
+ gl_shader_program.link(current_context)?;
Ok(gl_shader_program)
}
@@ -357,138 +1140,75 @@ fn get_glsl_shader_content(path: &Path) -> Result<Vec<u8>, std::io::Error>
))
}
-#[derive(Debug, Component)]
-struct GlObjects
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+struct GraphicsMeshId
{
- /// Vertex and index buffer has to live as long as the vertex array
- vertex_buffer: Buffer<Vertex>,
- index_buffer: Option<Buffer<u32>>,
- index_cnt: u32,
-
- vertex_arr: VertexArray,
-}
-
-impl GlObjects
-{
- #[cfg_attr(feature = "debug", tracing::instrument(skip_all))]
- fn new(mesh: &Mesh) -> Self
- {
- #[cfg(feature = "debug")]
- tracing::trace!(
- "Creating vertex array, vertex buffer{}",
- if mesh.indices().is_some() {
- " and index buffer"
- } else {
- ""
- }
- );
-
- let mut vertex_arr = VertexArray::new();
- let mut vertex_buffer = Buffer::new();
-
- vertex_buffer.store(mesh.vertices(), BufferUsage::Static);
-
- vertex_arr.bind_vertex_buffer(0, &vertex_buffer, 0);
-
- let mut offset = 0u32;
-
- for attrib in Vertex::attrs() {
- vertex_arr.enable_attrib(attrib.index);
-
- vertex_arr.set_attrib_format(
- attrib.index,
- match attrib.component_type {
- AttributeComponentType::Float => VertexArrayDataType::Float,
- },
- false,
- offset,
- );
-
- vertex_arr.set_attrib_vertex_buf_binding(attrib.index, 0);
-
- offset += attrib.component_size * attrib.component_cnt as u32;
- }
-
- if let Some(indices) = mesh.indices() {
- let mut index_buffer = Buffer::new();
-
- index_buffer.store(indices, BufferUsage::Static);
-
- vertex_arr.bind_element_buffer(&index_buffer);
-
- return Self {
- vertex_buffer,
- index_buffer: Some(index_buffer),
- index_cnt: indices.len().try_into().unwrap(),
- vertex_arr,
- };
- }
-
- Self {
- vertex_buffer,
- index_buffer: None,
- index_cnt: 0,
- vertex_arr,
- }
- }
-
- pub fn clone(&self) -> NeverDrop<Self>
- {
- NeverDrop::new(Self {
- // SAFETY: The vertex buffer will never become dropped (NeverDrop ensures it)
- vertex_buffer: unsafe { self.vertex_buffer.clone_weak() },
- index_buffer: self
- .index_buffer
- .as_ref()
- // SAFETY: The index buffer will never become dropped (NeverDrop ensures
- // it)
- .map(|index_buffer| unsafe { index_buffer.clone_weak() }),
- index_cnt: self.index_cnt,
- // SAFETY: The vertex array will never become dropped (NeverDrop ensures it)
- vertex_arr: unsafe { self.vertex_arr.clone_unsafe() },
- })
- }
+ inner: usize,
}
fn apply_transformation_matrices(
+ current_context: &CurrentContextWithFns<'_>,
transformation: Transformation,
gl_shader_program: &mut GlShaderProgram,
camera: &Camera,
- camera_pos: &Position,
- window_size: Dimens<u32>,
+ camera_world_pos: &WorldPosition,
+ window_size: &Dimens<u32>,
)
{
- gl_shader_program
- .set_uniform_matrix_4fv(c"model", &create_transformation_matrix(transformation));
+ gl_shader_program.set_uniform(
+ current_context,
+ c"model",
+ &opengl_bindings::data_types::Matrix {
+ items: create_transformation_matrix(transformation).items,
+ },
+ );
- let view = create_view(camera, camera_pos);
+ let view_matrix = create_view_matrix(camera, &camera_world_pos.position);
- gl_shader_program.set_uniform_matrix_4fv(c"view", &view);
+ gl_shader_program.set_uniform(
+ current_context,
+ c"view",
+ &opengl_bindings::data_types::Matrix { items: view_matrix.items },
+ );
#[allow(clippy::cast_precision_loss)]
- let projection = match &camera.projection {
- Projection::Perspective(perspective) => new_perspective_matrix(
- perspective,
+ 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_matrix_4fv(c"projection", &projection);
+ gl_shader_program.set_uniform(
+ current_context,
+ c"projection",
+ &opengl_bindings::data_types::Matrix { items: proj_matrix.items },
+ );
}
-fn apply_light<PointLightHolder>(
+fn apply_light<'point_light>(
+ current_context: &CurrentContextWithFns<'_>,
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"
);
@@ -498,16 +1218,20 @@ fn apply_light<PointLightHolder>(
);
for (dir_light_index, dir_light) in directional_lights.iter().enumerate() {
- gl_shader_program.set_uniform_vec_3fv(
+ let direction: opengl_bindings::data_types::Vec3<_> = dir_light.direction.into();
+
+ gl_shader_program.set_uniform(
+ current_context,
&create_light_uniform_name(
"directional_lights",
dir_light_index,
"direction",
),
- &dir_light.direction,
+ &direction,
);
set_light_phong_uniforms(
+ current_context,
gl_shader_program,
"directional_lights",
dir_light_index,
@@ -517,120 +1241,163 @@ 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);
+ gl_shader_program.set_uniform(
+ current_context,
+ 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()
+ {
+ let pos: opengl_bindings::data_types::Vec3<_> =
+ (point_light_world_pos.position + point_light.local_position).into();
+
+ gl_shader_program.set_uniform(
+ current_context,
&create_light_uniform_name("point_lights", point_light_index, "position"),
- &point_light.position,
+ &pos,
);
set_light_phong_uniforms(
+ current_context,
gl_shader_program,
"point_lights",
point_light_index,
- &**point_light,
+ &*point_light,
);
set_light_attenuation_uniforms(
+ current_context,
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(
+ current_context,
+ c"point_light_cnt",
+ &(point_light_cnt as i32),
+ );
- gl_shader_program.set_uniform_vec_3fv(
- c"material.ambient",
- &if material_flags.use_ambient_color {
+ let ambient: opengl_bindings::data_types::Vec3<_> =
+ Vec3::from(if material_flags.use_ambient_color {
material.ambient.clone()
} else {
global_light.ambient.clone()
- }
- .into(),
- );
+ })
+ .into();
- gl_shader_program
- .set_uniform_vec_3fv(c"material.diffuse", &material.diffuse.clone().into());
+ gl_shader_program.set_uniform(current_context, c"material.ambient", &ambient);
+
+ let diffuse: opengl_bindings::data_types::Vec3<_> =
+ Vec3::from(material.diffuse.clone()).into();
+
+ gl_shader_program.set_uniform(current_context, 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_vec_3fv(c"material.specular", &material.specular.clone().into());
+ gl_shader_program.set_uniform(current_context, c"material.specular", &specular);
#[allow(clippy::cast_possible_wrap)]
- gl_shader_program.set_uniform_1i(
+ gl_shader_program.set_uniform(
+ current_context,
c"material.ambient_map",
- material.ambient_map.into_inner() as i32,
+ &(AMBIENT_MAP_TEXTURE_UNIT as i32),
);
#[allow(clippy::cast_possible_wrap)]
- gl_shader_program.set_uniform_1i(
+ gl_shader_program.set_uniform(
+ current_context,
c"material.diffuse_map",
- material.diffuse_map.into_inner() as i32,
+ &(DIFFUSE_MAP_TEXTURE_UNIT as i32),
);
#[allow(clippy::cast_possible_wrap)]
- gl_shader_program.set_uniform_1i(
+ gl_shader_program.set_uniform(
+ current_context,
c"material.specular_map",
- material.specular_map.into_inner() as i32,
+ &(SPECULAR_MAP_TEXTURE_UNIT as i32),
+ );
+
+ gl_shader_program.set_uniform(
+ current_context,
+ c"material.shininess",
+ &material.shininess,
);
- gl_shader_program.set_uniform_1fv(c"material.shininess", material.shininess);
+ let view_pos: opengl_bindings::data_types::Vec3<_> = camera_world_pos.position.into();
- gl_shader_program.set_uniform_vec_3fv(c"view_pos", &camera_pos.position);
+ gl_shader_program.set_uniform(current_context, 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_1fv(
+ gl_shader_program.set_uniform(
+ current_context,
&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(
+ current_context,
&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(
+ current_context,
&create_light_uniform_name(
light_array,
light_index,
"attenuation_props.quadratic",
),
- light.attenuation_params.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_vec_3fv(
+ gl_shader_program.set_uniform(
+ current_context,
&create_light_uniform_name(light_array, light_index, "phong.diffuse"),
- &light.diffuse().clone().into(),
+ &opengl_bindings::data_types::Vec3 {
+ x: light.diffuse().red,
+ y: light.diffuse().green,
+ z: light.diffuse().blue,
+ },
);
- gl_shader_program.set_uniform_vec_3fv(
+ gl_shader_program.set_uniform(
+ current_context,
&create_light_uniform_name(light_array, light_index, "phong.specular"),
- &light.specular().clone().into(),
+ &opengl_bindings::data_types::Vec3 {
+ x: light.specular().red,
+ y: light.specular().green,
+ z: light.specular().blue,
+ },
);
}
@@ -679,16 +1446,15 @@ fn create_light_uniform_name(
}
}
-fn create_view(camera: &Camera, camera_pos: &Position) -> Matrix<f32, 4, 4>
+fn create_view_matrix(camera: &Camera, camera_pos: &Vec3<f32>) -> Matrix<f32, 4, 4>
{
let mut view = Matrix::new();
- view.look_at(&camera_pos.position, &camera.target, &camera.global_up);
+ view.look_at(&camera_pos, &camera.target, &camera.global_up);
view
}
-#[cfg(feature = "debug")]
#[tracing::instrument(skip_all)]
fn opengl_debug_message_cb(
source: MessageSource,
@@ -719,7 +1485,8 @@ fn opengl_debug_message_cb(
let backtrace = Backtrace::capture();
if matches!(backtrace.status(), BacktraceStatus::Captured) {
- event!(Level::TRACE, "{backtrace}");
+ tracing::error!("{backtrace}");
+ // event!(Level::TRACE, "{backtrace}");
}
}
MessageType::Other => {
@@ -747,3 +1514,74 @@ 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,
+ }
+}
+
+impl<Value: ReprC + Copy> From<Vec2<Value>> for opengl_bindings::data_types::Vec2<Value>
+{
+ fn from(vec2: Vec2<Value>) -> Self
+ {
+ Self { x: vec2.x, y: vec2.y }
+ }
+}
+
+impl<Value: ReprC + Copy> From<Vec3<Value>> for opengl_bindings::data_types::Vec3<Value>
+{
+ fn from(vec3: Vec3<Value>) -> Self
+ {
+ Self { x: vec3.x, y: vec3.y, z: vec3.z }
+ }
+}
+
+impl<Value: Copy> From<Dimens<Value>> for opengl_bindings::data_types::Dimens<Value>
+{
+ fn from(dimens: Dimens<Value>) -> Self
+ {
+ Self {
+ width: dimens.width,
+ height: dimens.height,
+ }
+ }
+}
+
+impl From<crate::draw_flags::PolygonMode> for opengl_bindings::misc::PolygonMode
+{
+ fn from(mode: crate::draw_flags::PolygonMode) -> Self
+ {
+ match mode {
+ crate::draw_flags::PolygonMode::Point => Self::Point,
+ crate::draw_flags::PolygonMode::Fill => Self::Fill,
+ crate::draw_flags::PolygonMode::Line => Self::Line,
+ }
+ }
+}
+
+impl From<crate::draw_flags::PolygonModeFace> for opengl_bindings::misc::PolygonModeFace
+{
+ fn from(face: crate::draw_flags::PolygonModeFace) -> Self
+ {
+ match face {
+ crate::draw_flags::PolygonModeFace::Front => Self::Front,
+ crate::draw_flags::PolygonModeFace::Back => Self::Back,
+ crate::draw_flags::PolygonModeFace::FrontAndBack => Self::FrontAndBack,
+ }
+ }
+}
diff --git a/engine/src/renderer/opengl/glsl/light.glsl b/engine/src/renderer/opengl/glsl/light.glsl
index 1bc23a4..f12b5fe 100644
--- a/engine/src/renderer/opengl/glsl/light.glsl
+++ b/engine/src/renderer/opengl/glsl/light.glsl
@@ -80,10 +80,10 @@ vec3 calc_specular_light(
{
vec3 view_direction = normalize(view_pos - frag_pos);
- vec3 reflect_direction = reflect(-light_dir, norm);
+ vec3 halfway_direction = normalize(light_dir + view_direction);
float spec =
- pow(max(dot(view_direction, reflect_direction), 0.0), material.shininess);
+ pow(max(dot(norm, halfway_direction), 0.0), material.shininess);
return light_phong.specular * (
spec * (vec3(texture(material.specular_map, texture_coords)) * material.specular)
diff --git a/engine/src/renderer/opengl/glutin_compat.rs b/engine/src/renderer/opengl/glutin_compat.rs
new file mode 100644
index 0000000..cfd6ea7
--- /dev/null
+++ b/engine/src/renderer/opengl/glutin_compat.rs
@@ -0,0 +1,268 @@
+// Original file:
+// https://github.com/rust-windowing/glutin/blob/
+// 0433af9018febe0696c485ed9d66c40dad41f2d4/glutin-winit/src/lib.rs
+//
+// Copyright © 2022 Kirill Chibisov
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the “Software”), to deal
+// in the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+// of the Software, and to permit persons to whom the Software is furnished to do
+// so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+//! This library provides helpers for cross-platform [`glutin`] bootstrapping
+//! with [`winit`].
+
+#![deny(rust_2018_idioms)]
+#![deny(rustdoc::broken_intra_doc_links)]
+#![deny(clippy::all)]
+#![deny(missing_debug_implementations)]
+#![deny(missing_docs)]
+#![cfg_attr(clippy, deny(warnings))]
+
+use glutin::config::{Config, ConfigTemplateBuilder};
+use glutin::display::{Display, DisplayApiPreference};
+use glutin::error::Error as GlutinError;
+#[cfg(x11_platform)]
+use glutin::platform::x11::X11GlConfigExt;
+use glutin::prelude::*;
+use raw_window_handle::{DisplayHandle, RawWindowHandle, WindowHandle};
+
+use crate::windowing::window::CreationAttributes as WindowCreationAttributes;
+
+#[cfg(all(not(egl_backend), not(glx_backend), not(wgl_backend), not(cgl_backend)))]
+compile_error!("Please select at least one api backend");
+
+/// The helper to perform [`Display`] creation and OpenGL platform
+/// bootstrapping with the help of [`winit`] with little to no platform specific
+/// code.
+///
+/// This is only required for the initial setup. If you want to create
+/// additional windows just use the [`finalize_window`] function and the
+/// configuration you've used either for the original window or picked with the
+/// existing [`Display`].
+///
+/// [`winit`]: winit
+/// [`Display`]: glutin::display::Display
+#[derive(Default, Debug, Clone)]
+pub struct DisplayBuilder
+{
+ preference: ApiPreference,
+ window_attributes: WindowCreationAttributes,
+}
+
+impl DisplayBuilder
+{
+ /// Create new display builder.
+ pub fn new() -> Self
+ {
+ Default::default()
+ }
+
+ /// The preference in picking the configuration.
+ #[allow(dead_code)]
+ pub fn with_preference(mut self, preference: ApiPreference) -> Self
+ {
+ self.preference = preference;
+ self
+ }
+
+ /// The window attributes to use when building a window.
+ ///
+ /// By default no window is created.
+ pub fn with_window_attributes(
+ mut self,
+ window_creation_attrs: WindowCreationAttributes,
+ ) -> Self
+ {
+ self.window_attributes = window_creation_attrs;
+ self
+ }
+
+ /// Initialize the OpenGL platform and create a compatible window to use
+ /// with it when the [`WindowAttributes`] was passed with
+ /// [`Self::with_window_attributes()`]. It's optional, since on some
+ /// platforms like `Android` it is not available early on, so you want to
+ /// find configuration and later use it with the [`finalize_window`].
+ /// But if you don't care about such platform you can always pass
+ /// [`WindowAttributes`].
+ ///
+ /// # Api-specific
+ ///
+ /// **WGL:** - [`WindowAttributes`] **must** be passed in
+ /// [`Self::with_window_attributes()`] if modern OpenGL(ES) is desired,
+ /// otherwise only builtin functions like `glClear` will be available.
+ pub fn build<ConfigPickerFn>(
+ self,
+ window_handle: Option<WindowHandle<'_>>,
+ display_handle: &DisplayHandle<'_>,
+ template_builder: ConfigTemplateBuilder,
+ config_picker_fn: ConfigPickerFn,
+ ) -> Result<(WindowCreationAttributes, Config), Error>
+ where
+ ConfigPickerFn: FnOnce(Box<dyn Iterator<Item = Config> + '_>) -> Option<Config>,
+ {
+ // XXX with WGL backend window should be created first.
+ let raw_window_handle = if cfg!(wgl_backend) {
+ let Some(window_handle) = window_handle else {
+ return Err(Error::WindowRequired);
+ };
+
+ Some(window_handle.as_raw())
+ } else {
+ None
+ };
+
+ let gl_display =
+ create_display(display_handle, self.preference, raw_window_handle)
+ .map_err(Error::CreateDisplayFailed)?;
+
+ // XXX the native window must be passed to config picker when WGL is used
+ // otherwise very limited OpenGL features will be supported.
+ #[cfg(wgl_backend)]
+ let template_builder = if let Some(raw_window_handle) = raw_window_handle {
+ template_builder.compatible_with_native_window(raw_window_handle)
+ } else {
+ template_builder
+ };
+
+ let template = template_builder.build();
+
+ // SAFETY: The RawWindowHandle passed on the config template
+ // (when cfg(wgl_backend)) will always point to a valid object since it is
+ // derived from the window_handle argument which when Some is a WindowHandle and
+ // WindowHandles always point to a valid object
+ let gl_configs = unsafe { gl_display.find_configs(template) }
+ .map_err(Error::FindConfigsFailed)?;
+
+ let picked_gl_config =
+ config_picker_fn(gl_configs).ok_or(Error::NoConfigPicked)?;
+
+ #[cfg(not(wgl_backend))]
+ let window_attrs =
+ { finalize_window_creation_attrs(self.window_attributes, &picked_gl_config) };
+
+ #[cfg(wgl_backend)]
+ let window_attrs = self.window_attributes;
+
+ Ok((window_attrs, picked_gl_config))
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error
+{
+ #[error("Failed to create display")]
+ CreateDisplayFailed(#[source] GlutinError),
+
+ #[error("Failed to find configs")]
+ FindConfigsFailed(#[source] GlutinError),
+
+ #[error("No config was picked by config picker function")]
+ NoConfigPicked,
+
+ #[error("Window required for building display on current platform")]
+ WindowRequired,
+}
+
+fn create_display(
+ display_handle: &DisplayHandle<'_>,
+ _api_preference: ApiPreference,
+ _raw_window_handle: Option<RawWindowHandle>,
+) -> Result<Display, GlutinError>
+{
+ #[cfg(egl_backend)]
+ let _preference = DisplayApiPreference::Egl;
+
+ #[cfg(glx_backend)]
+ let _preference = DisplayApiPreference::Glx(Box::new(
+ crate::windowing::window::platform::x11::register_xlib_error_hook,
+ ));
+
+ #[cfg(cgl_backend)]
+ let _preference = DisplayApiPreference::Cgl;
+
+ #[cfg(wgl_backend)]
+ let _preference = DisplayApiPreference::Wgl(_raw_window_handle);
+
+ #[cfg(all(egl_backend, glx_backend))]
+ let _preference = match _api_preference {
+ ApiPreference::PreferEgl => DisplayApiPreference::EglThenGlx(Box::new(
+ crate::windowing::window::platform::x11::register_xlib_error_hook,
+ )),
+ ApiPreference::FallbackEgl => DisplayApiPreference::GlxThenEgl(Box::new(
+ crate::windowing::window::platform::x11::register_xlib_error_hook,
+ )),
+ };
+
+ #[cfg(all(wgl_backend, egl_backend))]
+ let _preference = match _api_preference {
+ ApiPreference::PreferEgl => DisplayApiPreference::EglThenWgl(_raw_window_handle),
+ ApiPreference::FallbackEgl => {
+ DisplayApiPreference::WglThenEgl(_raw_window_handle)
+ }
+ };
+
+ let handle = display_handle.as_raw();
+ unsafe { Ok(Display::new(handle, _preference)?) }
+}
+
+/// Finalize [`Window`] creation by applying the options from the [`Config`], be
+/// aware that it could remove incompatible options from the window builder like
+/// `transparency`, when the provided config doesn't support it.
+///
+/// [`Window`]: winit::window::Window
+/// [`Config`]: glutin::config::Config
+#[cfg(not(wgl_backend))]
+fn finalize_window_creation_attrs(
+ mut attributes: WindowCreationAttributes,
+ gl_config: &Config,
+) -> WindowCreationAttributes
+{
+ // Disable transparency if the end config doesn't support it.
+ if gl_config.supports_transparency() == Some(false) {
+ attributes = attributes.with_transparent(false);
+ }
+
+ #[cfg(x11_platform)]
+ let attributes = if let Some(x11_visual) = gl_config.x11_visual() {
+ attributes.with_x11_visual(x11_visual.visual_id() as _)
+ } else {
+ attributes
+ };
+
+ attributes
+}
+
+/// Simplified version of the [`DisplayApiPreference`] which is used to simplify
+/// cross platform window creation.
+///
+/// To learn about platform differences the [`DisplayApiPreference`] variants.
+///
+/// [`DisplayApiPreference`]: glutin::display::DisplayApiPreference
+#[allow(dead_code)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub enum ApiPreference
+{
+ /// Prefer `EGL` over system provider like `GLX` and `WGL`.
+ PreferEgl,
+
+ /// Fallback to `EGL` when failed to create the system profile.
+ ///
+ /// This behavior is used by default. However consider using
+ /// [`Self::PreferEgl`] if you don't care about missing EGL features.
+ #[default]
+ FallbackEgl,
+}
diff --git a/engine/src/renderer/opengl/graphics_mesh.rs b/engine/src/renderer/opengl/graphics_mesh.rs
new file mode 100644
index 0000000..9b929c0
--- /dev/null
+++ b/engine/src/renderer/opengl/graphics_mesh.rs
@@ -0,0 +1,120 @@
+use opengl_bindings::buffer::{Buffer as GlBuffer, Usage as GlBufferUsage};
+use opengl_bindings::vertex_array::{
+ DataType as GlVertexArrayDataType,
+ VertexArray as GlVertexArray,
+};
+use opengl_bindings::CurrentContextWithFns as GlCurrentContextWithFns;
+
+use crate::mesh::Mesh;
+use crate::renderer::opengl::vertex::{
+ AttributeComponentType as VertexAttributeComponentType,
+ Vertex as RendererVertex,
+};
+
+#[derive(Debug)]
+pub struct GraphicsMesh
+{
+ /// Vertex and index buffer has to live as long as the vertex array
+ _vertex_buffer: GlBuffer<RendererVertex>,
+ pub index_buffer: Option<GlBuffer<u32>>,
+ pub element_cnt: u32,
+ pub vertex_arr: GlVertexArray,
+}
+
+impl GraphicsMesh
+{
+ #[tracing::instrument(skip_all)]
+ pub fn new(
+ current_context: &GlCurrentContextWithFns<'_>,
+ mesh: &Mesh,
+ ) -> Result<Self, Error>
+ {
+ tracing::trace!(
+ "Creating vertex array, vertex buffer{}",
+ if mesh.indices().is_some() {
+ " and index buffer"
+ } else {
+ ""
+ }
+ );
+
+ let vertex_arr = GlVertexArray::new(current_context);
+ let vertex_buffer = GlBuffer::new(current_context);
+
+ vertex_buffer
+ .store_mapped(
+ current_context,
+ mesh.vertices(),
+ GlBufferUsage::Static,
+ |vertex| RendererVertex {
+ pos: vertex.pos.into(),
+ texture_coords: vertex.texture_coords.into(),
+ normal: vertex.normal.into(),
+ },
+ )
+ .map_err(Error::StoreVerticesFailed)?;
+
+ vertex_arr.bind_vertex_buffer(current_context, 0, &vertex_buffer, 0);
+
+ let mut offset = 0u32;
+
+ for attrib in RendererVertex::attrs() {
+ vertex_arr.enable_attrib(current_context, attrib.index);
+
+ vertex_arr.set_attrib_format(
+ current_context,
+ attrib.index,
+ match attrib.component_type {
+ VertexAttributeComponentType::Float => GlVertexArrayDataType::Float,
+ },
+ false,
+ offset,
+ );
+
+ vertex_arr.set_attrib_vertex_buf_binding(current_context, attrib.index, 0);
+
+ offset += attrib.component_size * attrib.component_cnt as u32;
+ }
+
+ if let Some(indices) = mesh.indices() {
+ let index_buffer = GlBuffer::new(current_context);
+
+ index_buffer
+ .store(current_context, indices, GlBufferUsage::Static)
+ .map_err(Error::StoreIndicesFailed)?;
+
+ vertex_arr.bind_element_buffer(current_context, &index_buffer);
+
+ return Ok(Self {
+ _vertex_buffer: vertex_buffer,
+ index_buffer: Some(index_buffer),
+ element_cnt: indices
+ .len()
+ .try_into()
+ .expect("Mesh index count does not fit into a 32-bit unsigned int"),
+ vertex_arr,
+ });
+ }
+
+ Ok(Self {
+ _vertex_buffer: vertex_buffer,
+ index_buffer: None,
+ element_cnt: mesh
+ .vertices()
+ .len()
+ .try_into()
+ .expect("Mesh vertex count does not fit into a 32-bit unsigned int"),
+ vertex_arr,
+ })
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error
+{
+ #[error("Failed to store vertices in vertex buffer")]
+ StoreVerticesFailed(#[source] opengl_bindings::buffer::Error),
+
+ #[error("Failed to store indices in index buffer")]
+ StoreIndicesFailed(#[source] opengl_bindings::buffer::Error),
+}
diff --git a/engine/src/vertex.rs b/engine/src/renderer/opengl/vertex.rs
index 897ee97..5a1593e 100644
--- a/engine/src/vertex.rs
+++ b/engine/src/renderer/opengl/vertex.rs
@@ -1,23 +1,18 @@
-use std::mem::size_of;
+use safer_ffi::derive_ReprC;
-use crate::util::builder;
-use crate::vector::{Vec2, Vec3};
-
-builder! {
-#[builder(name = Builder, derives = (Debug, Default))]
-#[derive(Debug, Clone, Default)]
+#[derive(Debug, Clone)]
+#[derive_ReprC]
#[repr(C)]
pub struct Vertex
{
- pos: Vec3<f32>,
- texture_coords: Vec2<f32>,
- normal: Vec3<f32>,
-}
+ pub pos: opengl_bindings::data_types::Vec3<f32>,
+ pub texture_coords: opengl_bindings::data_types::Vec2<f32>,
+ pub normal: opengl_bindings::data_types::Vec3<f32>,
}
impl Vertex
{
- pub(crate) fn attrs() -> &'static [Attribute]
+ pub fn attrs() -> &'static [Attribute]
{
#[allow(clippy::cast_possible_truncation)]
&[
@@ -43,15 +38,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,
}
@@ -59,7 +56,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 f82b59d..d02b9ff 100644
--- a/engine/src/texture.rs
+++ b/engine/src/texture.rs
@@ -1,152 +1,37 @@
-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;
-
-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
{
- /// Opens a texture image.
- ///
- /// # Errors
- /// Will return `Err` if:
- /// - Opening the image fails
- /// - The image data is not 8-bit/color RGB
- #[allow(clippy::new_without_default)]
- pub fn open(path: &Path) -> Result<Self, 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::UnsupportedImageDataKind);
- }
- };
-
- let dimensions = Dimens {
- width: image.width(),
- height: image.height(),
- };
-
- let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
-
- Ok(Self {
- id: Id::new(id),
- image,
- pixel_data_format,
- dimensions,
- properties: Properties::default(),
- })
- }
-
- #[must_use]
- pub fn new_from_color(dimensions: &Dimens<u32>, color: &Color<u8>) -> Self
+ pub fn new(asset_handle: AssetHandle<Image>) -> 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);
-
Self {
- id: Id::new(id),
- image: image.into(),
- pixel_data_format: PixelDataFormat::Rgb8,
- dimensions: *dimensions,
+ asset_handle,
properties: Properties::default(),
}
}
- #[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)
+ pub fn with_properties(
+ asset_handle: AssetHandle<Image>,
+ properties: Properties,
+ ) -> Self
{
- NEXT_ID.fetch_sub(1, Ordering::Relaxed);
+ 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 kind")]
- UnsupportedImageDataKind,
-}
-
+builder! {
/// Texture properties
+#[builder(name = PropertiesBuilder, derives=(Debug, Clone))]
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Properties
@@ -155,6 +40,15 @@ pub struct Properties
pub magnifying_filter: Filtering,
pub minifying_filter: Filtering,
}
+}
+
+impl Properties
+{
+ pub fn builder() -> PropertiesBuilder
+ {
+ PropertiesBuilder::default()
+ }
+}
impl Default for Properties
{
@@ -168,30 +62,29 @@ impl Default for Properties
}
}
-/// Texture ID.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub struct Id
-{
- id: u32,
-}
-
-impl Id
+impl Default for PropertiesBuilder
{
- fn new(id: u32) -> Self
+ fn default() -> Self
{
- Self { id }
+ Properties::default().into()
}
+}
- pub(crate) fn into_inner(self) -> u32
- {
- self.id
- }
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[non_exhaustive]
+pub enum Filtering
+{
+ 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 a505a38..9174734 100644
--- a/engine/src/util.rs
+++ b/engine/src/util.rs
@@ -1,3 +1,73 @@
+use ecs::util::VecExt;
+
+#[derive(Debug)]
+pub struct MapVec<Key: Ord, Value>
+{
+ inner: Vec<(Key, Value)>,
+}
+
+impl<Key: Ord, Value> MapVec<Key, Value>
+{
+ pub fn insert(&mut self, key: Key, value: Value)
+ {
+ self.inner
+ .insert_at_part_pt_by_key((key, value), |(a_key, _)| a_key);
+ }
+
+ pub fn remove(&mut self, key: Key) -> Option<Value>
+ {
+ let index = self
+ .inner
+ .binary_search_by_key(&&key, |(a_key, _)| a_key)
+ .ok()?;
+
+ let (_, value) = self.inner.remove(index);
+
+ Some(value)
+ }
+
+ pub fn get(&self, key: &Key) -> Option<&Value>
+ {
+ let index = self
+ .inner
+ .binary_search_by_key(&key, |(a_key, _)| a_key)
+ .ok()?;
+
+ let Some((_, value)) = self.inner.get(index) else {
+ unreachable!(); // Reason: Index from binary search cannot be OOB
+ };
+
+ Some(value)
+ }
+
+ pub fn get_mut(&mut self, key: &Key) -> Option<&mut Value>
+ {
+ let index = self
+ .inner
+ .binary_search_by_key(&key, |(a_key, _)| a_key)
+ .ok()?;
+
+ let Some((_, value)) = self.inner.get_mut(index) else {
+ unreachable!(); // Reason: Index from binary search cannot be OOB
+ };
+
+ Some(value)
+ }
+
+ pub fn values(&self) -> impl Iterator<Item = &Value>
+ {
+ self.inner.iter().map(|(_, value)| value)
+ }
+}
+
+impl<Key: Ord, Value> Default for MapVec<Key, Value>
+{
+ fn default() -> Self
+ {
+ Self { inner: Vec::new() }
+ }
+}
+
macro_rules! try_option {
($expr: expr) => {
match $expr {
@@ -9,9 +79,6 @@ macro_rules! try_option {
};
}
-use std::mem::ManuallyDrop;
-use std::ops::{Deref, DerefMut};
-
pub(crate) use try_option;
macro_rules! or {
@@ -26,6 +93,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])*
@@ -37,7 +116,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,
)*
}
@@ -47,7 +127,7 @@ macro_rules! builder {
$visibility struct $name
{
$(
- $(#[$field_attr])*
+ $(#[doc = $field_doc])*
$field_visibility $field: $field_type,
)*
}
@@ -63,12 +143,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]
@@ -83,6 +169,7 @@ macro_rules! builder {
impl From<$name> for $builder_name
{
+ #[allow(unused_variables)]
fn from(built: $name) -> Self
{
Self {
@@ -94,39 +181,3 @@ macro_rules! builder {
}
};
}
-
-pub(crate) use builder;
-
-/// Wrapper that ensures the contained value will never be dropped.
-#[derive(Debug)]
-pub struct NeverDrop<Value>
-{
- value: ManuallyDrop<Value>,
-}
-
-impl<Value> NeverDrop<Value>
-{
- #[must_use]
- pub fn new(value: Value) -> Self
- {
- Self { value: ManuallyDrop::new(value) }
- }
-}
-
-impl<Value> Deref for NeverDrop<Value>
-{
- type Target = Value;
-
- fn deref(&self) -> &Self::Target
- {
- &self.value
- }
-}
-
-impl<Value> DerefMut for NeverDrop<Value>
-{
- fn deref_mut(&mut self) -> &mut Self::Target
- {
- &mut self.value
- }
-}
diff --git a/engine/src/window.rs b/engine/src/window.rs
deleted file mode 100644
index ccc1b8d..0000000
--- a/engine/src/window.rs
+++ /dev/null
@@ -1,318 +0,0 @@
-use std::borrow::Cow;
-use std::ffi::{CStr, CString};
-
-use ecs::actions::Actions;
-use ecs::extension::Collector as ExtensionCollector;
-use ecs::sole::Single;
-use ecs::Sole;
-use glfw::WindowSize;
-
-use crate::data_types::dimens::Dimens;
-use crate::event::{Conclude as ConcludeEvent, Start as StartEvent};
-use crate::vector::Vec2;
-
-mod reexports
-{
- pub use glfw::window::{
- CursorMode,
- Hint as CreationHint,
- HintValue as CreationHintValue,
- InputMode,
- Key,
- KeyModifiers,
- KeyState,
- };
-}
-
-pub use reexports::*;
-
-#[derive(Debug, Sole)]
-/// Has to be dropped last since it holds the OpenGL context.
-#[sole(drop_last)]
-pub struct Window
-{
- inner: glfw::Window,
-}
-
-impl Window
-{
- /// Returns a new Window builder.
- #[must_use]
- pub fn builder() -> Builder
- {
- Builder::default()
- }
-
- /// Sets the value of a input mode.
- ///
- /// # Errors
- /// Returns `Err` if the input mode is unsupported on the current system.
- pub fn set_input_mode(
- &self,
- input_mode: InputMode,
- enabled: bool,
- ) -> Result<(), Error>
- {
- Ok(self.inner.set_input_mode(input_mode, enabled)?)
- }
-
- /// Sets the cursor mode.
- ///
- /// # Errors
- /// If a platform error occurs.
- pub fn set_cursor_mode(&self, cursor_mode: CursorMode) -> Result<(), Error>
- {
- Ok(self.inner.set_cursor_mode(cursor_mode)?)
- }
-
- /// Returns whether or not the window should close. Will return true when the user has
- /// attempted to close the window.
- #[must_use]
- pub fn should_close(&self) -> bool
- {
- self.inner.should_close()
- }
-
- /// Processes all pending events.
- ///
- /// # Errors
- /// If a platform error occurs.
- pub fn poll_events(&self) -> Result<(), Error>
- {
- Ok(self.inner.poll_events()?)
- }
-
- /// Swaps the front and back buffers of the window.
- ///
- /// # Errors
- /// Will return `Err` if a platform error occurs or if no OpenGL window context
- /// is present.
- pub fn swap_buffers(&self) -> Result<(), Error>
- {
- Ok(self.inner.swap_buffers()?)
- }
-
- /// Returns the size of the window.
- ///
- /// # Errors
- /// Will return `Err` if a platform error occurs.
- pub fn size(&self) -> Result<Dimens<u32>, Error>
- {
- let size = self.inner.size()?;
-
- Ok(Dimens {
- width: size.width,
- height: size.height,
- })
- }
-
- /// Returns the address of the specified OpenGL function, if it is supported by the
- /// current OpenGL context.
- ///
- /// # Errors
- /// Will return `Err` if a platform error occurs or if no current context has
- /// been set.
- ///
- /// # Panics
- /// Will panic if the `proc_name` argument contains a nul byte.
- pub fn get_proc_address(
- &self,
- proc_name: &str,
- ) -> Result<unsafe extern "C" fn(), Error>
- {
- let proc_name_c: Cow<CStr> = CStr::from_bytes_with_nul(proc_name.as_bytes())
- .map(Cow::Borrowed)
- .or_else(|_| CString::new(proc_name).map(Cow::Owned))
- .expect("OpenGL function name contains a nul byte");
-
- Ok(self.inner.get_proc_address(&proc_name_c)?)
- }
-
- /// Makes the OpenGL context of the window current for the calling thread.
- ///
- /// # Errors
- /// Will return `Err` if a platform error occurs or if no OpenGL context is
- /// present.
- pub fn make_context_current(&self) -> Result<(), Error>
- {
- Ok(self.inner.make_context_current()?)
- }
-
- /// Sets the window's framebuffer size callback.
- pub fn set_framebuffer_size_callback(&self, callback: impl Fn(Dimens<u32>) + 'static)
- {
- self.inner.set_framebuffer_size_callback(move |size| {
- callback(Dimens {
- width: size.width,
- height: size.height,
- });
- });
- }
-
- /// Sets the window's key size callback.
- pub fn set_key_callback(
- &self,
- callback: impl Fn(Key, i32, KeyState, KeyModifiers) + 'static,
- )
- {
- self.inner.set_key_callback(callback);
- }
-
- /// Sets the window's cursor position callback.
- pub fn set_cursor_pos_callback(&self, callback: impl Fn(Vec2<f64>) + 'static)
- {
- self.inner
- .set_cursor_pos_callback(move |pos| callback(Vec2 { x: pos.x, y: pos.y }));
- }
-
- /// Sets the window's close callback.
- pub fn set_close_callback(&self, callback: impl Fn() + 'static)
- {
- self.inner.set_close_callback(callback);
- }
-
- /// Sets the window's focus callback. The callback is called when the window loses or
- /// gains input focus.
- pub fn set_focus_callback(&self, callback: impl Fn(bool) + 'static)
- {
- self.inner.set_focus_callback(callback);
- }
-}
-
-/// [`Window`] builder.
-#[derive(Debug, Clone, Default)]
-pub struct Builder
-{
- inner: glfw::WindowBuilder,
-}
-
-impl Builder
-{
- #[must_use]
- pub fn creation_hint(mut self, hint: CreationHint, value: CreationHintValue) -> Self
- {
- self.inner = self.inner.hint(hint, value);
-
- self
- }
-
- /// Creates a new window.
- ///
- /// # Errors
- /// Will return `Err` if the title contains a internal nul byte or if a platform error
- /// occurs.
- pub fn create(&self, size: Dimens<u32>, title: &str) -> Result<Window, Error>
- {
- let builder = self.inner.clone().hint(
- CreationHint::OpenGLDebugContext,
- CreationHintValue::Bool(cfg!(feature = "debug")),
- );
-
- let window = builder.create(
- &WindowSize {
- width: size.width,
- height: size.height,
- },
- title,
- )?;
-
- Ok(Window { inner: window })
- }
-}
-
-#[derive(Debug)]
-pub struct Extension
-{
- window_builder: Builder,
- window_size: Dimens<u32>,
- window_title: String,
-}
-
-impl Extension
-{
- #[must_use]
- pub fn new(window_builder: Builder) -> Self
- {
- Self { window_builder, ..Default::default() }
- }
-
- #[must_use]
- pub fn window_size(mut self, window_size: Dimens<u32>) -> Self
- {
- self.window_size = window_size;
-
- self
- }
-
- #[must_use]
- pub fn window_title(mut self, window_title: impl Into<String>) -> Self
- {
- self.window_title = window_title.into();
-
- self
- }
-}
-
-impl ecs::extension::Extension for Extension
-{
- fn collect(self, mut collector: ExtensionCollector<'_>)
- {
- collector.add_system(StartEvent, initialize);
- collector.add_system(ConcludeEvent, update);
-
- let window = self
- .window_builder
- .create(self.window_size, &self.window_title)
- .unwrap();
-
- window.set_cursor_mode(CursorMode::Normal).unwrap();
-
- collector.add_sole(window).ok();
- }
-}
-
-impl Default for Extension
-{
- fn default() -> Self
- {
- Self {
- window_builder: Builder::default(),
- window_size: Dimens { width: 1920, height: 1080 },
- window_title: String::new(),
- }
- }
-}
-
-#[derive(Debug, thiserror::Error)]
-#[error(transparent)]
-pub struct Error(glfw::Error);
-
-impl From<glfw::Error> for Error
-{
- fn from(err: glfw::Error) -> Self
- {
- Self(err)
- }
-}
-
-fn initialize(window: Single<Window>, actions: Actions)
-{
- let actions_weak_ref = actions.to_weak_ref();
-
- window.set_close_callback(move || {
- let actions_weak_ref = actions_weak_ref.clone();
-
- let actions_ref = actions_weak_ref.access().expect("No world");
-
- actions_ref.to_actions().stop();
- });
-}
-
-fn update(window: Single<Window>)
-{
- window
- .swap_buffers()
- .expect("Failed to swap window buffers");
-
- window.poll_events().expect("Failed to poll window events");
-}
diff --git a/engine/src/windowing.rs b/engine/src/windowing.rs
new file mode 100644
index 0000000..69adae9
--- /dev/null
+++ b/engine/src/windowing.rs
@@ -0,0 +1,669 @@
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::{Arc, Weak};
+use std::thread::{spawn, JoinHandle as ThreadJoinHandle};
+
+use crossbeam_channel::{
+ bounded as bounded_channel,
+ Receiver as ChannelReceiver,
+ Sender as ChannelSender,
+ TrySendError,
+};
+use ecs::actions::Actions;
+use ecs::component::Component;
+use ecs::entity::obtainer::Obtainer as EntityObtainer;
+use ecs::event::component::{Added, Changed, Removed};
+use ecs::pair::{ChildOf, Pair};
+use ecs::phase::{Phase, UPDATE as UPDATE_PHASE};
+use ecs::sole::Single;
+use ecs::system::observer::Observe;
+use ecs::uid::Uid;
+use ecs::{declare_entity, Query, Sole};
+use raw_window_handle::{DisplayHandle, HandleError, HasDisplayHandle, WindowHandle};
+use winit::application::ApplicationHandler;
+use winit::dpi::PhysicalPosition;
+use winit::error::EventLoopError;
+use winit::event::{DeviceEvent, DeviceId, StartCause, WindowEvent};
+use winit::event_loop::{
+ ActiveEventLoop,
+ ControlFlow as EventLoopControlFlow,
+ EventLoop,
+ OwnedDisplayHandle,
+};
+use winit::keyboard::PhysicalKey;
+use winit::window::{Window as WinitWindow, WindowId as WinitWindowId};
+
+use crate::data_types::dimens::Dimens;
+use crate::util::MapVec;
+use crate::vector::Vec2;
+use crate::windowing::keyboard::{Key, KeyState, Keyboard, UnknownKeyCodeError};
+use crate::windowing::mouse::{
+ Button as MouseButton,
+ ButtonState as MouseButtonState,
+ Buttons as MouseButtons,
+ Motion as MouseMotion,
+};
+use crate::windowing::window::{
+ Closed as WindowClosed,
+ CreationAttributes as WindowCreationAttributes,
+ CreationReady as WindowCreationReady,
+ CursorGrabMode,
+ Id as WindowId,
+ Window,
+};
+
+pub mod keyboard;
+pub mod mouse;
+pub mod window;
+
+const MESSAGE_FROM_APP_CHANNEL_CAP: usize = 128;
+
+const MESSAGE_TO_APP_CHANNEL_CAP: usize = 16; // Increase if more messages are added
+
+declare_entity!(
+ pub PHASE,
+ (
+ Phase,
+ Pair::builder()
+ .relation::<ChildOf>()
+ .target_id(*UPDATE_PHASE)
+ .build()
+ )
+);
+
+#[derive(Debug, Default)]
+#[non_exhaustive]
+pub struct Extension {}
+
+impl ecs::extension::Extension for Extension
+{
+ fn collect(self, mut collector: ecs::extension::Collector<'_>)
+ {
+ collector.add_sole(Context::default()).ok();
+ collector.add_sole(Keyboard::default()).ok();
+ collector.add_sole(MouseMotion::default()).ok();
+ collector.add_sole(MouseButtons::default()).ok();
+
+ collector.add_declared_entity(&PHASE);
+
+ collector.add_system(*PHASE, update_stuff);
+
+ collector.add_observer(handle_window_changed);
+ collector.add_observer(handle_window_removed);
+ collector.add_observer(handle_window_creation_ready);
+ }
+}
+
+fn handle_window_creation_ready(
+ observe: Observe<Pair<Added, WindowCreationReady>>,
+ context: Single<Context>,
+)
+{
+ for evt_match in &observe {
+ let Some(ent) = evt_match.get_entity() else {
+ unreachable!();
+ };
+
+ if ent.has_component(Window::id()) || ent.has_component(WindowClosed::id()) {
+ continue;
+ }
+
+ let Some(window_creation_attrs) = ent.get::<WindowCreationAttributes>() else {
+ unreachable!();
+ };
+
+ context.try_send_message_to_app(MessageToApp::CreateWindow(
+ ent.uid(),
+ window_creation_attrs.clone(),
+ ));
+ }
+}
+
+#[tracing::instrument(skip_all)]
+fn update_stuff(
+ mut context: Single<Context>,
+ mut keyboard: Single<Keyboard>,
+ mut mouse_motion: Single<MouseMotion>,
+ mut mouse_buttons: Single<MouseButtons>,
+ mut actions: Actions,
+ entity_obtainer: EntityObtainer,
+)
+{
+ keyboard.make_key_states_previous();
+ mouse_buttons.make_states_previous();
+ mouse_motion.position_delta = Vec2::default();
+
+ let Context {
+ ref message_from_app_receiver,
+ ref mut display_handle,
+ ref mut windows,
+ ..
+ } = *context;
+
+ for message in message_from_app_receiver.try_iter() {
+ match message {
+ MessageFromApp::Init(new_display_handle) => {
+ *display_handle = Some(new_display_handle);
+ }
+ MessageFromApp::WindowCreated(
+ window_ent_id,
+ winit_window,
+ window_creation_attrs,
+ ) => {
+ actions.add_components(
+ window_ent_id,
+ (Window::new(&winit_window, &window_creation_attrs),),
+ );
+
+ actions.remove_comps::<(WindowCreationReady,)>(window_ent_id);
+
+ tracing::debug!("Added window component to window entity");
+
+ windows.insert(
+ WindowId::from_inner(winit_window.id()),
+ (winit_window, window_ent_id),
+ );
+ }
+ MessageFromApp::WindowResized(window_id, new_window_size) => {
+ let Some(window_ent_id) =
+ windows.get(&window_id).map(|(_, ent_id)| ent_id)
+ else {
+ continue;
+ };
+
+ let Some(window_ent) = entity_obtainer.get_entity(*window_ent_id) else {
+ continue;
+ };
+
+ let Some(mut window) = window_ent.get_mut::<Window>() else {
+ continue;
+ };
+
+ window.set_inner_size(new_window_size);
+
+ window.set_changed();
+ }
+ MessageFromApp::WindowCloseRequested(window_id) => {
+ let Some(window_ent_id) =
+ windows.get(&window_id).map(|(_, ent_id)| ent_id)
+ else {
+ tracing::error!(
+ wid = ?window_id,
+ "Window does not exist in windowing context"
+ );
+ continue;
+ };
+
+ actions.remove_comps::<(Window,)>(*window_ent_id);
+ }
+ MessageFromApp::KeyboardKeyStateChanged(key, key_state) => {
+ keyboard.set_key_state(key, key_state);
+ }
+ MessageFromApp::MouseMoved { position_delta } => {
+ mouse_motion.position_delta += position_delta;
+ }
+ MessageFromApp::MouseButtonStateChanged(mouse_button, mouse_button_state) => {
+ mouse_buttons.set(mouse_button, mouse_button_state);
+ }
+ }
+ }
+}
+
+fn handle_window_changed(
+ observe: Observe<'_, Pair<Changed, Window>>,
+ context: Single<Context>,
+)
+{
+ for evt_match in &observe {
+ let window_ent_id = evt_match.id();
+
+ let window = evt_match.get_changed_comp();
+
+ let Some((winit_window, _)) = context.windows.get(&window.wid()) else {
+ tracing::error!(
+ wid = ?window.wid(),
+ entity_id = %window_ent_id,
+ "Window does not exist in windowing context",
+ );
+ continue;
+ };
+
+ window.apply(winit_window);
+
+ context.try_send_message_to_app(MessageToApp::SetWindowCursorGrabMode(
+ window.wid(),
+ window.cursor_grab_mode,
+ ));
+ }
+}
+
+fn handle_window_removed(
+ observe: Observe<Pair<Removed, Window>>,
+ window_query: Query<(&Window,)>,
+ mut context: Single<Context>,
+ mut actions: Actions,
+)
+{
+ for evt_match in &observe {
+ let window = evt_match.get_removed_comp();
+
+ context.windows.remove(window.wid());
+
+ actions.add_components(evt_match.id(), (WindowClosed,));
+ }
+
+ if window_query.iter().count() == 1 {
+ actions.stop();
+ }
+}
+
+#[derive(Debug, Sole)]
+pub struct Context
+{
+ _thread: ThreadJoinHandle<()>,
+ is_dropped: Arc<AtomicBool>,
+ message_from_app_receiver: ChannelReceiver<MessageFromApp>,
+ message_to_app_sender: ChannelSender<MessageToApp>,
+ display_handle: Option<OwnedDisplayHandle>,
+ windows: MapVec<WindowId, (Arc<winit::window::Window>, Uid)>,
+}
+
+impl Context
+{
+ pub fn display_handle(&self) -> Option<DisplayHandle<'_>>
+ {
+ let display_handle = self.display_handle.as_ref()?;
+
+ display_handle.display_handle().ok()
+ }
+
+ /// Returns the specified window as a window handle, if it exists.
+ ///
+ /// # Safety
+ /// The Window handle must only be used with thread safe APIs.
+ pub unsafe fn get_window_as_handle(
+ &self,
+ window_id: &WindowId,
+ ) -> Option<Result<WindowHandle<'_>, HandleError>>
+ {
+ self.windows.get(window_id).map(|(winit_window, _)| {
+ #[cfg(windows)]
+ {
+ use winit::platform::windows::WindowExtWindows;
+
+ // SAFETY: I don't care
+ unsafe { winit_window.window_handle_any_thread() }
+ }
+
+ #[cfg(not(windows))]
+ {
+ use raw_window_handle::HasWindowHandle;
+
+ winit_window.window_handle()
+ }
+ })
+ }
+
+ fn try_send_message_to_app(&self, message: MessageToApp)
+ {
+ if let Err(err) = self.message_to_app_sender.try_send(message) {
+ let error = match &err {
+ TrySendError::Full(_) => TrySendError::Full(()),
+ TrySendError::Disconnected(_) => TrySendError::Disconnected(()),
+ };
+
+ let message = err.into_inner();
+
+ tracing::error!("Failed to send message {error}: {message:?}");
+ }
+ }
+}
+
+impl Default for Context
+{
+ fn default() -> Self
+ {
+ let is_dropped = Arc::new(AtomicBool::new(false));
+
+ let is_dropped_b = is_dropped.clone();
+
+ let (message_from_app_sender, message_from_app_receiver) =
+ bounded_channel::<MessageFromApp>(MESSAGE_FROM_APP_CHANNEL_CAP);
+
+ let message_from_app_receiver_b = message_from_app_receiver.clone();
+
+ let (message_to_app_sender, message_to_app_receiver) =
+ bounded_channel::<MessageToApp>(MESSAGE_TO_APP_CHANNEL_CAP);
+
+ Self {
+ _thread: spawn(move || {
+ let mut app = App {
+ message_from_app_sender,
+ message_from_app_receiver: message_from_app_receiver_b,
+ message_to_app_receiver,
+ is_dropped: is_dropped_b,
+ windows: MapVec::default(),
+ focused_window_id: None,
+ };
+
+ let event_loop = match create_event_loop() {
+ Ok(event_loop) => event_loop,
+ Err(err) => {
+ tracing::error!("Failed to create event loop: {err}");
+ return;
+ }
+ };
+
+ event_loop.set_control_flow(EventLoopControlFlow::Poll);
+
+ if let Err(err) = event_loop.run_app(&mut app) {
+ tracing::error!("Event loop error occurred: {err}");
+ }
+ }),
+ is_dropped,
+ message_from_app_receiver,
+ message_to_app_sender,
+ display_handle: None,
+ windows: MapVec::default(),
+ }
+ }
+}
+
+impl Drop for Context
+{
+ fn drop(&mut self)
+ {
+ self.is_dropped.store(true, Ordering::Relaxed);
+ }
+}
+
+fn create_event_loop() -> Result<EventLoop<()>, EventLoopError>
+{
+ let mut event_loop_builder = EventLoop::builder();
+
+ #[cfg(any(x11_platform, wayland_platform))]
+ winit::platform::x11::EventLoopBuilderExtX11::with_any_thread(
+ &mut event_loop_builder,
+ true,
+ );
+
+ #[cfg(windows)]
+ winit::platform::windows::EventLoopBuilderExtWindows::with_any_thread(
+ &mut event_loop_builder,
+ true,
+ );
+
+ #[cfg(not(any(x11_platform, wayland_platform, windows)))]
+ compile_error!("Unsupported platform");
+
+ event_loop_builder.build()
+}
+
+#[derive(Debug)]
+enum MessageFromApp
+{
+ Init(OwnedDisplayHandle),
+ WindowCreated(Uid, Arc<WinitWindow>, WindowCreationAttributes),
+ WindowResized(WindowId, Dimens<u32>),
+ WindowCloseRequested(WindowId),
+ KeyboardKeyStateChanged(Key, KeyState),
+ MouseMoved
+ {
+ position_delta: Vec2<f64>,
+ },
+ MouseButtonStateChanged(MouseButton, MouseButtonState),
+}
+
+#[derive(Debug)]
+enum MessageToApp
+{
+ CreateWindow(Uid, WindowCreationAttributes),
+ SetWindowCursorGrabMode(WindowId, CursorGrabMode),
+}
+
+#[derive(Debug)]
+struct App
+{
+ message_from_app_sender: ChannelSender<MessageFromApp>,
+ message_from_app_receiver: ChannelReceiver<MessageFromApp>,
+ message_to_app_receiver: ChannelReceiver<MessageToApp>,
+ is_dropped: Arc<AtomicBool>,
+ windows: MapVec<WindowId, (Weak<WinitWindow>, WindowSettings)>,
+ focused_window_id: Option<WindowId>,
+}
+
+impl App
+{
+ fn handle_received_messages(&mut self, event_loop: &ActiveEventLoop)
+ {
+ for message in self.message_to_app_receiver.try_iter() {
+ match message {
+ MessageToApp::CreateWindow(window_ent_id, window_creation_attrs) => {
+ tracing::info!(
+ "Creating window with title {}",
+ window_creation_attrs.title()
+ );
+
+ let winit_window = Arc::new(
+ match event_loop
+ .create_window(window_creation_attrs.clone().into_inner())
+ {
+ Ok(window) => window,
+ Err(err) => {
+ tracing::error!("Failed to create window: {err}");
+ continue;
+ }
+ },
+ );
+
+ tracing::info!("Created window has title {}", winit_window.title());
+
+ self.windows.insert(
+ WindowId::from_inner(winit_window.id()),
+ (Arc::downgrade(&winit_window), WindowSettings::default()),
+ );
+
+ self.send_message(MessageFromApp::WindowCreated(
+ window_ent_id,
+ winit_window,
+ window_creation_attrs,
+ ));
+ }
+ MessageToApp::SetWindowCursorGrabMode(window_id, cursor_grab_mode) => {
+ let Some((_, window_settings)) = self.windows.get_mut(&window_id)
+ else {
+ tracing::warn!(
+ window_id=?window_id,
+ "Cannot set window cursor grab mode. Window not found"
+ );
+
+ continue;
+ };
+
+ window_settings.cursor_grab_mode = cursor_grab_mode;
+ }
+ }
+ }
+ }
+
+ fn send_message(&self, message: MessageFromApp)
+ {
+ if self.message_from_app_sender.is_full() {
+ tracing::warn!(
+ "Message channel is full! Dropping oldest message from channel"
+ );
+
+ self.message_from_app_receiver.try_recv().ok();
+ }
+
+ if let Err(err) = self.message_from_app_sender.try_send(message) {
+ let error = match &err {
+ TrySendError::Full(_) => TrySendError::Full(()),
+ TrySendError::Disconnected(_) => TrySendError::Disconnected(()),
+ };
+
+ let message = err.into_inner();
+
+ tracing::error!("Failed to send message {error}: {message:?}");
+ }
+ }
+}
+
+impl ApplicationHandler for App
+{
+ fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause)
+ {
+ match cause {
+ StartCause::Init => {
+ self.send_message(MessageFromApp::Init(
+ event_loop.owned_display_handle(),
+ ));
+ }
+ StartCause::Poll => {
+ if self.is_dropped.load(Ordering::Relaxed) {
+ event_loop.exit();
+ return;
+ }
+
+ self.handle_received_messages(event_loop);
+ }
+ _ => {}
+ }
+ }
+
+ fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop)
+ {
+ for (window, _) in self.windows.values() {
+ let Some(window) = window.upgrade() else {
+ continue;
+ };
+
+ window.request_redraw();
+ }
+ }
+
+ fn resumed(&mut self, _event_loop: &ActiveEventLoop) {}
+
+ fn window_event(
+ &mut self,
+ _event_loop: &ActiveEventLoop,
+ window_id: WinitWindowId,
+ event: WindowEvent,
+ )
+ {
+ match event {
+ WindowEvent::Resized(new_window_size) => {
+ self.send_message(MessageFromApp::WindowResized(
+ WindowId::from_inner(window_id),
+ new_window_size.into(),
+ ));
+ }
+ WindowEvent::CloseRequested => {
+ self.send_message(MessageFromApp::WindowCloseRequested(
+ WindowId::from_inner(window_id),
+ ));
+ }
+ WindowEvent::KeyboardInput {
+ device_id: _,
+ event: keyboard_event,
+ is_synthetic: _,
+ } => {
+ if keyboard_event.repeat {
+ return;
+ }
+
+ let key_code = match keyboard_event.physical_key {
+ PhysicalKey::Code(key_code) => key_code,
+ PhysicalKey::Unidentified(native_key) => {
+ tracing::warn!("Ignoring unidentified key: {native_key:?}");
+ return;
+ }
+ };
+
+ let key: Key = match key_code.try_into() {
+ Ok(key) => key,
+ Err(UnknownKeyCodeError) => {
+ tracing::warn!("Ignoring key with unknown key code {key_code:?}");
+ return;
+ }
+ };
+
+ self.send_message(MessageFromApp::KeyboardKeyStateChanged(
+ key,
+ keyboard_event.state.into(),
+ ));
+ }
+ WindowEvent::MouseInput { device_id: _, state, button } => {
+ self.send_message(MessageFromApp::MouseButtonStateChanged(
+ button.into(),
+ state.into(),
+ ));
+ }
+ WindowEvent::Focused(is_focused) => {
+ if is_focused {
+ self.focused_window_id = Some(WindowId::from_inner(window_id));
+ } else {
+ self.focused_window_id = None;
+ }
+ }
+ _ => {}
+ }
+ }
+
+ fn device_event(
+ &mut self,
+ _event_loop: &ActiveEventLoop,
+ _device_id: DeviceId,
+ device_event: DeviceEvent,
+ )
+ {
+ match device_event {
+ DeviceEvent::MouseMotion { delta } => {
+ self.send_message(MessageFromApp::MouseMoved {
+ position_delta: Vec2 { x: delta.0, y: delta.1 },
+ });
+
+ let Some(focused_window_id) = self.focused_window_id else {
+ return;
+ };
+
+ let Some((focused_window, focused_window_settings)) =
+ self.windows.get(&focused_window_id)
+ else {
+ tracing::error!(
+ window_id=?focused_window_id,
+ "Focused window not found"
+ );
+ return;
+ };
+
+ if focused_window_settings.cursor_grab_mode != CursorGrabMode::Locked {
+ return;
+ }
+
+ // TODO: This might need to be optimized
+ let Some(focused_window) = focused_window.upgrade() else {
+ return;
+ };
+
+ let focused_window_size = focused_window.inner_size();
+
+ if let Err(err) = focused_window.set_cursor_position(PhysicalPosition {
+ x: focused_window_size.width / 2,
+ y: focused_window_size.height / 2,
+ }) {
+ tracing::error!(
+ window_id=?focused_window_id,
+ "Failed to set cursor position in focused window: {err}"
+ );
+ };
+ }
+ _ => {}
+ }
+ }
+}
+
+#[derive(Debug, Default)]
+struct WindowSettings
+{
+ cursor_grab_mode: CursorGrabMode,
+}
diff --git a/engine/src/windowing/keyboard.rs b/engine/src/windowing/keyboard.rs
new file mode 100644
index 0000000..e4fffe5
--- /dev/null
+++ b/engine/src/windowing/keyboard.rs
@@ -0,0 +1,763 @@
+use std::collections::HashMap;
+
+use ecs::Sole;
+
+#[derive(Debug, Default, Sole)]
+pub struct Keyboard
+{
+ map: HashMap<Key, KeyData>,
+}
+
+impl Keyboard
+{
+ #[must_use]
+ pub fn get_key_state(&self, key: Key) -> KeyState
+ {
+ let Some(key_data) = self.map.get(&key) else {
+ return KeyState::Released;
+ };
+
+ key_data.curr_state
+ }
+
+ #[must_use]
+ pub fn get_prev_key_state(&self, key: Key) -> KeyState
+ {
+ let Some(key_data) = self.map.get(&key) else {
+ return KeyState::Released;
+ };
+
+ key_data.previous_state
+ }
+
+ pub fn set_key_state(&mut self, key: Key, key_state: KeyState)
+ {
+ let key_data = self.map.entry(key).or_default();
+
+ key_data.curr_state = key_state;
+ }
+
+ pub fn make_key_states_previous(&mut self)
+ {
+ for key_data in self.map.values_mut() {
+ key_data.previous_state = key_data.curr_state;
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[non_exhaustive]
+pub enum Key
+{
+ /// <kbd>`</kbd> on a US keyboard. This is also called a backtick or grave.
+ /// This is the <kbd>半角</kbd>/<kbd>全角</kbd>/<kbd>漢字</kbd>
+ /// (hankaku/zenkaku/kanji) key on Japanese keyboards
+ Backquote,
+ /// Used for both the US <kbd>\\</kbd> (on the 101-key layout) and also for the key
+ /// located between the <kbd>"</kbd> and <kbd>Enter</kbd> keys on row C of the 102-,
+ /// 104- and 106-key layouts.
+ /// Labeled <kbd>#</kbd> on a UK (102) keyboard.
+ Backslash,
+ /// <kbd>[</kbd> on a US keyboard.
+ BracketLeft,
+ /// <kbd>]</kbd> on a US keyboard.
+ BracketRight,
+ /// <kbd>,</kbd> on a US keyboard.
+ Comma,
+ /// <kbd>0</kbd> on a US keyboard.
+ Digit0,
+ /// <kbd>1</kbd> on a US keyboard.
+ Digit1,
+ /// <kbd>2</kbd> on a US keyboard.
+ Digit2,
+ /// <kbd>3</kbd> on a US keyboard.
+ Digit3,
+ /// <kbd>4</kbd> on a US keyboard.
+ Digit4,
+ /// <kbd>5</kbd> on a US keyboard.
+ Digit5,
+ /// <kbd>6</kbd> on a US keyboard.
+ Digit6,
+ /// <kbd>7</kbd> on a US keyboard.
+ Digit7,
+ /// <kbd>8</kbd> on a US keyboard.
+ Digit8,
+ /// <kbd>9</kbd> on a US keyboard.
+ Digit9,
+ /// <kbd>=</kbd> on a US keyboard.
+ Equal,
+ /// Located between the left <kbd>Shift</kbd> and <kbd>Z</kbd> keys.
+ /// Labeled <kbd>\\</kbd> on a UK keyboard.
+ IntlBackslash,
+ /// Located between the <kbd>/</kbd> and right <kbd>Shift</kbd> keys.
+ /// Labeled <kbd>\\</kbd> (ro) on a Japanese keyboard.
+ IntlRo,
+ /// Located between the <kbd>=</kbd> and <kbd>Backspace</kbd> keys.
+ /// Labeled <kbd>¥</kbd> (yen) on a Japanese keyboard. <kbd>\\</kbd> on a
+ /// Russian keyboard.
+ IntlYen,
+ /// <kbd>a</kbd> on a US keyboard.
+ /// Labeled <kbd>q</kbd> on an AZERTY (e.g., French) keyboard.
+ A,
+ /// <kbd>b</kbd> on a US keyboard.
+ B,
+ /// <kbd>c</kbd> on a US keyboard.
+ C,
+ /// <kbd>d</kbd> on a US keyboard.
+ D,
+ /// <kbd>e</kbd> on a US keyboard.
+ E,
+ /// <kbd>f</kbd> on a US keyboard.
+ F,
+ /// <kbd>g</kbd> on a US keyboard.
+ G,
+ /// <kbd>h</kbd> on a US keyboard.
+ H,
+ /// <kbd>i</kbd> on a US keyboard.
+ I,
+ /// <kbd>j</kbd> on a US keyboard.
+ J,
+ /// <kbd>k</kbd> on a US keyboard.
+ K,
+ /// <kbd>l</kbd> on a US keyboard.
+ L,
+ /// <kbd>m</kbd> on a US keyboard.
+ M,
+ /// <kbd>n</kbd> on a US keyboard.
+ N,
+ /// <kbd>o</kbd> on a US keyboard.
+ O,
+ /// <kbd>p</kbd> on a US keyboard.
+ P,
+ /// <kbd>q</kbd> on a US keyboard.
+ /// Labeled <kbd>a</kbd> on an AZERTY (e.g., French) keyboard.
+ Q,
+ /// <kbd>r</kbd> on a US keyboard.
+ R,
+ /// <kbd>s</kbd> on a US keyboard.
+ S,
+ /// <kbd>t</kbd> on a US keyboard.
+ T,
+ /// <kbd>u</kbd> on a US keyboard.
+ U,
+ /// <kbd>v</kbd> on a US keyboard.
+ V,
+ /// <kbd>w</kbd> on a US keyboard.
+ /// Labeled <kbd>z</kbd> on an AZERTY (e.g., French) keyboard.
+ W,
+ /// <kbd>x</kbd> on a US keyboard.
+ X,
+ /// <kbd>y</kbd> on a US keyboard.
+ /// Labeled <kbd>z</kbd> on a QWERTZ (e.g., German) keyboard.
+ Y,
+ /// <kbd>z</kbd> on a US keyboard.
+ /// Labeled <kbd>w</kbd> on an AZERTY (e.g., French) keyboard, and <kbd>y</kbd> on a
+ /// QWERTZ (e.g., German) keyboard.
+ Z,
+ /// <kbd>-</kbd> on a US keyboard.
+ Minus,
+ /// <kbd>.</kbd> on a US keyboard.
+ Period,
+ /// <kbd>'</kbd> on a US keyboard.
+ Quote,
+ /// <kbd>;</kbd> on a US keyboard.
+ Semicolon,
+ /// <kbd>/</kbd> on a US keyboard.
+ Slash,
+ /// <kbd>Alt</kbd>, <kbd>Option</kbd>, or <kbd>⌥</kbd>.
+ AltLeft,
+ /// <kbd>Alt</kbd>, <kbd>Option</kbd>, or <kbd>⌥</kbd>.
+ /// This is labeled <kbd>AltGr</kbd> on many keyboard layouts.
+ AltRight,
+ /// <kbd>Backspace</kbd> or <kbd>⌫</kbd>.
+ /// Labeled <kbd>Delete</kbd> on Apple keyboards.
+ Backspace,
+ /// <kbd>CapsLock</kbd> or <kbd>⇪</kbd>
+ CapsLock,
+ /// The application context menu key, which is typically found between the right
+ /// <kbd>Super</kbd> key and the right <kbd>Control</kbd> key.
+ ContextMenu,
+ /// <kbd>Control</kbd> or <kbd>⌃</kbd>
+ ControlLeft,
+ /// <kbd>Control</kbd> or <kbd>⌃</kbd>
+ ControlRight,
+ /// <kbd>Enter</kbd> or <kbd>↵</kbd>. Labeled <kbd>Return</kbd> on Apple keyboards.
+ Enter,
+ /// The Windows, <kbd>⌘</kbd>, <kbd>Command</kbd>, or other OS symbol key.
+ SuperLeft,
+ /// The Windows, <kbd>⌘</kbd>, <kbd>Command</kbd>, or other OS symbol key.
+ SuperRight,
+ /// <kbd>Shift</kbd> or <kbd>⇧</kbd>
+ ShiftLeft,
+ /// <kbd>Shift</kbd> or <kbd>⇧</kbd>
+ ShiftRight,
+ /// <kbd> </kbd> (space)
+ Space,
+ /// <kbd>Tab</kbd> or <kbd>⇥</kbd>
+ Tab,
+ /// Japanese: <kbd>変</kbd> (henkan)
+ Convert,
+ /// Japanese: <kbd>カタカナ</kbd>/<kbd>ひらがな</kbd>/<kbd>ローマ字</kbd>
+ /// (katakana/hiragana/romaji)
+ KanaMode,
+ /// Korean: HangulMode <kbd>한/영</kbd> (han/yeong)
+ ///
+ /// Japanese (Mac keyboard): <kbd>か</kbd> (kana)
+ Lang1,
+ /// Korean: Hanja <kbd>한</kbd> (hanja)
+ ///
+ /// Japanese (Mac keyboard): <kbd>英</kbd> (eisu)
+ Lang2,
+ /// Japanese (word-processing keyboard): Katakana
+ Lang3,
+ /// Japanese (word-processing keyboard): Hiragana
+ Lang4,
+ /// Japanese (word-processing keyboard): Zenkaku/Hankaku
+ Lang5,
+ /// Japanese: <kbd>無変換</kbd> (muhenkan)
+ NonConvert,
+ /// <kbd>⌦</kbd>. The forward delete key.
+ /// Note that on Apple keyboards, the key labelled <kbd>Delete</kbd> on the main part
+ /// of the keyboard is encoded as [`Backspace`].
+ ///
+ /// [`Backspace`]: Self::Backspace
+ Delete,
+ /// <kbd>Page Down</kbd>, <kbd>End</kbd>, or <kbd>↘</kbd>
+ End,
+ /// <kbd>Help</kbd>. Not present on standard PC keyboards.
+ Help,
+ /// <kbd>Home</kbd> or <kbd>↖</kbd>
+ Home,
+ /// <kbd>Insert</kbd> or <kbd>Ins</kbd>. Not present on Apple keyboards.
+ Insert,
+ /// <kbd>Page Down</kbd>, <kbd>PgDn</kbd>, or <kbd>⇟</kbd>
+ PageDown,
+ /// <kbd>Page Up</kbd>, <kbd>PgUp</kbd>, or <kbd>⇞</kbd>
+ PageUp,
+ /// <kbd>↓</kbd>
+ ArrowDown,
+ /// <kbd>←</kbd>
+ ArrowLeft,
+ /// <kbd>→</kbd>
+ ArrowRight,
+ /// <kbd>↑</kbd>
+ ArrowUp,
+ /// On the Mac, this is used for the numpad <kbd>Clear</kbd> key.
+ NumLock,
+ /// <kbd>0 Ins</kbd> on a keyboard. <kbd>0</kbd> on a phone or remote control
+ Numpad0,
+ /// <kbd>1 End</kbd> on a keyboard. <kbd>1</kbd> or <kbd>1 QZ</kbd> on a phone or
+ /// remote control
+ Numpad1,
+ /// <kbd>2 ↓</kbd> on a keyboard. <kbd>2 ABC</kbd> on a phone or remote control
+ Numpad2,
+ /// <kbd>3 PgDn</kbd> on a keyboard. <kbd>3 DEF</kbd> on a phone or remote control
+ Numpad3,
+ /// <kbd>4 ←</kbd> on a keyboard. <kbd>4 GHI</kbd> on a phone or remote control
+ Numpad4,
+ /// <kbd>5</kbd> on a keyboard. <kbd>5 JKL</kbd> on a phone or remote control
+ Numpad5,
+ /// <kbd>6 →</kbd> on a keyboard. <kbd>6 MNO</kbd> on a phone or remote control
+ Numpad6,
+ /// <kbd>7 Home</kbd> on a keyboard. <kbd>7 PQRS</kbd> or <kbd>7 PRS</kbd> on a phone
+ /// or remote control
+ Numpad7,
+ /// <kbd>8 ↑</kbd> on a keyboard. <kbd>8 TUV</kbd> on a phone or remote control
+ Numpad8,
+ /// <kbd>9 PgUp</kbd> on a keyboard. <kbd>9 WXYZ</kbd> or <kbd>9 WXY</kbd> on a phone
+ /// or remote control
+ Numpad9,
+ /// <kbd>+</kbd>
+ NumpadAdd,
+ /// Found on the Microsoft Natural Keyboard.
+ NumpadBackspace,
+ /// <kbd>C</kbd> or <kbd>A</kbd> (All Clear). Also for use with numpads that have a
+ /// <kbd>Clear</kbd> key that is separate from the <kbd>NumLock</kbd> key. On the
+ /// Mac, the numpad <kbd>Clear</kbd> key is encoded as [`NumLock`].
+ ///
+ /// [`NumLock`]: Self::NumLock
+ NumpadClear,
+ /// <kbd>C</kbd> (Clear Entry)
+ NumpadClearEntry,
+ /// <kbd>,</kbd> (thousands separator). For locales where the thousands separator
+ /// is a "." (e.g., Brazil), this key may generate a <kbd>.</kbd>.
+ NumpadComma,
+ /// <kbd>. Del</kbd>. For locales where the decimal separator is "," (e.g.,
+ /// Brazil), this key may generate a <kbd>,</kbd>.
+ NumpadDecimal,
+ /// <kbd>/</kbd>
+ NumpadDivide,
+ NumpadEnter,
+ /// <kbd>=</kbd>
+ NumpadEqual,
+ /// <kbd>#</kbd> on a phone or remote control device. This key is typically found
+ /// below the <kbd>9</kbd> key and to the right of the <kbd>0</kbd> key.
+ NumpadHash,
+ /// <kbd>M</kbd> Add current entry to the value stored in memory.
+ NumpadMemoryAdd,
+ /// <kbd>M</kbd> Clear the value stored in memory.
+ NumpadMemoryClear,
+ /// <kbd>M</kbd> Replace the current entry with the value stored in memory.
+ NumpadMemoryRecall,
+ /// <kbd>M</kbd> Replace the value stored in memory with the current entry.
+ NumpadMemoryStore,
+ /// <kbd>M</kbd> Subtract current entry from the value stored in memory.
+ NumpadMemorySubtract,
+ /// <kbd>*</kbd> on a keyboard. For use with numpads that provide mathematical
+ /// operations (<kbd>+</kbd>, <kbd>-</kbd> <kbd>*</kbd> and <kbd>/</kbd>).
+ ///
+ /// Use `NumpadStar` for the <kbd>*</kbd> key on phones and remote controls.
+ NumpadMultiply,
+ /// <kbd>(</kbd> Found on the Microsoft Natural Keyboard.
+ NumpadParenLeft,
+ /// <kbd>)</kbd> Found on the Microsoft Natural Keyboard.
+ NumpadParenRight,
+ /// <kbd>*</kbd> on a phone or remote control device.
+ ///
+ /// This key is typically found below the <kbd>7</kbd> key and to the left of
+ /// the <kbd>0</kbd> key.
+ ///
+ /// Use <kbd>"NumpadMultiply"</kbd> for the <kbd>*</kbd> key on
+ /// numeric keypads.
+ NumpadStar,
+ /// <kbd>-</kbd>
+ NumpadSubtract,
+ /// <kbd>Esc</kbd> or <kbd>⎋</kbd>
+ Escape,
+ /// <kbd>Fn</kbd> This is typically a hardware key that does not generate a separate
+ /// code.
+ Fn,
+ /// <kbd>FLock</kbd> or <kbd>FnLock</kbd>. Function Lock key. Found on the Microsoft
+ /// Natural Keyboard.
+ FnLock,
+ /// <kbd>PrtScr SysRq</kbd> or <kbd>Print Screen</kbd>
+ PrintScreen,
+ /// <kbd>Scroll Lock</kbd>
+ ScrollLock,
+ /// <kbd>Pause Break</kbd>
+ Pause,
+ /// Some laptops place this key to the left of the <kbd>↑</kbd> key.
+ ///
+ /// This also the "back" button (triangle) on Android.
+ BrowserBack,
+ BrowserFavorites,
+ /// Some laptops place this key to the right of the <kbd>↑</kbd> key.
+ BrowserForward,
+ /// The "home" button on Android.
+ BrowserHome,
+ BrowserRefresh,
+ BrowserSearch,
+ BrowserStop,
+ /// <kbd>Eject</kbd> or <kbd>⏏</kbd>. This key is placed in the function section on
+ /// some Apple keyboards.
+ Eject,
+ /// Sometimes labelled <kbd>My Computer</kbd> on the keyboard
+ LaunchApp1,
+ /// Sometimes labelled <kbd>Calculator</kbd> on the keyboard
+ LaunchApp2,
+ LaunchMail,
+ MediaPlayPause,
+ MediaSelect,
+ MediaStop,
+ MediaTrackNext,
+ MediaTrackPrevious,
+ /// This key is placed in the function section on some Apple keyboards, replacing the
+ /// <kbd>Eject</kbd> key.
+ Power,
+ Sleep,
+ AudioVolumeDown,
+ AudioVolumeMute,
+ AudioVolumeUp,
+ WakeUp,
+ // Legacy modifier key. Also called "Super" in certain places.
+ Meta,
+ // Legacy modifier key.
+ Hyper,
+ Turbo,
+ Abort,
+ Resume,
+ Suspend,
+ /// Found on Sun’s USB keyboard.
+ Again,
+ /// Found on Sun’s USB keyboard.
+ Copy,
+ /// Found on Sun’s USB keyboard.
+ Cut,
+ /// Found on Sun’s USB keyboard.
+ Find,
+ /// Found on Sun’s USB keyboard.
+ Open,
+ /// Found on Sun’s USB keyboard.
+ Paste,
+ /// Found on Sun’s USB keyboard.
+ Props,
+ /// Found on Sun’s USB keyboard.
+ Select,
+ /// Found on Sun’s USB keyboard.
+ Undo,
+ /// Use for dedicated <kbd>ひらがな</kbd> key found on some Japanese word processing
+ /// keyboards.
+ Hiragana,
+ /// Use for dedicated <kbd>カタカナ</kbd> key found on some Japanese word processing
+ /// keyboards.
+ Katakana,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F1,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F2,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F3,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F4,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F5,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F6,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F7,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F8,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F9,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F10,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F11,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F12,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F13,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F14,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F15,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F16,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F17,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F18,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F19,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F20,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F21,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F22,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F23,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F24,
+ /// General-purpose function key.
+ F25,
+ /// General-purpose function key.
+ F26,
+ /// General-purpose function key.
+ F27,
+ /// General-purpose function key.
+ F28,
+ /// General-purpose function key.
+ F29,
+ /// General-purpose function key.
+ F30,
+ /// General-purpose function key.
+ F31,
+ /// General-purpose function key.
+ F32,
+ /// General-purpose function key.
+ F33,
+ /// General-purpose function key.
+ F34,
+ /// General-purpose function key.
+ F35,
+}
+
+impl TryFrom<winit::keyboard::KeyCode> for Key
+{
+ type Error = UnknownKeyCodeError;
+
+ fn try_from(key_code: winit::keyboard::KeyCode) -> Result<Self, Self::Error>
+ {
+ match key_code {
+ winit::keyboard::KeyCode::Backquote => Ok(Self::Backquote),
+ winit::keyboard::KeyCode::Backslash => Ok(Self::Backslash),
+ winit::keyboard::KeyCode::BracketLeft => Ok(Self::BracketLeft),
+ winit::keyboard::KeyCode::BracketRight => Ok(Self::BracketRight),
+ winit::keyboard::KeyCode::Comma => Ok(Self::Comma),
+ winit::keyboard::KeyCode::Digit0 => Ok(Self::Digit0),
+ winit::keyboard::KeyCode::Digit1 => Ok(Self::Digit1),
+ winit::keyboard::KeyCode::Digit2 => Ok(Self::Digit2),
+ winit::keyboard::KeyCode::Digit3 => Ok(Self::Digit3),
+ winit::keyboard::KeyCode::Digit4 => Ok(Self::Digit4),
+ winit::keyboard::KeyCode::Digit5 => Ok(Self::Digit5),
+ winit::keyboard::KeyCode::Digit6 => Ok(Self::Digit6),
+ winit::keyboard::KeyCode::Digit7 => Ok(Self::Digit7),
+ winit::keyboard::KeyCode::Digit8 => Ok(Self::Digit8),
+ winit::keyboard::KeyCode::Digit9 => Ok(Self::Digit9),
+ winit::keyboard::KeyCode::Equal => Ok(Self::Equal),
+ winit::keyboard::KeyCode::IntlBackslash => Ok(Self::IntlBackslash),
+ winit::keyboard::KeyCode::IntlRo => Ok(Self::IntlRo),
+ winit::keyboard::KeyCode::IntlYen => Ok(Self::IntlYen),
+ winit::keyboard::KeyCode::KeyA => Ok(Self::A),
+ winit::keyboard::KeyCode::KeyB => Ok(Self::B),
+ winit::keyboard::KeyCode::KeyC => Ok(Self::C),
+ winit::keyboard::KeyCode::KeyD => Ok(Self::D),
+ winit::keyboard::KeyCode::KeyE => Ok(Self::E),
+ winit::keyboard::KeyCode::KeyF => Ok(Self::F),
+ winit::keyboard::KeyCode::KeyG => Ok(Self::G),
+ winit::keyboard::KeyCode::KeyH => Ok(Self::H),
+ winit::keyboard::KeyCode::KeyI => Ok(Self::I),
+ winit::keyboard::KeyCode::KeyJ => Ok(Self::J),
+ winit::keyboard::KeyCode::KeyK => Ok(Self::K),
+ winit::keyboard::KeyCode::KeyL => Ok(Self::L),
+ winit::keyboard::KeyCode::KeyM => Ok(Self::M),
+ winit::keyboard::KeyCode::KeyN => Ok(Self::N),
+ winit::keyboard::KeyCode::KeyO => Ok(Self::O),
+ winit::keyboard::KeyCode::KeyP => Ok(Self::P),
+ winit::keyboard::KeyCode::KeyQ => Ok(Self::Q),
+ winit::keyboard::KeyCode::KeyR => Ok(Self::R),
+ winit::keyboard::KeyCode::KeyS => Ok(Self::S),
+ winit::keyboard::KeyCode::KeyT => Ok(Self::T),
+ winit::keyboard::KeyCode::KeyU => Ok(Self::U),
+ winit::keyboard::KeyCode::KeyV => Ok(Self::V),
+ winit::keyboard::KeyCode::KeyW => Ok(Self::W),
+ winit::keyboard::KeyCode::KeyX => Ok(Self::X),
+ winit::keyboard::KeyCode::KeyY => Ok(Self::Y),
+ winit::keyboard::KeyCode::KeyZ => Ok(Self::Z),
+ winit::keyboard::KeyCode::Minus => Ok(Self::Minus),
+ winit::keyboard::KeyCode::Period => Ok(Self::Period),
+ winit::keyboard::KeyCode::Quote => Ok(Self::Quote),
+ winit::keyboard::KeyCode::Semicolon => Ok(Self::Semicolon),
+ winit::keyboard::KeyCode::Slash => Ok(Self::Slash),
+ winit::keyboard::KeyCode::AltLeft => Ok(Self::AltLeft),
+ winit::keyboard::KeyCode::AltRight => Ok(Self::AltRight),
+ winit::keyboard::KeyCode::Backspace => Ok(Self::Backspace),
+ winit::keyboard::KeyCode::CapsLock => Ok(Self::CapsLock),
+ winit::keyboard::KeyCode::ContextMenu => Ok(Self::ContextMenu),
+ winit::keyboard::KeyCode::ControlLeft => Ok(Self::ControlLeft),
+ winit::keyboard::KeyCode::ControlRight => Ok(Self::ControlRight),
+ winit::keyboard::KeyCode::Enter => Ok(Self::Enter),
+ winit::keyboard::KeyCode::SuperLeft => Ok(Self::SuperLeft),
+ winit::keyboard::KeyCode::SuperRight => Ok(Self::SuperRight),
+ winit::keyboard::KeyCode::ShiftLeft => Ok(Self::ShiftLeft),
+ winit::keyboard::KeyCode::ShiftRight => Ok(Self::ShiftRight),
+ winit::keyboard::KeyCode::Space => Ok(Self::Space),
+ winit::keyboard::KeyCode::Tab => Ok(Self::Tab),
+ winit::keyboard::KeyCode::Convert => Ok(Self::Convert),
+ winit::keyboard::KeyCode::KanaMode => Ok(Self::KanaMode),
+ winit::keyboard::KeyCode::Lang1 => Ok(Self::Lang1),
+ winit::keyboard::KeyCode::Lang2 => Ok(Self::Lang2),
+ winit::keyboard::KeyCode::Lang3 => Ok(Self::Lang3),
+ winit::keyboard::KeyCode::Lang4 => Ok(Self::Lang4),
+ winit::keyboard::KeyCode::Lang5 => Ok(Self::Lang5),
+ winit::keyboard::KeyCode::NonConvert => Ok(Self::NonConvert),
+ winit::keyboard::KeyCode::Delete => Ok(Self::Delete),
+ winit::keyboard::KeyCode::End => Ok(Self::End),
+ winit::keyboard::KeyCode::Help => Ok(Self::Help),
+ winit::keyboard::KeyCode::Home => Ok(Self::Home),
+ winit::keyboard::KeyCode::Insert => Ok(Self::Insert),
+ winit::keyboard::KeyCode::PageDown => Ok(Self::PageDown),
+ winit::keyboard::KeyCode::PageUp => Ok(Self::PageUp),
+ winit::keyboard::KeyCode::ArrowDown => Ok(Self::ArrowDown),
+ winit::keyboard::KeyCode::ArrowLeft => Ok(Self::ArrowLeft),
+ winit::keyboard::KeyCode::ArrowRight => Ok(Self::ArrowRight),
+ winit::keyboard::KeyCode::ArrowUp => Ok(Self::ArrowUp),
+ winit::keyboard::KeyCode::NumLock => Ok(Self::NumLock),
+ winit::keyboard::KeyCode::Numpad0 => Ok(Self::Numpad0),
+ winit::keyboard::KeyCode::Numpad1 => Ok(Self::Numpad1),
+ winit::keyboard::KeyCode::Numpad2 => Ok(Self::Numpad2),
+ winit::keyboard::KeyCode::Numpad3 => Ok(Self::Numpad3),
+ winit::keyboard::KeyCode::Numpad4 => Ok(Self::Numpad4),
+ winit::keyboard::KeyCode::Numpad5 => Ok(Self::Numpad5),
+ winit::keyboard::KeyCode::Numpad6 => Ok(Self::Numpad6),
+ winit::keyboard::KeyCode::Numpad7 => Ok(Self::Numpad7),
+ winit::keyboard::KeyCode::Numpad8 => Ok(Self::Numpad8),
+ winit::keyboard::KeyCode::Numpad9 => Ok(Self::Numpad9),
+ winit::keyboard::KeyCode::NumpadAdd => Ok(Self::NumpadAdd),
+ winit::keyboard::KeyCode::NumpadBackspace => Ok(Self::NumpadBackspace),
+ winit::keyboard::KeyCode::NumpadClear => Ok(Self::NumpadClear),
+ winit::keyboard::KeyCode::NumpadClearEntry => Ok(Self::NumpadClearEntry),
+ winit::keyboard::KeyCode::NumpadComma => Ok(Self::NumpadComma),
+ winit::keyboard::KeyCode::NumpadDecimal => Ok(Self::NumpadDecimal),
+ winit::keyboard::KeyCode::NumpadDivide => Ok(Self::NumpadDivide),
+ winit::keyboard::KeyCode::NumpadEnter => Ok(Self::NumpadEnter),
+ winit::keyboard::KeyCode::NumpadEqual => Ok(Self::NumpadEqual),
+ winit::keyboard::KeyCode::NumpadHash => Ok(Self::NumpadHash),
+ winit::keyboard::KeyCode::NumpadMemoryAdd => Ok(Self::NumpadMemoryAdd),
+ winit::keyboard::KeyCode::NumpadMemoryClear => Ok(Self::NumpadMemoryClear),
+ winit::keyboard::KeyCode::NumpadMemoryRecall => Ok(Self::NumpadMemoryRecall),
+ winit::keyboard::KeyCode::NumpadMemoryStore => Ok(Self::NumpadMemoryStore),
+ winit::keyboard::KeyCode::NumpadMemorySubtract => {
+ Ok(Self::NumpadMemorySubtract)
+ }
+ winit::keyboard::KeyCode::NumpadMultiply => Ok(Self::NumpadMultiply),
+ winit::keyboard::KeyCode::NumpadParenLeft => Ok(Self::NumpadParenLeft),
+ winit::keyboard::KeyCode::NumpadParenRight => Ok(Self::NumpadParenRight),
+ winit::keyboard::KeyCode::NumpadStar => Ok(Self::NumpadStar),
+ winit::keyboard::KeyCode::NumpadSubtract => Ok(Self::NumpadSubtract),
+ winit::keyboard::KeyCode::Escape => Ok(Self::Escape),
+ winit::keyboard::KeyCode::Fn => Ok(Self::Fn),
+ winit::keyboard::KeyCode::FnLock => Ok(Self::FnLock),
+ winit::keyboard::KeyCode::PrintScreen => Ok(Self::PrintScreen),
+ winit::keyboard::KeyCode::ScrollLock => Ok(Self::ScrollLock),
+ winit::keyboard::KeyCode::Pause => Ok(Self::Pause),
+ winit::keyboard::KeyCode::BrowserBack => Ok(Self::BrowserBack),
+ winit::keyboard::KeyCode::BrowserFavorites => Ok(Self::BrowserFavorites),
+ winit::keyboard::KeyCode::BrowserForward => Ok(Self::BrowserForward),
+ winit::keyboard::KeyCode::BrowserHome => Ok(Self::BrowserHome),
+ winit::keyboard::KeyCode::BrowserRefresh => Ok(Self::BrowserRefresh),
+ winit::keyboard::KeyCode::BrowserSearch => Ok(Self::BrowserSearch),
+ winit::keyboard::KeyCode::BrowserStop => Ok(Self::BrowserStop),
+ winit::keyboard::KeyCode::Eject => Ok(Self::Eject),
+ winit::keyboard::KeyCode::LaunchApp1 => Ok(Self::LaunchApp1),
+ winit::keyboard::KeyCode::LaunchApp2 => Ok(Self::LaunchApp2),
+ winit::keyboard::KeyCode::LaunchMail => Ok(Self::LaunchMail),
+ winit::keyboard::KeyCode::MediaPlayPause => Ok(Self::MediaPlayPause),
+ winit::keyboard::KeyCode::MediaSelect => Ok(Self::MediaSelect),
+ winit::keyboard::KeyCode::MediaStop => Ok(Self::MediaStop),
+ winit::keyboard::KeyCode::MediaTrackNext => Ok(Self::MediaTrackNext),
+ winit::keyboard::KeyCode::MediaTrackPrevious => Ok(Self::MediaTrackPrevious),
+ winit::keyboard::KeyCode::Power => Ok(Self::Power),
+ winit::keyboard::KeyCode::Sleep => Ok(Self::Sleep),
+ winit::keyboard::KeyCode::AudioVolumeDown => Ok(Self::AudioVolumeDown),
+ winit::keyboard::KeyCode::AudioVolumeMute => Ok(Self::AudioVolumeMute),
+ winit::keyboard::KeyCode::AudioVolumeUp => Ok(Self::AudioVolumeUp),
+ winit::keyboard::KeyCode::WakeUp => Ok(Self::WakeUp),
+ winit::keyboard::KeyCode::Meta => Ok(Self::Meta),
+ winit::keyboard::KeyCode::Hyper => Ok(Self::Hyper),
+ winit::keyboard::KeyCode::Turbo => Ok(Self::Turbo),
+ winit::keyboard::KeyCode::Abort => Ok(Self::Abort),
+ winit::keyboard::KeyCode::Resume => Ok(Self::Resume),
+ winit::keyboard::KeyCode::Suspend => Ok(Self::Suspend),
+ winit::keyboard::KeyCode::Again => Ok(Self::Again),
+ winit::keyboard::KeyCode::Copy => Ok(Self::Copy),
+ winit::keyboard::KeyCode::Cut => Ok(Self::Cut),
+ winit::keyboard::KeyCode::Find => Ok(Self::Find),
+ winit::keyboard::KeyCode::Open => Ok(Self::Open),
+ winit::keyboard::KeyCode::Paste => Ok(Self::Paste),
+ winit::keyboard::KeyCode::Props => Ok(Self::Props),
+ winit::keyboard::KeyCode::Select => Ok(Self::Select),
+ winit::keyboard::KeyCode::Undo => Ok(Self::Undo),
+ winit::keyboard::KeyCode::Hiragana => Ok(Self::Hiragana),
+ winit::keyboard::KeyCode::Katakana => Ok(Self::Katakana),
+ winit::keyboard::KeyCode::F1 => Ok(Self::F1),
+ winit::keyboard::KeyCode::F2 => Ok(Self::F2),
+ winit::keyboard::KeyCode::F3 => Ok(Self::F3),
+ winit::keyboard::KeyCode::F4 => Ok(Self::F4),
+ winit::keyboard::KeyCode::F5 => Ok(Self::F5),
+ winit::keyboard::KeyCode::F6 => Ok(Self::F6),
+ winit::keyboard::KeyCode::F7 => Ok(Self::F7),
+ winit::keyboard::KeyCode::F8 => Ok(Self::F8),
+ winit::keyboard::KeyCode::F9 => Ok(Self::F9),
+ winit::keyboard::KeyCode::F10 => Ok(Self::F10),
+ winit::keyboard::KeyCode::F11 => Ok(Self::F11),
+ winit::keyboard::KeyCode::F12 => Ok(Self::F12),
+ winit::keyboard::KeyCode::F13 => Ok(Self::F13),
+ winit::keyboard::KeyCode::F14 => Ok(Self::F14),
+ winit::keyboard::KeyCode::F15 => Ok(Self::F15),
+ winit::keyboard::KeyCode::F16 => Ok(Self::F16),
+ winit::keyboard::KeyCode::F17 => Ok(Self::F17),
+ winit::keyboard::KeyCode::F18 => Ok(Self::F18),
+ winit::keyboard::KeyCode::F19 => Ok(Self::F19),
+ winit::keyboard::KeyCode::F20 => Ok(Self::F20),
+ winit::keyboard::KeyCode::F21 => Ok(Self::F21),
+ winit::keyboard::KeyCode::F22 => Ok(Self::F22),
+ winit::keyboard::KeyCode::F23 => Ok(Self::F23),
+ winit::keyboard::KeyCode::F24 => Ok(Self::F24),
+ winit::keyboard::KeyCode::F25 => Ok(Self::F25),
+ winit::keyboard::KeyCode::F26 => Ok(Self::F26),
+ winit::keyboard::KeyCode::F27 => Ok(Self::F27),
+ winit::keyboard::KeyCode::F28 => Ok(Self::F28),
+ winit::keyboard::KeyCode::F29 => Ok(Self::F29),
+ winit::keyboard::KeyCode::F30 => Ok(Self::F30),
+ winit::keyboard::KeyCode::F31 => Ok(Self::F31),
+ winit::keyboard::KeyCode::F32 => Ok(Self::F32),
+ winit::keyboard::KeyCode::F33 => Ok(Self::F33),
+ winit::keyboard::KeyCode::F34 => Ok(Self::F34),
+ winit::keyboard::KeyCode::F35 => Ok(Self::F35),
+ _ => Err(UnknownKeyCodeError),
+ }
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error("Unknown key code")]
+pub struct UnknownKeyCodeError;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum KeyState
+{
+ Pressed,
+ Released,
+}
+
+impl KeyState
+{
+ #[must_use]
+ #[inline]
+ pub fn is_pressed(&self) -> bool
+ {
+ matches!(self, Self::Pressed)
+ }
+
+ #[must_use]
+ #[inline]
+ pub fn is_released(&self) -> bool
+ {
+ matches!(self, Self::Released)
+ }
+}
+
+impl From<winit::event::ElementState> for KeyState
+{
+ fn from(element_state: winit::event::ElementState) -> Self
+ {
+ match element_state {
+ winit::event::ElementState::Pressed => Self::Pressed,
+ winit::event::ElementState::Released => Self::Released,
+ }
+ }
+}
+
+#[derive(Debug)]
+struct KeyData
+{
+ curr_state: KeyState,
+ previous_state: KeyState,
+}
+
+impl Default for KeyData
+{
+ fn default() -> Self
+ {
+ KeyData {
+ curr_state: KeyState::Released,
+ previous_state: KeyState::Released,
+ }
+ }
+}
diff --git a/engine/src/windowing/mouse.rs b/engine/src/windowing/mouse.rs
new file mode 100644
index 0000000..1afe594
--- /dev/null
+++ b/engine/src/windowing/mouse.rs
@@ -0,0 +1,136 @@
+use std::collections::HashMap;
+
+use ecs::Sole;
+
+use crate::vector::Vec2;
+
+#[derive(Debug, Default, Clone, Sole)]
+#[non_exhaustive]
+pub struct Motion
+{
+ pub position_delta: Vec2<f64>,
+}
+
+/// Mouse buttons.
+#[derive(Debug, Default, Sole)]
+pub struct Buttons
+{
+ map: HashMap<Button, ButtonData>,
+}
+
+impl Buttons
+{
+ pub fn get(&self, button: Button) -> ButtonState
+ {
+ let Some(button_data) = self.map.get(&button) else {
+ return ButtonState::Released;
+ };
+
+ button_data.current_state
+ }
+
+ pub fn get_previous(&self, button: Button) -> ButtonState
+ {
+ let Some(button_data) = self.map.get(&button) else {
+ return ButtonState::Released;
+ };
+
+ button_data.previous_state
+ }
+
+ pub fn set(&mut self, button: Button, button_state: ButtonState)
+ {
+ let button_data = self.map.entry(button).or_default();
+
+ button_data.current_state = button_state;
+ }
+
+ pub(crate) fn make_states_previous(&mut self)
+ {
+ for button_data in self.map.values_mut() {
+ button_data.previous_state = button_data.current_state;
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum Button
+{
+ Left,
+ Right,
+ Middle,
+ Back,
+ Forward,
+ Other(u16),
+}
+
+impl From<winit::event::MouseButton> for Button
+{
+ fn from(mouse_button: winit::event::MouseButton) -> Self
+ {
+ match mouse_button {
+ winit::event::MouseButton::Left => Self::Left,
+ winit::event::MouseButton::Right => Self::Right,
+ winit::event::MouseButton::Middle => Self::Middle,
+ winit::event::MouseButton::Back => Self::Back,
+ winit::event::MouseButton::Forward => Self::Forward,
+ winit::event::MouseButton::Other(other_mouse_button) => {
+ Self::Other(other_mouse_button)
+ }
+ }
+ }
+}
+
+/// Mouse button state.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum ButtonState
+{
+ Pressed,
+ Released,
+}
+
+impl ButtonState
+{
+ #[must_use]
+ #[inline]
+ pub fn is_pressed(&self) -> bool
+ {
+ matches!(self, Self::Pressed)
+ }
+
+ #[must_use]
+ #[inline]
+ pub fn is_released(&self) -> bool
+ {
+ matches!(self, Self::Released)
+ }
+}
+
+impl From<winit::event::ElementState> for ButtonState
+{
+ fn from(element_state: winit::event::ElementState) -> Self
+ {
+ match element_state {
+ winit::event::ElementState::Pressed => Self::Pressed,
+ winit::event::ElementState::Released => Self::Released,
+ }
+ }
+}
+
+#[derive(Debug)]
+struct ButtonData
+{
+ current_state: ButtonState,
+ previous_state: ButtonState,
+}
+
+impl Default for ButtonData
+{
+ fn default() -> Self
+ {
+ Self {
+ current_state: ButtonState::Released,
+ previous_state: ButtonState::Released,
+ }
+ }
+}
diff --git a/engine/src/windowing/window.rs b/engine/src/windowing/window.rs
new file mode 100644
index 0000000..79b2102
--- /dev/null
+++ b/engine/src/windowing/window.rs
@@ -0,0 +1,171 @@
+use std::borrow::Cow;
+
+use ecs::Component;
+
+use crate::data_types::dimens::Dimens;
+
+pub mod platform;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Id
+{
+ inner: winit::window::WindowId,
+}
+
+impl Id
+{
+ pub(crate) fn from_inner(inner: winit::window::WindowId) -> Self
+ {
+ Self { inner }
+ }
+}
+
+macro_rules! impl_creation_attributes_field_fns {
+ ($field: ident, ($($getter_ret_pre: tt)?), $getter_ret_type: ty, $field_type: ty) => {
+ impl CreationAttributes
+ {
+ pub fn $field(&self) -> $getter_ret_type
+ {
+ $($getter_ret_pre)? self.attrs.$field
+ }
+
+ paste::paste! {
+ pub fn [<with_ $field>](mut self, $field: impl Into<$field_type>) -> Self {
+ self.attrs.$field = $field.into();
+ self
+ }
+ }
+ }
+ };
+}
+
+#[derive(Debug, Component, Clone)]
+#[non_exhaustive]
+pub struct CreationAttributes
+{
+ attrs: winit::window::WindowAttributes,
+}
+
+impl_creation_attributes_field_fns!(title, (&), &str, String);
+impl_creation_attributes_field_fns!(transparent, (), bool, bool);
+
+impl CreationAttributes
+{
+ #[cfg(target_os = "linux")]
+ pub fn with_x11_visual(mut self, visual_id: XVisualID) -> Self
+ {
+ use winit::platform::x11::WindowAttributesExtX11;
+
+ self.attrs = self.attrs.with_x11_visual(visual_id);
+
+ self
+ }
+}
+
+impl CreationAttributes
+{
+ pub(crate) fn into_inner(self) -> winit::window::WindowAttributes
+ {
+ self.attrs
+ }
+}
+
+impl Default for CreationAttributes
+{
+ fn default() -> Self
+ {
+ CreationAttributes {
+ attrs: winit::window::WindowAttributes::default().with_title("Application"),
+ }
+ }
+}
+
+#[derive(Debug, Component, Clone, Copy)]
+pub struct CreationReady;
+
+#[derive(Debug, Component)]
+#[non_exhaustive]
+pub struct Window
+{
+ wid: Id,
+ pub title: Cow<'static, str>,
+ pub cursor_visible: bool,
+ pub cursor_grab_mode: CursorGrabMode,
+ inner_size: Dimens<u32>,
+}
+
+impl Window
+{
+ pub fn wid(&self) -> Id
+ {
+ self.wid
+ }
+
+ pub fn inner_size(&self) -> &Dimens<u32>
+ {
+ &self.inner_size
+ }
+
+ pub(crate) fn new(
+ winit_window: &winit::window::Window,
+ creation_attrs: &CreationAttributes,
+ ) -> Self
+ {
+ Self {
+ wid: Id::from_inner(winit_window.id()),
+ title: creation_attrs.title().to_string().into(),
+ cursor_visible: true,
+ cursor_grab_mode: CursorGrabMode::None,
+ inner_size: winit_window.inner_size().into(),
+ }
+ }
+
+ pub(crate) fn apply(&self, winit_window: &winit::window::Window)
+ {
+ winit_window.set_title(&self.title);
+ winit_window.set_cursor_visible(self.cursor_visible);
+ }
+
+ pub(crate) fn set_inner_size(&mut self, inner_size: Dimens<u32>)
+ {
+ self.inner_size = inner_size;
+ }
+}
+
+#[derive(Debug, Component)]
+pub struct Closed;
+
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum CursorGrabMode
+{
+ #[default]
+ None,
+
+ /// The cursor is locked to a specific position in the window.
+ Locked,
+}
+
+/// A unique identifier for an X11 visual.
+pub type XVisualID = u32;
+
+impl<P> From<winit::dpi::PhysicalSize<P>> for Dimens<P>
+{
+ fn from(size: winit::dpi::PhysicalSize<P>) -> Self
+ {
+ Self {
+ width: size.width,
+ height: size.height,
+ }
+ }
+}
+
+impl<P> From<Dimens<P>> for winit::dpi::PhysicalSize<P>
+{
+ fn from(dimens: Dimens<P>) -> Self
+ {
+ Self {
+ width: dimens.width,
+ height: dimens.height,
+ }
+ }
+}
diff --git a/engine/src/windowing/window/platform.rs b/engine/src/windowing/window/platform.rs
new file mode 100644
index 0000000..f3908a2
--- /dev/null
+++ b/engine/src/windowing/window/platform.rs
@@ -0,0 +1,12 @@
+#[cfg(x11_platform)]
+pub mod x11
+{
+ use std::ffi::c_void;
+
+ pub type XlibErrorHook = Box<dyn Fn(*mut c_void, *mut c_void) -> bool + Send + Sync>;
+
+ pub fn register_xlib_error_hook(hook: XlibErrorHook)
+ {
+ winit::platform::x11::register_xlib_error_hook(hook);
+ }
+}
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");
+ }
+ }
+}