Compare commits

..

1 commit

Author SHA1 Message Date
3209d65b93
push code for debugging 2024-06-16 15:34:06 -04:00
372 changed files with 10792 additions and 27853 deletions

View file

@ -1,76 +0,0 @@
name: CI
env:
# Runners don't expose the TSC but we want to make sure these tests work, so we
# can ignore it.
TRACY_NO_INVARIANT_CHECK: 1
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
jobs:
build:
runs-on: ${{ matrix.os }}
#container: git.seanomik.net/seanomik/rust-nightly:2023-11-21-bookworm
strategy:
matrix:
os: [ubuntu-latest]
rust: [nightly]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-build-${{ matrix.rust }}-${{ hashFiles('**/Cargo.toml') }}
- name: Install system dependencies
run: |
apt update
apt install libudev-dev lua5.4 liblua5.4-dev -y
- uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.rust }}
- name: Build
run: |
cargo build
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
rust: [nightly]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-test-${{ matrix.rust }}-${{ hashFiles('**/Cargo.toml') }}
- name: Install system dependencies
run: |
apt update
apt install libudev-dev lua5.4 liblua5.4-dev -y
- uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.rust }}
- name: Build and run tests
run: |
cargo test --all

5
.gitmodules vendored
View file

@ -1,6 +1,3 @@
[submodule "lyra-scripting/elua"]
path = crates/lyra-scripting/elua
path = lyra-scripting/elua
url = ../elua.git # git@git.seanomik.net:SeanOMik/elua.git
[submodule "wgsl-preprocessor"]
path = crates/wgsl-preprocessor
url = git@git.seanomik.net:SeanOMik/wgsl-preprocessor.git

39
.vscode/launch.json vendored
View file

@ -7,30 +7,11 @@
{
"type": "lldb",
"request": "launch",
"name": "Debug lyra dim_2d example",
"cargo": {
"args": [
"build",
"--manifest-path", "${workspaceFolder}/examples/2d/Cargo.toml"
//"--bin=testbed",
],
"filter": {
"name": "dim_2d",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}/examples/2d"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug lyra lua-scripting",
"name": "Debug example lua-scripting",
"cargo": {
"args": [
"build",
"--manifest-path", "${workspaceFolder}/examples/lua-scripting/Cargo.toml"
//"--bin=testbed",
],
"filter": {
"name": "lua-scripting",
@ -58,24 +39,6 @@
"args": [],
"cwd": "${workspaceFolder}/examples/testbed"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug lyra shadows",
"cargo": {
"args": [
"build",
"--manifest-path", "${workspaceFolder}/examples/shadows/Cargo.toml"
//"--bin=shadows",
],
"filter": {
"name": "shadows",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}/examples/shadows"
},
{
"type": "lldb",
"request": "launch",

21
.woodpecker/.debug.yml Normal file
View file

@ -0,0 +1,21 @@
variables:
- &rust_image 'git.seanomik.net/seanomik/rust-nightly:2023-11-21-bookworm'
when:
event: [push, manual, pull_request]
branch: main
steps:
Build - Debug:
image: *rust_image
commands:
- apt update
- apt install libudev-dev lua5.4 liblua5.4-dev -y
- cargo build
Test - Debug:
image: *rust_image
commands:
- apt update
- apt install libudev-dev lua5.4 liblua5.4-dev -y
- cargo test --all

20
.woodpecker/.release.yml Normal file
View file

@ -0,0 +1,20 @@
variables:
- &rust_image 'git.seanomik.net/seanomik/rust-nightly:2023-11-21-bookworm'
when:
event: [release, pull_request, manual]
steps:
Build - Release:
image: *rust_image
commands:
- apt update
- apt install libudev-dev lua5.4 liblua5.4-dev -y
- cargo build --release
Test - Release:
image: *rust_image
commands:
- apt update
- apt install libudev-dev lua5.4 liblua5.4-dev -y
- cargo test --all --release

3441
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,27 +5,32 @@ edition = "2021"
[workspace]
members = [
"crates/*",
"examples/2d",
"examples/testbed",
"lyra-resource",
"lyra-ecs",
"lyra-reflect",
"lyra-scripting",
"lyra-game",
"lyra-math",
"lyra-scene",
"examples/many-lights",
"examples/fixed-timestep-rotating-model",
"examples/lua-scripting",
"examples/many-lights",
"examples/shadows",
"examples/simple_scene",
"examples/testbed",
"examples/simple_scene"
]
[features]
scripting = ["dep:lyra-scripting"]
lua_scripting = ["scripting", "lyra-scripting/lua"]
tracy = ["lyra-game/tracy"]
[dependencies]
lyra-game = { path = "crates/lyra-game" }
lyra-scripting = { path = "crates/lyra-scripting", optional = true }
lyra-game = { path = "lyra-game" }
lyra-scripting = { path = "lyra-scripting", optional = true }
#[profile.dev]
#opt-level = 1
[profile.dev]
opt-level = 1
[profile.release]
debug = true

View file

@ -1,69 +0,0 @@
use quote::quote;
use syn::{parse_macro_input, spanned::Spanned, DeriveInput};
#[proc_macro_derive(Component)]
pub fn derive_component(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let type_ident = &input.ident;
let type_name = type_ident.to_string();
proc_macro::TokenStream::from(quote! {
impl #impl_generics lyra_engine::ecs::Component for #type_ident #ty_generics #where_clause {
fn name() -> &'static str {
#type_name
}
}
})
}
#[proc_macro_derive(Bundle)]
pub fn derive_bundle(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let type_ident = &input.ident;
let s = match &input.data {
syn::Data::Struct(s) => {
s
},
_ => {
return syn::Error::new(input.span(), "Bundle derive macro only supports Structs")
.into_compile_error().into();
}
};
let field_names: Vec<_> = s.fields.iter().map(|f| f.ident.as_ref().unwrap()).collect();
proc_macro::TokenStream::from(quote! {
impl #impl_generics lyra_engine::ecs::Bundle for #type_ident #ty_generics #where_clause {
fn type_ids(&self) -> Vec<lyra_engine::ecs::DynTypeId> {
let mut v = vec![];
#(
v.extend(self.#field_names.type_ids().into_iter());
)*
v
}
fn info(&self) -> Vec<lyra_engine::ecs::ComponentInfo> {
let mut v = vec![];
#(
v.extend(self.#field_names.info().into_iter());
)*
v
}
fn take(self, f: &mut impl FnMut(std::ptr::NonNull<u8>, lyra_engine::ecs::DynTypeId, lyra_engine::ecs::ComponentInfo)) {
#(
self.#field_names.take(f);
)*
}
fn is_dynamic(&self) -> bool {
false
}
}
})
}

View file

@ -1,168 +0,0 @@
use std::ops::{Deref, DerefMut};
use crate::{query::Fetch, Entity, World};
use super::{DynamicType, FetchDynamicTypeUnsafe, QueryDynamicType};
/// A view of dynamic types (types that are not known to Rust).
///
/// This view gives you the ability to iterate over types that are unknown to Rust, which we call
/// dynamic types. This is great for embedding with a scripting language (*cough* *cough* WASM)
/// since Rust doesn't actually need to know the types of what its iterating over.
pub struct DynamicViewOne<'a> {
world: &'a World,
inner: DynamicViewOneOwned,
}
impl<'a> Deref for DynamicViewOne<'a> {
type Target = DynamicViewOneOwned;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<'a> DerefMut for DynamicViewOne<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl<'a> DynamicViewOne<'a> {
pub fn new(world: &'a World, entity: Entity) -> Self {
Self {
world,
inner: DynamicViewOneOwned::new(entity)
}
}
/// Create a new [`DynamicViewOne`] with queries.
pub fn new_with(world: &'a World, entity: Entity, queries: Vec<QueryDynamicType>) -> Self {
Self {
world,
inner: DynamicViewOneOwned::new_with(entity, queries)
}
}
pub fn get(self) -> Option<Vec<DynamicType>> {
self.inner.get(&self.world)
}
}
/// A variant of [`DynamicViewOne`] that doesn't store a borrow of the world.
#[derive(Clone)]
pub struct DynamicViewOneOwned {
pub entity: Entity,
pub queries: Vec<QueryDynamicType>
}
impl DynamicViewOneOwned {
pub fn new(entity: Entity) -> Self {
Self {
entity,
queries: vec![],
}
}
/// Create a new [`DynamicViewOne`] with queries.
pub fn new_with(entity: Entity, queries: Vec<QueryDynamicType>) -> Self {
Self {
entity,
queries
}
}
pub fn get(self, world: &World) -> Option<Vec<DynamicType>> {
dynamic_view_one_get_impl(world, &self.queries, self.entity)
}
}
fn dynamic_view_one_get_impl(world: &World, queries: &Vec<QueryDynamicType>, entity: Entity) -> Option<Vec<DynamicType>> {
let arch = world.entity_archetype(entity)?;
let aid = arch.entity_indexes().get(&entity)?;
// get all fetchers for the queries
let mut fetchers: Vec<FetchDynamicTypeUnsafe> = queries.iter()
.map(|q| unsafe { q.fetch(world, arch.id(), arch) } )
.collect();
let mut fetch_res = vec![];
for fetcher in fetchers.iter_mut() {
if !fetcher.can_visit_item(*aid) {
return None;
} else {
let i = unsafe { fetcher.get_item(*aid) };
fetch_res.push(i);
}
}
if fetch_res.is_empty() {
None
} else {
Some(fetch_res)
}
}
#[cfg(test)]
mod tests {
use std::{alloc::Layout, ptr::NonNull};
use crate::{World, ComponentInfo, DynTypeId, DynamicBundle, query::dynamic::QueryDynamicType};
use super::DynamicViewOne;
#[test]
fn single_dynamic_view_one_state() {
let comp_layout = Layout::new::<u32>();
let comp_info = ComponentInfo::new_unknown(Some("u32".to_string()), DynTypeId::Unknown(100), comp_layout);
let mut dynamic_bundle = DynamicBundle::default();
let comp = 50u32;
let ptr = NonNull::from(&comp).cast::<u8>();
dynamic_bundle.push_unknown(ptr, comp_info.clone());
let mut world = World::new();
let e = world.spawn(dynamic_bundle);
let query = QueryDynamicType::from_info(comp_info);
let view = DynamicViewOne::new_with(&world, e, vec![query]);
let view_row = view.get()
.expect("failed to get entity row");
assert_eq!(view_row.len(), 1);
let mut row_iter = view_row.iter();
let dynamic_type = row_iter.next().unwrap();
let component_data = unsafe { dynamic_type.ptr.cast::<u32>().as_ref() };
assert_eq!(*component_data, 50);
}
#[test]
fn single_dynamic_view_one() {
let comp_layout = Layout::new::<u32>();
let comp_info = ComponentInfo::new_unknown(Some("u32".to_string()), DynTypeId::Unknown(100), comp_layout);
let mut dynamic_bundle = DynamicBundle::default();
let comp = 50u32;
let ptr = NonNull::from(&comp).cast::<u8>();
dynamic_bundle.push_unknown(ptr, comp_info.clone());
let mut world = World::new();
let e = world.spawn(dynamic_bundle);
let query = QueryDynamicType::from_info(comp_info);
let view = DynamicViewOne::new_with(&world, e, vec![query]);
let view_row = view.get()
.expect("failed to get entity row");
assert_eq!(view_row.len(), 1);
let mut row_iter = view_row.iter();
let dynamic_type = row_iter.next().unwrap();
let component_data = unsafe { dynamic_type.ptr.cast::<u32>().as_ref() };
assert_eq!(*component_data, 50);
}
}

View file

@ -1,94 +0,0 @@
use std::marker::PhantomData;
use crate::{query::{AsQuery, Fetch, Query}, Component, ComponentColumn, DynTypeId, Tick, World};
pub struct ChangedFetcher<'a, T> {
col: &'a ComponentColumn,
tick: Tick,
_phantom: PhantomData<&'a T>,
}
impl<'a, T> Fetch<'a> for ChangedFetcher<'a, T>
where
T: 'a,
{
type Item = bool;
fn dangling() -> Self {
unreachable!()
}
unsafe fn get_item(&mut self, entity: crate::world::ArchetypeEntityId) -> Self::Item {
let tick = self.col.entity_ticks[entity.0 as usize];
if *tick > 50 {
//debug!("tick: {}, world tick: {}", *tick, *self.tick);
}
*tick >= *self.tick
}
}
/// A filter that fetches components that have changed.
///
/// Since [`AsQuery`] is implemented for `&T`, you can use this query like this:
/// ```nobuild
/// for ts in world.view::<&T>() {
/// println!("Got a &T!");
/// }
/// ```
pub struct Changed<T> {
type_id: DynTypeId,
_phantom: PhantomData<T>
}
impl<T: Component> Default for Changed<T> {
fn default() -> Self {
Self {
type_id: DynTypeId::of::<T>(),
_phantom: PhantomData,
}
}
}
// manually implemented to avoid a Copy bound on T
impl<T> Copy for Changed<T> {}
// manually implemented to avoid a Clone bound on T
impl<T> Clone for Changed<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: Component> Changed<T> {
pub fn new() -> Self {
Self::default()
}
}
impl<T: Component> Query for Changed<T>
where
T: 'static
{
type Item<'a> = bool;
type Fetch<'a> = ChangedFetcher<'a, T>;
fn new() -> Self {
Changed::<T>::new()
}
fn can_visit_archetype(&self, archetype: &crate::archetype::Archetype) -> bool {
archetype.has_column(self.type_id)
}
unsafe fn fetch<'a>(&self, _: &'a World, a: &'a crate::archetype::Archetype, tick: crate::Tick) -> Self::Fetch<'a> {
ChangedFetcher {
col: a.get_column(self.type_id).unwrap(),
tick,
_phantom: PhantomData::<&T>,
}
}
}
impl<T: Component> AsQuery for Changed<T> {
type Query = Self;
}

View file

@ -1,111 +0,0 @@
mod has;
pub use has::*;
mod or;
pub use or::*;
mod not;
pub use not::*;
mod changed;
pub use changed::*;
mod without;
pub use without::*;
use crate::{Archetype, ArchetypeEntityId, Tick, World};
use super::{Fetch, Query};
pub trait FilterFetch<'a> {
/// Returns true if the entity should be visited or skipped.
fn can_visit(&mut self, entity: ArchetypeEntityId) -> bool;
}
pub trait Filter: Copy {
/// The fetcher used for the filter
type Fetch<'a>: FilterFetch<'a>;
fn new() -> Self;
/// Returns true if the archetype should be visited or skipped.
fn can_visit_archetype(&self, archetype: &Archetype) -> bool;
unsafe fn fetch<'a>(&self, world: &'a World, archetype: &'a Archetype, tick: Tick) -> Self::Fetch<'a>;
/// Returns a fetcher that doesn't fetch from an archetype.
unsafe fn fetch_world<'a>(&self, world: &'a World) -> Option<Self::Fetch<'a>> {
let _ = world;
None
}
}
/// A trait for getting the filter of a type.
pub trait AsFilter {
/// The query for this type
type Filter: Filter;
}
impl<Q> Filter for Q
where
Q: for <'a> Query<Item<'a> = bool>,
{
type Fetch<'a> = Q::Fetch<'a>;
fn new() -> Self {
Query::new()
}
fn can_visit_archetype(&self, archetype: &Archetype) -> bool {
Query::can_visit_archetype(self, archetype)
}
unsafe fn fetch<'a>(&self, world: &'a World, archetype: &'a Archetype, tick: Tick) -> Self::Fetch<'a> {
Query::fetch(self, world, archetype, tick)
}
}
impl<'a, F> FilterFetch<'a> for F
where
F: Fetch<'a, Item = bool>
{
fn can_visit(&mut self, entity: ArchetypeEntityId) -> bool {
Fetch::can_visit_item(self, entity) && unsafe { Fetch::get_item(self, entity) }
}
}
impl<Q> AsFilter for Q
where
Q: for <'a> Query<Item<'a> = bool>
{
type Filter = Q;
}
/// A fetcher that just returns a provided value
pub struct StaticFetcher<T: Clone> {
value: T,
}
impl<'a, T: Clone> StaticFetcher<T> {
pub fn new(value: T) -> Self {
Self {
value
}
}
}
impl<'a, T> Fetch<'a> for StaticFetcher<T>
where
T: Clone + 'a,
{
type Item = T;
fn dangling() -> Self {
unreachable!()
}
unsafe fn get_item(&mut self, _: crate::world::ArchetypeEntityId) -> Self::Item {
self.value.clone()
}
}

View file

@ -1,218 +0,0 @@
use std::marker::PhantomData;
use crate::{query::{AsQuery, Fetch, Query}, Archetype, World};
use super::{AsFilter, Filter, FilterFetch};
/// The fetcher for [`OrQuery`].
pub struct OrFetch<'a, Q1: Query, Q2: Query> {
left: Option<Q1::Fetch<'a>>,
right: Option<Q2::Fetch<'a>>,
}
impl<'a, Q1: Query, Q2: Query> Fetch<'a> for OrFetch<'a, Q1, Q2> {
type Item = (Option<Q1::Item<'a>>, Option<Q2::Item<'a>>);
fn dangling() -> Self {
Self {
left: None,
right: None,
}
}
unsafe fn get_item(&mut self, entity: crate::ArchetypeEntityId) -> Self::Item {
let mut res = (None, None);
if let Some(left) = self.left.as_mut() {
let i = left.get_item(entity);
res.0 = Some(i);
}
if let Some(right) = self.right.as_mut() {
let i = right.get_item(entity);
res.1 = Some(i);
}
res
}
}
/// The fetcher for [`OrFilter`].
pub struct OrFilterFetch<'a, F1: Filter, F2: Filter> {
left: Option<F1::Fetch<'a>>,
right: Option<F2::Fetch<'a>>,
}
impl<'a, F1: Filter, F2: Filter> FilterFetch<'a> for OrFilterFetch<'a, F1, F2> {
fn can_visit(&mut self, entity: crate::ArchetypeEntityId) -> bool {
if let Some(left) = self.left.as_mut() {
left.can_visit(entity)
} else if let Some(right) = self.right.as_mut() {
right.can_visit(entity)
} else {
false
}
}
}
/// The [`Query`] implementation of [`Or`].
#[derive(Default)]
pub struct OrQuery<Q1: Clone + Copy, Q2: Clone + Copy, C> {
left: Q1,
right: Q2,
can_visit_left: bool,
can_visit_right: bool,
_marker: PhantomData<C>,
}
impl<Q1: AsQuery, Q2: AsQuery> Copy for OrQuery<Q1::Query, Q2::Query, (Q1, Q2)> {}
impl<Q1: AsQuery, Q2: AsQuery> Clone for OrQuery<Q1::Query, Q2::Query, (Q1, Q2)> {
fn clone(&self) -> Self {
Self {
left: self.left.clone(),
right: self.right.clone(),
can_visit_left: self.can_visit_left,
can_visit_right: self.can_visit_right,
_marker: self._marker,
}
}
}
impl<Q1: AsQuery, Q2: AsQuery> Query for OrQuery<Q1::Query, Q2::Query, (Q1, Q2)> {
type Item<'a> = (Option<<Q1::Query as Query>::Item<'a>>, Option<<Q2::Query as Query>::Item<'a>>);
type Fetch<'a> = OrFetch<'a, Q1::Query, Q2::Query>;
fn new() -> Self {
OrQuery {
left: Q1::Query::new(),
right: Q2::Query::new(),
can_visit_left: false,
can_visit_right: false,
_marker: PhantomData
}
}
fn can_visit_archetype(&self, archetype: &Archetype) -> bool {
self.left.can_visit_archetype(archetype) || self.right.can_visit_archetype(archetype)
}
unsafe fn fetch<'a>(&self, world: &'a World, archetype: &'a Archetype, tick: crate::Tick) -> Self::Fetch<'a> {
let mut f = OrFetch::<Q1::Query, Q2::Query>::dangling();
// TODO: store the result of Self::can_visit_archetype so this isn't ran twice
if self.left.can_visit_archetype(archetype) {
f.left = Some(self.left.fetch(world, archetype, tick));
}
if self.right.can_visit_archetype(archetype) {
f.right = Some(self.right.fetch(world, archetype, tick));
}
f
}
}
/// The [`Filter`] implementation of [`Or`].
#[derive(Default)]
pub struct OrFilter<F1: AsFilter, F2: AsFilter> {
left: F1::Filter,
right: F2::Filter,
can_visit_left: bool,
can_visit_right: bool,
}
impl<F1: AsFilter, F2: AsFilter> Copy for OrFilter<F1, F2> {}
impl<F1: AsFilter, F2: AsFilter> Clone for OrFilter<F1, F2> {
fn clone(&self) -> Self {
Self {
left: self.left.clone(),
right: self.right.clone(),
can_visit_left: self.can_visit_left,
can_visit_right: self.can_visit_right,
}
}
}
impl<F1: AsFilter, F2: AsFilter> Filter for OrFilter<F1, F2> {
type Fetch<'a> = OrFilterFetch<'a, F1::Filter, F2::Filter>;
fn new() -> Self {
OrFilter {
left: F1::Filter::new(),
right: F2::Filter::new(),
can_visit_left: false,
can_visit_right: false,
}
}
fn can_visit_archetype(&self, archetype: &Archetype) -> bool {
self.left.can_visit_archetype(archetype) || self.right.can_visit_archetype(archetype)
}
unsafe fn fetch<'a>(&self, world: &'a World, archetype: &'a Archetype, tick: crate::Tick) -> Self::Fetch<'a> {
let left = if self.left.can_visit_archetype(archetype) {
Some(self.left.fetch(world, archetype, tick))
} else { None };
let right = if self.right.can_visit_archetype(archetype) {
Some(self.right.fetch(world, archetype, tick))
} else { None };
OrFilterFetch {
left,
right,
}
}
}
/// A query and/or filter returning when either `Q1` or `Q2` returns.
///
/// This checks if `Q1` can fetch before checking `Q2`.
///
/// It can be used as a query:
/// ```nobuild
/// for (en, pos, mesh_or_scene) in world
/// .view::<(Entities, &Transform, Or<&Mesh, &Scene>)>()
/// .iter()
/// {
/// // do some things with the position of the entities
///
/// let (mesh, scene) = mesh_or_scene;
///
/// // now handle do things with the Mesh or Scene that the entity could have
/// if let Some(mesh) = mesh {
/// // do mesh things
/// }
///
/// if let Some(scene) = scene {
/// // do scene things
/// }
/// }
/// ```
///
/// Or as a filter
/// ```nobuild
/// for (en, pos) in world
/// .filtered_view::<
/// (Entities, &Transform),
/// Or<Has<Mesh>, Has<Scene>>
/// >()
/// .iter()
/// {
/// // this entity has a `Transform`, and either a Mesh or Scene, or both!
/// }
/// ```
pub struct Or<Q1, Q2> {
_marker: PhantomData<(Q1, Q2)>
}
impl<Q1: AsQuery, Q2: AsQuery> AsQuery for Or<Q1, Q2> {
type Query = OrQuery<Q1::Query, Q2::Query, (Q1, Q2)>;
}
impl<F1: AsFilter, F2: AsFilter> AsFilter for Or<F1, F2> {
type Filter = OrFilter<F1, F2>;
}

View file

@ -1,45 +0,0 @@
use std::marker::PhantomData;
use crate::{query::{AsQuery, Query}, Archetype, Component, DynTypeId, World};
use super::StaticFetcher;
/// A filter query for entities that do not have the component `C`.
///
/// See [`With`].
#[derive(Default)]
pub struct Without<C: Component> {
_marker: PhantomData<C>
}
impl<C: Component> Copy for Without<C> {}
impl<C: Component> Clone for Without<C> {
fn clone(&self) -> Self {
Self { _marker: self._marker.clone() }
}
}
impl<C: Component> Query for Without<C> {
type Item<'a> = bool;
type Fetch<'a> = StaticFetcher<bool>;
fn new() -> Self {
Without {
_marker: PhantomData
}
}
fn can_visit_archetype(&self, archetype: &Archetype) -> bool {
!archetype.has_column(DynTypeId::of::<C>())
}
unsafe fn fetch<'a>(&self, _world: &'a World, _: &'a Archetype, _: crate::Tick) -> Self::Fetch<'a> {
// if fetch is called, it means that 'can_visit_archetype' returned true
StaticFetcher::new(true)
}
}
impl<C: Component> AsQuery for Without<C> {
type Query = Self;
}

View file

@ -1,102 +0,0 @@
use std::ops::Deref;
use crate::{system::FnArgFetcher, Tick, World};
use super::{Fetch, Query, AsQuery};
/// Fetcher used to fetch the current tick of the world.
pub struct FetchWorldTick {
tick: Tick
}
impl<'a> Fetch<'a> for FetchWorldTick {
type Item = WorldTick;
fn dangling() -> Self {
unreachable!()
}
fn can_visit_item(&mut self, _entity: crate::ArchetypeEntityId) -> bool {
true
}
unsafe fn get_item(&mut self, _entity: crate::world::ArchetypeEntityId) -> Self::Item {
WorldTick(self.tick)
}
}
/// Query used to query the current tick of the world.
#[derive(Clone, Copy)]
pub struct QueryWorldTick;
impl Default for QueryWorldTick {
fn default() -> Self {
Self
}
}
impl Query for QueryWorldTick {
type Item<'a> = WorldTick;
type Fetch<'a> = FetchWorldTick;
const ALWAYS_FETCHES: bool = true;
fn new() -> Self {
QueryWorldTick
}
fn can_visit_archetype(&self, _archetype: &crate::archetype::Archetype) -> bool {
true
}
unsafe fn fetch<'a>(&self, world: &'a World, _archetype: &'a crate::archetype::Archetype, _tick: crate::Tick) -> Self::Fetch<'a> {
FetchWorldTick {
tick: world.current_tick()
}
}
unsafe fn fetch_world<'a>(&self, world: &'a World) -> Option<Self::Fetch<'a>> {
Some(FetchWorldTick {
tick: world.current_tick()
})
}
}
impl AsQuery for QueryWorldTick {
type Query = Self;
}
/// Type that can be used in an fn system for fetching the current world tick.
#[derive(Debug, Clone, Copy)]
pub struct WorldTick(Tick);
impl Deref for WorldTick {
type Target = Tick;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsQuery for WorldTick {
type Query = QueryWorldTick;
}
impl FnArgFetcher for WorldTick {
type State = ();
type Arg<'a, 'state> = WorldTick;
fn create_state(_: std::ptr::NonNull<World>) -> Self::State {
()
}
unsafe fn get<'a, 'state>(_: &'state mut Self::State, world: std::ptr::NonNull<World>) -> Self::Arg<'a, 'state> {
let world = world.as_ref();
WorldTick(world.current_tick())
}
fn apply_deferred<'state>(_: &'state mut Self::State, _: std::ptr::NonNull<World>) {
}
}

View file

@ -1,94 +0,0 @@
use std::{any::TypeId, cell::Ref, marker::PhantomData};
use crate::{query::{AsQuery, Fetch, Query}, ComponentColumn, Entity, World};
use super::{Relation, RelationTargetComponent};
pub struct FetchRelatedBy<'a, T> {
col: &'a ComponentColumn,
origin: Entity,
_phantom: PhantomData<&'a T>
}
impl<'a, R> Fetch<'a> for FetchRelatedBy<'a, R>
where
R: Relation,
{
type Item = bool;
fn dangling() -> Self {
unreachable!()
}
fn can_visit_item(&mut self, entity: crate::ArchetypeEntityId) -> bool {
unsafe {
let comp: Ref<RelationTargetComponent<R>> = self.col.get(entity.0 as usize);
comp.origin == self.origin
}
}
unsafe fn get_item(&mut self, _: crate::world::ArchetypeEntityId) -> Self::Item {
true
}
}
pub struct QueryRelatedBy<R> {
target: Option<Entity>,
_marker: PhantomData<R>,
}
impl<R> Copy for QueryRelatedBy<R> {}
impl<R> Clone for QueryRelatedBy<R> {
fn clone(&self) -> Self {
*self
}
}
impl<R> QueryRelatedBy<R> {
pub fn new(target: Option<Entity>) -> Self {
Self {
target,
_marker: PhantomData
}
}
}
impl<R> Query for QueryRelatedBy<R>
where
R: Relation + 'static
{
type Item<'a> = bool;
type Fetch<'a> = FetchRelatedBy<'a, R>;
fn new() -> Self {
panic!("RelatedBy MUST be made with View::related_by since it requires State provided by \
that function.")
}
fn can_visit_archetype(&self, archetype: &crate::archetype::Archetype) -> bool {
archetype.has_column(TypeId::of::<RelationTargetComponent<R>>())
}
unsafe fn fetch<'a>(&self, _world: &'a World, archetype: &'a crate::archetype::Archetype, tick: crate::Tick) -> Self::Fetch<'a> {
let _ = tick;
let col = archetype.get_column(TypeId::of::<RelationTargetComponent<R>>())
.expect("You ignored 'can_visit_archetype'!");
FetchRelatedBy {
col,
origin: self.target.expect("Filter not initialized"),
_phantom: PhantomData,
}
}
}
/// A filter that returns entities that are the target of a relation.
pub struct RelatedBy<R: Relation> {
_marker: PhantomData<R>,
}
impl<R: Relation> AsQuery for RelatedBy<R> {
type Query = QueryRelatedBy<R>;
}

File diff suppressed because it is too large Load diff

View file

@ -1,260 +0,0 @@
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
use atomic_refcell::AtomicRefCell;
use lyra_ecs::{
query::{ResMut, WorldTick},
system::FnArgFetcher,
Tick,
};
pub trait Event: Clone + Send + Sync + 'static {}
impl<T: Clone + Send + Sync + 'static> Event for T {}
/// A Vec with other Vecs in it to track relative age of items.
///
/// The vec has 3 levels, a `newest`, `medium` and `old`. Items are pushed to the `newest`
/// internal vec. When [`WaterfallVec::waterfall`] is called the items in `newest` are
/// put into `medium`, and items in `medium` goes to `old`.
///
/// By checking the items in each internal vec, you can see a relative age between the items.
/// The event system uses this to clear the `old` vec to ensure keep events for only two
/// frames at a time.
struct WaterfallVec<T> {
newest: Vec<T>,
medium: Vec<T>,
old: Vec<T>,
}
impl<T> Default for WaterfallVec<T> {
fn default() -> Self {
Self {
newest: Default::default(),
medium: Default::default(),
old: Default::default(),
}
}
}
impl<T> WaterfallVec<T> {
fn total_len(&self) -> usize {
self.newest.len() + self.medium.len() + self.old.len()
}
fn get(&self, mut i: usize) -> Option<&T> {
if i >= self.old.len() {
i -= self.old.len();
if i >= self.medium.len() {
i -= self.medium.len();
self.newest.get(i)
} else {
self.medium.get(i)
}
} else {
self.old.get(i)
}
}
/// Age elements.
///
/// This moves elements in `newest` to `medium` and elements in `medium` to `old`.
/// This is what drives the relative age of the [`WaterfallVec`].
fn waterfall(&mut self) {
self.old.append(&mut self.medium);
self.medium.append(&mut self.newest);
}
/// Push a new element to the newest queue.
fn push(&mut self, event: T) {
self.newest.push(event);
}
/// Clear oldest items.
fn clear_oldest(&mut self) {
self.old.clear();
}
}
pub struct Events<T: Event> {
events: Arc<AtomicRefCell<WaterfallVec<T>>>,
/// Used to track when the old events were last cleared.
last_cleared_at: Tick,
/// Used to indicate when the cursor in readers should be reset to zero.
/// This becomes true after the old events are cleared.
//reset_cursor: bool,
/// This is set to the amount of elements that were deleted.
///
/// Its used to decrease the cursor by the amount of elements deleted, instead of resetting
/// to zero to avoid rereading events that are in other levels of the vec.
decrease_cursor_by: Option<usize>,
}
impl<T: Event> Default for Events<T> {
fn default() -> Self {
Self {
events: Default::default(),
last_cleared_at: Default::default(),
decrease_cursor_by: None,
}
}
}
impl<T: Event> Events<T> {
pub fn new() -> Self {
Self::default()
}
pub fn push_event(&mut self, event: T) {
let mut events = self.events.borrow_mut();
events.push(event);
}
pub fn reader(&self) -> EventReader<T> {
EventReader {
events: self.events.clone(),
cursor: Arc::new(AtomicUsize::new(0)),
}
}
pub fn writer(&self) -> EventWriter<T> {
EventWriter {
events: self.events.clone(),
}
}
}
pub struct EventReader<T: Event> {
events: Arc<AtomicRefCell<WaterfallVec<T>>>,
cursor: Arc<AtomicUsize>,
}
impl<T: Event> EventReader<T> {
pub fn read(&self) -> Option<T> {
let events = self.events.borrow();
let cursor = self.cursor.load(Ordering::Acquire);
if cursor >= events.total_len() {
None
} else {
let e = events.get(cursor).unwrap();
self.cursor.store(cursor + 1, Ordering::Release);
Some(e.clone())
}
}
}
pub struct EventWriter<T: Event> {
events: Arc<AtomicRefCell<WaterfallVec<T>>>,
}
impl<T: Event> EventWriter<T> {
pub fn write(&self, event: T) {
let mut events = self.events.borrow_mut();
events.push(event);
}
}
/// Clean events of event type `T` every 2 ticks.
pub fn event_cleaner_system<T>(tick: WorldTick, mut events: ResMut<Events<T>>) -> anyhow::Result<()>
where
T: Event,
{
let last_tick = *events.last_cleared_at;
let world_tick = **tick;
if last_tick + 2 < world_tick {
events.last_cleared_at = *tick;
let mut events_fall = events.events.borrow_mut();
// Since oldest will be cleared, we need to decrease the cursor by the removed amount
// store the amount it needs to decrease by.
let old_len = events_fall.old.len();
events_fall.clear_oldest();
drop(events_fall);
events.decrease_cursor_by = Some(old_len);
} else {
events.decrease_cursor_by = None;
}
let mut events = events.events.borrow_mut();
events.waterfall();
Ok(())
}
impl<T: Event> FnArgFetcher for EventReader<T> {
type State = Arc<AtomicUsize>;
type Arg<'b, 'state> = EventReader<T>;
fn create_state(_: std::ptr::NonNull<lyra_ecs::World>) -> Self::State {
Arc::new(AtomicUsize::new(0))
}
unsafe fn get<'b, 'state>(
state: &'state mut Self::State,
world: std::ptr::NonNull<lyra_ecs::World>,
) -> Self::Arg<'b, 'state> {
let world = world.as_ref();
let events = world.get_resource::<Events<T>>().unwrap_or_else(|| {
panic!(
"world missing Events<{}> resource",
std::any::type_name::<T>()
)
});
if let Some(dec_by) = events.decrease_cursor_by {
// The waterfall vec had its oldest events deleted.
// The cursor needs to be decreased by the amount of elements deleted, instead of resetting
// to zero to avoid rereading events that are in other levels of the vec.
let mut s = state.load(Ordering::Acquire);
s = s.checked_sub(dec_by).unwrap_or_default();
state.store(s, Ordering::Release);
//*state = state.checked_sub(dec_by).unwrap_or_default();
}
let reader = EventReader {
events: events.events.clone(),
cursor: state.clone(),
};
reader
}
fn apply_deferred<'state>(_: &'state mut Self::State, _: std::ptr::NonNull<lyra_ecs::World>) {}
}
impl<T: Event> FnArgFetcher for EventWriter<T> {
type State = ();
type Arg<'a, 'state> = EventWriter<T>;
fn create_state(_: std::ptr::NonNull<lyra_ecs::World>) -> Self::State {
()
}
unsafe fn get<'a, 'state>(
_: &'state mut Self::State,
world: std::ptr::NonNull<lyra_ecs::World>,
) -> Self::Arg<'a, 'state> {
let world = world.as_ref();
let events = world.get_resource::<Events<T>>().unwrap_or_else(|| {
panic!(
"world missing Events<{}> resource",
std::any::type_name::<T>()
)
});
EventWriter {
events: events.events.clone(),
}
}
fn apply_deferred<'state>(_: &'state mut Self::State, _: std::ptr::NonNull<lyra_ecs::World>) {}
}

View file

@ -1,208 +0,0 @@
use std::{cell::OnceCell, collections::VecDeque, ptr::NonNull};
use lyra_ecs::{system::{IntoSystem, System}, ResourceObject, World};
use lyra_math::IVec2;
use tracing::{error, info};
use crate::{event_cleaner_system, plugin::Plugin, render::renderer::Renderer, Event, Events, Stage, StagedExecutor};
#[derive(Clone, Copy, Hash, Debug)]
pub enum GameStages {
/// This stage runs before all other stages.
First,
/// This stage runs before `Update`.
PreUpdate,
/// This stage is where most game logic would be.
Update,
/// This stage is ran after `Update`.
PostUpdate,
/// This stage runs after all other stages.
Last,
}
impl Stage for GameStages {}
pub struct Controls<'a> {
pub world: &'a mut World,
}
#[derive(Clone, Default)]
pub struct WindowState {
/// Indicates if the window is currently focused.
pub focused: bool,
/// Indicates if the window is currently occluded.
pub occluded: bool,
/// Indicates if the cursor is inside of the window.
pub cursor_inside_window: bool,
pub position: IVec2,
}
impl WindowState {
pub fn new() -> Self {
Self::default()
}
}
pub struct App {
pub(crate) renderer: OnceCell<Box<dyn Renderer>>,
pub world: World,
plugins: VecDeque<Box<dyn Plugin>>,
startup_systems: VecDeque<Box<dyn System>>,
staged_exec: StagedExecutor,
run_fn: OnceCell<Box<dyn FnOnce(App)>>,
}
impl App {
pub fn new() -> Self {
let world = World::new();
// initialize ecs system stages
let mut staged = StagedExecutor::new();
staged.add_stage(GameStages::First);
staged.add_stage_after(GameStages::First, GameStages::PreUpdate);
staged.add_stage_after(GameStages::PreUpdate, GameStages::Update);
staged.add_stage_after(GameStages::Update, GameStages::PostUpdate);
staged.add_stage_after(GameStages::PostUpdate, GameStages::Last);
Self {
renderer: OnceCell::new(),
world,
plugins: Default::default(),
startup_systems: Default::default(),
staged_exec: staged,
run_fn: OnceCell::new(),
}
}
pub fn update(&mut self) {
self.world.tick();
let wptr = NonNull::from(&self.world);
if let Err(e) = self.staged_exec.execute(wptr, true) {
error!("Error when executing staged systems: '{}'", e);
}
}
pub(crate) fn on_resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
self.renderer.get_mut()
.expect("renderer was not initialized")
.on_resize(&mut self.world, new_size);
}
pub(crate) fn on_exit(&mut self) {
info!("On exit!");
}
pub fn add_resource<T: ResourceObject>(&mut self, data: T) {
self.world.add_resource(data);
}
/// Add a system to the ecs world
pub fn with_system<S, A>(&mut self, name: &str, system: S, depends: &[&str]) -> &mut Self
where
S: IntoSystem<A>,
<S as IntoSystem<A>>::System: 'static
{
self.staged_exec.add_system_to_stage(GameStages::Update, name, system.into_system(), depends);
self
}
/// Add a stage.
///
/// This stage could run at any moment if nothing is dependent on it.
pub fn add_stage<T: Stage>(&mut self, stage: T) -> &mut Self {
self.staged_exec.add_stage(stage);
self
}
/// Add a stage that executes after another one.
///
/// Parameters:
/// * `before` - The stage that will run before `after`.
/// * `after` - The stage that will run after `before`.
pub fn add_stage_after<T: Stage, U: Stage>(&mut self, before: T, after: U) -> &mut Self {
self.staged_exec.add_stage_after(before, after);
self
}
/// Add a system to an already existing stage.
///
/// # Panics
/// Panics if the stage was not already added to the executor
pub fn add_system_to_stage<T, S, A>(&mut self, stage: T,
name: &str, system: S, depends: &[&str]) -> &mut Self
where
T: Stage,
S: IntoSystem<A>,
<S as IntoSystem<A>>::System: 'static
{
self.staged_exec.add_system_to_stage(stage, name, system.into_system(), depends);
self
}
/// Add a startup system that will be ran right after plugins are setup.
/// They will only be ran once
pub fn with_startup_system<S>(&mut self, system: S) -> &mut Self
where
S: System + 'static
{
self.startup_systems.push_back(Box::new(system));
self
}
/// Add a plugin to the game. These are executed as they are added.
pub fn with_plugin<P>(&mut self, mut plugin: P) -> &mut Self
where
P: Plugin + 'static
{
plugin.setup(self);
let plugin = Box::new(plugin);
self.plugins.push_back(plugin);
self
}
/// Override the default (empty) world
///
/// This isn't recommended, you should create a startup system and add it to `with_startup_system`
pub fn with_world(&mut self, world: World) -> &mut Self {
self.world = world;
self
}
pub fn set_run_fn<F>(&self, f: F)
where
F: FnOnce(App) + 'static
{
// ignore if a runner function was already set
let _ = self.run_fn.set(Box::new(f));
}
pub fn run(mut self) {
let f = self.run_fn.take()
.expect("No run function set");
f(self);
}
pub fn register_event<T: Event>(&mut self) {
let world = &mut self.world;
// only register the event if it isn't already registered.
if !world.has_resource::<Events<T>>() {
world.add_resource_default::<Events<T>>();
let sys_name = format!("{}_event_cleaner_system", std::any::type_name::<T>().to_lowercase());
self.add_system_to_stage(GameStages::First, &sys_name, event_cleaner_system::<T>, &[]);
}
}
pub fn push_event<T: Event>(&mut self, event: T) {
let world = &mut self.world;
let mut events = world.get_resource_mut::<Events<T>>()
.expect("missing events for event type! Must use `App::register_event` first");
events.push_event(event);
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,143 +0,0 @@
use glam::Vec2;
use lyra_ecs::query::ResMut;
use winit::{
event::{MouseScrollDelta, WindowEvent},
keyboard::PhysicalKey,
};
use crate::{game::GameStages, plugin::Plugin, winit::DeviceEventPair, EventReader, EventWriter};
use super::{events::*, InputButtons, KeyCode};
fn write_scroll_delta(mouse_scroll_ev: &mut EventWriter<MouseScroll>, delta: &MouseScrollDelta) {
let event = match delta {
MouseScrollDelta::LineDelta(x, y) => MouseScroll {
unit: MouseScrollUnit::Line(Vec2::new(*x, *y)),
},
MouseScrollDelta::PixelDelta(delta) => MouseScroll {
unit: MouseScrollUnit::Pixel(Vec2::new(delta.x as f32, delta.y as f32)),
},
};
mouse_scroll_ev.write(event);
}
fn write_key_event(
key_buttons: &mut ResMut<InputButtons<KeyCode>>,
physical_key: PhysicalKey,
state: winit::event::ElementState,
) {
if let PhysicalKey::Code(code) = physical_key {
key_buttons.add_input_from_winit(KeyCode::from(code), state);
}
}
pub fn input_system(
mut key_code_res: ResMut<InputButtons<KeyCode>>,
mut mouse_btn_res: ResMut<InputButtons<MouseButton>>,
mut touches_res: ResMut<Touches>,
window_ev: EventReader<WindowEvent>,
device_ev: EventReader<DeviceEventPair>,
mut mouse_scroll_ev: EventWriter<MouseScroll>,
mouse_btn_ev: EventWriter<MouseButton>,
mouse_exact_ev: EventWriter<MouseExact>,
mouse_entered_ev: EventWriter<CursorEnteredWindow>,
mouse_left_ev: EventWriter<CursorLeftWindow>,
mouse_motion_ev: EventWriter<MouseMotion>,
) -> anyhow::Result<()> {
while let Some(event) = window_ev.read() {
match event {
WindowEvent::KeyboardInput { event, .. } => {
write_key_event(&mut key_code_res, event.physical_key, event.state);
}
WindowEvent::CursorMoved { position, .. } => {
let exact = MouseExact {
pos: Vec2::new(position.x as f32, position.y as f32),
};
mouse_exact_ev.write(exact);
}
WindowEvent::CursorEntered { .. } => {
mouse_entered_ev.write(CursorEnteredWindow);
}
WindowEvent::CursorLeft { .. } => {
mouse_left_ev.write(CursorLeftWindow);
}
WindowEvent::MouseWheel { delta, .. } => {
write_scroll_delta(&mut mouse_scroll_ev, &delta);
}
WindowEvent::MouseInput { button, state, .. } => {
let button_event = match button {
winit::event::MouseButton::Left => MouseButton::Left,
winit::event::MouseButton::Right => MouseButton::Right,
winit::event::MouseButton::Middle => MouseButton::Middle,
winit::event::MouseButton::Back => MouseButton::Back,
winit::event::MouseButton::Forward => MouseButton::Forward,
winit::event::MouseButton::Other(v) => MouseButton::Other(v),
};
mouse_btn_ev.write(button_event);
mouse_btn_res.add_input_from_winit(button_event, state);
}
WindowEvent::Touch(t) => {
let touch = Touch {
phase: TouchPhase::from(t.phase),
location: Vec2::new(t.location.x as f32, t.location.y as f32),
force: t.force.map(Force::from),
finger_id: t.id,
};
touches_res.touches.push(touch);
}
_ => {}
}
}
while let Some(device) = device_ev.read() {
match &device.event {
winit::event::DeviceEvent::Motion { .. } => {
// TODO: handle device motion events
// A todo! isn't used since these are triggered alongside MouseMotion events
}
winit::event::DeviceEvent::MouseMotion { delta } => {
let delta = MouseMotion {
delta: Vec2::new(delta.0 as f32, delta.1 as f32),
};
mouse_motion_ev.write(delta);
}
winit::event::DeviceEvent::MouseWheel { delta } => {
write_scroll_delta(&mut mouse_scroll_ev, delta);
}
winit::event::DeviceEvent::Key(key) => {
write_key_event(&mut key_code_res, key.physical_key, key.state);
}
_ => {
todo!("unhandled device event: {:?}", device.event);
}
}
}
Ok(())
}
/// Plugin that runs InputSystem
#[derive(Default)]
pub struct InputPlugin;
impl Plugin for InputPlugin {
fn setup(&mut self, app: &mut crate::game::App) {
app.add_resource(InputButtons::<KeyCode>::default());
app.add_resource(InputButtons::<MouseButton>::default());
app.add_resource(Touches::default());
app.register_event::<MouseScroll>();
app.register_event::<MouseButton>();
app.register_event::<MouseMotion>();
app.register_event::<MouseExact>();
app.register_event::<CursorEnteredWindow>();
app.register_event::<CursorLeftWindow>();
app.add_system_to_stage(GameStages::PreUpdate, "input", input_system, &[]);
}
}

View file

@ -1,70 +0,0 @@
use glam::Vec2;
use lyra_math::Transform;
use crate::scene::CameraProjection;
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct CameraUniform {
/// The view matrix of the camera
pub view: glam::Mat4,
/// The inverse of the projection matrix of the camera
pub inverse_projection: glam::Mat4,
/// The view projection matrix
pub view_projection: glam::Mat4,
pub projection: glam::Mat4,
/// The position of the camera
pub position: glam::Vec3,
_padding: u32,
}
impl Default for CameraUniform {
fn default() -> Self {
Self {
view: glam::Mat4::IDENTITY,
inverse_projection: glam::Mat4::IDENTITY,
view_projection: glam::Mat4::IDENTITY,
projection: glam::Mat4::IDENTITY,
position: Default::default(),
_padding: 0,
}
}
}
impl CameraUniform {
pub fn new(
view: glam::Mat4,
inverse_projection: glam::Mat4,
view_projection: glam::Mat4,
projection: glam::Mat4,
position: glam::Vec3,
) -> Self {
Self {
view,
inverse_projection,
view_projection,
projection,
position,
_padding: 0,
}
}
pub fn from_component(transform: Transform, projection: CameraProjection, viewport_size: Vec2) -> Self {
let position = transform.translation;
let forward = transform.forward();
let up = transform.up();
let view = glam::Mat4::look_to_rh(position, forward, up);
let projection = projection.to_mat4(viewport_size);
let view_projection = projection * view;
Self {
view,
inverse_projection: projection.inverse(),
view_projection,
projection,
position,
_padding: 0,
}
}
}

View file

@ -1,68 +0,0 @@
use std::{any::TypeId, cell::Ref};
use lyra_ecs::{query::{AsQuery, Fetch, Query}, Archetype, ArchetypeEntityId, ComponentColumn, Tick, World};
use lyra_math::Transform;
use lyra_scene::WorldTransform;
/// Fetcher for [`EitherTransform`].
pub struct FetchEitherTransform<'a> {
col: &'a ComponentColumn,
is_world: bool,
}
impl<'a> Fetch<'a> for FetchEitherTransform<'a> {
type Item = Ref<'a, Transform>;
fn dangling() -> Self {
unreachable!()
}
unsafe fn get_item(&mut self, entity: ArchetypeEntityId) -> Self::Item {
if self.is_world {
let wt = self.col.get::<WorldTransform>(entity.0 as _);
Ref::map(wt, |wt| &**wt)
} else {
self.col.get(entity.0 as _)
}
}
}
/// ECS query that retrieves the Transform from an Entity's [`WorldTransform`], or [`Transform`] component.
///
/// [`WorldTransform`] is preferred over [`Transform`].
#[derive(Default, Clone, Copy)]
pub struct EitherTransform;
impl Query for EitherTransform {
type Item<'a> = Ref<'a, Transform>;
type Fetch<'a> = FetchEitherTransform<'a>;
fn new() -> Self {
EitherTransform
}
fn can_visit_archetype(&self, archetype: &Archetype) -> bool {
archetype.has_column(TypeId::of::<Transform>()) ||
archetype.has_column(TypeId::of::<WorldTransform>())
}
unsafe fn fetch<'a>(&self, _: &'a World, archetype: &'a Archetype, _: Tick) -> Self::Fetch<'a> {
if let Some(col) = archetype.get_column(TypeId::of::<WorldTransform>()) {
FetchEitherTransform {
col,
is_world: true,
}
} else if let Some(col) = archetype.get_column(TypeId::of::<Transform>()) {
FetchEitherTransform {
col,
is_world: false,
}
} else { unreachable!() }
}
}
impl AsQuery for EitherTransform {
type Query = Self;
}

View file

@ -1,101 +0,0 @@
use std::{collections::VecDeque, sync::Arc};
use tracing::instrument;
use super::{RenderGraphLabel, RenderGraphLabelValue};
/// A queued write to a GPU buffer targeting a graph slot.
pub(crate) struct GraphBufferWrite {
/// The name of the slot that has the resource that will be written
pub(crate) target_slot: RenderGraphLabelValue,
pub(crate) offset: u64,
pub(crate) bytes: Vec<u8>,
}
#[allow(dead_code)]
pub struct RenderGraphContext<'a> {
/// The [`wgpu::CommandEncoder`] used to encode GPU operations.
///
/// This is `None` during the `prepare` stage.
pub encoder: Option<wgpu::CommandEncoder>,
/// The gpu device that is being used.
pub device: Arc<wgpu::Device>,
pub queue: Arc<wgpu::Queue>,
pub(crate) buffer_writes: VecDeque<GraphBufferWrite>,
renderpass_desc: Vec<wgpu::RenderPassDescriptor<'a>>,
/// The label of this Node.
pub label: RenderGraphLabelValue,
}
impl<'a> RenderGraphContext<'a> {
pub(crate) fn new(device: Arc<wgpu::Device>, queue: Arc<wgpu::Queue>, encoder: Option<wgpu::CommandEncoder>, label: RenderGraphLabelValue) -> Self {
Self {
encoder,
device,
queue,
buffer_writes: Default::default(),
renderpass_desc: vec![],
label,
}
}
pub fn begin_render_pass(
&'a mut self,
desc: wgpu::RenderPassDescriptor<'a>,
) -> wgpu::RenderPass<'a> {
self.encoder
.as_mut()
.expect(
"RenderGraphContext is missing a command encoder. This is likely \
because you are trying to run render commands in the prepare stage.",
)
.begin_render_pass(&desc)
}
pub fn begin_compute_pass(&mut self, desc: &wgpu::ComputePassDescriptor) -> wgpu::ComputePass {
self.encoder
.as_mut()
.expect(
"RenderGraphContext is missing a command encoder. This is likely \
because you are trying to run render commands in the prepare stage.",
)
.begin_compute_pass(desc)
}
/// Queue a data write to a buffer at that is contained in `target_slot`.
///
/// This does not submit the data to the GPU immediately, or add it to the `wgpu::Queue`. The
/// data will be submitted to the GPU queue right after the prepare stage for all passes
/// is ran.
#[instrument(skip(self, bytes), level="trace", fields(size = bytes.len()))]
pub fn queue_buffer_write(&mut self, target_slot: impl RenderGraphLabel, offset: u64, bytes: &[u8]) {
self.buffer_writes.push_back(GraphBufferWrite {
target_slot: target_slot.into(),
offset,
bytes: bytes.to_vec(),
})
}
/// Queue a data write of a type that to a buffer at that is contained in `target_slot`.
#[instrument(skip(self, bytes), level="trace", fields(size = std::mem::size_of::<T>()))]
pub fn queue_buffer_write_with<T: bytemuck::NoUninit>(
&mut self,
target_slot: impl RenderGraphLabel,
offset: u64,
bytes: T,
) {
self.queue_buffer_write(target_slot, offset, bytemuck::bytes_of(&bytes));
}
/// Submit the encoder to the gpu queue.
///
/// The `encoder` of this context will be `None` until the next node is executed, then another
/// one will be made. You likely don't need to run this yourself until you are manually
/// presenting a surface texture.
pub fn submit_encoder(&mut self) {
let en = self.encoder.take()
.unwrap()
.finish();
self.queue.submit(std::iter::once(en));
}
}

View file

@ -1,596 +0,0 @@
mod node;
use std::{
cell::{Ref, RefCell, RefMut}, collections::VecDeque, fmt::Debug, hash::Hash, rc::Rc, sync::Arc
};
use lyra_ecs::World;
use lyra_resource::{RequestError, ResHandle, ResourceManager};
pub use node::*;
mod passes;
pub use passes::*;
mod slot_desc;
pub use slot_desc::*;
mod context;
pub use context::*;
mod render_target;
pub use render_target::*;
use rustc_hash::FxHashMap;
use tracing::{debug_span, instrument, trace, warn};
use wgpu::{util::DeviceExt, BufferUsages, CommandEncoder};
use super::{resource::{ComputePipeline, Pass, Pipeline, RenderPipeline}, Shader};
/// A trait that represents the label of a resource, slot, or node in the [`RenderGraph`].
pub trait RenderGraphLabel: Debug + 'static {
fn rc_clone(&self) -> Rc<dyn RenderGraphLabel>;
fn as_label_hash(&self) -> u64;
fn label_eq_rc(&self, other: &Rc<dyn RenderGraphLabel>) -> bool {
self.as_label_hash() == other.as_label_hash()
}
fn label_eq(&self, other: &dyn RenderGraphLabel) -> bool {
self.as_label_hash() == other.as_label_hash()
}
}
/// An owned [`RenderGraphLabel`].
#[derive(Clone)]
pub struct RenderGraphLabelValue(Rc<dyn RenderGraphLabel>);
impl Debug for RenderGraphLabelValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl<L: RenderGraphLabel> From<L> for RenderGraphLabelValue {
fn from(value: L) -> Self {
Self(Rc::new(value))
}
}
impl From<Rc<dyn RenderGraphLabel>> for RenderGraphLabelValue {
fn from(value: Rc<dyn RenderGraphLabel>) -> Self {
Self(value)
}
}
impl From<&Rc<dyn RenderGraphLabel>> for RenderGraphLabelValue {
fn from(value: &Rc<dyn RenderGraphLabel>) -> Self {
Self(value.clone())
}
}
impl Hash for RenderGraphLabelValue {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
state.write_u64(self.0.as_label_hash());
}
}
impl PartialEq for RenderGraphLabelValue {
fn eq(&self, other: &Self) -> bool {
self.0.label_eq_rc(&other.0)
}
}
impl Eq for RenderGraphLabelValue {}
struct NodeEntry {
/// The Node
inner: Arc<RefCell<dyn Node>>,
/// The Node descriptor
desc: Rc<RefCell<NodeDesc>>,
/// The index of the node in the execution graph
graph_index: petgraph::matrix_graph::NodeIndex<usize>,
/// The Node's optional pipeline
pipeline: Rc<RefCell<Option<Pipeline>>>,
}
#[derive(Clone)]
struct BindGroupEntry {
label: RenderGraphLabelValue,
/// BindGroup
bg: Arc<wgpu::BindGroup>,
/// BindGroupLayout
layout: Option<Arc<wgpu::BindGroupLayout>>,
}
#[allow(dead_code)]
#[derive(Clone)]
struct ResourceSlot {
label: RenderGraphLabelValue,
ty: SlotType,
value: SlotValue,
}
pub struct RenderGraph {
device: Arc<wgpu::Device>,
queue: Arc<wgpu::Queue>,
slots: FxHashMap<RenderGraphLabelValue, ResourceSlot>,
nodes: FxHashMap<RenderGraphLabelValue, NodeEntry>,
sub_graphs: FxHashMap<RenderGraphLabelValue, RenderGraph>,
bind_groups: FxHashMap<RenderGraphLabelValue, BindGroupEntry>,
/// A directed graph used to determine dependencies of nodes.
node_graph: petgraph::matrix_graph::DiMatrix<RenderGraphLabelValue, (), Option<()>, usize>,
view_target: Rc<RefCell<ViewTarget>>,
/// Type erased data of ResourceManager ecs resource
resource_manager: lyra_ecs::ResourceData,
}
impl RenderGraph {
pub fn new(device: Arc<wgpu::Device>, queue: Arc<wgpu::Queue>, view_target: Rc<RefCell<ViewTarget>>, world: &World) -> Self {
let rm = world.get_resource_data::<ResourceManager>()
.expect("RenderGraph requires ResourceManager ECS resource");
Self {
device,
queue,
slots: Default::default(),
nodes: Default::default(),
sub_graphs: Default::default(),
bind_groups: Default::default(),
node_graph: Default::default(),
view_target,
resource_manager: rm,
}
}
pub fn device(&self) -> &wgpu::Device {
&self.device
}
/// Add a [`Node`] to the RenderGraph.
///
/// When the node is added, its [`Node::desc`] method will be executed.
///
/// Additionally, all [`Slot`](node::NodeSlot)s of the node will be iterated,
/// 1. Ensuring that there are no two slots of the same name, with different value types
/// 2. Changing the id of insert slots to match the id of the output slot of the same name.
/// * This means that the id of insert slots **ARE NOT STABLE**. **DO NOT** rely on them to
/// not change. The IDs of output slots do stay the same.
/// 3. Ensuring that no two slots share the same ID when the names do not match.
#[instrument(skip(self, node), level = "debug")]
pub fn add_node<P: Node>(&mut self, label: impl RenderGraphLabel, mut node: P) {
let mut desc = node.desc(self);
// collect all the slots of the node
for slot in &mut desc.slots {
if let Some(other) = self
.slots
.get_mut(&slot.label)
{
debug_assert_eq!(
slot.ty, other.ty,
"slot {:?} in node {:?} does not match existing slot of same name",
slot.label, label
);
} else {
debug_assert!(!self.slots.contains_key(&slot.label),
"Reuse of id detected in render graph! Node: {:?}, slot: {:?}",
label, slot.label,
);
let res_slot = ResourceSlot {
label: slot.label.clone(),
ty: slot.ty,
value: slot.value.clone().unwrap_or(SlotValue::None),
};
self.slots.insert(slot.label.clone(), res_slot);
}
}
// get clones of the bind groups and layouts
for (label, bg, bgl) in &desc.bind_groups {
self.bind_groups.insert(label.clone(), BindGroupEntry {
label: label.clone(),
bg: bg.clone(),
layout: bgl.clone(),
});
}
let label: RenderGraphLabelValue = label.into();
let index = self.node_graph.add_node(label.clone());
self.nodes.insert(
label,
NodeEntry {
inner: Arc::new(RefCell::new(node)),
desc: Rc::new(RefCell::new(desc)),
graph_index: index,
pipeline: Rc::new(RefCell::new(None)),
},
);
}
/// Creates all buffers required for the nodes.
///
/// This only needs to be ran when the [`Node`]s in the graph change, or they are removed or
/// added.
#[instrument(skip(self, device))]
pub fn setup(&mut self, device: &wgpu::Device) {
// For all nodes, create their pipelines
for node in self.nodes.values_mut() {
let desc = (*node.desc).borrow();
if let Some(pipeline_desc) = &desc.pipeline_desc {
let pipeline = match desc.ty {
NodeType::Render => Pipeline::Render(RenderPipeline::create(
device,
pipeline_desc
.as_render_pipeline_descriptor()
.expect("got compute pipeline descriptor in a render node"),
)),
NodeType::Compute => Pipeline::Compute(ComputePipeline::create(
device,
pipeline_desc
.as_compute_pipeline_descriptor()
.expect("got render pipeline descriptor in a compute node"),
)),
NodeType::Presenter | NodeType::Node | NodeType::Graph => {
panic!("Present or Node RenderGraph nodes should not have a pipeline descriptor!");
},
};
drop(desc);
let mut node_pipeline = node.pipeline.borrow_mut();
*node_pipeline = Some(pipeline);
}
}
for sub in self.sub_graphs.values_mut() {
sub.setup(device);
}
}
#[instrument(skip(self, world))]
pub fn prepare(&mut self, world: &mut World) {
let mut buffer_writes = VecDeque::<GraphBufferWrite>::new();
// reserve some buffer writes. not all nodes write so half the amount of them is probably
// fine.
buffer_writes.reserve(self.nodes.len() / 2);
let mut sorted: VecDeque<RenderGraphLabelValue> = petgraph::algo::toposort(&self.node_graph, None)
.expect("RenderGraph had cycled!")
.iter()
.map(|i| self.node_graph[*i].clone())
.collect();
while let Some(node_label) = sorted.pop_front() {
let node = self.nodes.get(&node_label).unwrap();
let device = self.device.clone();
let queue = self.queue.clone();
let inner = node.inner.clone();
let mut inner = inner.borrow_mut();
let mut context = RenderGraphContext::new(device, queue, None, node_label.clone());
inner.prepare(self, world, &mut context);
buffer_writes.append(&mut context.buffer_writes);
}
{
// Queue all buffer writes to the gpu
let s = debug_span!("queue_buffer_writes");
let _e = s.enter();
while let Some(bufwr) = buffer_writes.pop_front() {
let slot = self
.slots
.get(&bufwr.target_slot)
.unwrap_or_else(|| panic!("Failed to find slot '{:?}' for buffer write",
bufwr.target_slot));
let buf = slot
.value
.as_buffer()
.unwrap_or_else(|| panic!("Slot '{:?}' is not a buffer", bufwr.target_slot));
self.queue.write_buffer(buf, bufwr.offset, &bufwr.bytes);
}
}
}
fn create_encoder(&self) -> CommandEncoder {
self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("graph encoder"),
})
}
#[instrument(skip(self))]
pub fn render(&mut self) {
let mut sorted: VecDeque<RenderGraphLabelValue> = petgraph::algo::toposort(&self.node_graph, None)
.expect("RenderGraph had cycled!")
.iter()
.map(|i| self.node_graph[*i].clone())
.collect();
// A bit of 'encoder hot potato' is played using this.
// Although the encoder is an option, its only an option so ownership of it can be given
// to the context for the time of the node execution.
// After the node is executed, the encoder is taken back. If the node is a presenter node,
// the encoder will be submitted and a new one will be made.
let mut encoder = Some(self.create_encoder());
while let Some(node_label) = sorted.pop_front() {
let node = self.nodes.get(&node_label).unwrap();
let node_inn = node.inner.clone();
let node_desc = node.desc.clone();
let node_desc = (*node_desc).borrow();
// clone of the Rc's is required to appease the borrow checker
let device = self.device.clone();
let queue = self.queue.clone();
// create a new encoder if the last one was submitted
if encoder.is_none() {
encoder = Some(self.create_encoder());
}
let mut context = RenderGraphContext::new(device, queue, encoder.take(), node_label.clone());
trace!("Executing {:?}", node_label.0);
let mut inner = node_inn.borrow_mut();
inner.execute(self, &node_desc, &mut context);
// take back the encoder from the context
encoder = context.encoder;
}
if let Some(encoder) = encoder {
self.queue.submit(std::iter::once(encoder.finish()));
}
}
pub fn slot_value<L: Into<RenderGraphLabelValue>>(&self, label: L) -> Option<&SlotValue> {
self.slots.get(&label.into()).map(|s| &s.value)
}
pub fn slot_value_mut<L: Into<RenderGraphLabelValue>>(&mut self, label: L) -> Option<&mut SlotValue> {
self.slots.get_mut(&label.into()).map(|s| &mut s.value)
}
pub fn node_desc<L: Into<RenderGraphLabelValue>>(&self, label: L) -> Option<Ref<NodeDesc>> {
self.nodes.get(&label.into()).map(|s| (*s.desc).borrow())
}
#[inline(always)]
pub fn pipeline<L: Into<RenderGraphLabelValue>>(&self, label: L) -> Option<Ref<Pipeline>> {
self.nodes.get(&label.into())
.and_then(|p| {
let v = p.pipeline.borrow();
#[allow(clippy::manual_map)]
match &*v {
Some(_) => Some(Ref::map(v, |p| p.as_ref().unwrap())),
None => None,
}
})
}
#[inline(always)]
pub fn try_bind_group<L: Into<RenderGraphLabelValue>>(&self, label: L) -> Option<&Arc<wgpu::BindGroup>> {
self.bind_groups.get(&label.into()).map(|e| &e.bg)
}
#[inline(always)]
pub fn bind_group<L: Into<RenderGraphLabelValue>>(&self, label: L) -> &Arc<wgpu::BindGroup> {
let l = label.into();
self.try_bind_group(l.clone()).unwrap_or_else(|| panic!("Unknown label '{:?}' for bind group layout", l.clone()))
}
#[inline(always)]
pub fn try_bind_group_layout<L: Into<RenderGraphLabelValue>>(&self, label: L) -> Option<&Arc<wgpu::BindGroupLayout>> {
self.bind_groups.get(&label.into()).and_then(|e| e.layout.as_ref())
}
#[inline(always)]
pub fn bind_group_layout<L: Into<RenderGraphLabelValue>>(&self, label: L) -> &Arc<wgpu::BindGroupLayout> {
let l = label.into();
self.try_bind_group_layout(l.clone())
.unwrap_or_else(|| panic!("Unknown label '{:?}' for bind group layout", l.clone()))
}
pub fn add_edge(&mut self, from: impl RenderGraphLabel, to: impl RenderGraphLabel)
{
let from = RenderGraphLabelValue::from(from);
let to = RenderGraphLabelValue::from(to);
let from_idx = self
.nodes
.iter()
.find(|p| *p.0 == from)
.map(|p| p.1.graph_index)
.expect("Failed to find from node");
let to_idx = self
.nodes
.iter()
.find(|p| *p.0 == to)
.map(|p| p.1.graph_index)
.expect("Failed to find to node");
debug_assert_ne!(from_idx, to_idx, "cannot add edges between the same node");
self.node_graph.add_edge(from_idx, to_idx, ());
}
/// Utility method for setting the bind groups for a node.
///
/// The parameter `bind_groups` can be used to specify the labels of a bind group, and the
/// index of the bind group in the pipeline for the node. If a bind group of the provided
/// name is not found in the graph, a panic will occur.
///
/// # Example:
/// ```nobuild
/// graph.set_bind_groups(
/// &mut pass,
/// &[
/// // retrieves the `BasePassSlots::DepthTexture` bind group and sets the index 0 in the
/// // node to it.
/// (&BaseNodeSlots::DepthTexture, 0),
/// (&BaseNodeSlots::Camera, 1),
/// (&LightBaseNodeSlots::Lights, 2),
/// (&LightCullComputeNodeSlots::LightIndicesGridGroup, 3),
/// (&BaseNodeSlots::ScreenSize, 4),
/// ],
/// );
/// ```
///
/// # Panics
/// Panics if a bind group of a provided name is not found.
pub fn set_bind_groups<'a, P: Pass<'a>>(
&'a self,
pass: &mut P,
bind_groups: &[(&dyn RenderGraphLabel, u32)],
) {
for (label, index) in bind_groups {
let bg = self
.bind_group(label.rc_clone());
pass.set_bind_group(*index, bg, &[]);
}
}
pub fn sub_graph_mut<L: Into<RenderGraphLabelValue>>(&mut self, label: L) -> Option<&mut RenderGraph> {
self.sub_graphs.get_mut(&label.into())
}
/// Add a sub graph.
///
/// > Note: the sub graph is not ran unless you add a node that executes it. See [`SubGraphNode`].
pub fn add_sub_graph<L: Into<RenderGraphLabelValue>>(&mut self, label: L, sub: RenderGraph) {
self.sub_graphs.insert(label.into(), sub);
}
/// Clone rendering resources (slots, bind groups, etc.) to a sub graph.
fn clone_resources_to_sub(&mut self, sub_graph: RenderGraphLabelValue, slots: Vec<RenderGraphLabelValue>) {
// instead of inserting the slots to the sub graph as they are extracted from the parent graph,
// they are done separately to make the borrow checker happy. If this is not done,
// the borrow checker complains about multiple mutable borrows (or an inmutable borrow
// while mutable borrowing) to self; caused by borrowing the sub graph from self, and
// self.slots.
let mut collected_slots = VecDeque::new();
let mut collected_bind_groups = VecDeque::new();
for slot in slots.iter() {
let mut found_res = false;
// Since slots and bind groups may go by the same label,
// there must be a way to collect both of them. A flag variable is used to detect
// if neither was found.
if let Some(slot_res) = self.slots.get(slot) {
collected_slots.push_back(slot_res.clone());
found_res = true;
}
if let Some(bg_res) = self.bind_groups.get(slot) {
collected_bind_groups.push_back(bg_res.clone());
found_res = true;
}
if !found_res {
panic!("sub graph is missing {:?} input slot or bind group", slot);
}
}
let sg = self.sub_graph_mut(sub_graph.clone()).unwrap();
while let Some(res) = collected_slots.pop_front() {
sg.slots.insert(res.label.clone(), res);
}
while let Some(bg) = collected_bind_groups.pop_front() {
sg.bind_groups.insert(bg.label.clone(), bg);
}
}
pub fn view_target(&self) -> Ref<ViewTarget> {
self.view_target.borrow()
}
pub fn view_target_mut(&self) -> RefMut<ViewTarget> {
self.view_target.borrow_mut()
}
/// Register a shader with the preprocessor.
///
/// This step also parses the shader and will return errors if it failed to parse.
///
/// Returns: The shader module import path if the module specified one.
/* #[inline(always)]
pub fn register_shader(&mut self, shader_src: &str) -> Result<Option<String>, wgsl_preprocessor::Error> {
self.shader_prepoc.parse_module(shader_src)
}
/// Preprocess a shader, returning the source.
#[inline(always)]
pub fn preprocess_shader(&mut self, shader_path: &str) -> Result<String, wgsl_preprocessor::Error> {
self.shader_prepoc.preprocess_module(shader_path)
} */
/// Load a shader from a `str`
///
/// This will also wait until the shader is loaded before returning.
pub fn load_shader_str(&self, shader_name: &str, shader_src: &str) -> Result<ResHandle<Shader>, RequestError> {
let rm = self.resource_manager.get::<ResourceManager>();
let shader = rm.load_str(shader_name, "text/wgsl", shader_src)?;
shader.wait_for_load()?;
Ok(shader)
}
/// Create a buffer with a single item inside of it
pub fn create_buffer_with_data<T: bytemuck::NoUninit>(&self, label: Option<&'static str>, usage: BufferUsages, data: &T) -> wgpu::Buffer {
self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label,
usage,
contents: bytemuck::bytes_of(data),
})
}
}
pub struct SubGraphNode {
subg: RenderGraphLabelValue,
slots: Vec<RenderGraphLabelValue>,
}
impl SubGraphNode {
pub fn new<L: Into<RenderGraphLabelValue>>(sub_label: L, slot_labels: Vec<RenderGraphLabelValue>) -> Self {
Self {
subg: sub_label.into(),
slots: slot_labels,
}
}
}
impl Node for SubGraphNode {
fn desc(&mut self, _: &mut RenderGraph) -> NodeDesc {
NodeDesc::new(NodeType::Graph, None, vec![])
}
fn prepare(&mut self, graph: &mut RenderGraph, world: &mut World, _: &mut RenderGraphContext) {
graph.clone_resources_to_sub(self.subg.clone(), self.slots.clone());
let sg = graph.sub_graph_mut(self.subg.clone())
.unwrap_or_else(|| panic!("failed to find sub graph for SubGraphNode: {:?}", self.subg));
sg.prepare(world);
}
fn execute(
&mut self,
graph: &mut RenderGraph,
_: &NodeDesc,
_: &mut RenderGraphContext,
) {
graph.clone_resources_to_sub(self.subg.clone(), self.slots.clone());
let sg = graph.sub_graph_mut(self.subg.clone())
.unwrap_or_else(|| panic!("failed to find sub graph for SubGraphNode: {:?}", self.subg));
sg.render();
}
}

View file

@ -1,155 +0,0 @@
use std::sync::Arc;
use glam::UVec2;
use lyra_game_derive::RenderGraphLabel;
use lyra_math::Transform;
use tracing::warn;
use crate::{
render::{
camera::CameraUniform,
graph::{
Node, NodeDesc, NodeType, RenderGraph, RenderGraphContext, SlotAttribute, SlotValue
},
render_buffer::BufferWrapper, texture::RenderTexture,
}, scene::{Camera, CameraProjection},
};
#[derive(Debug, Hash, Clone, Default, PartialEq, RenderGraphLabel)]
pub struct BasePassLabel;
#[derive(Debug, Hash, Clone, PartialEq, RenderGraphLabel)]
pub enum BasePassSlots {
DepthTexture,
ScreenSize,
Camera,
DepthTextureView,
}
/// Supplies some basic things other passes needs.
///
/// screen size buffer, camera buffer,
#[derive(Default)]
pub struct BasePass {
screen_size: UVec2,
}
impl BasePass {
pub fn new() -> Self {
Self::default()
}
}
impl Node for BasePass {
fn desc(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
) -> crate::render::graph::NodeDesc {
let vt = graph.view_target();
self.screen_size = vt.size();
let (screen_size_bgl, screen_size_bg, screen_size_buf, _) = BufferWrapper::builder()
.buffer_usage(wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST)
.label_prefix("ScreenSize")
.visibility(wgpu::ShaderStages::COMPUTE)
.buffer_dynamic_offset(false)
.contents(&[self.screen_size])
.finish_parts(graph.device());
let screen_size_bgl = Arc::new(screen_size_bgl);
let screen_size_bg = Arc::new(screen_size_bg);
let (camera_bgl, camera_bg, camera_buf, _) = BufferWrapper::builder()
.buffer_usage(wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST)
.label_prefix("camera")
.visibility(wgpu::ShaderStages::all())
.buffer_dynamic_offset(false)
.contents(&[CameraUniform::default()])
.finish_parts(graph.device());
let camera_bgl = Arc::new(camera_bgl);
let camera_bg = Arc::new(camera_bg);
// create the depth texture using the utility struct, then take all the required fields
let mut depth_texture = RenderTexture::create_depth_texture(graph.device(), self.screen_size, "depth_texture");
depth_texture.create_bind_group(&graph.device);
let dt_bg_pair = depth_texture.bindgroup_pair.unwrap();
let depth_texture_bg = Arc::new(dt_bg_pair.bindgroup);
let depth_texture_bgl = dt_bg_pair.layout;
let depth_texture_view = Arc::new(depth_texture.view);
let mut desc = NodeDesc::new(
NodeType::Node,
None,
vec![
// TODO: Make this a trait maybe?
// Could impl it for (RenderGraphLabel, wgpu::BindGroup) and also
// (RenderGraphLabel, wgpu::BindGroup, wgpu::BindGroupLabel) AND
// (RenderGraphLabel, wgpu::BindGroup, Option<wgpu::BindGroupLabel>)
//
// This could make it slightly easier to create this
(&BasePassSlots::DepthTexture, depth_texture_bg, Some(depth_texture_bgl)),
(&BasePassSlots::ScreenSize, screen_size_bg, Some(screen_size_bgl)),
(&BasePassSlots::Camera, camera_bg, Some(camera_bgl)),
],
);
desc.add_texture_view_slot(
BasePassSlots::DepthTextureView,
SlotAttribute::Output,
Some(SlotValue::TextureView(depth_texture_view)),
);
desc.add_buffer_slot(
BasePassSlots::ScreenSize,
SlotAttribute::Output,
Some(SlotValue::Buffer(Arc::new(screen_size_buf))),
);
desc.add_buffer_slot(
BasePassSlots::Camera,
SlotAttribute::Output,
Some(SlotValue::Buffer(Arc::new(camera_buf))),
);
desc
}
fn prepare(&mut self, graph: &mut RenderGraph, world: &mut lyra_ecs::World, context: &mut RenderGraphContext) {
let mut found_camera = false;
for (camera, projection, transform) in world.view_iter::<(&Camera, &CameraProjection, &Transform)>() {
if camera.is_active {
let screen_size = graph.view_target().size();
let uniform = CameraUniform::from_component(*transform, *projection, screen_size.as_vec2());
context.queue_buffer_write_with(BasePassSlots::Camera, 0, uniform);
found_camera = true;
break;
}
}
if !found_camera {
warn!("Missing camera!");
}
}
fn execute(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
_desc: &crate::render::graph::NodeDesc,
context: &mut crate::render::graph::RenderGraphContext,
) {
let mut vt = graph.view_target_mut();
vt.primary.create_frame();
vt.primary.create_frame_view();
/* debug_assert!(
!rt.current_texture.is_some(),
"main render target surface was not presented!"
); */
// update the screen size buffer if the size changed.
let rt_size = vt.size();
if rt_size != self.screen_size {
self.screen_size = rt_size;
context.queue_buffer_write_with(BasePassSlots::ScreenSize, 0, self.screen_size)
}
}
}

View file

@ -1,171 +0,0 @@
use std::{collections::HashMap, sync::Arc};
use lyra_game_derive::RenderGraphLabel;
use crate::render::{
graph::{Node, NodeDesc, NodeType},
resource::{FragmentState, PipelineDescriptor, RenderPipelineDescriptor, VertexState},
};
#[derive(Default, Debug, Clone, Copy, Hash, RenderGraphLabel)]
pub struct FxaaPassLabel;
#[derive(Debug, Default)]
pub struct FxaaPass {
target_sampler: Option<wgpu::Sampler>,
bgl: Option<Arc<wgpu::BindGroupLayout>>,
/// Store bind groups for the input textures.
/// The texture may change due to resizes, or changes to the view target chain
/// from other nodes.
bg_cache: HashMap<wgpu::Id<wgpu::TextureView>, wgpu::BindGroup>,
}
impl FxaaPass {
pub fn new() -> Self {
Self::default()
}
}
impl Node for FxaaPass {
fn desc(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
) -> crate::render::graph::NodeDesc {
let device = &graph.device;
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("fxaa_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let bgl = Arc::new(bgl);
self.bgl = Some(bgl.clone());
self.target_sampler = Some(device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("fxaa sampler"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Linear,
..Default::default()
}));
let shader = graph.load_shader_str("fxaa_shader", include_str!("../../shaders/fxaa.wgsl"))
.expect("failed to load wgsl shader from manager");
let vt = graph.view_target();
NodeDesc::new(
NodeType::Render,
Some(PipelineDescriptor::Render(RenderPipelineDescriptor {
label: Some("fxaa_pass".into()),
layouts: vec![bgl.clone()],
push_constant_ranges: vec![],
vertex: VertexState {
module: shader.clone(),
entry_point: "vs_main".into(),
buffers: vec![],
},
fragment: Some(FragmentState {
module: shader,
entry_point: "fs_main".into(),
targets: vec![Some(wgpu::ColorTargetState {
format: vt.format(),
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
depth_stencil: None,
primitive: wgpu::PrimitiveState::default(),
multisample: wgpu::MultisampleState::default(),
multiview: None,
})),
vec![],
)
}
fn prepare(
&mut self,
_: &mut crate::render::graph::RenderGraph,
_: &mut lyra_ecs::World,
_: &mut crate::render::graph::RenderGraphContext,
) {
//todo!()
}
fn execute(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
_: &crate::render::graph::NodeDesc,
context: &mut crate::render::graph::RenderGraphContext,
) {
let pipeline = graph
.pipeline(context.label.clone())
.expect("Failed to find pipeline for FxaaPass");
let mut vt = graph.view_target_mut();
let chain = vt.get_chain();
let source_view = chain.source.frame_view.as_ref().unwrap();
let dest_view = chain.dest.frame_view.as_ref().unwrap();
let bg = self
.bg_cache
.entry(source_view.global_id())
.or_insert_with(|| {
graph
.device()
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("fxaa_bg"),
layout: self.bgl.as_ref().unwrap(),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(source_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(
self.target_sampler.as_ref().unwrap(),
),
},
],
})
});
{
let encoder = context.encoder.as_mut().unwrap();
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("fxaa_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: dest_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None, // TODO: occlusion queries
});
pass.set_pipeline(pipeline.as_render());
pass.set_bind_group(0, bg, &[]);
pass.draw(0..3, 0..1);
}
}
}

View file

@ -1,33 +0,0 @@
use lyra_game_derive::RenderGraphLabel;
use crate::render::graph::{Node, NodeDesc, NodeSlot, NodeType};
#[derive(Debug, Default, Clone, Copy, Hash, RenderGraphLabel)]
pub struct InitNodeLabel;
pub struct InitNode {
slots: Vec<NodeSlot>,
}
impl Node for InitNode {
fn desc(&mut self, _: &mut crate::render::graph::RenderGraph) -> crate::render::graph::NodeDesc {
let mut desc = NodeDesc::new(NodeType::Node, None, vec![]);
// the slots can just be cloned since the slot attribute doesn't really matter much.
desc.slots = self.slots.clone();
desc
}
fn prepare(&mut self, _: &mut crate::render::graph::RenderGraph, _: &mut lyra_ecs::World, _: &mut crate::render::graph::RenderGraphContext) {
}
fn execute(
&mut self,
_: &mut crate::render::graph::RenderGraph,
_: &crate::render::graph::NodeDesc,
_: &mut crate::render::graph::RenderGraphContext,
) {
}
}

View file

@ -1,667 +0,0 @@
use std::{
collections::{HashSet, VecDeque},
ops::{Deref, DerefMut},
sync::Arc,
};
use glam::{UVec2, Vec3};
use image::GenericImageView;
use itertools::izip;
use lyra_ecs::{
query::{filter::Or, Entities, ResMut, TickOf},
Entity, ResourceObject, World,
};
use lyra_game_derive::RenderGraphLabel;
use lyra_resource::ResHandle;
use lyra_gltf::Mesh;
use lyra_scene::SceneGraph;
use rustc_hash::FxHashMap;
use tracing::{debug, instrument};
use uuid::Uuid;
use wgpu::util::DeviceExt;
use crate::render::{
graph::{Node, NodeDesc, NodeType},
render_buffer::BufferStorage,
render_job::RenderJob,
texture::{res_filter_to_wgpu, res_wrap_to_wgpu},
transform_buffer_storage::TransformIndex,
vertex::Vertex,
};
type MeshHandle = ResHandle<Mesh>;
type SceneHandle = ResHandle<SceneGraph>;
pub struct MeshBufferStorage {
pub buffer_vertex: BufferStorage,
pub buffer_indices: Option<(wgpu::IndexFormat, BufferStorage)>,
// maybe this should just be a Uuid and the material can be retrieved though
// MeshPass's `material_buffers` field?
pub material: Option<Arc<GpuMaterial>>,
}
#[derive(Default, Debug, Clone, Copy, Hash, RenderGraphLabel)]
pub struct MeshPrepNodeLabel;
#[derive(Debug)]
pub struct MeshPrepNode {
pub material_bgl: Arc<wgpu::BindGroupLayout>,
}
impl MeshPrepNode {
pub fn new(device: &wgpu::Device) -> Self {
let bgl = GpuMaterial::create_bind_group_layout(device);
Self { material_bgl: bgl }
}
/// Checks if the mesh buffers in the GPU need to be updated.
#[instrument(skip(self, device, mesh_buffers, queue, mesh_han))]
fn check_mesh_buffers(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
mesh_buffers: &mut FxHashMap<uuid::Uuid, MeshBufferStorage>,
mesh_han: &ResHandle<Mesh>,
) {
let mesh_uuid = mesh_han.uuid();
if let (Some(mesh), Some(buffers)) = (mesh_han.data_ref(), mesh_buffers.get_mut(&mesh_uuid))
{
// check if the buffer sizes dont match. If they dont, completely remake the buffers
let vertices = mesh.position().unwrap();
if buffers.buffer_vertex.count() != vertices.len() {
debug!("Recreating buffers for mesh {}", mesh_uuid.to_string());
let (vert, idx) = self.create_vertex_index_buffers(device, &mesh);
// have to re-get buffers because of borrow checker
let buffers = mesh_buffers.get_mut(&mesh_uuid).unwrap();
buffers.buffer_indices = idx;
buffers.buffer_vertex = vert;
return;
}
// update vertices
let vertex_buffer = buffers.buffer_vertex.buffer();
let vertices = vertices.as_slice();
// align the vertices to 4 bytes (u32 is 4 bytes, which is wgpu::COPY_BUFFER_ALIGNMENT)
let (_, vertices, _) = bytemuck::pod_align_to::<Vec3, u32>(vertices);
queue.write_buffer(vertex_buffer, 0, bytemuck::cast_slice(vertices));
// update the indices if they're given
if let Some(index_buffer) = buffers.buffer_indices.as_ref() {
let aligned_indices = match mesh.indices.as_ref().unwrap() {
// U16 indices need to be aligned to u32, for wpgu, which are 4-bytes in size.
lyra_gltf::MeshIndices::U16(v) => {
bytemuck::pod_align_to::<u16, u32>(v).1
}
lyra_gltf::MeshIndices::U32(v) => {
bytemuck::pod_align_to::<u32, u32>(v).1
}
};
let index_buffer = index_buffer.1.buffer();
queue.write_buffer(index_buffer, 0, bytemuck::cast_slice(aligned_indices));
}
}
}
#[instrument(skip(self, device, mesh))]
fn create_vertex_index_buffers(
&mut self,
device: &wgpu::Device,
mesh: &Mesh,
) -> (BufferStorage, Option<(wgpu::IndexFormat, BufferStorage)>) {
let positions = mesh.position().unwrap();
let tex_coords: Vec<glam::Vec2> = mesh
.tex_coords()
.cloned()
.unwrap_or_else(|| vec![glam::Vec2::new(0.0, 0.0); positions.len()]);
let normals = mesh.normals().unwrap();
assert!(positions.len() == tex_coords.len() && positions.len() == normals.len());
let mut vertex_inputs = vec![];
for (v, t, n) in izip!(positions.iter(), tex_coords.iter(), normals.iter()) {
vertex_inputs.push(Vertex::new(*v, *t, *n));
}
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Vertex Buffer"),
contents: bytemuck::cast_slice(vertex_inputs.as_slice()), //vertex_combined.as_slice(),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
let vertex_buffer = BufferStorage::new(vertex_buffer, 0, vertex_inputs.len());
let indices = match mesh.indices.as_ref() {
Some(indices) => {
let (idx_type, len, contents) = match indices {
lyra_gltf::MeshIndices::U16(v) => {
(wgpu::IndexFormat::Uint16, v.len(), bytemuck::cast_slice(v))
}
lyra_gltf::MeshIndices::U32(v) => {
(wgpu::IndexFormat::Uint32, v.len(), bytemuck::cast_slice(v))
}
};
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Index Buffer"),
contents,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
});
let buffer_indices = BufferStorage::new(index_buffer, 0, len);
Some((idx_type, buffer_indices))
}
None => None,
};
(vertex_buffer, indices)
}
#[instrument(skip(self, device, queue, material_buffers, mesh))]
fn create_mesh_buffers(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
material_buffers: &mut RenderAssets<Arc<GpuMaterial>>,
mesh: &Mesh,
) -> MeshBufferStorage {
let (vertex_buffer, buffer_indices) = self.create_vertex_index_buffers(device, mesh);
let material = mesh
.material
.as_ref()
.expect("Material resource not loaded yet");
let material_ref = material.data_ref().unwrap();
let material = material_buffers.entry(material.uuid()).or_insert_with(|| {
debug!(
uuid = material.uuid().to_string(),
"Sending material to gpu"
);
Arc::new(GpuMaterial::from_resource(
device,
queue,
&self.material_bgl,
&material_ref,
))
});
MeshBufferStorage {
buffer_vertex: vertex_buffer,
buffer_indices,
material: Some(material.clone()),
}
}
/// Processes the mesh for the renderer, storing and creating buffers as needed. Returns true if a new mesh was processed.
#[instrument(skip(
self,
device,
queue,
mesh_buffers,
material_buffers,
entity_meshes,
mesh,
entity
))]
fn process_mesh(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
mesh_buffers: &mut RenderAssets<MeshBufferStorage>,
material_buffers: &mut RenderAssets<Arc<GpuMaterial>>,
entity_meshes: &mut FxHashMap<Entity, uuid::Uuid>,
entity: Entity,
mesh: &Mesh,
mesh_uuid: Uuid,
) -> bool {
#[allow(clippy::map_entry)]
if !mesh_buffers.contains_key(&mesh_uuid) {
// create the mesh's buffers
let buffers = self.create_mesh_buffers(device, queue, material_buffers, mesh);
mesh_buffers.insert(mesh_uuid, buffers);
entity_meshes.insert(entity, mesh_uuid);
true
} else {
false
}
}
/// If the resource does not exist in the world, add the default
fn try_init_resource<T: ResourceObject + Default>(world: &mut World) {
if !world.has_resource::<T>() {
world.add_resource_default::<T>();
}
}
}
impl Node for MeshPrepNode {
fn desc(
&mut self,
_: &mut crate::render::graph::RenderGraph,
) -> crate::render::graph::NodeDesc {
NodeDesc::new(NodeType::Node, None, vec![])
}
fn prepare(
&mut self,
_: &mut crate::render::graph::RenderGraph,
world: &mut lyra_ecs::World,
context: &mut crate::render::graph::RenderGraphContext,
) {
let device = &context.device;
let queue = &context.queue;
let last_epoch = world.current_tick();
let mut alive_entities = HashSet::new();
{
// prepare the world with resources
Self::try_init_resource::<RenderMeshes>(world);
Self::try_init_resource::<RenderAssets<MeshBufferStorage>>(world);
Self::try_init_resource::<RenderAssets<Arc<GpuMaterial>>>(world);
Self::try_init_resource::<FxHashMap<Entity, uuid::Uuid>>(world);
let mut render_meshes = world
.get_resource_mut::<RenderMeshes>()
.expect("world missing RenderMeshes resource");
render_meshes.clear();
}
let view = world.view_iter::<(
Entities,
&TransformIndex,
Or<(&MeshHandle, TickOf<MeshHandle>), (&SceneHandle, TickOf<SceneHandle>)>,
ResMut<RenderMeshes>,
ResMut<RenderAssets<MeshBufferStorage>>,
ResMut<RenderAssets<Arc<GpuMaterial>>>,
ResMut<FxHashMap<Entity, uuid::Uuid>>,
)>();
// used to store InterpTransform components to add to entities later
for (
entity,
transform_index,
(mesh_pair, scene_pair),
mut render_meshes,
mut mesh_buffers,
mut material_buffers,
mut entity_meshes,
) in view
{
alive_entities.insert(entity);
if let Some((mesh_han, mesh_epoch)) = mesh_pair {
if let Some(mesh) = mesh_han.data_ref() {
// if process mesh did not just create a new mesh, and the epoch
// shows that the scene has changed, verify that the mesh buffers
// dont need to be resent to the gpu.
if !self.process_mesh(
device,
queue,
&mut mesh_buffers,
&mut material_buffers,
&mut entity_meshes,
entity,
&mesh,
mesh_han.uuid(),
) && mesh_epoch == last_epoch
{
self.check_mesh_buffers(device, queue, &mut mesh_buffers, &mesh_han);
}
let material = mesh.material.as_ref().unwrap().data_ref().unwrap();
let shader = material.shader_uuid.unwrap_or(0);
let job = RenderJob::new(entity, shader, mesh_han.uuid(), *transform_index);
render_meshes.push_back(job);
}
}
if let Some((scene_han, scene_epoch)) = scene_pair {
if let Some(scene) = scene_han.data_ref() {
for (mesh_han, transform_index) in
scene.world().view_iter::<(&MeshHandle, &TransformIndex)>()
{
if let Some(mesh) = mesh_han.data_ref() {
// if process mesh did not just create a new mesh, and the epoch
// shows that the scene has changed, verify that the mesh buffers
// dont need to be resent to the gpu.
if !self.process_mesh(
device,
queue,
&mut mesh_buffers,
&mut material_buffers,
&mut entity_meshes,
entity,
&mesh,
mesh_han.uuid(),
) && scene_epoch == last_epoch
{
self.check_mesh_buffers(
device,
queue,
&mut mesh_buffers,
&mesh_han,
);
}
let material = mesh.material.as_ref().unwrap().data_ref().unwrap();
let shader = material.shader_uuid.unwrap_or(0);
let job =
RenderJob::new(entity, shader, mesh_han.uuid(), *transform_index);
render_meshes.push_back(job);
}
}
}
}
}
}
fn execute(
&mut self,
_: &mut crate::render::graph::RenderGraph,
_: &crate::render::graph::NodeDesc,
_: &mut crate::render::graph::RenderGraphContext,
) {
}
}
#[repr(transparent)]
pub struct RenderAssets<T>(FxHashMap<Uuid, T>);
impl<T> Deref for RenderAssets<T> {
type Target = FxHashMap<Uuid, T>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for RenderAssets<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<T> Default for RenderAssets<T> {
fn default() -> Self {
Self(Default::default())
}
}
impl<T> RenderAssets<T> {
pub fn new() -> Self {
Self::default()
}
}
#[allow(dead_code)]
pub struct GpuMaterial {
pub bind_group: Arc<wgpu::BindGroup>,
bind_group_layout: Arc<wgpu::BindGroupLayout>,
material_properties_buffer: wgpu::Buffer,
diffuse_texture: wgpu::Texture,
diffuse_texture_sampler: wgpu::Sampler,
/* specular_texture: wgpu::Texture,
specular_texture_sampler: wgpu::Sampler, */
}
impl GpuMaterial {
fn create_bind_group_layout(device: &wgpu::Device) -> Arc<wgpu::BindGroupLayout> {
Arc::new(
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("bgl_material"),
entries: &[
// material properties
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None, /* Some(
NonZeroU64::new(mem::size_of::<MaterialPropertiesUniform>() as _)
.unwrap(),
) */
},
count: None,
},
// diffuse texture
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// diffuse texture sampler
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
// specular texture
/* wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// specular texture sampler
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
count: None,
}, */
],
}),
)
}
fn texture_desc(label: &str, size: UVec2) -> wgpu::TextureDescriptor {
//debug!("Texture desc size: {:?}", size);
wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: 1,
},
mip_level_count: 1, // TODO
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
}
}
fn write_texture(queue: &wgpu::Queue, texture: &wgpu::Texture, img: &lyra_resource::Image) {
let dim = img.dimensions();
//debug!("Write texture size: {:?}", dim);
queue.write_texture(
wgpu::ImageCopyTexture {
aspect: wgpu::TextureAspect::All,
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
},
&img.to_rgba8(),
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(4 * dim.0),
rows_per_image: Some(dim.1),
},
wgpu::Extent3d {
width: dim.0,
height: dim.1,
depth_or_array_layers: 1,
},
);
}
fn from_resource(
device: &wgpu::Device,
queue: &wgpu::Queue,
layout: &Arc<wgpu::BindGroupLayout>,
mat: &lyra_gltf::Material,
) -> Self {
//let specular = mat.specular.as_ref().unwrap_or_default();
//let specular_
let prop = MaterialPropertiesUniform {
ambient: Vec3::ONE,
_padding1: 0,
diffuse: Vec3::ONE,
shininess: 32.0,
specular_factor: 0.0,
_padding2: [0; 3],
specular_color_factor: Vec3::ZERO,
_padding3: 0,
};
let properties_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("buffer_material"),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
contents: bytemuck::bytes_of(&prop),
});
let diffuse_tex = mat.base_color_texture.as_ref().unwrap();
let diffuse_tex = diffuse_tex.data_ref().unwrap();
let diffuse_tex_img = diffuse_tex.image.data_ref().unwrap();
let diffuse_tex_dim = diffuse_tex_img.dimensions();
let diffuse_texture = device.create_texture(&Self::texture_desc(
"material_diffuse_texture",
UVec2::new(diffuse_tex_dim.0, diffuse_tex_dim.1),
));
let diffuse_tex_view = diffuse_texture.create_view(&wgpu::TextureViewDescriptor::default());
let sampler_desc = match &diffuse_tex.sampler {
Some(sampler) => {
let magf = res_filter_to_wgpu(
sampler
.mag_filter
.unwrap_or(lyra_resource::FilterMode::Linear),
);
let minf = res_filter_to_wgpu(
sampler
.min_filter
.unwrap_or(lyra_resource::FilterMode::Nearest),
);
let mipf = res_filter_to_wgpu(
sampler
.mipmap_filter
.unwrap_or(lyra_resource::FilterMode::Nearest),
);
let wrap_u = res_wrap_to_wgpu(sampler.wrap_u);
let wrap_v = res_wrap_to_wgpu(sampler.wrap_v);
let wrap_w = res_wrap_to_wgpu(sampler.wrap_w);
wgpu::SamplerDescriptor {
address_mode_u: wrap_u,
address_mode_v: wrap_v,
address_mode_w: wrap_w,
mag_filter: magf,
min_filter: minf,
mipmap_filter: mipf,
..Default::default()
}
}
None => wgpu::SamplerDescriptor {
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
},
};
let diffuse_sampler = device.create_sampler(&sampler_desc);
Self::write_texture(queue, &diffuse_texture, &diffuse_tex_img);
debug!("TODO: specular texture");
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("bg_material"),
layout: &layout,
entries: &[
// material properties
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &properties_buffer,
offset: 0,
size: None,
}),
},
// diffuse texture
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&diffuse_tex_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&diffuse_sampler),
},
// TODO: specular textures
],
});
Self {
bind_group: Arc::new(bg),
bind_group_layout: layout.clone(),
material_properties_buffer: properties_buffer,
diffuse_texture,
diffuse_texture_sampler: diffuse_sampler,
}
}
}
/// Uniform for MaterialProperties in a shader
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct MaterialPropertiesUniform {
ambient: glam::Vec3,
_padding1: u32,
diffuse: glam::Vec3,
shininess: f32,
specular_factor: f32,
_padding2: [u32; 3],
specular_color_factor: glam::Vec3,
_padding3: u32,
}
#[derive(Default)]
pub struct RenderMeshes(VecDeque<RenderJob>);
impl Deref for RenderMeshes {
type Target = VecDeque<RenderJob>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for RenderMeshes {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

View file

@ -1,500 +0,0 @@
use std::sync::Arc;
use lyra_ecs::{AtomicRef, ResourceData};
use lyra_game_derive::RenderGraphLabel;
use tracing::{instrument, warn};
use crate::render::{
desc_buf_lay::DescVertexBufferLayout,
graph::{Node, NodeDesc, NodeType, RenderGraph, RenderGraphContext},
resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, VertexState},
texture::RenderTexture,
transform_buffer_storage::TransformBuffers,
vertex::Vertex,
};
use super::{
BasePassSlots, LightBasePassSlots, LightCullComputePassSlots, MeshBufferStorage, RenderAssets, RenderMeshes, ShadowMapsPassSlots
};
#[derive(Debug, Hash, Clone, Default, PartialEq, RenderGraphLabel)]
pub struct MeshesPassLabel;
#[derive(Debug, Hash, Clone, PartialEq, RenderGraphLabel)]
pub enum MeshesPassSlots {
Material,
}
/// Stores the bind group and bind group layout for the shadow atlas texture
struct ShadowsAtlasBgPair {
layout: Arc<wgpu::BindGroupLayout>,
bg: Arc<wgpu::BindGroup>,
}
//#[derive(Default)]
#[allow(dead_code)]
pub struct MeshPass {
default_texture: Option<RenderTexture>,
pipeline: Option<RenderPipeline>,
material_bgl: Arc<wgpu::BindGroupLayout>,
// TODO: find a better way to extract these resources from the main world to be used in the
// render stage.
transform_buffers: Option<ResourceData>,
render_meshes: Option<ResourceData>,
mesh_buffers: Option<ResourceData>,
shadows_atlas: Option<ShadowsAtlasBgPair>,
}
impl MeshPass {
pub fn new(material_bgl: Arc<wgpu::BindGroupLayout>) -> Self {
Self {
default_texture: None,
pipeline: None,
material_bgl,
transform_buffers: None,
render_meshes: None,
mesh_buffers: None,
shadows_atlas: None,
}
}
fn transform_buffers(&self) -> AtomicRef<TransformBuffers> {
self.transform_buffers.as_ref().unwrap().get()
}
fn render_meshes(&self) -> AtomicRef<RenderMeshes> {
self.render_meshes.as_ref().unwrap().get()
}
fn mesh_buffers(&self) -> AtomicRef<RenderAssets<MeshBufferStorage>> {
self.mesh_buffers.as_ref().unwrap().get()
}
}
impl Node for MeshPass {
fn desc(
&mut self,
_: &mut crate::render::graph::RenderGraph,
) -> crate::render::graph::NodeDesc {
// load the default texture
//let bytes = include_bytes!("../../default_texture.png");
//self.default_texture = Some(RenderTexture::from_bytes(device, &graph.queue, texture_bind_group_layout.clone(), bytes, "default_texture").unwrap());
NodeDesc::new(
NodeType::Render,
None,
vec![
//(&MeshesPassSlots::Material, material_bg, Some(material_bgl)),
],
)
}
#[instrument(skip(self, graph, world))]
fn prepare(
&mut self,
graph: &mut RenderGraph,
world: &mut lyra_ecs::World,
_: &mut RenderGraphContext,
) {
if self.pipeline.is_none() {
let shader = graph.load_shader_str("mesh_base", include_str!("../../shaders/base.wgsl"))
.expect("failed to load base mesh shader");
let device = graph.device();
let surface_config_format = graph.view_target().format();
let atlas_view = graph
.slot_value(ShadowMapsPassSlots::ShadowAtlasTextureView)
.expect("missing ShadowMapsPassSlots::ShadowAtlasTextureView")
.as_texture_view()
.unwrap();
let atlas_sampler = graph
.slot_value(ShadowMapsPassSlots::ShadowAtlasSampler)
.expect("missing ShadowMapsPassSlots::ShadowAtlasSampler")
.as_sampler()
.unwrap();
let atlas_sampler_compare = graph
.slot_value(ShadowMapsPassSlots::ShadowAtlasSamplerComparison)
.expect("missing ShadowMapsPassSlots::ShadowAtlasSamplerComparison")
.as_sampler()
.unwrap();
let shadow_settings_buf = graph
.slot_value(ShadowMapsPassSlots::ShadowSettingsUniform)
.expect("missing ShadowMapsPassSlots::ShadowSettingsUniform")
.as_buffer()
.unwrap();
let light_uniform_buf = graph
.slot_value(ShadowMapsPassSlots::ShadowLightUniformsBuffer)
.expect("missing ShadowMapsPassSlots::ShadowLightUniformsBuffer")
.as_buffer()
.unwrap();
let pcf_poisson_disc = graph
.slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer)
.expect("missing ShadowMapsPassSlots::PcfPoissonDiscBuffer")
.as_buffer()
.unwrap();
let pcf_poisson_disc_3d = graph
.slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer3d)
.expect("missing ShadowMapsPassSlots::PcfPoissonDiscBuffer3d")
.as_buffer()
.unwrap();
let pcss_poisson_disc = graph
.slot_value(ShadowMapsPassSlots::PcssPoissonDiscBuffer)
.expect("missing ShadowMapsPassSlots::PcssPoissonDiscBuffer")
.as_buffer()
.unwrap();
let atlas_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("bgl_shadows_atlas"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Depth,
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 5,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 6,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 7,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let atlas_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("bg_shadows_atlas"),
layout: &atlas_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(atlas_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(atlas_sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(atlas_sampler_compare),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: shadow_settings_buf,
offset: 0,
size: None,
}),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: light_uniform_buf,
offset: 0,
size: None,
}),
},
wgpu::BindGroupEntry {
binding: 5,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: pcf_poisson_disc,
offset: 0,
size: None,
}),
},
wgpu::BindGroupEntry {
binding: 6,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: pcf_poisson_disc_3d,
offset: 0,
size: None,
}),
},
wgpu::BindGroupEntry {
binding: 7,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: pcss_poisson_disc,
offset: 0,
size: None,
}),
},
],
});
self.shadows_atlas = Some(ShadowsAtlasBgPair {
layout: Arc::new(atlas_layout),
bg: Arc::new(atlas_bg),
});
let camera_bgl = graph.bind_group_layout(BasePassSlots::Camera);
let lights_bgl = graph.bind_group_layout(LightBasePassSlots::Lights);
let light_grid_bgl =
graph.bind_group_layout(LightCullComputePassSlots::LightIndicesGridGroup);
let atlas_bgl = self.shadows_atlas.as_ref().unwrap().layout.clone();
let transforms = world
.get_resource_data::<TransformBuffers>()
.expect("Missing transform buffers");
self.transform_buffers = Some(transforms.clone());
let render_meshes = world
.get_resource_data::<RenderMeshes>()
.expect("Missing transform buffers");
self.render_meshes = Some(render_meshes.clone());
let mesh_buffers = world
.get_resource_data::<RenderAssets<MeshBufferStorage>>()
.expect("Missing render meshes");
self.mesh_buffers = Some(mesh_buffers.clone());
let transforms = transforms.get::<TransformBuffers>();
self.pipeline = Some(RenderPipeline::create(
device,
&RenderPipelineDescriptor {
label: Some("meshes".into()),
layouts: vec![
self.material_bgl.clone(),
transforms.bindgroup_layout.clone(),
camera_bgl.clone(),
lights_bgl.clone(),
light_grid_bgl.clone(),
atlas_bgl,
],
push_constant_ranges: vec![],
vertex: VertexState {
module: shader.clone(),
entry_point: "vs_main".into(),
buffers: vec![Vertex::desc().into()],
},
fragment: Some(FragmentState {
module: shader,
entry_point: "fs_main".into(),
targets: vec![Some(wgpu::ColorTargetState {
format: surface_config_format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
depth_stencil: Some(wgpu::DepthStencilState {
format: RenderTexture::DEPTH_FORMAT,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::Less,
stencil: wgpu::StencilState::default(), // TODO: stencil buffer
bias: wgpu::DepthBiasState::default(),
}),
primitive: wgpu::PrimitiveState {
cull_mode: Some(wgpu::Face::Back),
..Default::default()
},
multisample: wgpu::MultisampleState::default(),
multiview: None,
},
));
}
}
fn execute(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
_: &crate::render::graph::NodeDesc,
context: &mut crate::render::graph::RenderGraphContext,
) {
let encoder = context.encoder.as_mut().unwrap();
/* let view = graph
.slot_value(BasePassSlots::WindowTextureView)
.unwrap()
.as_texture_view()
.expect("BasePassSlots::WindowTextureView was not a TextureView slot"); */
let vt = graph.view_target();
let view = vt.render_view();
let depth_view = graph
.slot_value(BasePassSlots::DepthTextureView)
.unwrap()
.as_texture_view()
.expect("BasePassSlots::DepthTextureView was not a TextureView slot");
let camera_bg = graph.bind_group(BasePassSlots::Camera);
let lights_bg = graph.bind_group(LightBasePassSlots::Lights);
let light_grid_bg = graph.bind_group(LightCullComputePassSlots::LightIndicesGridGroup);
let shadows_atlas_bg = &self.shadows_atlas.as_ref().unwrap().bg;
//let material_bg = graph.bind_group(MeshesPassSlots::Material);
/* let pipeline = graph.pipeline(context.label.clone())
.expect("Failed to find pipeline for MeshPass"); */
let pipeline = self.pipeline.as_ref().unwrap();
let transforms = self.transform_buffers();
let render_meshes = self.render_meshes();
let mesh_buffers = self.mesh_buffers();
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,/* wgpu::LoadOp::Clear(wgpu::Color {
r: 0.1,
g: 0.2,
b: 0.3,
a: 1.0,
}), */
store: wgpu::StoreOp::Store,
},
})],
// enable depth buffer
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
..Default::default()
});
pass.set_pipeline(pipeline);
//let default_texture = self.default_texture.as_ref().unwrap();
for job in render_meshes.iter() {
// get the mesh (containing vertices) and the buffers from storage
let buffers = mesh_buffers.get(&job.asset_uuid);
if buffers.is_none() {
warn!("Skipping job since its mesh is missing {:?}", job.asset_uuid);
continue;
}
let buffers = buffers.unwrap();
// Bind the optional texture
/* if let Some(tex) = buffers.material.as_ref()
.and_then(|m| m.diffuse_texture.as_ref()) {
pass.set_bind_group(0, tex.bind_group(), &[]);
} else {
pass.set_bind_group(0, default_texture.bind_group(), &[]);
}
if let Some(tex) = buffers.material.as_ref()
.and_then(|m| m.specular.as_ref())
.and_then(|s| s.texture.as_ref().or(s.color_texture.as_ref())) {
pass.set_bind_group(5, tex.bind_group(), &[]);
} else {
pass.set_bind_group(5, default_texture.bind_group(), &[]);
} */
if let Some(mat) = buffers.material.as_ref() {
pass.set_bind_group(0, &mat.bind_group, &[]);
} else {
todo!("cannot render mesh without material");
}
// Get the bindgroup for job's transform and bind to it using an offset.
let bindgroup = transforms.bind_group(job.transform_id);
let offset = transforms.buffer_offset(job.transform_id);
pass.set_bind_group(1, bindgroup, &[offset]);
pass.set_bind_group(2, camera_bg, &[]);
pass.set_bind_group(3, lights_bg, &[]);
//pass.set_bind_group(4, material_bg, &[]);
pass.set_bind_group(4, light_grid_bg, &[]);
pass.set_bind_group(5, shadows_atlas_bg, &[]);
// if this mesh uses indices, use them to draw the mesh
if let Some((idx_type, indices)) = buffers.buffer_indices.as_ref() {
let indices_len = indices.count() as u32;
pass.set_vertex_buffer(
buffers.buffer_vertex.slot(),
buffers.buffer_vertex.buffer().slice(..),
);
pass.set_index_buffer(indices.buffer().slice(..), *idx_type);
pass.draw_indexed(0..indices_len, 0, 0..1);
} else {
let vertex_count = buffers.buffer_vertex.count();
pass.set_vertex_buffer(
buffers.buffer_vertex.slot(),
buffers.buffer_vertex.buffer().slice(..),
);
pass.draw(0..vertex_count as u32, 0..1);
}
}
}
}
}

View file

@ -1,35 +0,0 @@
mod light_cull_compute;
pub use light_cull_compute::*;
mod base;
pub use base::*;
mod meshes;
pub use meshes::*;
mod light_base;
pub use light_base::*;
mod present_pass;
pub use present_pass::*;
mod init;
pub use init::*;
mod tint;
pub use tint::*;
mod fxaa;
pub use fxaa::*;
mod shadows;
pub use shadows::*;
mod mesh_prepare;
pub use mesh_prepare::*;
mod transform;
pub use transform::*;
mod sprite;
pub use sprite::*;

View file

@ -1,44 +0,0 @@
use std::hash::Hash;
use lyra_game_derive::RenderGraphLabel;
use crate::render::graph::{Node, NodeDesc, NodeType, RenderGraph, RenderGraphContext};
#[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)]
pub struct PresentPassLabel;
/// Supplies some basic things other passes needs.
///
/// screen size buffer, camera buffer,
#[derive(Default, Debug)]
pub struct PresentPass;
impl PresentPass {
pub fn new() -> Self {
Self
}
}
impl Node for PresentPass {
fn desc(&mut self, _graph: &mut crate::render::graph::RenderGraph) -> crate::render::graph::NodeDesc {
NodeDesc::new(
NodeType::Presenter,
None,
vec![],
)
}
fn prepare(&mut self, _graph: &mut RenderGraph, _world: &mut lyra_ecs::World, _context: &mut RenderGraphContext) {
}
fn execute(&mut self, graph: &mut crate::render::graph::RenderGraph, _desc: &crate::render::graph::NodeDesc, context: &mut crate::render::graph::RenderGraphContext) {
let mut vt = graph.view_target_mut();
vt.copy_to_primary(context.encoder.as_mut().unwrap());
context.submit_encoder();
let frame = vt.primary.frame.take()
.expect("ViewTarget.primary was already presented");
frame.present();
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,558 +0,0 @@
use std::{collections::VecDeque, sync::Arc};
use glam::{UVec2, Vec2, Vec3};
use image::GenericImageView;
use lyra_ecs::{
query::{filter::Or, Entities, ResMut, TickOf},
AtomicRef, Entity, ResourceData,
};
use lyra_game_derive::RenderGraphLabel;
use lyra_math::URect;
use lyra_resource::Image;
use rustc_hash::FxHashMap;
use tracing::{debug, instrument, warn};
use uuid::Uuid;
use wgpu::util::DeviceExt;
use crate::{
render::{
graph::{Node, NodeDesc, NodeType, SlotAttribute},
resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, VertexState},
texture::RenderTexture,
vertex::Vertex2D,
EitherTransform,
},
scene::{CameraSortingAxis, SortingOffset},
sprite::{AtlasSprite, Sprite},
};
use super::{BasePassSlots, RenderAssets};
#[derive(Clone)]
pub struct RenderJob {
pub entity: Entity,
pub shader_id: u64,
pub asset_uuid: uuid::Uuid,
pub atlas_frame_id: u64,
pub position: Vec3,
pub sort_offset: Vec3,
pub is_transparent: bool,
}
#[derive(Default, Debug, Clone, Copy, Hash, RenderGraphLabel)]
pub struct SpritePassLabel;
#[derive(Debug, Hash, Clone, PartialEq, RenderGraphLabel)]
pub enum SpritePassSlots {
SpriteTexture,
SpriteTextureView,
SpriteTextureSampler,
}
struct SpriteTexture {
// this field is actually read, its given to the bind group
#[allow(dead_code)]
texture: wgpu::Texture,
// this field is actually read, its given to the bind group
#[allow(dead_code)]
sampler: wgpu::Sampler,
texture_bg: Arc<wgpu::BindGroup>,
}
struct SpriteBuffers {
vertex_buffers: wgpu::Buffer,
index_buffers: wgpu::Buffer,
}
#[derive(Default)]
pub struct SpritePass {
pipeline: Option<RenderPipeline>,
texture_bgl: Option<Arc<wgpu::BindGroupLayout>>,
jobs: VecDeque<RenderJob>,
sprite_instances_buf: Option<Arc<wgpu::Buffer>>,
texture_store: Option<ResourceData>,
buffer_store: Option<ResourceData>,
}
impl SpritePass {
pub fn new() -> Self {
Self::default()
}
#[instrument(skip(self, device))]
fn create_vertex_index_buffers(
&mut self,
device: &wgpu::Device,
dimensions: UVec2,
) -> (wgpu::Buffer, wgpu::Buffer) {
let vertices = vec![
// top left
Vertex2D::new(Vec3::new(0.0, 0.0, 0.0), Vec2::new(0.0, 1.0)),
// bottom left
Vertex2D::new(
Vec3::new(0.0, dimensions.y as f32, 0.0),
Vec2::new(0.0, 0.0),
),
// top right
Vertex2D::new(
Vec3::new(dimensions.x as f32, 0.0, 0.0),
Vec2::new(1.0, 1.0),
),
// bottom right
Vertex2D::new(
Vec3::new(dimensions.x as f32, dimensions.y as f32, 0.0),
Vec2::new(1.0, 0.0),
),
];
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Vertex Buffer"),
contents: bytemuck::cast_slice(vertices.as_slice()),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
let contents: [u32; 6] = [
//3, 1, 0,
//0, 2, 3
3, 1, 0, // second tri
0, 2, 3, // first tri
//0, 2, 3, // second tri
];
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Index Buffer"),
contents: bytemuck::cast_slice(&contents),
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
});
(vertex_buffer, index_buffer)
}
fn load_sprite_texture(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
uuid: &Uuid,
image: &Image,
) -> Option<(wgpu::Texture, wgpu::Sampler, wgpu::BindGroup)> {
let uuid_str = uuid.to_string();
let image_dim = image.dimensions();
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some(&format!("sprite_texture_{}", uuid_str)),
size: wgpu::Extent3d {
width: image_dim.0,
height: image_dim.1,
depth_or_array_layers: 1,
},
mip_level_count: 1, // TODO
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let tex_view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
queue.write_texture(
wgpu::ImageCopyTexture {
aspect: wgpu::TextureAspect::All,
texture: &tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
},
&image.to_rgba8(),
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(4 * image_dim.0),
rows_per_image: Some(image_dim.1),
},
wgpu::Extent3d {
width: image_dim.0,
height: image_dim.1,
depth_or_array_layers: 1,
},
);
let sprite_instances = self.sprite_instances_buf.as_ref().unwrap();
let bgl = self.texture_bgl.as_ref().unwrap();
let tex_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(&format!("sprite_texture_bg_{}", uuid_str)),
layout: bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&tex_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &sprite_instances,
offset: 0,
size: None,
}),
},
],
});
Some((tex, sampler, tex_bg))
}
}
impl Node for SpritePass {
fn desc(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
) -> crate::render::graph::NodeDesc {
let device = &graph.device;
let sprite_instances = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("sprite_instances"),
size: std::mem::size_of::<SpriteInstance>() as u64 * 5000,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.sprite_instances_buf = Some(Arc::new(sprite_instances));
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("bgl_sprite_main"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
self.texture_bgl = Some(Arc::new(bgl));
let mut desc = NodeDesc::new(NodeType::Render, None, vec![]);
desc.add_buffer_slot(BasePassSlots::Camera, SlotAttribute::Input, None);
desc
}
fn prepare(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
world: &mut lyra_ecs::World,
_: &mut crate::render::graph::RenderGraphContext,
) {
let device = graph.device();
let vt = graph.view_target();
if self.pipeline.is_none() {
let shader = graph
.load_shader_str(
"sprite_shader",
include_str!("../../shaders/2d/sprite_main.wgsl"),
)
.expect("failed to load wgsl shader from manager");
let diffuse_bgl = self.texture_bgl.clone().unwrap();
let camera_bgl = graph.bind_group_layout(BasePassSlots::Camera).clone();
self.pipeline = Some(RenderPipeline::create(
device,
&RenderPipelineDescriptor {
label: Some("sprite_pass".into()),
layouts: vec![diffuse_bgl, camera_bgl],
push_constant_ranges: vec![],
vertex: VertexState {
module: shader.clone(),
entry_point: "vs_main".into(),
buffers: vec![Vertex2D::desc().into()],
},
fragment: Some(FragmentState {
module: shader,
entry_point: "fs_main".into(),
targets: vec![Some(wgpu::ColorTargetState {
format: vt.format(),
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
depth_stencil: Some(wgpu::DepthStencilState {
format: RenderTexture::DEPTH_FORMAT,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::Less,
stencil: wgpu::StencilState::default(), // TODO: stencil buffer
bias: wgpu::DepthBiasState::default(),
}),
primitive: wgpu::PrimitiveState {
cull_mode: Some(wgpu::Face::Back),
..Default::default()
},
multisample: wgpu::MultisampleState::default(),
multiview: None,
},
));
world.add_resource_default_if_absent::<RenderAssets<SpriteTexture>>();
let texture_store = world
.get_resource_data::<RenderAssets<SpriteTexture>>()
.unwrap();
self.texture_store = Some(texture_store.clone());
world.add_resource_default_if_absent::<FxHashMap<Entity, SpriteBuffers>>();
let buffer_store = world
.get_resource_data::<FxHashMap<Entity, SpriteBuffers>>()
.unwrap();
self.buffer_store = Some(buffer_store.clone());
}
let mut sprite_instances = Vec::with_capacity(500);
let world_tick = world.current_tick();
let queue = &graph.queue;
for (
entity,
(sprite, atlas_sprite),
transform,
sorting_offset,
mut texture_store,
mut buffer_store,
) in world
.view::<(
Entities,
Or<&Sprite, (&AtlasSprite, TickOf<AtlasSprite>)>,
EitherTransform,
Option<&SortingOffset>,
ResMut<RenderAssets<SpriteTexture>>,
ResMut<FxHashMap<Entity, SpriteBuffers>>,
)>()
.iter()
{
let tex = if let Some(sprite) = &sprite {
sprite.texture.clone() //.data_ref()
} else if let Some((a, _)) = &atlas_sprite {
a.atlas.data_ref().unwrap().texture.clone()
} else {
continue;
};
let rect = atlas_sprite.as_ref().map(|(a, _)| a.sprite);
if let Some(image) = tex.data_ref() {
let texture_uuid = tex.uuid();
if !texture_store.contains_key(&texture_uuid) {
// returns `None` if the Texture image is not loaded.
if let Some((texture, sampler, tex_bg)) =
self.load_sprite_texture(device, queue, &texture_uuid, &image)
{
texture_store.insert(
texture_uuid,
SpriteTexture {
texture,
sampler,
texture_bg: Arc::new(tex_bg),
},
);
}
}
let dim = rect.map(|r| r.dimensions()).unwrap_or_else(|| {
let i = image.dimensions();
UVec2::new(i.0, i.1)
});
if !buffer_store.contains_key(&entity) {
let (vertex, index) = self.create_vertex_index_buffers(device, dim);
buffer_store.insert(
entity,
SpriteBuffers {
vertex_buffers: vertex,
index_buffers: index,
},
);
} else if let Some((ats, tick)) = &atlas_sprite {
// detect a change for the vertex and index buffers of the sprite
if tick.checked_sub(1).unwrap_or(0) >= *world_tick {
debug!("Updating buffer for entity after change detected in atlas sprite");
let dim = ats.sprite.dimensions();
let (vertex, index) = self.create_vertex_index_buffers(device, dim);
buffer_store.insert(
entity,
SpriteBuffers {
vertex_buffers: vertex,
index_buffers: index,
},
);
}
}
let pivot = atlas_sprite
.map(|ats| ats.0.pivot)
// unwrap is safe since its either AtlasSprite or Sprite.
.unwrap_or_else(|| sprite.unwrap().pivot)
.as_vec();
let pivot_pos = dim.as_vec2() * (pivot - Vec2::splat(0.5));
let transform =
*transform + lyra_math::Transform::from_translation(pivot_pos.extend(0.0));
let inst = SpriteInstance {
atlas_frame: rect.unwrap_or(URect::ZERO),
transform: transform.calculate_mat4(),
};
sprite_instances.push(inst);
let inst_id = sprite_instances.len() as u64 - 1;
self.jobs.push_back(RenderJob {
entity,
shader_id: 0,
asset_uuid: texture_uuid,
atlas_frame_id: inst_id,
position: transform.translation,
sort_offset: sorting_offset.map(|o| o.0).unwrap_or_default(),
is_transparent: image.is_transparent,
});
};
}
// Sort the jobs be the sorting axis if one exists.
// If there is one, there should only be one, so `next` is fine.
if let Some(sort_order) = world.view_iter::<&CameraSortingAxis>().next() {
self.jobs
.make_contiguous()
.sort_by(|aj: &RenderJob, bj: &RenderJob| {
let a = (aj.position + aj.sort_offset) * sort_order.0;
let b = (bj.position + bj.sort_offset) * sort_order.0;
// If just one of the job is a transparent texture, only consider that.
// Else compare the Vec3
if !aj.is_transparent ^ !bj.is_transparent {
aj.is_transparent.cmp(&bj.is_transparent)
} else {
// Vec3 does not implement PartialOrd
a.x.partial_cmp(&b.x)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal))
.then(a.z.partial_cmp(&b.z).unwrap_or(std::cmp::Ordering::Equal))
}
});
}
let buf = self.sprite_instances_buf.as_ref().unwrap();
// skip default rect
queue.write_buffer(buf, 0, bytemuck::cast_slice(&sprite_instances));
}
fn execute(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
_: &crate::render::graph::NodeDesc,
context: &mut crate::render::graph::RenderGraphContext,
) {
let pipeline = self.pipeline.as_ref().unwrap();
let texture_store = self.texture_store.clone().unwrap();
let texture_store: AtomicRef<RenderAssets<SpriteTexture>> = texture_store.get();
let buffer_store = self.buffer_store.clone().unwrap();
let buffer_store: AtomicRef<FxHashMap<Entity, SpriteBuffers>> = buffer_store.get();
let vt = graph.view_target();
let view = vt.render_view();
let depth_view = graph
.slot_value(BasePassSlots::DepthTextureView)
.unwrap()
.as_texture_view()
.expect("BasePassSlots::DepthTextureView was not a TextureView slot");
let camera_bg = graph.bind_group(BasePassSlots::Camera);
{
let encoder = context.encoder.as_mut().unwrap();
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("sprite_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.1,
g: 0.2,
b: 0.3,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(pipeline);
while let Some(job) = self.jobs.pop_front() {
// bind texture
let tex = texture_store
.get(&job.asset_uuid)
.expect("failed to find SpriteTexture for job asset_uuid");
pass.set_bind_group(0, &tex.texture_bg, &[]);
pass.set_bind_group(1, camera_bg, &[]);
// set vertex and index buffers
let bufs = buffer_store
.get(&job.entity)
.expect("failed to find buffers for job entity");
pass.set_vertex_buffer(0, bufs.vertex_buffers.slice(..));
pass.set_index_buffer(bufs.index_buffers.slice(..), wgpu::IndexFormat::Uint32);
// use the atlas frame id as the instance
let inst = job.atlas_frame_id as u32;
pass.draw_indexed(0..6, 0, inst..inst + 1);
}
}
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct SpriteInstance {
atlas_frame: URect,
transform: glam::Mat4,
}

View file

@ -1,164 +0,0 @@
use std::{collections::HashMap, sync::Arc};
use lyra_game_derive::RenderGraphLabel;
use crate::render::{
graph::{Node, NodeDesc, NodeType},
resource::{FragmentState, PipelineDescriptor, RenderPipelineDescriptor, VertexState},
};
#[derive(Default, Debug, Clone, Copy, Hash, RenderGraphLabel)]
pub struct TintPassLabel;
#[derive(Debug, Default)]
pub struct TintPass {
target_sampler: Option<wgpu::Sampler>,
bgl: Option<Arc<wgpu::BindGroupLayout>>,
/// Store bind groups for the input textures.
/// The texture may change due to resizes, or changes to the view target chain
/// from other nodes.
bg_cache: HashMap<wgpu::Id<wgpu::TextureView>, wgpu::BindGroup>,
}
impl TintPass {
pub fn new() -> Self {
Self::default()
}
}
impl Node for TintPass {
fn desc(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
) -> crate::render::graph::NodeDesc {
let device = &graph.device;
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("tint_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
count: None,
},
],
});
let bgl = Arc::new(bgl);
self.bgl = Some(bgl.clone());
self.target_sampler = Some(device.create_sampler(&wgpu::SamplerDescriptor::default()));
let vt = graph.view_target();
let shader = graph.load_shader_str("tint_shader", include_str!("../../shaders/tint.wgsl"))
.expect("failed to load tint shader");
NodeDesc::new(
NodeType::Render,
Some(PipelineDescriptor::Render(RenderPipelineDescriptor {
label: Some("tint_pass".into()),
layouts: vec![bgl.clone()],
push_constant_ranges: vec![],
vertex: VertexState {
module: shader.clone(),
entry_point: "vs_main".into(),
buffers: vec![],
},
fragment: Some(FragmentState {
module: shader,
entry_point: "fs_main".into(),
targets: vec![Some(wgpu::ColorTargetState {
format: vt.format(),
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
depth_stencil: None,
primitive: wgpu::PrimitiveState::default(),
multisample: wgpu::MultisampleState::default(),
multiview: None,
})),
vec![],
)
}
fn prepare(
&mut self,
_: &mut crate::render::graph::RenderGraph,
_: &mut lyra_ecs::World,
_: &mut crate::render::graph::RenderGraphContext,
) {
//todo!()
}
fn execute(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
_: &crate::render::graph::NodeDesc,
context: &mut crate::render::graph::RenderGraphContext,
) {
let pipeline = graph
.pipeline(context.label.clone())
.expect("Failed to find pipeline for TintPass");
let mut vt = graph.view_target_mut();
let chain = vt.get_chain();
let source_view = chain.source.frame_view.as_ref().unwrap();
let dest_view = chain.dest.frame_view.as_ref().unwrap();
let bg = self
.bg_cache
.entry(source_view.global_id())
.or_insert_with(|| {
graph
.device()
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("tint_bg"),
layout: self.bgl.as_ref().unwrap(),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(source_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(
self.target_sampler.as_ref().unwrap(),
),
},
],
})
});
{
let encoder = context.encoder.as_mut().unwrap();
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("tint_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: dest_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(pipeline.as_render());
pass.set_bind_group(0, bg, &[]);
pass.draw(0..3, 0..1);
}
}
}

View file

@ -1,165 +0,0 @@
use lyra_ecs::{
query::{
filter::{Changed, Or},
Entities,
},
Entity,
};
use lyra_game_derive::RenderGraphLabel;
use lyra_math::Transform;
use lyra_resource::ResHandle;
use lyra_scene::{SceneGraph, WorldTransform};
use tracing::debug;
use crate::{
render::{
graph::{Node, NodeDesc, NodeType},
transform_buffer_storage::{TransformBuffers, TransformIndex},
EitherTransform,
},
DeltaTime,
};
#[derive(Default, Debug, Clone, Copy, Hash, RenderGraphLabel)]
pub struct TransformsNodeLabel;
#[derive(Debug, Default)]
pub struct TransformsNode;
impl TransformsNode {
pub fn new() -> Self {
Self
}
}
fn process_component_queue(
world: &mut lyra_ecs::World,
component_queue: Vec<(Entity, Option<TransformIndex>)>,
) {
for (en, index) in component_queue {
if let Some(index) = index {
world.insert(en, index);
}
}
}
fn update_transforms(
device: &wgpu::Device,
queue: &wgpu::Queue,
limits: &wgpu::Limits,
world: &mut lyra_ecs::World,
delta_time: DeltaTime,
buffers: &mut TransformBuffers,
parent_transform: Transform,
) {
let mut component_queue = vec![];
let view = world.filtered_view_iter::<(
Entities,
EitherTransform,
Option<&TransformIndex>,
Option<&ResHandle<SceneGraph>>,
), Or<Changed<WorldTransform>, Changed<Transform>>>();
for (entity, transform, transform_index, scene_graph) in view {
// expand the transform buffers if they need to be.
if buffers.needs_expand() {
debug!("Expanding transform buffers");
buffers.expand_buffers(device);
}
// offset this transform by its parent
let transform = *transform + parent_transform;
// Get the TransformIndex from the entity, or reserve a new one if the entity doesn't have
// the component.
let index = match transform_index {
Some(i) => *i,
None => {
let i = buffers.reserve_transform(&device);
component_queue.push((entity, Some(i)));
i
}
};
// TODO: only update if the transform changed.
buffers.update(
&queue,
index,
transform.calculate_mat4(),
glam::Mat3::from_quat(transform.rotation),
);
if let Some(scene) = scene_graph {
if let Some(mut scene) = scene.data_mut() {
update_transforms(
device,
queue,
limits,
scene.world_mut(),
delta_time,
buffers,
transform,
);
}
}
}
process_component_queue(world, component_queue);
}
impl Node for TransformsNode {
fn desc(
&mut self,
_: &mut crate::render::graph::RenderGraph,
) -> crate::render::graph::NodeDesc {
NodeDesc::new(NodeType::Node, None, vec![])
}
fn prepare(
&mut self,
_: &mut crate::render::graph::RenderGraph,
world: &mut lyra_ecs::World,
context: &mut crate::render::graph::RenderGraphContext,
) {
let device = &context.device;
let queue = &context.queue;
let render_limits = device.limits();
// prepare the world with resources
if !world.has_resource::<TransformBuffers>() {
let buffers = TransformBuffers::new(device);
world.add_resource(buffers);
}
// I have to do this weird garbage to borrow the `TransformBuffers`
// without running into a borrow checker error from passing `world` as mutable.
// This is safe since I know that the recursive function isn't accessing this
// TransformBuffers, or any other ones in other worlds.
let buffers = world
.get_resource_data::<TransformBuffers>()
.map(|r| r.clone())
.unwrap();
let mut buffers = buffers.get_mut();
let dt = world.get_resource::<DeltaTime>().unwrap().clone();
update_transforms(
&device,
&queue,
&render_limits,
world,
dt,
&mut buffers,
Transform::default(),
);
}
fn execute(
&mut self,
_: &mut crate::render::graph::RenderGraph,
_: &crate::render::graph::NodeDesc,
_: &mut crate::render::graph::RenderGraphContext,
) {
}
}

View file

@ -1,355 +0,0 @@
use std::sync::Arc;
use tracing::debug;
use crate::math;
enum RenderTargetInner {
Surface {
/// The surface that will be rendered to.
///
/// You can create a new surface with a `'static` lifetime if you have an `Arc<Window>`:
/// ```nobuild
/// let window = Arc::new(window);
/// let surface = instance.create_surface(Arc::clone(&window))?;
/// ```
surface: wgpu::Surface<'static>,
/// the configuration of the surface render target..
config: wgpu::SurfaceConfiguration,
},
Texture {
/// The texture that will be rendered to.
texture: Arc<wgpu::Texture>,
}
}
/// A render target that is a surface or a texture.
#[repr(transparent)]
pub struct RenderTarget(RenderTargetInner);
impl From<wgpu::Texture> for RenderTarget {
fn from(value: wgpu::Texture) -> Self {
Self(RenderTargetInner::Texture { texture: Arc::new(value) })
}
}
impl RenderTarget {
pub fn from_surface(surface: wgpu::Surface<'static>, config: wgpu::SurfaceConfiguration) -> Self {
Self(RenderTargetInner::Surface { surface, config })
}
pub fn new_texture(device: &wgpu::Device, format: wgpu::TextureFormat, size: math::UVec2) -> Self {
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
Self(RenderTargetInner::Texture { texture: Arc::new(tex) })
}
pub fn format(&self) -> wgpu::TextureFormat {
match &self.0 {
RenderTargetInner::Surface { config, .. } => config.format,
RenderTargetInner::Texture { texture } => texture.format(),
}
}
pub fn size(&self) -> math::UVec2 {
match &self.0 {
RenderTargetInner::Surface { config, .. } => math::UVec2::new(config.width, config.height),
RenderTargetInner::Texture { texture } => {
let s = texture.size();
math::UVec2::new(s.width, s.height)
},
}
}
/// Get the frame texture of the [`RenderTarget`]
///
/// If this is target is a surface and the frame texture was already retrieved from the swap
/// chain, a [`wgpu::SurfaceError`] error will be returned.
pub fn frame_texture(&self) -> Result<FrameTexture, wgpu::SurfaceError> {
match &self.0 {
RenderTargetInner::Surface { surface, .. } => Ok(FrameTexture::Surface(surface.get_current_texture()?)),
RenderTargetInner::Texture { texture } => Ok(FrameTexture::Texture(texture.clone())),
}
}
pub fn resize(&mut self, device: &wgpu::Device, new_size: math::UVec2) {
match &mut self.0 {
RenderTargetInner::Surface { surface, config } => {
config.width = new_size.x;
config.height = new_size.y;
surface.configure(device, config);
},
RenderTargetInner::Texture { texture } => {
let format = texture.format();
let size = self.size();
*self = Self::new_texture(device, format, size);
},
}
}
/// Create the frame of the RenderTarget.
///
/// If this is target is a surface and the frame texture was already retrieved from the
/// swap chain, a [`wgpu::SurfaceError`] error will be returned.
pub fn create_frame(&self) -> Frame {
let texture = self.frame_texture()
.expect("failed to create frame texture"); // TODO: should be returned to the user
let size = self.size();
Frame {
size,
texture,
}
}
}
pub enum FrameTexture {
Surface(wgpu::SurfaceTexture),
Texture(Arc<wgpu::Texture>),
}
/// Represents the current frame that is being rendered to.
//#[allow(dead_code)]
pub struct Frame {
pub(crate) size: math::UVec2,
pub(crate) texture: FrameTexture,
}
impl Frame {
pub fn texture(&self) -> &wgpu::Texture {
match &self.texture {
FrameTexture::Surface(s) => &s.texture,
FrameTexture::Texture(t) => t,
}
}
/// Present the frame
///
/// If this frame is from a surface, it will be present, else nothing will happen.
pub fn present(self) {
match self.texture {
FrameTexture::Surface(s) => s.present(),
FrameTexture::Texture(_) => {},
}
}
/// The size of the frame
pub fn size(&self) -> math::UVec2 {
self.size
}
}
/// Stores the current frame, and the render target it came from.
pub struct FrameTarget {
pub render_target: RenderTarget,
/// None when a frame has not been created yet
pub frame: Option<Frame>,
/// The view to use to render to the frame.
pub frame_view: Option<wgpu::TextureView>,
}
impl FrameTarget {
pub fn new(render_target: RenderTarget) -> Self {
Self {
render_target,
frame: None,
frame_view: None,
}
}
/// Returns the size of the [`RenderTarget`].
pub fn size(&self) -> math::UVec2 {
self.render_target.size()
}
/// Returns the [`wgpu::TextureFormat`] of the [`RenderTarget`].
pub fn format(&self) -> wgpu::TextureFormat {
self.render_target.format()
}
/// Create the frame using the inner [`RenderTarget`].
pub fn create_frame(&mut self) -> &mut Frame {
self.frame = Some(self.render_target.create_frame());
self.frame.as_mut().unwrap()
}
/// Create the [`wgpu::TextureView`] for the [`Frame`], storing it in self and returning a reference to it.
pub fn create_frame_view(&mut self) -> &wgpu::TextureView {
let frame = self.frame.as_ref().expect("frame was not created, cannot create view");
self.frame_view = Some(frame.texture().create_view(&wgpu::TextureViewDescriptor::default()));
self.frame_view.as_ref().unwrap()
}
}
pub struct TargetViewChain<'a> {
pub source: &'a mut FrameTarget,
pub dest: &'a mut FrameTarget,
}
struct ViewChain {
source: FrameTarget,
dest: FrameTarget,
/// tracks the target that is currently being presented
active: u8,
}
impl ViewChain {
/// Returns the currently active [`FrameTarget`].
fn active(&self) -> &FrameTarget {
if self.active == 0 {
&self.source
} else if self.active == 1 {
&self.dest
} else {
panic!("active chain index became invalid! ({})", self.active);
}
}
}
pub struct ViewTarget {
device: Arc<wgpu::Device>,
/// The primary RenderTarget, likely a Surface
pub primary: FrameTarget,
chain: Option<ViewChain>,
}
impl ViewTarget {
pub fn new(device: Arc<wgpu::Device>, primary: RenderTarget) -> Self {
let mut s = Self {
device,
primary: FrameTarget::new(primary),
chain: None,
};
s.create_chain(s.primary.format(), s.primary.size());
s
}
/// Returns the size of the target.
pub fn size(&self) -> math::UVec2 {
self.primary.size()
}
/// Returns the [`wgpu::TextureFormat`]
pub fn format(&self) -> wgpu::TextureFormat {
self.primary.format()
}
/// Resize all the targets, causes the chain to be recreated.
pub fn resize(&mut self, device: &wgpu::Device, size: math::UVec2) {
if size != self.primary.size() {
self.primary.render_target.resize(device, size);
self.create_chain(self.primary.format(), size);
}
}
fn create_chain(&mut self, format: wgpu::TextureFormat, size: math::UVec2) {
debug!("Creating chain with {:?} format and {:?} size", format, size);
let mut source = FrameTarget::new(RenderTarget::new_texture(&self.device, format, size));
source.create_frame();
source.create_frame_view();
let mut dest = FrameTarget::new(RenderTarget::new_texture(&self.device, format, size));
dest.create_frame();
dest.create_frame_view();
self.chain = Some(ViewChain {
source,
dest,
active: 0,
});
}
/// Cycle the target view chain, storing it in self, and returning a mutable borrow to it.
pub fn get_chain(&mut self) -> TargetViewChain {
let format = self.primary.format();
let size = self.primary.size();
if let Some(chain) = &self.chain {
// check if the chain needs to be recreated
if chain.source.format() != format || chain.source.size() != size {
self.create_chain(format, size);
}
} else {
self.create_chain(format, size);
}
let chain = self.chain.as_mut().unwrap();
if chain.active == 0 {
chain.active = 1;
TargetViewChain {
source: &mut chain.source,
dest: &mut chain.dest,
}
} else if chain.active == 1 {
chain.active = 0;
TargetViewChain {
source: &mut chain.dest,
dest: &mut chain.source,
}
} else {
panic!("active chain index became invalid! ({})", chain.active);
}
}
/// Get the [`wgpu::TextureView`] to render to.
pub fn render_view(&self) -> &wgpu::TextureView {
let chain = self.chain.as_ref().unwrap();
chain.active().frame_view.as_ref().unwrap()
}
/// Copy the chain target to the primary target
///
/// The primary target must have `wgpu::TextureUsages::COPY_DST`. This also resets the active
/// chain texture.
pub fn copy_to_primary(&mut self, encoder: &mut wgpu::CommandEncoder) {
let chain = self.chain.as_mut().unwrap();
let active_tex = chain.active().frame.as_ref().unwrap().texture();
let active_copy = wgpu::ImageCopyTexture {
texture: active_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
};
let dest_tex = self.primary.frame.as_ref().unwrap().texture();
let dest_copy = wgpu::ImageCopyTexture {
texture: dest_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
};
let size = self.primary.size();
let size = wgpu::Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: 1,
};
encoder.copy_texture_to_texture(active_copy, dest_copy, size);
// reset active texture after a render
// must get the chain again because of the borrow checker
let chain = self.chain.as_mut().unwrap();
chain.active = 0;
}
}

View file

@ -1,296 +0,0 @@
use std::cell::RefCell;
use std::collections::VecDeque;
use std::ops::{Deref, DerefMut};
use std::rc::Rc;
use std::sync::Arc;
use lyra_ecs::World;
use lyra_game_derive::RenderGraphLabel;
use tracing::{debug, instrument, warn};
use winit::window::Window;
use crate::render::graph::{BasePass, BasePassLabel, BasePassSlots, FxaaPass, FxaaPassLabel, LightBasePass, LightBasePassLabel, LightCullComputePass, LightCullComputePassLabel, MeshPass, MeshPrepNode, MeshPrepNodeLabel, MeshesPassLabel, PresentPass, PresentPassLabel, RenderGraphLabelValue, RenderTarget, ShadowMapsPass, ShadowMapsPassLabel, SpritePass, SpritePassLabel, SubGraphNode, TransformsNode, TransformsNodeLabel, ViewTarget};
use super::graph::RenderGraph;
use super::{resource::RenderPipeline, render_job::RenderJob};
use crate::math;
#[derive(Clone, Copy, Debug)]
pub struct ScreenSize(glam::UVec2);
impl Deref for ScreenSize {
type Target = glam::UVec2;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ScreenSize {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Debug, Clone, Copy, Hash, RenderGraphLabel)]
struct TestSubGraphLabel;
pub trait Renderer {
fn prepare(&mut self, main_world: &mut World);
fn render(&mut self) -> Result<(), wgpu::SurfaceError>;
fn on_resize(&mut self, world: &mut World, new_size: winit::dpi::PhysicalSize<u32>);
fn surface_size(&self) -> winit::dpi::PhysicalSize<u32>;
fn add_render_pipeline(&mut self, shader_id: u64, pipeline: Arc<RenderPipeline>);
}
pub trait RenderPass {
fn prepare(&mut self, main_world: &mut World);
fn render(&mut self, encoder: &mut wgpu::CommandEncoder) -> Result<(), wgpu::SurfaceError>;
fn on_resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>);
}
pub struct BasicRenderer {
pub device: Arc<wgpu::Device>, // device does not need to be mutable, no need for refcell
pub queue: Arc<wgpu::Queue>,
pub size: winit::dpi::PhysicalSize<u32>,
pub window: Arc<Window>,
pub clear_color: wgpu::Color,
pub render_pipelines: rustc_hash::FxHashMap<u64, Arc<RenderPipeline>>,
pub render_jobs: VecDeque<RenderJob>,
graph: RenderGraph,
}
impl BasicRenderer {
#[instrument(skip(world, window))]
pub async fn create_with_window(world: &mut World, window: Arc<Window>) -> BasicRenderer {
let size = window.inner_size();
world.add_resource(ScreenSize(glam::UVec2::new(size.width, size.height)));
// Get a GPU handle
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
dx12_shader_compiler: Default::default(),
flags: wgpu::InstanceFlags::default(),
gles_minor_version: wgpu::Gles3MinorVersion::Automatic,
});
let surface: wgpu::Surface::<'static> = instance.create_surface(window.clone()).unwrap();
let adapter = instance.request_adapter(
&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
},
).await.unwrap();
let (device, queue) = adapter.request_device(
&wgpu::DeviceDescriptor {
label: None,
required_features: wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES | wgpu::Features::ADDRESS_MODE_CLAMP_TO_BORDER,
// WebGL does not support all wgpu features.
// Not sure if the engine will ever completely support WASM,
// but its here just in case
required_limits: if cfg!(target_arch = "wasm32") {
wgpu::Limits::downlevel_webgl2_defaults()
} else {
wgpu::Limits {
max_bind_groups: 8,
..Default::default()
}
},
memory_hints: wgpu::MemoryHints::MemoryUsage,
},
None,
).await.unwrap();
let surface_caps = surface.get_capabilities(&adapter);
let present_mode = surface_caps.present_modes[0];
debug!("present mode: {:?}", present_mode);
let surface_format = surface_caps.formats.iter()
.copied()
.find(|f| f.is_srgb())
.unwrap_or(surface_caps.formats[0]);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST,
format: surface_format,
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::default(), //wgpu::PresentMode::Mailbox, // "Fast Vsync"
alpha_mode: surface_caps.alpha_modes[0],
desired_maximum_frame_latency: 2,
view_formats: vec![],
};
surface.configure(&device, &config);
let device = Arc::new(device);
let queue = Arc::new(queue);
let surface_target = RenderTarget::from_surface(surface, config);
let view_target = Rc::new(RefCell::new(ViewTarget::new(device.clone(), surface_target)));
let mut main_graph = RenderGraph::new(device.clone(), queue.clone(), view_target.clone(), &world);
debug!("Adding base pass");
main_graph.add_node(BasePassLabel, BasePass::new());
{
let mut forward_plus_graph = RenderGraph::new(device.clone(), queue.clone(), view_target.clone(), &world);
debug!("Adding light base pass");
forward_plus_graph.add_node(LightBasePassLabel, LightBasePass::new());
debug!("Adding light cull compute pass");
forward_plus_graph.add_node(LightCullComputePassLabel, LightCullComputePass::new(size));
debug!("Adding Transforms node");
forward_plus_graph.add_node(TransformsNodeLabel, TransformsNode::new());
debug!("Adding shadow maps pass");
forward_plus_graph.add_node(ShadowMapsPassLabel, ShadowMapsPass::new(&device));
debug!("Adding mesh prep node");
let mesh_prep = MeshPrepNode::new(&device);
let material_bgl = mesh_prep.material_bgl.clone();
forward_plus_graph.add_node(MeshPrepNodeLabel, mesh_prep);
debug!("Adding mesh pass");
forward_plus_graph.add_node(MeshesPassLabel, MeshPass::new(material_bgl));
forward_plus_graph.add_edge(TransformsNodeLabel, MeshPrepNodeLabel);
debug!("Adding sprite pass");
forward_plus_graph.add_node(SpritePassLabel, SpritePass::new());
forward_plus_graph.add_edge(TransformsNodeLabel, SpritePassLabel);
forward_plus_graph.add_edge(LightBasePassLabel, LightCullComputePassLabel);
forward_plus_graph.add_edge(LightCullComputePassLabel, MeshesPassLabel);
forward_plus_graph.add_edge(MeshPrepNodeLabel, MeshesPassLabel);
// run ShadowMapsPass after MeshPrep and before MeshesPass
forward_plus_graph.add_edge(MeshPrepNodeLabel, ShadowMapsPassLabel);
forward_plus_graph.add_edge(ShadowMapsPassLabel, MeshesPassLabel);
main_graph.add_sub_graph(TestSubGraphLabel, forward_plus_graph);
main_graph.add_node(TestSubGraphLabel, SubGraphNode::new(TestSubGraphLabel,
vec![
/* RenderGraphLabelValue::from(BasePassSlots::WindowTextureView),
RenderGraphLabelValue::from(BasePassSlots::MainRenderTarget), */
RenderGraphLabelValue::from(BasePassSlots::DepthTexture),
RenderGraphLabelValue::from(BasePassSlots::DepthTextureView),
RenderGraphLabelValue::from(BasePassSlots::Camera),
RenderGraphLabelValue::from(BasePassSlots::ScreenSize),
]
));
}
main_graph.add_node(FxaaPassLabel, FxaaPass::default());
main_graph.add_edge(TestSubGraphLabel, FxaaPassLabel);
//let present_pass_label = PresentPassLabel::new(BasePassSlots::Frame);//TintPassSlots::Frame);
let p = PresentPass;
main_graph.add_node(PresentPassLabel, p);
main_graph.add_edge(BasePassLabel, TestSubGraphLabel);
main_graph.add_edge(TestSubGraphLabel, PresentPassLabel);
/* debug!("Adding base pass");
g.add_node(BasePassLabel, BasePass::new(surface_target));
//debug!("Adding triangle pass");
//g.add_node(TrianglePass::new());
debug!("Adding present pass");
let present_pass_label = PresentPassLabel::new(BasePassSlots::Frame);//TintPassSlots::Frame);
let p = PresentPass::from_node_label(present_pass_label.clone());
g.add_node(p.label.clone(), p); */
/* debug!("adding tint pass");
g.add_node(TintPassLabel, TintPass::new(surface_target));
g.add_edge(BasePassLabel, TintPassLabel);
g.add_edge(LightCullComputePassLabel, TintPassLabel);
g.add_edge(MeshesPassLabel, TintPassLabel);
g.add_edge(TintPassLabel, present_pass_label.clone());
*/
/* g.add_edge(BasePassLabel, LightBasePassLabel);
g.add_edge(LightBasePassLabel, LightCullComputePassLabel);
g.add_edge(BasePassLabel, MeshesPassLabel);
g.add_edge(BasePassLabel, present_pass_label.clone());
g.add_edge(LightCullComputePassLabel, present_pass_label.clone());
g.add_edge(MeshesPassLabel, present_pass_label.clone()); */
main_graph.setup(&device);
Self {
window,
device,
queue,
size,
clear_color: wgpu::Color {
r: 0.1,
g: 0.2,
b: 0.3,
a: 1.0,
},
render_pipelines: Default::default(),
render_jobs: Default::default(),
graph: main_graph,
}
}
}
impl Renderer for BasicRenderer {
#[instrument(skip(self, main_world))]
fn prepare(&mut self, main_world: &mut World) {
self.graph.prepare(main_world);
}
#[instrument(skip(self))]
fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
self.graph.render();
Ok(())
}
#[instrument(skip(world, self))]
fn on_resize(&mut self, world: &mut World, new_size: winit::dpi::PhysicalSize<u32>) {
if new_size.width > 0 && new_size.height > 0 {
self.size = new_size;
// update surface config and the surface
/* let mut rt = self.graph.slot_value_mut(BasePassSlots::MainRenderTarget)
.unwrap().as_render_target_mut().unwrap();
rt.resize(&self.device, math::UVec2::new(new_size.width, new_size.height)); */
self.graph.view_target_mut().resize(&self.device, math::UVec2::new(new_size.width, new_size.height));
/* rt.surface_config.width = new_size.width;
rt.surface_config.height = new_size.height;
rt.surface.configure(&self.device, &rt.surface_config); */
// update screen size resource in ecs
let mut world_ss = world.get_resource_mut::<ScreenSize>()
.expect("world missing ScreenSize resource");
world_ss.0 = glam::UVec2::new(new_size.width, new_size.height);
}
}
fn surface_size(&self) -> winit::dpi::PhysicalSize<u32> {
self.size
}
fn add_render_pipeline(&mut self, shader_id: u64, pipeline: Arc<RenderPipeline>) {
self.render_pipelines.insert(shader_id, pipeline);
}
}

View file

@ -1,33 +0,0 @@
mod shader;
use std::collections::HashMap;
pub use shader::*;
mod pipeline;
pub use pipeline::*;
mod compute_pipeline;
pub use compute_pipeline::*;
mod render_pipeline;
pub use render_pipeline::*;
mod pass;
pub use pass::*;
#[derive(Default, Clone)]
pub struct PipelineCompilationOptions {
pub constants: HashMap<String, f64>,
pub zero_initialize_workgroup_memory: bool,
pub vertex_pulling_transform: bool,
}
impl PipelineCompilationOptions {
pub fn as_wgpu(&self) -> wgpu::PipelineCompilationOptions {
wgpu::PipelineCompilationOptions {
constants: &self.constants,
zero_initialize_workgroup_memory: self.zero_initialize_workgroup_memory,
vertex_pulling_transform: self.vertex_pulling_transform,
}
}
}

View file

@ -1,16 +0,0 @@
/// A trait that represents a [`wgpu::ComputePass`] or [`wgpu::RenderPass`].
pub trait Pass<'a> {
fn set_bind_group(&mut self, index: u32, bind_group: &'a wgpu::BindGroup, offsets: &[wgpu::DynamicOffset]);
}
impl<'a> Pass<'a> for wgpu::ComputePass<'a> {
fn set_bind_group(&mut self, index: u32, bind_group: &'a wgpu::BindGroup, offsets: &[wgpu::DynamicOffset]) {
self.set_bind_group(index, bind_group, offsets);
}
}
impl<'a> Pass<'a> for wgpu::RenderPass<'a> {
fn set_bind_group(&mut self, index: u32, bind_group: &'a wgpu::BindGroup, offsets: &[wgpu::DynamicOffset]) {
self.set_bind_group(index, bind_group, offsets);
}
}

View file

@ -1,97 +0,0 @@
use std::sync::Arc;
use async_std::sync::RwLock;
use lyra_resource::{loader::{LoaderError, ResourceLoader}, ResHandle, ResourceData};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Shader {
pub label: Option<String>,
pub source: String,
pub dependent_modules: Option<String>,
}
impl ResourceData for Shader {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn dependencies(&self) -> Vec<lyra_resource::UntypedResHandle> {
vec![]
}
}
/// A WGSL shader module loader with a preprocessor.
///
/// This will load resources with mimetype of `text/wgsl` and `.wgsl` file extensions.
#[derive(Default)]
pub struct PreprocessShaderLoader {
processor: Arc<RwLock<wgsl_preprocessor::Processor>>,
}
async fn load_shader_str(processor: Arc<RwLock<wgsl_preprocessor::Processor>>, content: &str) -> Result<Shader, LoaderError> {
let mut processor = processor.write().await;
let mod_path = processor.parse_module(content)
.map_err(|e| LoaderError::DecodingError(Arc::new(e.into())))?;
let content = match &mod_path {
Some(path) => processor.preprocess_module(&path)
.map_err(|e| LoaderError::DecodingError(Arc::new(e.into())))?,
None => content.to_owned(),
};
Ok(Shader {
label: mod_path,
source: content,
dependent_modules: None,
})
}
impl ResourceLoader for PreprocessShaderLoader {
fn extensions(&self) -> &[&str] {
&[
"wgsl"
]
}
fn mime_types(&self) -> &[&str] {
&[
"text/wgsl"
]
}
fn load(&self, _: lyra_resource::ResourceManager, path: &str) -> lyra_resource::loader::PinedBoxLoaderFuture {
// cant use &str across async
let path = path.to_string();
let processor = self.processor.clone();
Box::pin(async move {
let module_str = std::fs::read_to_string(&path)
.map_err(|e| LoaderError::DecodingError(Arc::new(e.into())))?;
let shader = load_shader_str(processor, &module_str).await?;
Ok(Box::new(shader) as Box<dyn ResourceData>)
})
}
fn load_bytes(&self, _: lyra_resource::ResourceManager, bytes: Vec<u8>, offset: usize, length: usize) -> lyra_resource::loader::PinedBoxLoaderFuture {
let processor = self.processor.clone();
Box::pin(async move {
let end = offset + length;
let contents = std::str::from_utf8(&bytes[offset..end])
.map_err(|e| LoaderError::DecodingError(Arc::new(e.into())))?;
let shader = load_shader_str(processor, &contents).await?;
Ok(Box::new(shader) as Box<dyn ResourceData>)
})
}
fn create_erased_handle(&self) -> std::sync::Arc<dyn lyra_resource::ResourceStorage> {
Arc::from(ResHandle::<Shader>::new_loading(None))
}
}

View file

@ -1,91 +0,0 @@
const ALPHA_CUTOFF = 0.1;
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) tex_coords: vec2<f32>,
@builtin(instance_index) instance_index: u32,
}
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
@location(1) world_position: vec3<f32>,
@location(2) instance_index: u32
}
struct TransformData {
transform: mat4x4<f32>,
normal_matrix: mat4x4<f32>,
}
struct URect {
min: vec2<u32>,
max: vec2<u32>,
}
struct SpriteInstance {
atlas_frame: URect,
transform: mat4x4<f32>,
}
struct CameraUniform {
view: mat4x4<f32>,
inverse_projection: mat4x4<f32>,
view_projection: mat4x4<f32>,
projection: mat4x4<f32>,
position: vec3<f32>,
tile_debug: u32,
}
@group(1) @binding(0)
var<uniform> u_camera: CameraUniform;
@vertex
fn vs_main(
in: VertexInput,
) -> VertexOutput {
let transform = u_sprite_instances[in.instance_index].transform;
var world_position: vec4<f32> = transform * vec4<f32>(in.position, 1.0);
var out: VertexOutput;
out.world_position = world_position.xyz;
out.tex_coords = in.tex_coords;
out.clip_position = u_camera.view_projection * world_position;
out.instance_index = in.instance_index;
return out;
}
@group(0) @binding(0)
var t_diffuse: texture_2d<f32>;
@group(0) @binding(1)
var s_diffuse: sampler;
@group(0) @binding(2)
var<storage, read> u_sprite_instances: array<SpriteInstance>;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let sprite = u_sprite_instances[in.instance_index];
let frame = sprite.atlas_frame;
var region_coords = in.tex_coords;
if (frame.min.x != 0 || frame.min.y != 0 || frame.max.x != 0 || frame.max.y != 0) {
let dim = vec2<f32>(textureDimensions(t_diffuse));
// convert tex coords to frame
region_coords = vec2<f32>(
mix(f32(frame.min.x), f32(frame.max.x), in.tex_coords.x),
mix(f32(frame.min.y), f32(frame.max.y), in.tex_coords.y)
);
// convert frame coords to texture coords
region_coords /= dim;
}
let object_color: vec4<f32> = textureSample(t_diffuse, s_diffuse, region_coords);
if (object_color.a < ALPHA_CUTOFF) {
discard;
}
return object_color;
}

View file

@ -1,263 +0,0 @@
// Largely based off of https://blog.simonrodriguez.fr/articles/2016/07/implementing_fxaa.html
const EDGE_THRESHOLD_MIN: f32 = 0.0312;
const EDGE_THRESHOLD_MAX: f32 = 0.125;
const ITERATIONS: i32 = 12;
const SUBPIXEL_QUALITY: f32 = 0.75;
@group(0) @binding(0)
var t_screen: texture_2d<f32>;
@group(0) @binding(1)
var s_screen: sampler;
struct VertexOutput {
@builtin(position)
clip_position: vec4<f32>,
@location(0)
tex_coords: vec2<f32>,
}
fn QUALITY(q: i32) -> f32 {
switch (q) {
default: { return 1.0; }
case 5: { return 1.5; }
case 6, 7, 8, 9: { return 2.0; }
case 10: { return 4.0; }
case 11: { return 8.0; }
}
}
fn rgb2luma(rgb: vec3<f32>) -> f32 {
return sqrt(dot(rgb, vec3<f32>(0.299, 0.587, 0.114)));
}
@vertex
fn vs_main(
@builtin(vertex_index) vertex_index: u32,
) -> VertexOutput {
let tex_coords = vec2<f32>(f32(vertex_index >> 1u), f32(vertex_index & 1u)) * 2.0;
let clip_position = vec4<f32>(tex_coords * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0), 0.0, 1.0);
return VertexOutput(clip_position, tex_coords);
}
fn texture_offset(tex: texture_2d<f32>, samp: sampler, point: vec2<f32>, offset: vec2<i32>) -> vec3<f32> {
var tex_coords = point + vec2<f32>(offset);
return textureSample(tex, samp, tex_coords).xyz;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let resolution = vec2<f32>(textureDimensions(t_screen));
let inverse_screen_size = 1.0 / resolution.xy;
let tex_coords = in.clip_position.xy * inverse_screen_size;
var color_center: vec3<f32> = textureSampleLevel(t_screen, s_screen, tex_coords, 0.0).xyz;
// Luma at the current fragment
let luma_center = rgb2luma(color_center);
// Luma at the four direct neighbours of the current fragment.
let luma_down = rgb2luma(textureSampleLevel(t_screen, s_screen, tex_coords, 0.0, vec2<i32>(0, -1)).xyz);
let luma_up = rgb2luma(textureSampleLevel(t_screen, s_screen, tex_coords, 0.0, vec2<i32>(0, 1)).xyz);
let luma_left = rgb2luma(textureSampleLevel(t_screen, s_screen, tex_coords, 0.0, vec2<i32>(-1, 0)).xyz);
let luma_right = rgb2luma(textureSampleLevel(t_screen, s_screen, tex_coords, 0.0, vec2<i32>(1, 0)).xyz);
// Find the maximum and minimum luma around the current fragment.
let luma_min = min(luma_center, min(min(luma_down, luma_up), min(luma_left, luma_right)));
let luma_max = max(luma_center, max(max(luma_down, luma_up), max(luma_left, luma_right)));
// Compute the delta
let luma_range = luma_max - luma_min;
// If the luma variation is lower that a threshold (or if we are in a really dark area),
// we are not on an edge, don't perform any AA.
if (luma_range < max(EDGE_THRESHOLD_MIN, luma_max * EDGE_THRESHOLD_MAX)) {
return vec4<f32>(color_center, 1.0);
}
// Query the 4 remaining corners lumas
let luma_down_left = rgb2luma(textureSampleLevel(t_screen, s_screen, tex_coords, 0.0, vec2<i32>(-1, -1)).xyz);
let luma_up_right = rgb2luma(textureSampleLevel(t_screen, s_screen, tex_coords, 0.0, vec2<i32>(1, 1)).xyz);
let luma_up_left = rgb2luma(textureSampleLevel(t_screen, s_screen, tex_coords, 0.0, vec2<i32>(-1, 1)).xyz);
let luma_down_right = rgb2luma(textureSampleLevel(t_screen, s_screen, tex_coords, 0.0, vec2<i32>(1, -1)).xyz);
// Combine the four edges lumas (using intermediary variables for future computations with the same values).
let luma_down_up = luma_down + luma_up;
let luma_left_right = luma_left + luma_right;
// Same for corners
let luma_left_corners = luma_down_left + luma_up_left;
let luma_down_corners = luma_down_left + luma_down_right;
let luma_right_corners = luma_down_right + luma_up_right;
let luma_up_corners = luma_up_right + luma_up_left;
// Compute an estimation of the gradient along the horizontal and verical axis.
let edge_horizontal = abs(-2.0 * luma_left + luma_left_corners)
+ abs(-2.0 * luma_center + luma_down_up) * 2.0
+ abs(-2.0 * luma_right + luma_right_corners);
let edge_vertical = abs(-2.0 * luma_up + luma_up_corners)
+ abs(-2.0 * luma_center + luma_left_right) * 2.0
+ abs(-2.0 * luma_down + luma_down_corners);
// Is the local edge horizontal or vertical?
let is_horizontal = edge_horizontal >= edge_vertical;
// Select the two neighboring texels lumas in the opposite direction to the local edge.
let luma1 = select(luma_left, luma_down, is_horizontal);
let luma2 = select(luma_right, luma_up, is_horizontal);
// Compute gradients in this direction
let gradient1 = luma1 - luma_center;
let gradient2 = luma2 - luma_center;
// Which direction is the steepest?
let is_1_steepest = abs(gradient1) >= abs(gradient2);
// Gradient in the corresponding direction, normalized
let gradient_scaled = 0.25 * max(abs(gradient1), abs(gradient2));
// Choose the step size (one pixel) according to the edge direction.
var step_length: f32;
if (is_horizontal) {
step_length = inverse_screen_size.y;
} else {
step_length = inverse_screen_size.x;
}
// Average luma in the correct direction.
var luma_local_average = 0.0;
if (is_1_steepest) {
// Switch the direction
step_length = -step_length;
luma_local_average = 0.5 * (luma1 + luma_center);
} else {
luma_local_average = 0.5 * (luma2 + luma_center);
}
// Shift UV in the correct direction by half a pixel.
var current_uv = tex_coords;
if (is_horizontal) {
current_uv.y += step_length * 0.5;
} else {
current_uv.x += step_length * 0.5;
}
// Compute offset (for each iteration step) in the right direction.
var offset: vec2<f32>;
if (is_horizontal) {
offset = vec2<f32>(inverse_screen_size.x, 0.0);
} else {
offset = vec2<f32>(0.0, inverse_screen_size.y);
}
// Compute UVs to explore on each side of the edge, orthogonally. The QUALITY allows us to
// step faster.
var uv1 = current_uv - offset;
var uv2 = current_uv + offset;
// Read the lumas at both current extremities of the exploration segment, and compute the
// delta wrt to the local average luma.
var luma_end1 = rgb2luma(textureSampleLevel(t_screen, s_screen, uv1, 0.0).xyz);
var luma_end2 = rgb2luma(textureSampleLevel(t_screen, s_screen, uv2, 0.0).xyz);
luma_end1 -= luma_local_average;
luma_end2 -= luma_local_average;
// If the luma deltas at the current extremities are larger than the local gradient, we have
// reached the side of the edge.
var reached1 = abs(luma_end1) >= gradient_scaled;
var reached2 = abs(luma_end2) >= gradient_scaled;
var reached_both = reached1 && reached2;
// If the side is not reached, we continue to explore in this direction.
if (!reached1) {
uv1 -= offset;
}
if (!reached2) {
uv2 += offset;
}
if (!reached_both) {
for (var i = 2; i < ITERATIONS; i++) {
// If needed, read luma in 1st direction, compute delta.
if (!reached1) {
luma_end1 = rgb2luma(textureSampleLevel(t_screen, s_screen, uv1, 0.0).xyz);
luma_end1 = luma_end1 - luma_local_average;
}
// If needed, read luma in opposite direction, compute delta.
if (!reached2) {
luma_end2 = rgb2luma(textureSampleLevel(t_screen, s_screen, uv2, 0.0).xyz);
luma_end2 = luma_end2 - luma_local_average;
}
// If the luma deltas at the current extremities is larger than the local gradient, we have reached the side of the edge.
reached1 = abs(luma_end1) >= gradient_scaled;
reached2 = abs(luma_end2) >= gradient_scaled;
reached_both = reached1 && reached2;
// If the side is not reached, we continue to explore in this direction, with a variable quality.
if (!reached1) {
uv1 -= offset * QUALITY(i);
}
if (!reached2) {
uv2 += offset * QUALITY(i);
}
// If both sides have been reached, stop the exploration
if (reached_both) {
break;
}
}
}
// Compute the distances to each extremity of the edge.
var distance1 = select(tex_coords.y - uv1.y, tex_coords.x - uv1.x, is_horizontal);
var distance2 = select(uv2.y - tex_coords.y, uv2.x - tex_coords.x, is_horizontal);
// In which direction is the extremity of the edge closer?
let is_direction1 = distance1 < distance2;
let distance_final = min(distance1, distance2);
// Length of the edge.
let edge_thickness = (distance1 + distance2);
// UV offset: read in the direction of the closest side of the edge.
let pixel_offset = -distance_final / edge_thickness + 0.5;
// Is the luma at center smaller than the local average?
let is_luma_center_smaller = luma_center < luma_local_average;
// If the luma at center is smaller than at its neighbour, the delta luma at each end should
// be positive (same variation). (in the direction of the closer side of the edge.)
var direction_luma_end: f32;
if (is_direction1) {
direction_luma_end = luma_end1;
} else {
direction_luma_end = luma_end2;
}
let correct_variation = (direction_luma_end < 0.0) != is_luma_center_smaller;
// If the luma variation is incorrect, do not offset.
var final_offset = select(0.0, pixel_offset, correct_variation);
// Sub-pixel shifting
// Full weighted average of the luma over the 3x3 neighborhood.
let luma_average = (1.0 / 12.0) * (2.0 * (luma_down_up + luma_left_right) + luma_left_corners + luma_right_corners);
// Ratio of the delta between the global average and the center luma, over the luma range
// in the 3x3 neighborhood.
let sub_pixel_offset1 = clamp(abs(luma_average - luma_center) / luma_range, 0.0, 1.0);
let sub_pixel_offset2 = (-2.0 * sub_pixel_offset1 + 3.0) * sub_pixel_offset1 * sub_pixel_offset1;
// Compute a sub-pixel offset based on this delta.
let sub_pixel_offset_final = sub_pixel_offset2 * sub_pixel_offset2 * SUBPIXEL_QUALITY;
// Pick the biggest of the two offsets.
final_offset = max(final_offset, sub_pixel_offset_final);
var final_uv = tex_coords;
if (is_horizontal) {
final_uv.y += final_offset * step_length;
} else {
final_uv.x += final_offset * step_length;
}
let color = textureSampleLevel(t_screen, s_screen, final_uv, 0.0).xyz;
return vec4<f32>(color, 1.0);
}

View file

@ -1,19 +0,0 @@
#define_module lyra::shadows::bindings
#import lyra::shadows::structs::{ShadowSettingsUniform, LightShadowMapUniform}
@group(5) @binding(0)
var t_shadow_maps_atlas: texture_depth_2d;
@group(5) @binding(1)
var s_shadow_maps_atlas: sampler;
@group(5) @binding(2)
var s_shadow_maps_atlas_compare: sampler_comparison;
@group(5) @binding(3)
var<uniform> u_shadow_settings: ShadowSettingsUniform;
@group(5) @binding(4)
var<storage, read> u_light_shadow: array<LightShadowMapUniform>;
@group(5) @binding(5)
var<storage, read> u_pcf_poisson_disc: array<vec2<f32>>;
@group(5) @binding(6)
var<storage, read> u_pcf_poisson_disc_3d: array<vec3<f32>>;
@group(5) @binding(7)
var<storage, read> u_pcss_poisson_disc: array<vec2<f32>>;

View file

@ -1,352 +0,0 @@
#define_module lyra::shadows::calc
#import lyra::shadows::structs::{ShadowSettingsUniform, LightShadowMapUniform}
#import lyra::shadows::bindings::{t_shadow_maps_atlas, s_shadow_maps_atlas, s_shadow_maps_atlas_compare, u_shadow_settings, u_light_shadow, u_pcf_poisson_disc, u_pcss_poisson_disc}
/// Convert 3d coords for an unwrapped cubemap to 2d coords and a side index of the cube map.
///
/// The `xy` components are the 2d coordinates in the side of the cube, and `z` is the cube
/// map side index.
///
/// Cube map index results:
/// 0 -> UNKNOWN
/// 1 -> right
/// 2 -> left
/// 3 -> top
/// 4 -> bottom
/// 5 -> near
/// 6 -> far
fn coords_to_cube_atlas(tex_coord: vec3<f32>) -> vec3<f32> {
let abs_x = abs(tex_coord.x);
let abs_y = abs(tex_coord.y);
let abs_z = abs(tex_coord.z);
var major_axis: f32 = 0.0;
var cube_idx: i32 = 0;
var res = vec2<f32>(0.0);
// Determine the dominant axis
if (abs_x >= abs_y && abs_x >= abs_z) {
major_axis = tex_coord.x;
if (tex_coord.x > 0.0) {
cube_idx = 1;
res = vec2<f32>(-tex_coord.z, -tex_coord.y);
} else {
cube_idx = 2;
res = vec2<f32>(tex_coord.z, -tex_coord.y);
}
} else if (abs_y >= abs_x && abs_y >= abs_z) {
major_axis = tex_coord.y;
if (tex_coord.y > 0.0) {
cube_idx = 3;
res = vec2<f32>(tex_coord.x, tex_coord.z);
} else {
cube_idx = 4;
res = vec2<f32>(tex_coord.x, -tex_coord.z);
}
} else {
major_axis = tex_coord.z;
if (tex_coord.z > 0.0) {
cube_idx = 5;
res = vec2<f32>(tex_coord.x, -tex_coord.y);
} else {
cube_idx = 6;
res = vec2<f32>(-tex_coord.x, -tex_coord.y);
}
}
res = (res / abs(major_axis) + 1.0) * 0.5;
res.y = 1.0 - res.y;
return vec3<f32>(res, f32(cube_idx));
}
/// Get shadow settings for a light.
/// Returns x as `pcf_samples_num` and y as `pcss_blocker_search_samples`.
fn get_shadow_settings(shadow_u: LightShadowMapUniform) -> vec2<u32> {
if shadow_u.has_shadow_settings == 1u {
return vec2<u32>(shadow_u.pcf_samples_num, shadow_u.pcss_blocker_search_samples);
} else {
return vec2<u32>(u_shadow_settings.pcf_samples_num, u_shadow_settings.pcss_blocker_search_samples);
}
}
fn calc_shadow_dir_light(world_pos: vec3<f32>, world_normal: vec3<f32>, light_dir: vec3<f32>, light: Light) -> f32 {
let map_data: LightShadowMapUniform = u_light_shadow[light.light_shadow_uniform_index[0]];
let frag_pos_light_space = map_data.light_space_matrix * vec4<f32>(world_pos, 1.0);
var proj_coords = frag_pos_light_space.xyz / frag_pos_light_space.w;
// for some reason the y component is flipped after transforming
proj_coords.y = -proj_coords.y;
// Remap xy to [0.0, 1.0]
let xy_remapped = proj_coords.xy * 0.5 + 0.5;
// use a bias to avoid shadow acne
let current_depth = proj_coords.z - map_data.constant_depth_bias;
// get settings
let settings = get_shadow_settings(map_data);
let pcf_samples_num = settings.x;
let pcss_blocker_search_samples = settings.y;
var shadow = 0.0;
// hardware 2x2 PCF via camparison sampler
if pcf_samples_num == 2u {
let region_coords = to_atlas_frame_coords(map_data, xy_remapped, false);
shadow = textureSampleCompareLevel(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, region_coords, current_depth);
}
// PCSS
else if pcf_samples_num > 0u && pcss_blocker_search_samples > 0u {
shadow = pcss_dir_light(xy_remapped, current_depth, map_data);
}
// only PCF
else if pcf_samples_num > 0u {
let texel_size = 1.0 / f32(map_data.atlas_frame.width);
shadow = pcf_dir_light(xy_remapped, current_depth, map_data, texel_size);
}
// no filtering
else {
let region_coords = to_atlas_frame_coords(map_data, xy_remapped, false);
let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, region_coords, 0.0);
shadow = select(1.0, 0.0, current_depth > closest_depth);
}
// dont cast shadows outside the light's far plane
if (proj_coords.z > 1.0) {
shadow = 1.0;
}
// dont cast shadows if the texture coords would go past the shadow maps
if (xy_remapped.x > 1.0 || xy_remapped.x < 0.0 || xy_remapped.y > 1.0 || xy_remapped.y < 0.0) {
shadow = 1.0;
}
return shadow;
}
// Comes from https://developer.download.nvidia.com/whitepapers/2008/PCSS_Integration.pdf
fn search_width(light_near: f32, uv_light_size: f32, receiver_depth: f32) -> f32 {
return uv_light_size * (receiver_depth - light_near) / receiver_depth;
}
/// Convert texture coords to be texture coords of an atlas frame.
///
/// If `safety_offset` is true, the frame will be shrank by a tiny amount to avoid bleeding
/// into adjacent frames from fiiltering.
fn to_atlas_frame_coords(shadow_u: LightShadowMapUniform, coords: vec2<f32>, safety_offset: bool) -> vec2<f32> {
let atlas_dimensions = textureDimensions(t_shadow_maps_atlas);
// get the rect of the frame as a vec4
var region_rect = vec4<f32>(f32(shadow_u.atlas_frame.x), f32(shadow_u.atlas_frame.y),
f32(shadow_u.atlas_frame.width), f32(shadow_u.atlas_frame.height));
// put the frame rect in atlas UV space
region_rect /= f32(atlas_dimensions.x);
// if safety_offset is true, calculate a relatively tiny offset to avoid getting the end of
// the frame and causing linear or nearest filtering to bleed to the adjacent frame.
let texel_size = select(0.0, (1.0 / f32(shadow_u.atlas_frame.x)) * 4.0, safety_offset);
// lerp input coords
let region_coords = vec2<f32>(
mix(region_rect.x + texel_size, region_rect.x + region_rect.z - texel_size, coords.x),
mix(region_rect.y + texel_size, region_rect.y + region_rect.w - texel_size, coords.y)
);
return region_coords;
}
/// Find the average blocker distance for a directiona llight
fn find_blocker_distance_dir_light(tex_coords: vec2<f32>, receiver_depth: f32, bias: f32, shadow_u: LightShadowMapUniform) -> vec2<f32> {
let search_width = search_width(shadow_u.near_plane, shadow_u.light_size_uv, receiver_depth);
var blockers = 0;
var avg_dist = 0.0;
let samples = i32(u_shadow_settings.pcss_blocker_search_samples);
for (var i = 0; i < samples; i++) {
let offset_coords = tex_coords + u_pcss_poisson_disc[i] * search_width;
let new_coords = to_atlas_frame_coords(shadow_u, offset_coords, false);
let z = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, new_coords, 0.0);
if z < (receiver_depth - bias) {
blockers += 1;
avg_dist += z;
}
}
let b = f32(blockers);
return vec2<f32>(avg_dist / b, b);
}
fn pcss_dir_light(tex_coords: vec2<f32>, receiver_depth: f32, shadow_u: LightShadowMapUniform) -> f32 {
let blocker_search = find_blocker_distance_dir_light(tex_coords, receiver_depth, 0.0, shadow_u);
// If no blockers were found, exit now to save in filtering
if blocker_search.y == 0.0 {
return 1.0;
}
let blocker_depth = blocker_search.x;
// penumbra estimation
let penumbra_width = (receiver_depth - blocker_depth) / blocker_depth;
// PCF
let uv_radius = penumbra_width * shadow_u.light_size_uv * shadow_u.near_plane / receiver_depth;
return pcf_dir_light(tex_coords, receiver_depth, shadow_u, uv_radius);
}
/// Calculate the shadow coefficient using PCF of a directional light
fn pcf_dir_light(tex_coords: vec2<f32>, test_depth: f32, shadow_u: LightShadowMapUniform, uv_radius: f32) -> f32 {
var shadow = 0.0;
let samples_num = i32(u_shadow_settings.pcf_samples_num);
for (var i = 0; i < samples_num; i++) {
let offset = tex_coords + u_pcf_poisson_disc[i] * uv_radius;
let new_coords = to_atlas_frame_coords(shadow_u, offset, false);
shadow += textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, new_coords, test_depth);
}
shadow /= f32(samples_num);
// clamp shadow to [0; 1]
return saturate(shadow);
}
fn calc_shadow_point_light(world_pos: vec3<f32>, world_normal: vec3<f32>, light_dir: vec3<f32>, light: Light) -> f32 {
var frag_to_light = world_pos - light.position;
let temp = coords_to_cube_atlas(normalize(frag_to_light));
var coords_2d = temp.xy;
let cube_idx = i32(temp.z);
var indices = light.light_shadow_uniform_index;
let i = indices[cube_idx - 1];
let u: LightShadowMapUniform = u_light_shadow[i];
let uniforms = array<LightShadowMapUniform, 6>(
u_light_shadow[indices[0]],
u_light_shadow[indices[1]],
u_light_shadow[indices[2]],
u_light_shadow[indices[3]],
u_light_shadow[indices[4]],
u_light_shadow[indices[5]]
);
var current_depth = length(frag_to_light);
current_depth /= u.far_plane;
current_depth -= u.constant_depth_bias;
// get settings
let settings = get_shadow_settings(u);
let pcf_samples_num = settings.x;
let pcss_blocker_search_samples = settings.y;
var shadow = 0.0;
// hardware 2x2 PCF via camparison sampler
if pcf_samples_num == 2u {
let region_coords = to_atlas_frame_coords(u, coords_2d, true);
shadow = textureSampleCompareLevel(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, region_coords, current_depth);
}
// PCSS
else if pcf_samples_num > 0u && pcss_blocker_search_samples > 0u {
shadow = pcss_dir_light(coords_2d, current_depth, u);
}
// only PCF
else if pcf_samples_num > 0u {
let texel_size = 1.0 / f32(u.atlas_frame.width);
shadow = pcf_point_light(frag_to_light, current_depth, uniforms, pcf_samples_num, 0.007);
//shadow = pcf_point_light(coords_2d, current_depth, u, pcf_samples_num, texel_size);
}
// no filtering
else {
let region_coords = to_atlas_frame_coords(u, coords_2d, true);
let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, region_coords, 0.0);
shadow = select(1.0, 0.0, current_depth > closest_depth);
}
return shadow;
}
/// Calculate the shadow coefficient using PCF of a directional light
fn pcf_point_light(tex_coords: vec3<f32>, test_depth: f32, shadow_us: array<LightShadowMapUniform, 6>, samples_num: u32, uv_radius: f32) -> f32 {
var shadow_unis = shadow_us;
var shadow = 0.0;
for (var i = 0; i < i32(samples_num); i++) {
var temp = coords_to_cube_atlas(tex_coords);
var coords_2d = temp.xy;
var cube_idx = i32(temp.z);
var shadow_u = shadow_unis[cube_idx - 1];
coords_2d += u_pcf_poisson_disc[i] * uv_radius;
let new_coords = to_atlas_frame_coords(shadow_u, coords_2d, true);
shadow += textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, new_coords, test_depth);
}
shadow /= f32(samples_num);
// clamp shadow to [0; 1]
return saturate(shadow);
}
fn calc_shadow_spot_light(world_pos: vec3<f32>, world_normal: vec3<f32>, light_dir: vec3<f32>, light: Light) -> f32 {
let map_data: LightShadowMapUniform = u_light_shadow[light.light_shadow_uniform_index[0]];
let frag_pos_light_space = map_data.light_space_matrix * vec4<f32>(world_pos, 1.0);
var proj_coords = frag_pos_light_space.xyz / frag_pos_light_space.w;
// for some reason the y component is flipped after transforming
proj_coords.y = -proj_coords.y;
// Remap xy to [0.0, 1.0]
let xy_remapped = proj_coords.xy * 0.5 + 0.5;
// use a bias to avoid shadow acne
let current_depth = proj_coords.z - map_data.constant_depth_bias;
// get settings
let settings = get_shadow_settings(map_data);
let pcf_samples_num = settings.x;
let pcss_blocker_search_samples = settings.y;
var shadow = 0.0;
// hardware 2x2 PCF via camparison sampler
if pcf_samples_num == 2u {
let region_coords = to_atlas_frame_coords(map_data, xy_remapped, false);
shadow = textureSampleCompareLevel(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, region_coords, current_depth);
}
// only PCF is supported for spot lights
else if pcf_samples_num > 0u {
let texel_size = 1.0 / f32(map_data.atlas_frame.width);
shadow = pcf_spot_light(xy_remapped, current_depth, map_data, i32(pcf_samples_num), texel_size);
}
// no filtering
else {
let region_coords = to_atlas_frame_coords(map_data, xy_remapped, false);
let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, region_coords, 0.0);
shadow = select(1.0, 0.0, current_depth > closest_depth);
}
// dont cast shadows outside the light's far plane
if (proj_coords.z > 1.0) {
shadow = 1.0;
}
// dont cast shadows if the texture coords would go past the shadow maps
if (xy_remapped.x > 1.0 || xy_remapped.x < 0.0 || xy_remapped.y > 1.0 || xy_remapped.y < 0.0) {
shadow = 1.0;
}
return shadow;
}
/// Calculate the shadow coefficient using PCF of a directional light
fn pcf_spot_light(tex_coords: vec2<f32>, test_depth: f32, shadow_u: LightShadowMapUniform, samples_num: i32, uv_radius: f32) -> f32 {
var shadow = 0.0;
for (var i = 0; i < samples_num; i++) {
let offset = tex_coords + u_pcf_poisson_disc[i] * uv_radius;
let new_coords = to_atlas_frame_coords(shadow_u, offset, false);
shadow += textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, new_coords, test_depth);
}
shadow /= f32(samples_num);
// clamp shadow to [0; 1]
return saturate(shadow);
}

View file

@ -1,48 +0,0 @@
#define_module lyra::shadows::depth_pass
#import lyra::shadows::structs::{LightShadowMapUniform}
struct TransformData {
transform: mat4x4<f32>,
normal_matrix: mat4x4<f32>,
}
@group(0) @binding(0)
var<storage, read> u_light_shadow: array<LightShadowMapUniform>;
@group(1) @binding(0)
var<uniform> u_model_transform_data: TransformData;
struct VertexOutput {
@builtin(position)
clip_position: vec4<f32>,
@location(0) world_pos: vec3<f32>,
@location(1) instance_index: u32,
}
@vertex
fn vs_main(
@location(0) position: vec3<f32>,
@builtin(instance_index) instance_index: u32,
) -> VertexOutput {
let world_pos = u_model_transform_data.transform * vec4<f32>(position, 1.0);
let pos = u_light_shadow[instance_index].light_space_matrix * world_pos;
return VertexOutput(pos, world_pos.xyz, instance_index);
}
struct FragmentOutput {
@builtin(frag_depth) depth: f32,
}
/// Fragment shader used for point lights (or other perspective lights) to create linear depth
@fragment
fn fs_point_light_main(
in: VertexOutput
) -> FragmentOutput {
let u = u_light_shadow[in.instance_index];
var light_dis = length(in.world_pos - u.light_pos);
// map to [0; 1] range by dividing by far plane
light_dis = light_dis / u.far_plane;
return FragmentOutput(light_dis);
}

View file

@ -1,29 +0,0 @@
#define_module lyra::shadows::structs
struct TextureAtlasFrame {
/*offset: vec2<u32>,
size: vec2<u32>,*/
x: u32,
y: u32,
width: u32,
height: u32,
}
struct LightShadowMapUniform {
light_space_matrix: mat4x4<f32>,
atlas_frame: TextureAtlasFrame,
near_plane: f32,
far_plane: f32,
light_size_uv: f32,
light_pos: vec3<f32>,
/// boolean casted as u32
has_shadow_settings: u32,
pcf_samples_num: u32,
pcss_blocker_search_samples: u32,
constant_depth_bias: f32,
}
struct ShadowSettingsUniform {
pcf_samples_num: u32,
pcss_blocker_search_samples: u32,
}

View file

@ -1,32 +0,0 @@
@group(0) @binding(0)
var t_screen: texture_2d<f32>;
@group(0) @binding(1)
var s_screen: sampler;
struct VertexOutput {
@builtin(position)
clip_position: vec4<f32>,
@location(0)
tex_coords: vec2<f32>,
}
@vertex
fn vs_main(
@builtin(vertex_index) vertex_index: u32,
) -> VertexOutput {
let tex_coords = vec2<f32>(f32(vertex_index >> 1u), f32(vertex_index & 1u)) * 2.0;
let clip_position = vec4<f32>(tex_coords * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0), 0.0, 1.0);
return VertexOutput(clip_position, tex_coords);
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let resolution = vec2<f32>(textureDimensions(t_screen));
let inverse_screen_size = 1.0 / resolution.xy;
let tex_coords = in.clip_position.xy * inverse_screen_size;
var rgb: vec3<f32> = textureSample(t_screen, s_screen, tex_coords).xyz;
rgb *= vec3<f32>(1.0, 0.2, 0.2);
return vec4<f32>(rgb, 1.0);
}

View file

@ -1,149 +0,0 @@
use std::{collections::VecDeque, marker::PhantomData, mem, sync::Arc};
/// A buffer on the GPU that has persistent indices.
///
/// `GpuSlotBuffer` allocates a buffer on the GPU and keeps stable indices of elements and
/// reuses ones that were removed. It supports aligned buffers with [`GpuSlotBuffer::new_aligned`],
/// as well as unaligned buffers with [`GpuSlotBuffer::new`].
pub struct GpuSlotBuffer<T: bytemuck::Pod + bytemuck::Zeroable> {
/// The amount of elements that can fit in the buffer.
capacity: u64,
/// The ending point of the buffer elements.
len: u64,
/// The list of dead and reusable indices in the buffer.
dead_indices: VecDeque<u64>,
/// The optional alignment of elements in the buffer.
alignment: Option<u64>,
/// The actual gpu buffer
buffer: Arc<wgpu::Buffer>,
_marker: PhantomData<T>,
}
impl<T: bytemuck::Pod + bytemuck::Zeroable> GpuSlotBuffer<T> {
/// Create a new GpuSlotBuffer with unaligned elements.
///
/// See [`GpuSlotBuffer::new_aligned`].
pub fn new(device: &wgpu::Device, label: Option<&str>, usage: wgpu::BufferUsages, capacity: u64) -> Self {
Self::new_impl(device, label, usage, capacity, None)
}
/// Create a new buffer with **aligned** elements.
///
/// See [`GpuSlotBuffer::new`].
pub fn new_aligned(device: &wgpu::Device, label: Option<&str>, usage: wgpu::BufferUsages, capacity: u64, alignment: u64) -> Self {
Self::new_impl(device, label, usage, capacity, Some(alignment))
}
fn new_impl(device: &wgpu::Device, label: Option<&str>, usage: wgpu::BufferUsages, capacity: u64, alignment: Option<u64>) -> Self {
let buffer = Arc::new(device.create_buffer(&wgpu::BufferDescriptor {
label,
size: capacity * mem::size_of::<T>() as u64,
usage,
mapped_at_creation: false,
}));
Self {
capacity,
len: 0,
dead_indices: VecDeque::default(),
buffer,
alignment,
_marker: PhantomData
}
}
/// Calculates the byte offset in the buffer of the element at `i`.
pub fn offset_of(&self, i: u64) -> u64 {
if let Some(align) = self.alignment {
let transform_index = i % self.capacity;
transform_index * align
} else {
i * mem::size_of::<T>() as u64
}
}
/// Set an element at `i` in the buffer to `val`.
pub fn set_at(&self, queue: &wgpu::Queue, i: u64, val: &T) {
let offset = self.offset_of(i);
queue.write_buffer(&self.buffer, offset, bytemuck::bytes_of(val));
}
/// Attempt to insert an element to the GPU buffer, returning the index it was inserted at.
///
/// Returns `None` when the buffer has no space to fit the element.
pub fn try_insert(&mut self, queue: &wgpu::Queue, val: &T) -> Option<u64> {
// reuse a dead index or get the next one
let i = match self.dead_indices.pop_front() {
Some(i) => i,
None => {
if self.len == self.capacity {
return None;
}
let i = self.len;
self.len += 1;
i
}
};
self.set_at(queue, i, val);
Some(i)
}
/// Insert an element to the GPU buffer, returning the index it was inserted at.
///
/// The index is not guaranteed to be the end of the buffer since this structure reuses
/// indices after they're removed.
///
/// # Panics
/// Panics if the buffer does not have space to fit `val`, see [`GpuSlotBuffer::try_insert`].
pub fn insert(&mut self, queue: &wgpu::Queue, val: &T) -> u64 {
self.try_insert(queue, val)
.expect("GPU slot buffer ran out of slots to push elements into")
}
/// Remove the element at `i`, clearing the elements slot in the buffer.
///
/// If you do not care that the slot in the buffer is emptied, use
/// [`GpuSlotBuffer::remove_quick`].
pub fn remove(&mut self, queue: &wgpu::Queue, i: u64) {
let mut zeros = Vec::new();
zeros.resize(mem::size_of::<T>(), 0);
let offset = self.offset_of(i);
queue.write_buffer(&self.buffer, offset, bytemuck::cast_slice(zeros.as_slice()));
self.dead_indices.push_back(i);
}
/// Remove the element at `i` without clearing its space in the buffer.
///
/// If you want to ensure that the slot in the buffer is emptied, use
/// [`GpuSlotBuffer::remove`].
pub fn remove_quick(&mut self, i: u64) {
self.dead_indices.push_back(i);
}
/// Returns the backing [`wgpu::Buffer`].
pub fn buffer(&self) -> &Arc<wgpu::Buffer> {
&self.buffer
}
/// Return the length of the buffer.
///
/// This value may not reflect the amount of elements that are actually alive in the buffer if
/// elements were removed and not re-added.
pub fn len(&self) -> u64 {
self.len
}
/// Return the amount of inuse indices in the buffer.
pub fn inuse_len(&self) -> u64 {
self.len - self.dead_indices.len() as u64
}
/// Returns the amount of elements the buffer can fit.
pub fn capacity(&self) -> u64 {
self.capacity
}
}

View file

@ -1,297 +0,0 @@
use std::{
cmp::max, collections::HashMap, sync::Arc
};
use glam::UVec2;
#[derive(Debug, thiserror::Error)]
pub enum AtlasPackError {
/// The rectangles can't be placed into the atlas. The atlas must increase in size
#[error("There is not enough space in the atlas for the textures")]
NotEnoughSpace,
}
#[repr(C)]
#[derive(Debug, Default, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct AtlasFrame {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl AtlasFrame {
pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
Self {
x, y, width, height
}
}
}
pub struct PackedTextureAtlas<P: AtlasPacker = SkylinePacker> {
atlas_size: UVec2,
texture_format: wgpu::TextureFormat,
texture: Arc<wgpu::Texture>,
view: Arc<wgpu::TextureView>,
packer: P,
}
impl<P: AtlasPacker> PackedTextureAtlas<P> {
pub fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
usages: wgpu::TextureUsages,
atlas_size: UVec2,
) -> Self {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("texture_atlas"),
size: wgpu::Extent3d {
width: atlas_size.x,
height: atlas_size.y,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: usages,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
Self {
atlas_size,
texture_format: format,
texture: Arc::new(texture),
view: Arc::new(view),
packer: P::new(atlas_size),
}
}
/// Add a texture of `size` and pack it into the atlas, returning the id of the texture in
/// the atlas.
///
/// If you are adding multiple textures at a time and want to wait to pack the atlas, use
/// [`TextureAtlas::add_texture_unpacked`] and then after you're done adding them, pack them
/// with [`TextureAtlas::pack_atlas`].
pub fn pack(&mut self, width: u32, height: u32) -> Result<u64, AtlasPackError> {
let id = self.packer.pack(width, height)?;
Ok(id as u64)
}
/// Get the viewport of a texture index in the atlas.
pub fn texture_frame(&self, atlas_index: u64) -> Option<AtlasFrame> {
self.packer.frame(atlas_index as _)
}
pub fn view(&self) -> &Arc<wgpu::TextureView> {
&self.view
}
pub fn texture(&self) -> &Arc<wgpu::Texture> {
&self.texture
}
pub fn texture_format(&self) -> &wgpu::TextureFormat {
&self.texture_format
}
/// Returns the size of the entire texture atlas.
pub fn atlas_size(&self) -> UVec2 {
self.atlas_size
}
}
pub trait AtlasPacker {
fn new(size: UVec2) -> Self;
/// Get an [`AtlasFrame`] of a texture with `id`.
fn frame(&self, id: usize) -> Option<AtlasFrame>;
/// Get all [`AtlasFrame`]s in the atlas.
fn frames(&self) -> &HashMap<usize, AtlasFrame>;
/// Pack a new rect into the atlas.
fn pack(&mut self, width: u32, height: u32) -> Result<usize, AtlasPackError>;
}
struct Skyline {
/// Starting x of the skyline
x: usize,
/// Starting y of the skyline
y: usize,
/// Width of the skyline
width: usize,
}
impl Skyline {
fn right(&self) -> usize {
self.x + self.width
}
}
pub struct SkylinePacker {
size: UVec2,
skylines: Vec<Skyline>,
frame_idx: usize,
frames: HashMap<usize, AtlasFrame>,
}
impl SkylinePacker {
pub fn new(size: UVec2) -> Self {
let skylines = vec![Skyline {
x: 0,
y: 0,
width: size.x as _,
}];
Self {
size,
skylines,
frame_idx: 0,
frames: Default::default(),
}
}
fn can_add(&self, mut i: usize, w: u32, h: u32) -> Option<usize> {
let x = self.skylines[i].x as u32;
if x + w > self.size.x {
return None;
}
let mut width_left = w;
let mut y = self.skylines[i].y as u32;
loop {
y = max(y, self.skylines[i].y as u32);
if y + h > self.size.y {
return None;
}
if self.skylines[i].width as u32 >= width_left {
return Some(y as usize);
}
width_left -= self.skylines[i].width as u32;
i += 1;
if i >= self.skylines.len() {
return None;
}
}
}
fn find_skyline(&self, width: u32, height: u32) -> Option<(usize, AtlasFrame)> {
let mut min_height = std::u32::MAX;
let mut min_width = std::u32::MAX;
let mut index = None;
let mut frame = AtlasFrame::default();
// keep the min height as small as possible
for i in 0..self.skylines.len() {
if let Some(y) = self.can_add(i, width, height) {
let y = y as u32;
/* if r.bottom() < min_height
|| (r.bottom() == min_height && self.skylines[i].width < min_width as usize) */
if y + height < min_height ||
(y + height == min_height && self.skylines[i].width < min_width as usize)
{
min_height = y + height;
min_width = self.skylines[i].width as _;
index = Some(i);
frame = AtlasFrame::new(self.skylines[i].x as _, y, width, height);
}
}
// TODO: rotation
}
if let Some(index) = index {
Some((index, frame))
} else {
None
}
}
fn split(&mut self, i: usize, frame: &AtlasFrame) {
let skyline = Skyline {
x: frame.x as _,
y: (frame.y + frame.height) as _,
width: frame.width as _
};
assert!(skyline.right() <= self.size.x as usize);
assert!(skyline.y <= self.size.y as usize);
self.skylines.insert(i, skyline);
let i = i + 1;
while i < self.skylines.len() {
assert!(self.skylines[i - 1].x <= self.skylines[i].x);
if self.skylines[i].x < self.skylines[i - 1].x + self.skylines[i - 1].width {
let shrink = self.skylines[i-1].x + self.skylines[i-1].width - self.skylines[i].x;
if self.skylines[i].width <= shrink {
self.skylines.remove(i);
} else {
self.skylines[i].x += shrink;
self.skylines[i].width -= shrink;
break;
}
} else {
break;
}
}
}
/// Merge skylines with the same y value
fn merge(&mut self) {
let mut i = 1;
while i < self.skylines.len() {
if self.skylines[i - 1].y == self.skylines[i].y {
self.skylines[i - 1].width += self.skylines[i].width;
self.skylines.remove(i);
} else {
i += 1;
}
}
}
//pub fn pack(&mut self, )
}
impl AtlasPacker for SkylinePacker {
fn new(size: UVec2) -> Self {
SkylinePacker::new(size)
}
fn frame(&self, id: usize) -> Option<AtlasFrame> {
self.frames.get(&id).cloned()
}
fn frames(&self) -> &HashMap<usize, AtlasFrame> {
&self.frames
}
fn pack(&mut self, width: u32, height: u32) -> Result<usize, AtlasPackError> {
if let Some((i, frame)) = self.find_skyline(width, height) {
self.split(i, &frame);
self.merge();
let frame_idx = self.frame_idx;
self.frame_idx += 1;
self.frames.insert(frame_idx, frame);
Ok(frame_idx)
} else {
Err(AtlasPackError::NotEnoughSpace)
}
}
}

View file

@ -1,94 +0,0 @@
use super::desc_buf_lay::DescVertexBufferLayout;
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Vertex {
pub position: glam::Vec3,
pub tex_coords: glam::Vec2,
pub normals: glam::Vec3,
}
impl Vertex {
pub fn new(position: glam::Vec3, tex_coords: glam::Vec2, normals: glam::Vec3) -> Self {
Self {
position, tex_coords, normals
}
}
/// Returns a [`wgpu::VertexBufferLayout`] with only the position as a vertex attribute.
///
/// The stride is still `std::mem::size_of::<Vertex>()`, but only position is included.
pub fn position_desc<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3, // Vec3
},
]
}
}
}
impl DescVertexBufferLayout for Vertex {
fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3, // Vec3
},
wgpu::VertexAttribute {
offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
shader_location: 1,
format: wgpu::VertexFormat::Float32x2, // Vec2
},
wgpu::VertexAttribute {
offset: std::mem::size_of::<[f32; 5]>() as wgpu::BufferAddress,
shader_location: 2,
format: wgpu::VertexFormat::Float32x3, // Vec3
}
]
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Vertex2D {
pub position: glam::Vec3,
pub tex_coords: glam::Vec2,
}
impl Vertex2D {
pub fn new(position: glam::Vec3, tex_coords: glam::Vec2) -> Self {
Self {
position, tex_coords
}
}
pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex2D>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3, // Vec3
},
wgpu::VertexAttribute {
offset: std::mem::size_of::<glam::Vec3>() as wgpu::BufferAddress,
shader_location: 1,
format: wgpu::VertexFormat::Float32x2, // Vec2
},
]
}
}
}

View file

@ -1,215 +0,0 @@
use glam::{Mat4, Vec2, Vec3};
use lyra_ecs::{Bundle, Component};
use lyra_math::{Angle, Rect};
use lyra_reflect::Reflect;
/// The axis to sort render jobs by.
///
/// By default, [`Camera2dBundle`] sets the sorting axis to the positive Y axis.
#[derive(Default, Debug, Clone, Copy, PartialEq, Reflect, Component)]
pub struct CameraSortingAxis(pub Vec3);
/// The offset to apply to the entity's position when sorting for rendering.
///
/// This is only used if [`CameraSortingAxis`] is spawned on an entity.
#[derive(Default, Debug, Clone, Copy, PartialEq, Reflect, Component)]
pub struct SortingOffset(pub Vec3);
/// The method of scaling for an [`OrthographicProjection`].
#[derive(Debug, Clone, Copy, PartialEq, Reflect)]
pub enum ScaleMode {
/// No scaling, keep everything the same size as the viewport.
Viewport,
/// The width of the projection in world units.
///
/// Height will be set based off of the viewport's aspect ratio.
Width(f32),
/// The height of the projection in world units.
///
/// Width will be set based off of the viewport's aspect ratio.
Height(f32),
/// The exact size of the projection in world units.
///
/// Ignoring viewport size, likely causes stretching.
Size(Vec2),
/// Keep the projection size below a maximum value in world units, while keeping the aspect ratio.
MaxSize(Vec2),
/// Keep the projection size above a minimum value in world units, while keeping the aspect ratio.
MinSize(Vec2),
}
impl Default for ScaleMode {
fn default() -> Self {
ScaleMode::Viewport
}
}
#[derive(Debug, Clone, Copy, PartialEq, Reflect)]
pub struct OrthographicProjection {
pub viewport_origin: Vec2,
pub scale_mode: ScaleMode,
pub scale: f32,
pub znear: f32,
pub zfar: f32,
}
impl Default for OrthographicProjection {
fn default() -> Self {
Self {
viewport_origin: Vec2::new(0.5, 0.5),
scale_mode: Default::default(),
scale: 1.0,
znear: 0.0,
zfar: 1000.0,
}
}
}
impl OrthographicProjection {
fn get_rect(&self, viewport_size: Vec2) -> Rect {
//let origin;
let size;
match self.scale_mode {
ScaleMode::Viewport => {
size = viewport_size;
},
ScaleMode::Width(width) => {
let aspect = viewport_size.x / viewport_size.y;
let scaled_height = width / aspect;
size = Vec2::new(width, scaled_height);
},
ScaleMode::Height(height) => {
let aspect = viewport_size.x / viewport_size.y;
let scaled_width = height * aspect;
size = Vec2::new(scaled_width, height);
},
ScaleMode::Size(s) => {
size = s;
},
ScaleMode::MaxSize(s) => {
let clamped = s.min(viewport_size);
size = clamped;
},
ScaleMode::MinSize(s) => {
let clamped = s.max(viewport_size);
size = clamped;
}
}
let origin = size * self.viewport_origin;
Rect::new(self.scale * -origin.x,
self.scale * -origin.y,
self.scale * (size.x - origin.x),
self.scale * (size.y - origin.y))
}
pub fn to_mat(&self, viewport_size: Vec2) -> Mat4 {
let rect = self.get_rect(viewport_size);
glam::Mat4::orthographic_rh(
rect.min.x,
rect.max.x,
rect.min.y,
rect.max.y,
-1000.0,
1000.0,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Reflect)]
pub struct PerspectiveProjection {
pub fov: Angle,
pub znear: f32,
pub zfar: f32,
}
impl Default for PerspectiveProjection {
fn default() -> Self {
Self {
fov: Angle::Degrees(45.0),
znear: 0.1,
zfar: 100.0,
}
}
}
impl PerspectiveProjection {
pub fn to_mat(&self, viewport_size: Vec2) -> Mat4 {
let aspect = viewport_size.x / viewport_size.y;
glam::Mat4::perspective_rh(self.fov.to_radians(), aspect, self.znear, self.zfar)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Reflect, Component)]
pub enum CameraProjection {
/// 3d camera projection
Perspective(PerspectiveProjection),
/// 2d camera projection
Orthographic(OrthographicProjection),
}
impl Default for CameraProjection {
fn default() -> Self {
Self::Perspective(PerspectiveProjection::default())
}
}
impl CameraProjection {
pub fn to_mat4(&self, viewport_size: Vec2) -> Mat4 {
match self {
CameraProjection::Perspective(proj) => proj.to_mat(viewport_size),
CameraProjection::Orthographic(proj) => proj.to_mat(viewport_size),
}
}
}
#[derive(Clone, Default, Component, Reflect)]
pub struct Camera2d;
#[derive(Clone, Component, Reflect)]
pub struct Camera {
pub is_active: bool,
}
impl Default for Camera {
fn default() -> Self {
Self { is_active: true }
}
}
/// A component bundle for a Camera entity.
#[derive(Clone, Default, Bundle)]
pub struct CameraBundle {
pub camera: Camera,
pub projection: CameraProjection,
pub sorting_axis: CameraSortingAxis
}
/// A component bundle for a 2d Camera entity.
#[derive(Clone, Bundle)]
pub struct Camera2dBundle {
pub camera: Camera,
pub projection: CameraProjection,
pub camera_2d: Camera2d,
pub sorting_axis: CameraSortingAxis
}
impl Default for Camera2dBundle {
fn default() -> Self {
Self {
camera: Default::default(),
projection: CameraProjection::Orthographic(OrthographicProjection {
znear: -1000.0,
zfar: 1000.0,
..Default::default()
}),
camera_2d: Default::default(),
sorting_axis: CameraSortingAxis(Vec3::Y),
}
}
}

View file

@ -1,10 +0,0 @@
mod camera;
pub use camera::*;
mod free_fly_camera;
pub use free_fly_camera::*;
mod top_down_controller;
pub use top_down_controller::*;
pub use lyra_scene::*;

View file

@ -1,151 +0,0 @@
use std::ops::DerefMut;
use glam::Vec3;
use lyra_ecs::{
query::{Res, View},
Component,
};
use lyra_math::Transform;
use lyra_reflect::Reflect;
use crate::{
game::App,
input::{ActionHandler, ActionLabelValue},
plugin::Plugin,
DeltaTime,
};
use super::{
CameraProjection, ACTLBL_MOVE_FORWARD_BACKWARD, ACTLBL_MOVE_LEFT_RIGHT, ACTLBL_MOVE_UP_DOWN,
};
/// Defines the action label inputs for the [`TopDown2dCamera`] controller.
///
/// The modifier for each control is multiplied by whatever the modifier is of the action.
/// So if your input is reversed, set the modifier to `-1.0`.
///
/// ```
/// TopDown2dCameraControls {
/// forward_backward_label: ActionLabelValue::new(ACTLBL_MOVE_FORWARD_BACKWARD),
/// forward_backward_modifier: 1.0,
/// left_right_label: ActionLabelValue::new(ACTLBL_MOVE_LEFT_RIGHT),
/// left_right_modifier: 1.0,
/// up_down_label: ActionLabelValue::new(ACTLBL_MOVE_UP_DOWN),
/// up_down_modifier: 1.0,
/// }
/// ```
#[derive(Clone, Reflect)]
pub struct TopDown2dCameraControls {
forward_backward_label: ActionLabelValue,
forward_backward_modifier: f32,
left_right_label: ActionLabelValue,
left_right_modifier: f32,
up_down_label: ActionLabelValue,
up_down_modifier: f32,
}
#[derive(Clone, Component, Reflect)]
pub struct TopDown2dCamera {
pub speed: f32,
/// The zoom speed of the camera, set to `None` to disable zooming.
pub zoom_speed: Option<f32>,
/// The smallest scale of the orthographic projection view.
///
/// When the scale of the projection approaches zero, the more zoomed in the camera
/// will appear.
pub max_zoom: f32,
pub min_zoom: f32,
/// The controls for the camera.
///
/// Default value is
/// ```
/// TopDown2dCameraControls {
/// forward_backward_label: ActionLabelValue::new(ACTLBL_MOVE_FORWARD_BACKWARD),
/// forward_backward_modifier: 1.0,
/// left_right_label: ActionLabelValue::new(ACTLBL_MOVE_LEFT_RIGHT),
/// left_right_modifier: 1.0,
/// up_down_label: ActionLabelValue::new(ACTLBL_MOVE_UP_DOWN),
/// up_down_modifier: 1.0,
/// }
/// ```
pub controls: TopDown2dCameraControls,
}
impl Default for TopDown2dCamera {
fn default() -> Self {
Self {
speed: 5.0,
zoom_speed: None,
max_zoom: 0.36,
min_zoom: 1.0,
controls: TopDown2dCameraControls {
forward_backward_label: ActionLabelValue::new(ACTLBL_MOVE_FORWARD_BACKWARD),
forward_backward_modifier: 1.0,
left_right_label: ActionLabelValue::new(ACTLBL_MOVE_LEFT_RIGHT),
left_right_modifier: 1.0,
up_down_label: ActionLabelValue::new(ACTLBL_MOVE_UP_DOWN),
up_down_modifier: 1.0,
},
}
}
}
pub fn top_down_2d_camera_controller(
delta_time: Res<DeltaTime>,
handler: Res<ActionHandler>,
view: View<(&mut Transform, &mut CameraProjection, &TopDown2dCamera)>,
) -> anyhow::Result<()> {
let delta_time = **delta_time;
for (mut transform, mut proj, controller) in view.into_iter() {
let left = transform.left();
let up = Vec3::Y;
let move_y = handler
.get_axis_modifier(controller.controls.forward_backward_label)
.unwrap_or(0.0)
* controller.controls.forward_backward_modifier;
let move_x = handler
.get_axis_modifier(controller.controls.left_right_label)
.unwrap_or(0.0)
* controller.controls.left_right_modifier;
let move_z = handler
.get_axis_modifier(controller.controls.up_down_label)
.map(|m| m * controller.controls.up_down_modifier);
let mut velocity = Vec3::ZERO;
velocity += move_y * up;
velocity += move_x * left;
if let (Some(zoom_speed), Some(move_z)) = (controller.zoom_speed, move_z) {
match proj.deref_mut() {
CameraProjection::Orthographic(ortho) => {
let m = move_z * zoom_speed * delta_time;
ortho.scale = (ortho.scale + m).clamp(controller.max_zoom, controller.min_zoom);
}
_ => {}
}
}
if velocity != Vec3::ZERO {
transform.translation += velocity.normalize() * controller.speed * delta_time;
}
}
Ok(())
}
/// A plugin that adds the top down 2d camera controller system to the world.
///
/// It is expected that there is a [`TopDown2dCamera`] in the world, if there isn't,
/// the camera would not move.
pub struct TopDown2dCameraPlugin;
impl Plugin for TopDown2dCameraPlugin {
fn setup(&mut self, app: &mut App) {
app.with_system(
"top_down_2d_camera_system",
top_down_2d_camera_controller,
&[],
);
}
}

View file

@ -1,325 +0,0 @@
use std::cell::RefMut;
use std::collections::HashMap;
use lyra_ecs::query::filter::Or;
use lyra_ecs::query::{Entities, Res, View};
use lyra_ecs::{Commands, Component, Entity};
use lyra_math::URect;
use lyra_reflect::Reflect;
use lyra_resource::{ResHandle, ResourceStorage};
use tracing::error;
use crate::DeltaTime;
use super::{AtlasSprite, Pivot, TextureAtlas};
/// A struct describing an animation of a Sprite.
///
/// This is a single animation for a [`TextureAtlas`]. This is used alongside [`AtlasAnimations`]
/// to use animations from an atlas.
#[derive(Clone, Component, Reflect)]
pub struct SpriteAnimation {
/// The name of the animation.
pub name: String,
/// The frames of the animation.
pub frames: Vec<URect>,
/// The length of time a frame is displayed.
pub frame_time: f32,
/// Should the animation loop after its completed.
pub auto_loop: bool,
}
impl SpriteAnimation {
/// Create an animation from a texture atlas.
///
/// Parameters:
/// * `name` - The name of the animation. Used to identify the animation in [`AtlasAnimations`].
/// * `frame_time` - The time per frame of the animation.
/// * `atlas` - The texture atlas that this animation is from, used to acquire `self.frames`.
/// * `sprites` are the rect indexes in the atlas for this animation.
pub fn from_atlas<I>(name: &str, frame_time: f32, atlas: &TextureAtlas, auto_loop: bool, sprites: I) -> Self
where
I: Iterator<Item = u32>,
{
let mut frames = vec![]; //Vec::with_capacity(sprites.len());
for i in sprites {
let r = atlas.frames.get(i as usize).cloned().unwrap();
frames.push(r);
}
Self {
name: name.into(),
frames,
frame_time,
auto_loop,
}
}
}
/// A helper trait that makes it easier to create the animations for an [`AtlasAnimations`] component.
///
/// See [`AtlasAnimations::new`].
pub trait IntoSpriteAnimation {
fn into_animation(&self, atlas: &TextureAtlas) -> SpriteAnimation;
}
impl IntoSpriteAnimation for SpriteAnimation {
fn into_animation(&self, _: &TextureAtlas) -> SpriteAnimation {
self.clone()
}
}
impl<'a, I: Iterator<Item = u32> + Clone> IntoSpriteAnimation for (&'a str, f32, I) {
fn into_animation(&self, atlas: &TextureAtlas) -> SpriteAnimation {
SpriteAnimation::from_atlas(self.0, self.1, atlas, true, self.2.clone())
}
}
impl<'a, I: Iterator<Item = u32> + Clone> IntoSpriteAnimation for (&'a str, f32, bool, I) {
fn into_animation(&self, atlas: &TextureAtlas) -> SpriteAnimation {
SpriteAnimation::from_atlas(self.0, self.1, atlas, self.2, self.3.clone())
}
}
#[derive(Clone, Component, Reflect)]
pub struct AtlasAnimations {
/// The texture atlas to get the animations from.
pub atlas: ResHandle<TextureAtlas>,
/// Animations in the atlas.
pub animations: HashMap<String, SpriteAnimation>,
pub sprite_pivot: Pivot,
}
impl AtlasAnimations {
pub fn from_animations(
atlas: ResHandle<TextureAtlas>,
animations: Vec<SpriteAnimation>,
sprite_pivot: Pivot
) -> Self {
let animations = animations
.into_iter()
.map(|a| (a.name.clone(), a))
.collect::<HashMap<_, _>>();
Self { atlas, animations, sprite_pivot }
}
/// Helper for creating [`AtlasAnimations`].
///
/// If you already have the [`SpriteAnimation`]s, you can just use
/// [`AtlasAnimations::from_animations`] instead of this helper function.
///
/// Example:
/// ```
/// let animations = AtlasAnimations::new(atlas, &[
/// // This slice accepts anything that implements `IntoSpriteAnimation`:
/// // * tuple of (name: &str, frame_time: f32, frame_indexes: Iterator<Item = u32>)
/// // * `SpriteAnimation` (will be cloned)
///
/// // The animation is named "soldier_run", with a frame time of 0.1, and the frames
/// // 9 to 16 (inclusive) from the atlas.
/// ("soldier_run", 0.1, 9..=16),
/// ]);
/// ```
pub fn new<A>(atlas: ResHandle<TextureAtlas>, sprite_pivot: Pivot, animations: &[A]) -> Self
where
A: IntoSpriteAnimation,
{
let animations = {
let atlas = atlas.data_ref().unwrap();
animations
.into_iter()
.map(|a| {
let a = a.into_animation(&atlas);
(a.name.clone(), a)
})
//.map(|(name, ft, fi)| (name.to_string(), SpriteAnimation::from_atlas(name, *ft, &atlas, fi.clone())))
.collect::<HashMap<_, _>>()
};
Self { atlas, animations, sprite_pivot }
}
/// Get the [`ActiveAtlasAnimation`] for an animation with `name`.
///
/// > NOTE: this asserts that the animation exists in self in debug builds (uses `debug_assert`).
pub fn get_active(&self, name: &str) -> ActiveAtlasAnimation {
debug_assert!(
self.animations.contains_key(name),
"The animation with name '{name}' does not exist!"
);
ActiveAtlasAnimation::new(name)
}
}
impl lyra_resource::ResourceData for AtlasAnimations {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn dependencies(&self) -> Vec<lyra_resource::UntypedResHandle> {
vec![self.atlas.untyped_clone()]
}
}
/// The active sprite animation from an [`AtlasAnimations`].
#[derive(Clone, Component, Reflect)]
pub struct ActiveAtlasAnimation {
/// The name of the active [`SpriteAnimation`].
pub name: String,
/// The current frame index in the active [`SpriteAnimation`].
///
/// This is not the index of the rect in the atlas.
pub index: u32,
pub paused: bool,
/// A boolean indicating if the animation has reached its last frame
pub complete: bool,
/// The time since last animation frame change.
///
/// This is used to detect if enough time has passed for the frame.
timer: f32,
}
impl ActiveAtlasAnimation {
///Create an [`ActiveAtlasAnimation`].
///
/// The animation will not be paused.
pub fn new(name: &str) -> Self {
Self {
name: name.into(),
index: 0,
paused: false,
complete: false,
timer: 0.0,
}
}
/// Create an [`ActiveAtlasAnimation`] that starts at a specific point in the animation.
///
/// The animation will not be paused.
pub fn new_at(name: &str, index: u32) -> Self {
Self {
name: name.into(),
index,
paused: false,
complete: false,
timer: 0.0,
}
}
}
pub fn system_sprite_atlas_animation(
mut commands: Commands,
dt: Res<DeltaTime>,
view: View<(
Entities,
Option<&mut AtlasSprite>,
// support animations from assets or non-asset handles.
Or<&AtlasAnimations, &ResHandle<AtlasAnimations>>,
&mut ActiveAtlasAnimation,
)>,
) -> anyhow::Result<()> {
let dt = **dt;
for (en, mut sprite, animations, mut active) in view.iter() {
if active.paused {
// Don't touch paused animations
continue;
}
if let Some(animations) = animations.0 {
system_animation_entity_impl(
&mut commands,
dt,
en,
&mut sprite,
&animations,
&mut active,
);
} else {
let animations = animations.1.unwrap();
if let Some(animations) = animations.data_ref() {
system_animation_entity_impl(
&mut commands,
dt,
en,
&mut sprite,
&animations,
&mut active,
);
};
}
}
Ok(())
}
fn system_animation_entity_impl(
commands: &mut Commands,
dt: f32,
en: Entity,
sprite: &mut Option<RefMut<AtlasSprite>>,
animations: &AtlasAnimations,
active: &mut ActiveAtlasAnimation,
) {
if let Some(anim) = animations.animations.get(&active.name) {
if animations.atlas.is_loaded() {
active.timer += dt;
// Initialize this entity by giving it the first sprite animation frame.
if sprite.is_none() {
// Get the first sprite in the animation.
let rect = anim.frames[active.index as usize];
let sprite = AtlasSprite {
atlas: animations.atlas.clone(),
sprite: rect,
pivot: animations.sprite_pivot,
};
commands.insert(en, sprite);
return;
}
if anim.auto_loop && active.complete {
active.complete = false;
}
if active.timer >= anim.frame_time && !active.complete && !active.paused {
active.timer = 0.0;
active.index += 1;
if active.index as usize >= anim.frames.len() {
if anim.auto_loop {
// wrap the animation around
active.index = 0;
} else {
active.index = anim.frames.len() as u32 - 1;
active.complete = true;
return;
}
}
// Get the sprite for the animation frame
let rect = anim.frames[active.index as usize];
let new_sprite = AtlasSprite {
atlas: animations.atlas.clone(),
sprite: rect,
pivot: animations.sprite_pivot,
};
let sprite = sprite.as_mut().unwrap();
**sprite = new_sprite;
}
}
} else {
error!("Unknown active animation: '{}'", active.name);
}
}

View file

@ -1,61 +0,0 @@
use lyra_ecs::Component;
use lyra_reflect::Reflect;
use lyra_resource::ResHandle;
use lyra_math::{Vec3, Vec2};
mod texture_atlas;
pub use texture_atlas::*;
mod animation_sheet;
pub use animation_sheet::*;
mod tilemap;
pub use tilemap::*;
/// How the sprite is positioned and rotated relative to its [`Transform`].
///
/// Default pivot is `Pivot::Center`, this makes it easier to rotate the sprites.
#[derive(Debug, Copy, Clone, PartialEq, Default, Component, Reflect)]
pub enum Pivot {
#[default]
Center,
CenterLeft,
CenterRight,
TopLeft,
TopRight,
TopCenter,
BottomLeft,
BottomRight,
BottomCenter,
/// A custom anchor point.
///
/// Top left is (-0.5, 0.5), center is (0.0, 0.0).
Custom(Vec2)
}
impl Pivot {
/// Get the pivot point as a Vec2.
///
/// The point is offset from the top left `(0.0, 0.0)`.
pub fn as_vec(&self) -> Vec2 {
match self {
Pivot::Center => Vec2::ZERO,
Pivot::CenterLeft => Vec2::new(-0.5, 0.0),
Pivot::CenterRight => Vec2::new(0.5, 0.0),
Pivot::TopLeft => Vec2::new(-0.5, 0.5),
Pivot::TopRight => Vec2::new(0.5, 0.5),
Pivot::TopCenter => Vec2::new(0.0, 0.5),
Pivot::BottomLeft => Vec2::new(-0.5, -0.5),
Pivot::BottomRight => Vec2::new(0.5, -0.5),
Pivot::BottomCenter => Vec2::new(0.0, -0.5),
Pivot::Custom(v) => *v,
}
}
}
#[derive(Clone, Component, Reflect)]
pub struct Sprite {
pub texture: ResHandle<lyra_resource::Image>,
pub color: Vec3,
pub pivot: Pivot,
}

View file

@ -1,107 +0,0 @@
use glam::UVec2;
use lyra_ecs::Component;
use lyra_math::URect;
use lyra_reflect::Reflect;
use lyra_resource::ResHandle;
use super::Pivot;
/// A texture atlas of multiple sprites.
#[derive(Clone, Component, Reflect)]
pub struct TextureAtlas {
pub texture: ResHandle<lyra_resource::Image>,
pub frames: Vec<URect>,
}
impl TextureAtlas {
/// Create a texture atlas with rectangles of a grid.
///
/// Parameters:
/// * `texture` - The asset handle of the texture to get the sprites from.
/// * `grid_size` - The number of the cells in the grid (i.e., 9x7 grid).
/// * `cell_size` - The dimensions of each cell in the grid (i.e., 100x100 sprites).
pub fn from_grid(
texture: ResHandle<lyra_resource::Image>,
grid_size: UVec2,
cell_size: UVec2,
padding: Option<UVec2>,
offset: Option<UVec2>,
) -> Self {
let mut frames = vec![];
// offset: (16, 16)
// padding: (32, 32)
let offset = offset.unwrap_or_default();
let padding = padding.unwrap_or_default();
let mut p = UVec2::ZERO;
for y in 0..grid_size.y {
if y > 0 {
p.y = padding.y;
}
for x in 0..grid_size.x {
if x > 0 {
p.x = padding.x;
}
let start = (cell_size + padding) * UVec2::new(x, y) + offset;
let end = start + cell_size;
let r = URect::new(
start.x,
start.y,
end.x,
end.y,
);
frames.push(r);
}
}
Self { texture, frames }
}
}
impl lyra_resource::ResourceData for TextureAtlas {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn dependencies(&self) -> Vec<lyra_resource::UntypedResHandle> {
vec![self.texture.untyped_clone()]
}
}
/// A sprite from a texture atlas.
#[derive(Clone, Component, Reflect)]
pub struct AtlasSprite {
pub atlas: ResHandle<TextureAtlas>,
pub sprite: URect,
pub pivot: Pivot,
}
impl AtlasSprite {
/// Create an [`AtlasSprite`] from an atlas and get its rect from the index.
#[inline(always)]
pub fn from_atlas(atlas: ResHandle<TextureAtlas>, i: u32) -> Self {
Self::from_atlas_pivot(atlas, i, Pivot::default())
}
/// Create an [`AtlasSprite`] from an atlas, get its rect from the index, and set its pivot.
#[inline(always)]
pub fn from_atlas_pivot(atlas: ResHandle<TextureAtlas>, i: u32, pivot: Pivot) -> Self {
let a = atlas.data_ref().unwrap();
let rect = a.frames.get(i as usize).cloned().unwrap();
Self {
atlas: atlas.clone(),
sprite: rect,
pivot,
}
}
}

View file

@ -1,289 +0,0 @@
use std::collections::VecDeque;
use glam::{UVec2, UVec3, Vec2, Vec3};
use lyra_ecs::{
query::{
filter::{Changed, Not, With},
Entities, View, ViewOne,
},
relation::{ChildOf, RelationOriginComponent},
Commands, Component, Entity,
};
use lyra_math::Transform;
use lyra_reflect::Reflect;
use lyra_resource::ResHandle;
use lyra_scene::{TransformBundle, WorldTransform};
use tracing::error;
use crate::{game::GameStages, plugin::Plugin};
use super::{AtlasSprite, TextureAtlas};
/// A position on a tile map.
///
/// When this component is spawned on an entity, the [`system_relative_tile_position_update`]
/// system will position the entity on the tile. This works by making the entity a child of the
/// tile entity (using a [`ChildOf`](lyra_ecs::relation::ChildOf) ECS relation) and
/// inserting a [`Transform`] and [`WorldTransform`] component onto the entity.
#[derive(Clone, Copy, Component, Reflect)]
pub struct TileMapPos {
#[reflect(skip)] // TODO: impl reflect for Entity
pub tilemap_entity: Entity,
/// The position of the tile to spawn at.
pub position: UVec2,
pub z_level: i32,
}
/// A tile in a [`TileMap`].
#[derive(Clone, Copy, Component, Reflect)]
pub struct Tile {
/// The index in the atlas for the tile.
pub atlas_index: u32,
/// The tile position in the map.
pub position: UVec2,
pub z_level: i32,
}
/// A tile and its entity
#[derive(Clone, Component, Reflect)]
struct TileInstance {
tile: Tile,
#[reflect(skip)] // TODO: impl reflect for Entity
entity: Option<Entity>,
}
#[derive(Clone, Component, Reflect)]
struct Layer {
tiles: Vec<TileInstance>,
level: u32,
}
#[derive(Clone, Component, Reflect)]
pub struct TileMap {
pub atlas: ResHandle<TextureAtlas>,
/// The size of the map in tiles.
///
/// The Z-axis is used to specify the amount of layers in the map.
pub size: UVec3,
/// Dimensions of each tile.
pub tile_size: UVec2,
layers: Vec<Layer>,
/// Tile entities that need to be updated (layer_index, tile_index, Entity).
#[reflect(skip)]
to_update_entities: VecDeque<(usize, usize, Entity)>,
}
impl TileMap {
pub fn new(
atlas: ResHandle<TextureAtlas>,
map_size: UVec2,
layer_num: u32,
tile_size: UVec2,
) -> Self {
let size = map_size.extend(layer_num);
let layer_size = map_size.element_product(); // x*y
let mut layers = Vec::with_capacity(layer_num as _);
for i in 0..layer_num {
layers.push(Layer {
tiles: Vec::with_capacity(layer_size as _),
level: i,
});
}
Self {
atlas,
size,
tile_size,
layers,
to_update_entities: Default::default(),
}
}
pub fn insert_tile(&mut self, layer: u32, atlas_index: u32, x: u32, y: u32) {
let l = &mut self.layers[layer as usize];
if let Some((tile_index, tile_inst)) = l
.tiles
.iter_mut()
.enumerate()
.find(|(_, t)| t.tile.position.x == x && t.tile.position.y == y)
{
tile_inst.tile.atlas_index = atlas_index;
if let Some(entity) = tile_inst.entity {
self.to_update_entities
.push_back((layer as usize, tile_index, entity));
}
} else {
l.tiles.push(TileInstance {
tile: Tile {
atlas_index,
position: UVec2::new(x, y),
z_level: 0,
},
entity: None,
});
}
}
/// Get the relative position of a tile
pub fn position_of(&self, x: u32, y: u32) -> Vec2 {
Vec2::new(x as _, y as _) * self.tile_size.as_vec2()
}
}
/// A system to update the tilemap when tiles are inserted/removed.
pub fn system_tilemap_update(
mut commands: Commands,
view: View<(Entities, &mut TileMap)>,
) -> anyhow::Result<()> {
for (map_en, mut map) in view.into_iter() {
let tile_size = map.tile_size;
let atlas_handle = map.atlas.clone();
let atlas = match atlas_handle.data_ref() {
Some(a) => a,
None => continue,
};
while let Some((layer_index, tile_index, en)) = map.to_update_entities.pop_front() {
let tile_inst = &map.layers[layer_index].tiles[tile_index];
if let Some(frame) = atlas.frames.get(tile_inst.tile.atlas_index as usize) {
let sprite = AtlasSprite {
atlas: atlas_handle.clone(),
sprite: *frame,
pivot: super::Pivot::TopLeft,
};
commands.insert(en, sprite);
} else {
error!(
"Invalid atlas index '{}' for tile at pos '{:?}'",
tile_inst.tile.atlas_index, tile_inst.tile.position
);
}
}
for layer in &mut map.layers {
for tile in &mut layer.tiles {
if tile.entity.is_none() {
if let Some(frame) = atlas.frames.get(tile.tile.atlas_index as usize) {
let sprite = AtlasSprite {
atlas: atlas_handle.clone(),
sprite: *frame,
pivot: super::Pivot::TopLeft,
};
let grid = tile.tile.position * tile_size;
let sprite_pos = Transform::from_xyz(
grid.x as _,
// expand the grid downwards so 0,0 is top left of the tilemap
-(grid.y as f32),
tile.tile.z_level as _,
);
let tile_en = commands.spawn((
sprite,
TileMapPos {
tilemap_entity: map_en,
position: tile.tile.position,
z_level: tile.tile.z_level,
},
tile.tile,
TransformBundle::from(sprite_pos),
));
commands.add_relation(tile_en, ChildOf, map_en);
tile.entity = Some(tile_en);
} else {
error!(
"Invalid atlas index '{}' for tile at pos '{:?}'",
tile.tile.atlas_index, tile.tile.position
);
}
}
}
}
}
Ok(())
}
fn system_relative_tile_position_update(
mut commands: Commands,
view: View<
(
Entities,
&TileMapPos,
Option<&mut Transform>,
Option<&WorldTransform>,
),
(Changed<TileMapPos>, Not<With<Tile>>),
>,
tile_map_view: ViewOne<&TileMap>,
child_of_rel_view: ViewOne<&RelationOriginComponent<ChildOf>>,
) -> anyhow::Result<()> {
for (en, rel, pos, wpos) in view.into_iter() {
match tile_map_view.get(rel.tilemap_entity) {
Some(map) => {
let layer = map.layers.last().unwrap();
if let Some(tile_en) = layer
.tiles
.iter()
.find(|t| t.tile.position == rel.position)
.and_then(|t| t.entity)
{
if child_of_rel_view
.get(en)
.map(|rel| rel.target() != tile_en)
.unwrap_or(true)
{
commands.add_relation(en, ChildOf, tile_en);
}
if let Some(mut pos) = pos {
*pos = Transform::from_translation(Vec3::new(0.0, 0.0, rel.z_level as _));
if wpos.is_none() {
commands.insert(en, WorldTransform::from(*pos));
}
} else {
let pos =
Transform::from_translation(Vec3::new(0.0, 0.0, rel.z_level as _));
commands.insert(en, (pos, WorldTransform::from(pos)));
}
} else {
error!("Unknown tile in map at {:?}", rel.position);
}
}
None => {
error!(
"Unknown tilemap for relative tile position of {:?}",
rel.position
);
}
}
}
Ok(())
}
#[derive(Default)]
pub struct TileMapPlugin;
impl Plugin for TileMapPlugin {
fn setup(&mut self, app: &mut crate::game::App) {
// insert in postupdate to apply changes that other systems may have made to the tilemap
app.add_system_to_stage(
GameStages::PostUpdate,
"tilemap_update",
system_tilemap_update,
&[],
);
app.add_system_to_stage(
GameStages::PostUpdate,
"relative_tile_position_update",
system_relative_tile_position_update,
&[],
);
}
}

View file

@ -1,8 +0,0 @@
mod plugin;
pub use plugin::*;
mod window;
pub use window::*;
pub use winit::dpi as dpi;

View file

@ -1,336 +0,0 @@
use std::{collections::VecDeque, sync::Arc};
use async_std::task::block_on;
use glam::{DVec2, IVec2, UVec2};
use lyra_ecs::Entity;
use lyra_reflect::Reflect;
use rustc_hash::FxHashMap;
use tracing::{debug, error, warn};
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, EventLoop},
window::{Window, WindowAttributes, WindowId},
};
pub use winit::event::{DeviceId, DeviceEvent, MouseScrollDelta, ElementState, RawKeyEvent};
pub use winit::keyboard::PhysicalKey;
use crate::{
game::{App, WindowState},
plugin::Plugin,
render::renderer::BasicRenderer, winit::{FullscreenMode, LastWindow, PrimaryWindow},
};
use super::WindowOptions;
/// A struct that contains a [`DeviceEvent`](winit::event::DeviceEvent) with its source
/// [`DeviceId`](winit::event::DeviceId).
#[derive(Debug, Clone, Reflect)]
pub struct DeviceEventPair {
#[reflect(skip)]
pub device_src: DeviceId,
#[reflect(skip)]
pub event: DeviceEvent,
}
pub struct WinitPlugin {
/// The primary window that will be created.
///
/// This will become `None` after the window is created. If you want to get the
/// primary world later, query for an entity with the [`PrimaryWindow`] and
/// [`WindowOptions`] components.
pub primary_window: Option<WindowOptions>,
}
impl Default for WinitPlugin {
fn default() -> Self {
Self {
primary_window: Some(WindowOptions::default()),
}
}
}
impl Plugin for WinitPlugin {
fn setup(&mut self, app: &mut crate::game::App) {
app.set_run_fn(winit_app_runner);
app.register_event::<WindowEvent>();
app.register_event::<DeviceEventPair>();
if let Some(prim) = self.primary_window.take() {
app.add_resource(WinitWindows::with_window(prim));
} else {
app.add_resource(WinitWindows::default());
}
}
fn is_ready(&self, _app: &mut crate::game::App) -> bool {
true
}
fn complete(&self, _app: &mut crate::game::App) {}
fn cleanup(&self, _app: &mut crate::game::App) {}
}
#[derive(Default)]
pub struct WinitWindows {
pub windows: FxHashMap<WindowId, Arc<Window>>,
pub entity_to_window: FxHashMap<Entity, WindowId>,
pub window_to_entity: FxHashMap<WindowId, Entity>,
/// windows that will be created when the Winit runner first starts.
window_queue: VecDeque<WindowOptions>,
}
impl WinitWindows {
pub fn with_window(window: WindowOptions) -> Self {
Self {
window_queue: vec![window].into(),
..Default::default()
}
}
pub fn create_window(
&mut self,
event_loop: &ActiveEventLoop,
entity: Entity,
attr: WindowAttributes,
) -> Result<WindowId, winit::error::OsError> {
let win = event_loop.create_window(attr)?;
let id = win.id();
self.windows.insert(id, Arc::new(win));
self.entity_to_window.insert(entity, id);
self.window_to_entity.insert(id, entity);
Ok(id)
}
pub fn get_entity_window(&self, entity: Entity) -> Option<&Arc<Window>> {
self.entity_to_window
.get(&entity)
.and_then(|id| self.windows.get(id))
}
}
pub fn winit_app_runner(app: App) {
let evloop = EventLoop::new().expect("failed to create winit EventLoop");
let mut winit_runner = WinitRunner { app };
evloop.run_app(&mut winit_runner).expect("loop error");
}
struct WinitRunner {
app: App,
}
impl ApplicationHandler for WinitRunner {
fn about_to_wait(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
self.app.update();
let renderer = self
.app
.renderer
.get_mut()
.expect("renderer was not initialized");
renderer.prepare(&mut self.app.world);
match renderer.render() {
Ok(_) => {}
// Reconfigure the surface if lost
//Err(wgpu::SurfaceError::Lost) => self.on_resize(.surface_size()),
// The system is out of memory, we should probably quit
Err(wgpu::SurfaceError::OutOfMemory) => {
error!("OOM");
event_loop.exit();
}
// All other errors (Outdated, Timeout) should be resolved by the next frame
Err(e) => eprintln!("{:?}", e),
}
let windows = self.app.world.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource");
for window in windows.windows.values() {
window.request_redraw();
}
}
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
let world = &mut self.app.world;
let en = world.reserve_entity();
let mut windows = world.get_resource_mut::<WinitWindows>()
.expect("world missing WinitWindows resource");
let mut to_create_window = windows.window_queue.pop_front().unwrap_or_default();
let window_attr = to_create_window.as_attributes();
//drop(windows);
//let en = world.spawn((to_create_window, last, PrimaryWindow));
//let mut windows = world.get_resource_mut::<WinitWindows>()
//.expect("world missing WinitWindows resource");
let wid = windows.create_window(event_loop, en, window_attr).unwrap();
let window = windows.windows.get(&wid).unwrap().clone();
drop(windows);
// update fields that default to `None`
to_create_window.position = window.outer_position()
.or_else(|_| window.inner_position())
.ok()
.map(|p| IVec2::new(p.x, p.y));
// See [`WindowOptions::as_attributes`], it defaults to Windowed fullscreen mode, so we
// must trigger an update in the sync system;
let mut last = LastWindow { last: to_create_window.clone() };
last.last.fullscreen_mode = FullscreenMode::Windowed;
world.insert(en, (to_create_window, last, PrimaryWindow));
debug!("Created window after resume");
let renderer = block_on(BasicRenderer::create_with_window(world, window));
if self.app.renderer.set(Box::new(renderer)).is_err() {
warn!("renderer was re-initialized");
}
}
fn device_event(
&mut self,
_: &ActiveEventLoop,
device_src: winit::event::DeviceId,
event: winit::event::DeviceEvent,
) {
self.app.push_event(DeviceEventPair { device_src, event });
}
fn window_event(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
window_id: winit::window::WindowId,
event: WindowEvent,
) {
/* let windows = self.app.world.get_resource::<WinitWindows>();
let window = match windows.windows.get(&window_id) {
Some(w) => w.clone(),
None => return,
};
drop(windows); */
self.app.push_event(event.clone());
match event {
WindowEvent::CursorMoved { position, .. } => {
let windows = self.app.world.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource");
let en = windows.window_to_entity.get(&window_id)
.expect("missing window entity");
// update the window and its cache so the sync system doesn't try to update the window
let (mut en_window, mut en_last_win) = self.app.world.view_one::<(&mut WindowOptions, &mut LastWindow)>(*en).unwrap();
let pos = Some(DVec2::new(position.x, position.y));
en_window.set_physical_cursor_position(pos);
en_last_win.set_physical_cursor_position(pos);
},
WindowEvent::ActivationTokenDone { .. } => todo!(),
WindowEvent::Resized(physical_size) => {
self.app.on_resize(physical_size);
let (mut window, mut last_window) = self
.app
.world
.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource")
.window_to_entity
.get(&window_id)
.and_then(|e| self.app.world.view_one::<(&mut WindowOptions, &mut LastWindow)>(*e))
.unwrap();
// update the window and its cache so the sync system doesn't try to update the window
let size = UVec2::new(physical_size.width, physical_size.height);
window.set_physical_size(size);
last_window.set_physical_size(size);
},
// Mark the cursor as outside the window when it leaves
WindowEvent::CursorLeft { .. } => {
let (mut window, mut last_window) = self
.app
.world
.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource")
.window_to_entity
.get(&window_id)
.and_then(|e| self.app.world.view_one::<(&mut WindowOptions, &mut LastWindow)>(*e))
.unwrap();
window.set_physical_cursor_position(None);
last_window.set_physical_cursor_position(None);
},
WindowEvent::Moved(physical_position) => {
let mut state = self.app.world.get_resource_or_else(WindowState::new);
state.position = IVec2::new(physical_position.x, physical_position.y);
},
WindowEvent::CloseRequested => {
self.app.on_exit();
event_loop.exit();
},
WindowEvent::Destroyed => todo!(),
WindowEvent::DroppedFile(_path_buf) => todo!(),
WindowEvent::HoveredFile(_path_buf) => todo!(),
WindowEvent::HoveredFileCancelled => todo!(),
WindowEvent::Focused(focused) => {
let mut window_opts = self
.app
.world
.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource")
.window_to_entity
.get(&window_id)
.and_then(|e| self.app.world.view_one::<&mut WindowOptions>(*e))
.unwrap();
window_opts.focused = focused;
},
WindowEvent::ModifiersChanged(modifiers) => {
debug!("modifiers changed: {:?}", modifiers)
},
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
let mut window_opts = self
.app
.world
.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource")
.window_to_entity
.get(&window_id)
.and_then(|e| self.app.world.view_one::<&mut WindowOptions>(*e))
.unwrap();
window_opts.scale_factor = scale_factor;
},
WindowEvent::ThemeChanged(theme) => {
let mut window_opts = self
.app
.world
.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource")
.window_to_entity
.get(&window_id)
.and_then(|e| self.app.world.view_one::<&mut WindowOptions>(*e))
.unwrap();
window_opts.theme = Some(theme);
},
WindowEvent::Occluded(occ) => {
let mut window_opts = self
.app
.world
.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource")
.window_to_entity
.get(&window_id)
.and_then(|e| self.app.world.view_one::<&mut WindowOptions>(*e))
.unwrap();
window_opts.occluded = occ;
},
WindowEvent::RedrawRequested => {
//debug!("should redraw");
},
_ => {}
}
}
}

View file

@ -1,723 +0,0 @@
use std::ops::{Deref, DerefMut};
use glam::{DVec2, IVec2, UVec2, Vec2};
use lyra_ecs::{query::{filter::Changed, Entities, Res, View}, Component};
use lyra_math::Area;
use lyra_reflect::Reflect;
use lyra_resource::Image;
use tracing::{error, warn};
use winit::{dpi::{LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}, monitor::{MonitorHandle, VideoModeHandle}, window::{CustomCursor, Window}};
pub use winit::window::{CursorGrabMode, CursorIcon, Icon, Theme, WindowButtons, WindowLevel};
use crate::{plugin::Plugin, winit::WinitWindows, lyra_engine};
/// Flag component that
#[derive(Clone, Component)]
pub struct PrimaryWindow;
#[derive(Clone, PartialEq, Eq)]
pub enum CursorAppearance {
Icon(CursorIcon),
Custom(CustomCursor)
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Reflect)]
pub enum FullscreenMode{
#[default]
Windowed,
BorderlessFullscreen,
SizedFullscreen,
Fullscreen,
}
impl FullscreenMode {
pub fn as_winit_fullscreen(&self, monitor: MonitorHandle, physical_size: UVec2) -> Option<winit::window::Fullscreen> {
match &self {
FullscreenMode::Windowed => None,
FullscreenMode::BorderlessFullscreen => Some(winit::window::Fullscreen::Borderless(None)),
// find closest video mode for full screen sizes
_ => {
let closest = find_closest_video_mode(monitor, physical_size);
if let Some(closest) = closest {
Some(winit::window::Fullscreen::Exclusive(closest))
} else {
warn!("Could not find closest video mode, falling back to windowed.");
None
}
}
}
}
}
#[derive(Clone)]
pub struct Cursor {
/// Modifies the cursor icon of the window.
///
/// Platform-specific
/// * **iOS / Android / Orbital:** Unsupported.
/// * **Web:** Custom cursors have to be loaded and decoded first, until then the previous cursor is shown.
pub appearance: CursorAppearance,
/// Gets/sets the window's cursor grab mode
///
/// # Tip:
/// First try confining the cursor, and if it fails, try locking it instead.
pub grab: CursorGrabMode,
/// Gets/sets whether the window catches cursor events.
///
/// If `false`, events are passed through the window such that any other window behind it
/// receives them. By default hittest is enabled.
///
/// Platform-specific
/// * **iOS / Android / Web / Orbital:** Unsupported.
pub hittest: bool,
/// Gets/sets the cursor's visibility
///
/// Platform-specific
/// * **Windows / X11 / Wayland:** The cursor is only hidden within the confines of the window.
/// * **macOS:** The cursor is hidden as long as the window has input focus, even if the
/// cursor is outside of the window.
/// * **iOS / Android:** Unsupported.
pub visible: bool,
//cursor_position: Option<PhysicalPosition<i32>>,
}
/// Options that the window will be created with.
#[derive(Clone, Component, Reflect)]
pub struct WindowOptions {
/// The enabled window buttons.
///
/// Platform-specific
/// * **Wayland / X11 / Orbital:** Not implemented. Always set to [`WindowButtons::all`].
/// * **Web / iOS / Android:** Unsupported. Always set to [`WindowButtons::all`].
#[reflect(skip)]
pub enabled_buttons: WindowButtons,
/// Gets or sets if the window is in focus.
///
/// Platform-specific
/// * **iOS / Android / Wayland / Orbital:** Unsupported.
pub focused: bool,
/// Gets or sets the fullscreen setting.
pub fullscreen_mode: FullscreenMode,
/// Gets/sets the position of the top-left hand corner of the window relative to
/// the top-left hand corner of the desktop.
///
/// Note that the top-left hand corner of the desktop is not necessarily the same
/// as the screen. If the user uses a desktop with multiple monitors, the top-left
/// hand corner of the desktop is the top-left hand corner of the monitor at the
/// top-left of the desktop.
///
/// If this is none, the position will be chosen by the windowing manager at creation, then set
/// when the window is created.
///
/// Platform-specific
/// * **iOS:** Value is the top left coordinates of the windows safe area in the screen
/// space coordinate system.
/// * **Web:** Value is the top-left coordinates relative to the viewport. Note: this will be
/// the same value as [`WindowOptions::outer_position`].
/// * **Android / Wayland:** Unsupported.
#[reflect(skip)]
pub position: Option<IVec2>,
/// Gets/sets the size of the view in the window.
///
/// The size does not include the window title bars and borders.
///
/// Platform-specific
/// * **Web:** The size of the canvas element. Doesnt account for CSS `transform`.
#[reflect(skip)]
physical_size: UVec2,
/// Gets/sets if the window has decorations.
///
/// Platform-specific
/// * **iOS / Android / Web:** Always set to `true`.
pub decorated: bool,
/// Gets/sets the window's current maximized state
///
/// Platform-specific
/// * **iOS / Android / Web:** Unsupported.
pub maximized: bool,
/// Gets/sets the window's current minimized state.
///
/// Is `None` if the minimized state could not be determined.
///
/// Platform-specific
/// * **Wayland:** always `None`, un-minimize is unsupported.
/// * **iOS / Android / Web / Orbital:** Unsupported.
pub minimized: Option<bool>,
/// Gets/sets the window's current resizable state
///
/// If this is false, the window can still be resized by changing [`WindowOptions::size`].
///
/// Platform-specific
/// Setting this only has an affect on desktop platforms.
///
/// * **X11:** Due to a bug in XFCE, setting this has no effect..
/// * **iOS / Android / Web:** Unsupported.
pub resizable: bool,
/// Gets/sets the window's current visibility state.
///
/// `None` means it couldn't be determined.
///
/// Platform-specific
/// * **X11:** Not implemented.
/// * **Wayland / Android / Web:** Unsupported.
/// * **iOS:** Setting is not implemented, getting is unsupported.
pub visible: Option<bool>,
/// Gets/sets the window resize increments.
///
/// This is a niche constraint hint usually employed by terminal emulators and other apps
/// that need “blocky” resizes.
///
/// Platform-specific
/// * **macOS:** Increments are converted to logical size and then macOS rounds them to whole numbers.
/// * **Wayland:** Not implemented, always `None`.
/// * **iOS / Android / Web / Orbital:** Unsupported.
#[reflect(skip)]
pub resize_increments: Option<Size>,
/// Gets the scale factor.
///
/// The scale factor is the ratio of physical pixels to logical pixels.
/// See [winit docs](https://docs.rs/winit/latest/winit/window/struct.Window.html#method.scale_factor)
/// for more information.
pub scale_factor: f64,
/// Gets/sets the window's blur state.
///
/// Platform-specific
/// * **Android / iOS / X11 / Web / Windows:** Unsupported.
/// * **Wayland:** Only works with org_kde_kwin_blur_manager protocol.
pub blur: bool,
#[reflect(skip)]
pub cursor: Cursor,
/// Sets whether the window should get IME events
///
/// When IME is allowed, the window will receive [`Ime`](winit::event::WindowEvent::Ime)
/// events, and during the preedit phase the window will NOT get KeyboardInput events.
/// The window should allow IME while it is expecting text input.
///
/// When IME is not allowed, the window wont receive [`Ime`](winit::event::WindowEvent::Ime)
/// events, and will receive [`KeyboardInput`](winit::event::WindowEvent::KeyboardInput) events
/// for every keypress instead. Not allowing IME is useful for games for example.
/// IME is not allowed by default.
///
/// Platform-specific
/// * **macOS:** IME must be enabled to receive text-input where dead-key sequences are combined.
/// * **iOS / Android / Web / Orbital:** Unsupported.
/// * **X11:** Enabling IME will disable dead keys reporting during compose.
pub ime_allowed: bool,
/// Sets area of IME box in physical coordinates relative to the top left.
///
/// Platform-specific
/// * **X11:** - area is not supported, only position.
/// * **iOS / Android / Web / Orbital:** Unsupported.
#[reflect(skip)]
physical_ime_cursor_area: Option<Area<Vec2, Vec2>>,
/// Gets/sets the minimum size of the window.
///
/// Units are in logical pixels.
///
/// Platform-specific
/// * **iOS / Android / Orbital:** Unsupported.
#[reflect(skip)]
pub min_size: Option<Vec2>,
/// Gets/sets the maximum size of the window.
///
/// Units are in logical pixels.
///
/// Platform-specific
/// * **iOS / Android / Orbital:** Unsupported.
#[reflect(skip)]
pub max_size: Option<Vec2>,
/// Gets/sets the current window theme.
///
/// Specify `None` to reset the theme to the system default. May also be `None` on unsupported
/// platforms.
///
/// Platform-specific
/// * **Wayland:** Sets the theme for the client side decorations. Using `None` will use dbus
/// to get the system preference.
/// * **X11:** Sets `_GTK_THEME_VARIANT` hint to `dark` or `light` and if `None` is used,
/// it will default to [`Theme::Dark`](winit::window::Theme::Dark).
/// * **iOS / Android / Web / Orbital:** Unsupported.
#[reflect(skip)]
pub theme: Option<Theme>,
/// Gets/sets the title of the window.
///
/// Platform-specific
/// * **iOS / Android:** Unsupported.
/// * **X11 / Wayland / Web:** Cannot get, will always be an empty string.
pub title: String,
/// Gets/sets the window's transparency state.
///
/// This is just a hint that may not change anything about the window transparency, however
/// doing a mismatch between the content of your window and this hint may result in visual
/// artifacts.
///
/// Platform-specific
/// * **macOS:** This will reset the windows background color.
/// * **Web / iOS / Android:** Unsupported.
/// * **X11:** Can only be set while building the window.
pub transparent: bool,
/// Sets the window's icon.
///
/// On Windows and X11, this is typically the small icon in the top-left corner of
/// the titlebar.
///
/// Platform-specific
/// * **iOS / Android / Web / Wayland / macOS / Orbital:** Unsupported.
/// * **Windows:** Sets `ICON_SMALL`. The base size for a window icon is 16x16, but its
/// recommended to account for screen scaling and pick a multiple of that, i.e. 32x32.
/// * **X11:** Has no universal guidelines for icon sizes, so youre at the whims of
/// the WM. That said, its usually in the same ballpark as on Windows.
pub window_icon: Option<lyra_resource::ResHandle<Image>>,
/// Change the window level.
///
/// This is just a hint to the OS, and the system could ignore it.
///
/// See [`WindowLevel`] for details.
#[reflect(skip)]
pub window_level: WindowLevel,
/// Show [window menu](https://en.wikipedia.org/wiki/Common_menus_in_Microsoft_Windows#System_menu)
/// at a specified position in physical coordinates.
///
/// This is the context menu that is normally shown when interacting with the title bar. This is useful when implementing custom decorations.
/// Platform-specific
/// * **Android / iOS / macOS / Orbital / Wayland / Web / X11:** Unsupported.
//pub physical_window_menu_pos: Option<Vec2>,
/// Gets the window's occluded state (completely hidden from view).
///
/// This is different to window visibility as it depends on whether the window is
/// closed, minimised, set invisible, or fully occluded by another window.
///
/// Platform-specific
/// * **iOS:** this is set to `false` in response to an applicationWillEnterForeground
/// callback which means the application should start preparing its data.
/// Its `true` in response to an applicationDidEnterBackground callback which means
/// the application should free resources (according to the iOS application lifecycle).
/// * **Web:** Doesn't take into account CSS border, padding, or transform.
/// * **Android / Wayland / Windows / Orbital:** Unsupported.
// TODO: update
pub(crate) occluded: bool,
/// Gets/sets the position of the cursor in physical coordinates.
///
/// Platform-specific
/// * **Wayland:** Cursor must be in [`CursorGrabMode::Locked`].
/// * **iOS / Android / Web / Orbital:** Unsupported.
#[reflect(skip)]
physical_cursor_position: Option<DVec2>,
}
/* fn physical_to_vec2<P: winit::dpi::Pixel>(size: PhysicalSize<P>) -> Vec2 {
let size = size.cast::<f32>();
Vec2::new(size.width, size.height)
} */
fn logical_to_vec2(size: LogicalSize<f32>) -> Vec2 {
Vec2::new(size.width, size.height)
}
impl From<winit::window::WindowAttributes> for WindowOptions {
fn from(value: winit::window::WindowAttributes) -> Self {
Self {
enabled_buttons: value.enabled_buttons,
focused: false,
fullscreen_mode: value.fullscreen.map(|m| match m {
winit::window::Fullscreen::Exclusive(video_mode_handle) => {
if video_mode_handle.size() == video_mode_handle.monitor().size() {
FullscreenMode::Fullscreen
} else {
FullscreenMode::SizedFullscreen
}
},
winit::window::Fullscreen::Borderless(_) => FullscreenMode::BorderlessFullscreen,
}).unwrap_or(FullscreenMode::Windowed),
position: value.position.map(|p| {
let s = p.to_physical::<i32>(1.0);
IVec2::new(s.x, s.y)
}),
physical_size: value.inner_size.map(|s| {
let s = s.to_physical::<u32>(1.0);
UVec2::new(s.width, s.height)
}).unwrap_or(UVec2::new(1280, 720)),
decorated: value.decorations,
maximized: value.maximized,
minimized: None,
resizable: value.resizable,
visible: Some(value.visible),
resize_increments: value.resize_increments.map(|r| r.into()),
scale_factor: 1.0,
blur: value.blur,
cursor: Cursor {
appearance: match value.cursor {
winit::window::Cursor::Icon(icon) => CursorAppearance::Icon(icon),
winit::window::Cursor::Custom(custom) => CursorAppearance::Custom(custom),
},
grab: CursorGrabMode::None,
hittest: true,
visible: true,
},
ime_allowed: false,
physical_ime_cursor_area: None,
min_size: value.min_inner_size.map(|m| logical_to_vec2(m.to_logical(1.0))),
max_size: value.max_inner_size.map(|m| logical_to_vec2(m.to_logical(1.0))),
theme: value.preferred_theme,
title: value.title,
transparent: value.transparent,
window_icon: None,
window_level: value.window_level,
occluded: false,
physical_cursor_position: None,
}
}
}
impl Default for WindowOptions {
fn default() -> Self {
Self::from(Window::default_attributes())
}
}
fn find_closest_video_mode(monitor: MonitorHandle, physical_size: UVec2) -> Option<VideoModeHandle> {
let mut modes = monitor.video_modes();
let mut closest = modes.next()?;
let closest_size = closest.size();
let mut closest_size = UVec2::new(closest_size.width, closest_size.height);
for mode in modes {
let s = closest.size();
let s = UVec2::new(s.width, s.height);
if (physical_size - s).length_squared() < (physical_size - closest_size).length_squared() {
closest = mode;
closest_size = s;
}
}
Some(closest)
}
impl WindowOptions {
/// Create winit [`WindowAttributes`] from self.
///
/// This will ignore [`WindowOptions::fullscreen`] mode on self, defaulting to
/// [`FullscreenMode::Windowed`]. It will be updated on first run of the sync system.
pub(crate) fn as_attributes(&self) -> winit::window::WindowAttributes {
let mut att = winit::window::Window::default_attributes();
att.enabled_buttons = self.enabled_buttons.clone();
att.fullscreen = None;
att.inner_size = Some(Size::Physical(PhysicalSize::new(self.physical_size.x, self.physical_size.y)));
att.decorations = self.decorated;
att.maximized = self.maximized;
att.resizable = self.resizable;
att.visible = self.visible.unwrap_or(true);
att.position = self.position.map(|p| Position::Physical(PhysicalPosition::new(p.x, p.y)));
att.resize_increments = self.resize_increments.map(|i| i.into());
att.blur = self.blur;
att.cursor = match self.cursor.appearance.clone() {
CursorAppearance::Icon(icon) => winit::window::Cursor::Icon(icon),
CursorAppearance::Custom(custom) => winit::window::Cursor::Custom(custom),
};
att.min_inner_size = self.min_size.map(|s| Size::Logical(LogicalSize::new(s.x as _, s.y as _)));
att.max_inner_size = self.max_size.map(|s| Size::Logical(LogicalSize::new(s.x as _, s.y as _)));
att.preferred_theme = self.theme;
att.title = self.title.clone();
att.transparent = self.transparent;
if self.window_icon.is_some() {
todo!("cannot set window attribute icon yet");
}
att.window_level = self.window_level;
att
}
/// The size of the window in physical coordinates.
pub fn physical_size(&self) -> UVec2 {
self.physical_size
}
/// Set the size of the window in physical coordinates.
pub fn set_physical_size(&mut self, size: UVec2) {
self.physical_size = size;
}
/// The size of the window in logical coordinates.
pub fn size(&self) -> Vec2 {
self.physical_size.as_vec2() / self.scale_factor as f32
}
/// Set the size of the window in logical coordinates.
pub fn set_size(&mut self, size: Vec2) {
self.physical_size = (size * self.scale_factor as f32).as_uvec2();
}
/// Returns a boolean indicating if the mouse is inside the window.
pub fn is_mouse_inside(&self) -> bool {
if let Some(pos) = self.physical_cursor_position {
let s = self.physical_size;
return pos.x >= 0.0 && pos.x <= s.x as f64
&& pos.y >= 0.0 && pos.y <= s.y as f64;
}
false
}
/// The cursor position in the window in logical coordinates.
///
/// Returns `None` if the cursor is not in the window.
pub fn cursor_position(&self) -> Option<Vec2> {
if !self.is_mouse_inside() {
return None;
}
self.physical_cursor_position.map(|p| (p / self.scale_factor).as_vec2())
}
/// The cursor position in the window in physical coordinates.
///
/// Returns `None` if the cursor is not in the window.
pub fn physical_cursor_position(&self) -> Option<Vec2> {
if !self.is_mouse_inside() {
return None;
}
self.physical_cursor_position.map(|p| p.as_vec2())
}
/// Set the cursor position in logical coordinates.
///
/// Can be used to mark the cursor outside of the window as well.
pub fn set_cursor_position(&mut self, pos: Option<Vec2>) {
self.physical_cursor_position = pos.map(|p| p.as_dvec2() * self.scale_factor);
}
/// Set the cursor position in physical coordinates.
///
/// Can be used to mark the cursor outside of the window as well.
pub fn set_physical_cursor_position(&mut self, pos: Option<DVec2>) {
self.physical_cursor_position = pos;
}
/// The window's occluded state (completely hidden from view).
///
/// This is different to window visibility as it depends on whether the window is
/// closed, minimised, set invisible, or fully occluded by another window.
///
/// Platform-specific
/// * **iOS:** this is set to `false` in response to an applicationWillEnterForeground
/// callback which means the application should start preparing its data.
/// Its `true` in response to an applicationDidEnterBackground callback which means
/// the application should free resources (according to the iOS application lifecycle).
/// * **Web:** Doesn't take into account CSS border, padding, or transform.
/// * **Android / Wayland / Windows / Orbital:** Unsupported.
pub fn occluded(&self) -> bool {
self.occluded
}
}
/// The state of the window last time it was changed.
///
/// This is used in [`window_sync_system`] to see what fields of [`WindowOptions`] changed
/// when syncing the winit window with the component.
#[derive(Clone, Component)]
pub struct LastWindow {
pub last: WindowOptions,
}
impl Deref for LastWindow {
type Target = WindowOptions;
fn deref(&self) -> &Self::Target {
&self.last
}
}
impl DerefMut for LastWindow {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.last
}
}
#[derive(Default)]
pub struct WindowPlugin {
#[allow(dead_code)]
create_options: WindowOptions,
}
/// A system that syncs Winit Windows with [`WindowOptions`] components.
pub fn window_sync_system(windows: Res<WinitWindows>, view: View<(Entities, &WindowOptions, &mut LastWindow), Changed<WindowOptions>>) -> anyhow::Result<()> {
for (entity, opts, mut last) in view.iter() {
let window = windows.get_entity_window(entity)
.expect("entity's window is missing");
if opts.enabled_buttons != last.enabled_buttons {
window.set_enabled_buttons(opts.enabled_buttons);
}
if opts.focused != last.focused && opts.focused {
window.focus_window();
}
if opts.fullscreen_mode != last.fullscreen_mode {
let monitor = window.primary_monitor().unwrap_or_else(|| {
let mut m = window.available_monitors();
m.next().expect("failed to find any available monitor")
});
window.set_fullscreen(opts.fullscreen_mode.as_winit_fullscreen(monitor, opts.physical_size));
}
if opts.physical_size != last.physical_size {
let size = PhysicalSize::new(opts.physical_size.x, opts.physical_size.y);
if window.request_inner_size(size).is_some() {
error!("request to increase window size failed");
}
}
if opts.decorated != last.decorated {
window.set_decorations(opts.decorated);
}
if opts.maximized != last.maximized {
window.set_maximized(opts.maximized);
}
if opts.minimized != last.minimized && opts.minimized.is_some() {
window.set_minimized(opts.minimized.unwrap());
}
if opts.visible != last.visible && opts.visible.is_some() {
window.set_visible(opts.visible.unwrap());
}
if opts.position != last.position && opts.position.is_some() {
let pos = opts.position.unwrap();
let pos = PhysicalPosition::new(pos.x, pos.y);
window.set_outer_position(pos);
}
if opts.resize_increments != last.resize_increments {
window.set_resize_increments(opts.resize_increments);
}
if opts.blur != last.blur {
window.set_blur(opts.blur);
}
if opts.cursor.appearance != last.cursor.appearance {
match opts.cursor.appearance.clone() {
CursorAppearance::Icon(icon) => window.set_cursor(winit::window::Cursor::Icon(icon)),
CursorAppearance::Custom(custom) => window.set_cursor(winit::window::Cursor::Custom(custom)),
}
}
if opts.cursor.grab != last.cursor.grab {
if let Err(e) = window.set_cursor_grab(opts.cursor.grab) {
error!("could not set cursor grab mode: {}", e);
}
}
if opts.cursor.hittest != last.cursor.hittest {
if let Err(e) = window.set_cursor_hittest(opts.cursor.hittest) {
error!("could not set cursor hittest: {}", e);
}
}
if opts.cursor.visible != last.cursor.visible {
window.set_cursor_visible(opts.cursor.visible);
}
if opts.ime_allowed != last.ime_allowed {
window.set_ime_allowed(opts.ime_allowed);
}
if opts.physical_ime_cursor_area != last.physical_ime_cursor_area && opts.physical_ime_cursor_area.is_some() {
let area = opts.physical_ime_cursor_area.unwrap();
let pos = PhysicalPosition::new(area.position.x, area.position.y);
let size = PhysicalSize::new(area.size.x, area.size.y);
window.set_ime_cursor_area(pos, size);
}
if opts.min_size != last.min_size {
let s = opts.min_size.map(|s| LogicalSize::new(s.x, s.y));
window.set_min_inner_size(s);
}
if opts.max_size != last.max_size {
let s = opts.max_size.map(|s| LogicalSize::new(s.x, s.y));
window.set_max_inner_size(s);
}
if opts.theme != last.theme {
window.set_theme(opts.theme);
}
if opts.title != last.title {
window.set_title(&opts.title);
}
if opts.transparent != last.transparent {
window.set_transparent(opts.transparent);
}
// compare the resource version and uuid. These will get changed
// when the image is reloaded
let opts_icon = opts.window_icon.as_ref()
.map(|i| (i.version(), i.uuid()));
let last_icon = last.window_icon.as_ref()
.map(|i| (i.version(), i.uuid()));
if opts_icon != last_icon {
todo!("cannot set window icon yet");
}
if opts.window_level != last.window_level {
window.set_window_level(opts.window_level);
}
if opts.physical_cursor_position != last.physical_cursor_position && opts.physical_cursor_position.is_some() {
let pos = opts.physical_cursor_position.unwrap();
let pos = PhysicalPosition::new(pos.x, pos.y);
if let Err(e) = window.set_cursor_position(pos) {
error!("failed to set cursor position: {}", e);
}
}
last.last = opts.clone();
}
Ok(())
}
impl Plugin for WindowPlugin {
fn setup(&mut self, app: &mut crate::game::App) {
app.with_system("window_sync", window_sync_system, &[]);
}
}

View file

@ -1,34 +0,0 @@
use lyra_math::Transform;
use lyra_resource::{optionally_add_to_dep, ResourceData, UntypedResHandle};
use super::Mesh;
use crate::ResHandle;
/// A Node in the Gltf file
#[derive(Clone, Default)]
pub struct GltfNode {
pub name: Option<String>,
pub mesh: Option<ResHandle<Mesh>>,
pub transform: Transform,
pub children: Vec<GltfNode>,
}
impl ResourceData for GltfNode {
fn dependencies(&self) -> Vec<crate::UntypedResHandle> {
let mut deps: Vec<UntypedResHandle> = self.children.iter()
.flat_map(|c| c.mesh.as_ref().map(|h| h.untyped_clone()))
.collect();
optionally_add_to_dep(&mut deps, &self.mesh);
deps
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}

View file

@ -1,22 +0,0 @@
#[derive(Clone, Copy, PartialEq)]
pub struct Area<P, S>
where
P: Clone + Copy + PartialEq,
S: Clone + Copy + PartialEq,
{
pub position: P,
pub size: S
}
impl<P, S> Area<P, S>
where
P: Clone + Copy + PartialEq,
S: Clone + Copy + PartialEq,
{
pub fn new(pos: P, size: S) -> Self {
Self {
position: pos,
size,
}
}
}

View file

@ -1,2 +0,0 @@
mod rect;
pub use rect::*;

View file

@ -1,113 +0,0 @@
use glam::IVec2;
#[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct IRect {
pub min: IVec2,
pub max: IVec2,
}
impl IRect {
pub const ZERO: IRect = IRect {
min: IVec2::ZERO,
max: IVec2::ZERO,
};
pub fn new(x1: i32, y1: i32, x2: i32, y2: i32) -> Self {
Self {
min: IVec2::new(x1, y1),
max: IVec2::new(x2, y2),
}
}
pub fn from_vec(min: IVec2, max: IVec2) -> Self {
Self { min, max }
}
pub fn dimensions(&self) -> IVec2 {
self.max - self.min
}
}
impl std::ops::Add for IRect {
type Output = IRect;
fn add(self, rhs: Self) -> Self::Output {
IRect::from_vec(self.min + rhs.min, self.max + rhs.max)
}
}
impl std::ops::AddAssign for IRect {
fn add_assign(&mut self, rhs: Self) {
self.min += rhs.min;
self.max += rhs.max;
}
}
impl std::ops::Sub for IRect {
type Output = IRect;
fn sub(self, rhs: Self) -> Self::Output {
IRect::from_vec(self.min - rhs.min, self.max - rhs.max)
}
}
impl std::ops::SubAssign for IRect {
fn sub_assign(&mut self, rhs: Self) {
self.min -= rhs.min;
self.max -= rhs.max;
}
}
impl std::ops::Mul for IRect {
type Output = IRect;
fn mul(self, rhs: Self) -> Self::Output {
IRect::from_vec(self.min * rhs.min, self.max * rhs.max)
}
}
impl std::ops::MulAssign for IRect {
fn mul_assign(&mut self, rhs: Self) {
self.min *= rhs.min;
self.max *= rhs.max;
}
}
impl std::ops::Div for IRect {
type Output = IRect;
fn div(self, rhs: Self) -> Self::Output {
IRect::from_vec(self.min / rhs.min, self.max / rhs.max)
}
}
impl std::ops::DivAssign for IRect {
fn div_assign(&mut self, rhs: Self) {
self.min /= rhs.min;
self.max /= rhs.max;
}
}
impl std::ops::Rem for IRect {
type Output = IRect;
fn rem(self, rhs: Self) -> Self::Output {
Self::from_vec(self.min % rhs.min, self.max % self.max)
}
}
impl std::ops::RemAssign for IRect {
fn rem_assign(&mut self, rhs: Self) {
self.min %= rhs.min;
self.max %= rhs.max;
}
}
impl std::ops::Neg for IRect {
type Output = IRect;
fn neg(self) -> Self::Output {
Self::from_vec(-self.min, -self.max)
}
}

View file

@ -1,129 +0,0 @@
use glam::Vec2;
#[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Rect {
pub min: Vec2,
pub max: Vec2,
}
impl Rect {
pub const ZERO: Rect = Rect {
min: Vec2::ZERO,
max: Vec2::ZERO,
};
pub fn new(x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
Self {
min: Vec2::new(x1, y1),
max: Vec2::new(x2, y2),
}
}
pub fn from_vec(min: Vec2, max: Vec2) -> Self {
Self { min, max }
}
pub fn dimensions(&self) -> Vec2 {
(self.max - self.min).abs()
}
pub fn as_urect(&self) -> crate::u32::URect {
self.clone().into()
}
}
impl From<crate::u32::URect> for Rect {
fn from(value: crate::u32::URect) -> Self {
Self::from_vec(value.min.as_vec2(), value.max.as_vec2())
}
}
impl From<crate::i32::IRect> for Rect {
fn from(value: crate::i32::IRect) -> Self {
Self::from_vec(value.min.as_vec2(), value.max.as_vec2())
}
}
impl std::ops::Add for Rect {
type Output = Rect;
fn add(self, rhs: Self) -> Self::Output {
Rect::from_vec(self.min + rhs.min, self.max + rhs.max)
}
}
impl std::ops::AddAssign for Rect {
fn add_assign(&mut self, rhs: Self) {
self.min += rhs.min;
self.max += rhs.max;
}
}
impl std::ops::Sub for Rect {
type Output = Rect;
fn sub(self, rhs: Self) -> Self::Output {
Rect::from_vec(self.min - rhs.min, self.max - rhs.max)
}
}
impl std::ops::SubAssign for Rect {
fn sub_assign(&mut self, rhs: Self) {
self.min -= rhs.min;
self.max -= rhs.max;
}
}
impl std::ops::Mul for Rect {
type Output = Rect;
fn mul(self, rhs: Self) -> Self::Output {
Rect::from_vec(self.min * rhs.min, self.max * rhs.max)
}
}
impl std::ops::MulAssign for Rect {
fn mul_assign(&mut self, rhs: Self) {
self.min *= rhs.min;
self.max *= rhs.max;
}
}
impl std::ops::Div for Rect {
type Output = Rect;
fn div(self, rhs: Self) -> Self::Output {
Rect::from_vec(self.min / rhs.min, self.max / rhs.max)
}
}
impl std::ops::DivAssign for Rect {
fn div_assign(&mut self, rhs: Self) {
self.min /= rhs.min;
self.max /= rhs.max;
}
}
impl std::ops::Rem for Rect {
type Output = Rect;
fn rem(self, rhs: Self) -> Self::Output {
Rect::from_vec(self.min % rhs.min, self.max % self.max)
}
}
impl std::ops::RemAssign for Rect {
fn rem_assign(&mut self, rhs: Self) {
self.min %= rhs.min;
self.max %= rhs.max;
}
}
impl std::ops::Neg for Rect {
type Output = Rect;
fn neg(self) -> Self::Output {
Self::from_vec(-self.min, -self.max)
}
}

View file

@ -1,2 +0,0 @@
mod rect;
pub use rect::*;

View file

@ -1,121 +0,0 @@
use glam::UVec2;
#[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct URect {
pub min: UVec2,
pub max: UVec2,
}
impl URect {
pub const ZERO: URect = URect {
min: UVec2::ZERO,
max: UVec2::ZERO,
};
pub fn new(x1: u32, y1: u32, x2: u32, y2: u32) -> Self {
Self {
min: UVec2::new(x1, y1),
max: UVec2::new(x2, y2),
}
}
pub fn from_vec(min: UVec2, max: UVec2) -> Self {
Self { min, max }
}
pub fn dimensions(&self) -> UVec2 {
self.max - self.min
}
pub fn as_rect(&self) -> crate::Rect {
crate::Rect::from(*self)
}
}
impl From<crate::Rect> for URect {
fn from(value: crate::Rect) -> Self {
Self::from_vec(value.min.as_uvec2(), value.max.as_uvec2())
}
}
impl From<crate::i32::IRect> for URect {
fn from(value: crate::i32::IRect) -> Self {
Self::from_vec(value.min.as_uvec2(), value.max.as_uvec2())
}
}
impl std::ops::Add for URect {
type Output = URect;
fn add(self, rhs: Self) -> Self::Output {
URect::from_vec(self.min + rhs.min, self.max + rhs.max)
}
}
impl std::ops::AddAssign for URect {
fn add_assign(&mut self, rhs: Self) {
self.min += rhs.min;
self.max += rhs.max;
}
}
impl std::ops::Sub for URect {
type Output = URect;
fn sub(self, rhs: Self) -> Self::Output {
URect::from_vec(self.min - rhs.min, self.max - rhs.max)
}
}
impl std::ops::SubAssign for URect {
fn sub_assign(&mut self, rhs: Self) {
self.min -= rhs.min;
self.max -= rhs.max;
}
}
impl std::ops::Mul for URect {
type Output = URect;
fn mul(self, rhs: Self) -> Self::Output {
URect::from_vec(self.min * rhs.min, self.max * rhs.max)
}
}
impl std::ops::MulAssign for URect {
fn mul_assign(&mut self, rhs: Self) {
self.min *= rhs.min;
self.max *= rhs.max;
}
}
impl std::ops::Div for URect {
type Output = URect;
fn div(self, rhs: Self) -> Self::Output {
URect::from_vec(self.min / rhs.min, self.max / rhs.max)
}
}
impl std::ops::DivAssign for URect {
fn div_assign(&mut self, rhs: Self) {
self.min /= rhs.min;
self.max /= rhs.max;
}
}
impl std::ops::Rem for URect {
type Output = URect;
fn rem(self, rhs: Self) -> Self::Output {
Self::from_vec(self.min % rhs.min, self.max % self.max)
}
}
impl std::ops::RemAssign for URect {
fn rem_assign(&mut self, rhs: Self) {
self.min %= rhs.min;
self.max %= rhs.max;
}
}

View file

@ -1,158 +0,0 @@
use lyra_math::Angle;
use lyra_reflect_derive::{impl_reflect_simple_struct, impl_reflect_trait_value};
use crate::{lyra_engine, Enum, Method, Reflect, ReflectMut, ReflectRef};
impl_reflect_simple_struct!(lyra_math::Vec2, fields(x = f32, y = f32));
impl_reflect_simple_struct!(lyra_math::Vec3, fields(x = f32, y = f32, z = f32));
impl_reflect_simple_struct!(lyra_math::Vec4, fields(x = f32, y = f32, z = f32, w = f32));
impl_reflect_simple_struct!(lyra_math::UVec2, fields(x = u32, y = u32));
impl_reflect_simple_struct!(lyra_math::UVec3, fields(x = u32, y = u32, z = u32));
impl_reflect_simple_struct!(lyra_math::UVec4, fields(x = u32, y = u32, z = u32, w = u32));
impl_reflect_simple_struct!(lyra_math::IVec2, fields(x = i32, y = i32));
impl_reflect_simple_struct!(lyra_math::IVec3, fields(x = i32, y = i32, z = i32));
impl_reflect_simple_struct!(lyra_math::IVec4, fields(x = i32, y = i32, z = i32, w = i32));
impl_reflect_simple_struct!(lyra_math::Quat, fields(x = f32, y = f32, z = f32, w = f32));
impl_reflect_simple_struct!(lyra_math::Rect, fields(min = lyra_math::Vec2, max = lyra_math::Vec2));
impl_reflect_simple_struct!(lyra_math::URect, fields(min = lyra_math::UVec2, max = lyra_math::UVec2));
impl_reflect_simple_struct!(lyra_math::IRect, fields(min = lyra_math::IVec2, max = lyra_math::IVec2));
impl_reflect_simple_struct!(
lyra_math::Transform,
fields(
translation = lyra_math::Vec3,
rotation = lyra_math::Quat,
scale = lyra_math::Vec3
)
);
impl_reflect_trait_value!(lyra_math::Mat4);
impl Reflect for Angle {
fn name(&self) -> String {
"Angle".into()
}
fn type_id(&self) -> std::any::TypeId {
std::any::TypeId::of::<Self>()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn as_boxed_any(self: Box<Self>) -> Box<dyn std::any::Any> {
self
}
fn apply(&mut self, val: &dyn Reflect) {
if let ReflectRef::Enum(e) = val.reflect_ref() {
let s = e.as_any().downcast_ref::<Self>()
.expect("cannot apply mismatched reflected enum");
*self = *s;
} else {
panic!("Provided value was not an enum!");
}
}
fn clone_inner(&self) -> Box<dyn Reflect> {
Box::new(self.clone())
}
fn reflect_ref(&self) -> crate::ReflectRef {
ReflectRef::Enum(self)
}
fn reflect_mut(&mut self) -> crate::ReflectMut {
ReflectMut::Enum(self)
}
fn reflect_val(&self) -> &dyn Reflect {
self
}
fn reflect_val_mut(&mut self) -> &mut dyn Reflect {
self
}
}
impl Enum for Angle {
fn field(&self, _: &str) -> Option<&dyn Reflect> {
// no struct variants
None
}
fn field_mut(&mut self, _: &str) -> Option<&mut dyn Reflect> {
// no struct variants
None
}
fn field_at(&self, idx: usize) -> Option<&dyn Reflect> {
// all variants only have one tuple field
if idx != 0 {
return None;
}
match self {
Angle::Degrees(v) => Some(v),
Angle::Radians(v) => Some(v),
}
}
fn field_at_mut(&mut self, idx: usize) -> Option<&mut dyn Reflect> {
// all variants only have one tuple field
if idx != 0 {
return None;
}
match self {
Angle::Degrees(v) => Some(v),
Angle::Radians(v) => Some(v),
}
}
fn field_name_at(&self, _: usize) -> Option<String> {
// no struct variants
None
}
fn has_field(&self, _: &str) -> bool {
// no struct variants
false
}
fn fields_len(&self) -> usize {
1
}
fn variants_len(&self) -> usize {
2
}
fn variant_name(&self) -> String {
match self {
Angle::Degrees(_) => "degrees".into(),
Angle::Radians(_) => "radians".into(),
}
}
fn variant_index(&self) -> usize {
match self {
Angle::Degrees(_) => 0,
Angle::Radians(_) => 1,
}
}
fn is_variant_name(&self, name: &str) -> bool {
self.variant_name() == name
}
fn variant_type(&self) -> crate::EnumType {
crate::EnumType::Tuple
}
}

View file

@ -1,31 +0,0 @@
[package]
name = "lyra-resource"
version = "0.0.1"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
lyra-ecs = { path = "../lyra-ecs", features = [ "math" ] }
lyra-reflect = { path = "../lyra-reflect", features = [ "math" ] }
lyra-math = { path = "../lyra-math" }
anyhow = "1.0.75"
base64 = "0.21.4"
crossbeam = { version = "0.8.4", features = [ "crossbeam-channel" ] }
glam = "0.29.0"
image = "0.25.2"
# not using custom matcher, or file type from file path
infer = { version = "0.15.0", default-features = false }
mime = "0.3.17"
notify = "6.1.1"
notify-debouncer-full = "0.3.1"
#notify = { version = "6.1.1", default-features = false, features = [ "fsevent-sys", "macos_fsevent" ]} # disables crossbeam-channel
percent-encoding = "2.3.0"
thiserror = "1.0.48"
tracing = "0.1.37"
uuid = { version = "1.4.1", features = ["v4"] }
instant = "0.1"
async-std = "1.12.0"
[dev-dependencies]
rand = "0.8.5"

View file

@ -1,339 +0,0 @@
use std::ops::Deref;
use crate::{lyra_engine, SceneGraph};
use lyra_ecs::{
query::{filter::Without, Entities, View},
relation::{ChildOf, RelationOriginComponent},
Bundle, Component, Entity, World,
};
use lyra_math::Transform;
use lyra_reflect::Reflect;
use lyra_resource::ResHandle;
/// The world transform of an entity.
///
/// A Transform represents the relative position of the entity to its parent entity, while
/// a world transform is the position relative to the World. When wanting to move an entity,
/// you should use its [`Transform`]. You cannot mutate [`WorldTransform`] as its managed completey
/// by the [`system_update_world_transforms`] system. For the WorldTransform to work properly, you
/// must have both a [`Transform`] and [`WorldTransform`] on the entities in the scene.
#[derive(Debug, Copy, Clone, PartialEq, Default, Component, Reflect)]
pub struct WorldTransform(pub(crate) Transform);
impl Deref for WorldTransform {
type Target = Transform;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<Transform> for WorldTransform {
fn from(value: Transform) -> Self {
Self(value)
}
}
/// A [`Bundle`] of a [`Transform`] and [`WorldTransform`].
///
/// Example:
/// ```
/// world.spawn((
/// tilemap,
/// TransformBundle::from(Transform::from_xyz(0.0, 0.0, -10.0)),
/// GroundTileMap,
/// ));
/// ```
///
/// See [`WorldTransform`]
#[derive(Debug, Copy, Clone, PartialEq, Default, Reflect, Bundle)]
pub struct TransformBundle {
world: WorldTransform,
local: Transform,
}
impl From<Transform> for TransformBundle {
fn from(value: Transform) -> Self {
Self {
world: WorldTransform::from(value),
local: value,
}
}
}
/// A system that updates the [`WorldTransform`]'s for entities and their children.
///
/// For entities without parents, this will update world transform to match local transform.
/// For any children entities, their [`WorldTransform`]s will be updated to reflect the changes
/// of its parent entity.
pub fn system_update_world_transforms(
world: &World,
view: View<
(
Entities,
&mut WorldTransform,
&Transform,
Option<&ResHandle<SceneGraph>>,
),
Without<RelationOriginComponent<ChildOf>>,
>,
) -> anyhow::Result<()> {
for (en, mut world_tran, tran, scene) in view.into_iter() {
world_tran.0 = *tran;
recurse_update_trans(world, &world_tran, en)?;
// if there was a scene, update it as well
if let Some(scene) = scene {
if let Some(scene) = scene.data_ref() {
let sworld = &scene.world;
let view = sworld.filtered_view::<(
Entities,
&mut WorldTransform,
&Transform,
Option<&ResHandle<SceneGraph>>,
), Without<RelationOriginComponent<ChildOf>>>();
system_update_world_transforms(&scene.world, view)?;
}
}
}
Ok(())
}
fn recurse_update_trans(
world: &World,
parent_transform: &WorldTransform,
entity: Entity,
) -> anyhow::Result<()> {
// store entities and their world transform to process outside of the view.
// it must be done after to avoid attempts of multiple mutable borrows to the archetype column
// with WorldTransform.
let mut next_entities = vec![];
for ((en, mut world_tran, tran, scene), _) in world
.view::<(
Entities,
&mut WorldTransform,
&Transform,
Option<&ResHandle<SceneGraph>>,
)>()
.relates_to::<ChildOf>(entity)
.into_iter()
{
world_tran.0 = *tran;
world_tran.0.translation = parent_transform.0.translation + tran.translation;
next_entities.push((en, world_tran.0.clone()));
// if there was a scene, update it as well
if let Some(scene) = scene {
if let Some(scene) = scene.data_ref() {
let sworld = &scene.world;
let view = sworld.filtered_view::<(
Entities,
&mut WorldTransform,
&Transform,
Option<&ResHandle<SceneGraph>>,
), Without<RelationOriginComponent<ChildOf>>>();
system_update_world_transforms(&scene.world, view)?;
}
}
}
for (en, pos) in next_entities.into_iter() {
recurse_update_trans(world, &WorldTransform(pos), en)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use lyra_ecs::{
query::{filter::Without, Entities},
relation::{ChildOf, RelationOriginComponent},
World,
};
use lyra_math::Transform;
use lyra_resource::ResHandle;
use crate::{system_update_world_transforms, SceneGraph, WorldTransform};
#[test]
fn test_system() {
let mut world = World::new();
let parent = world.spawn((
WorldTransform::default(),
Transform::from_xyz(10.0, 10.0, 10.0),
));
let child = world.spawn((
WorldTransform::default(),
Transform::from_xyz(15.0, 15.0, 15.0),
));
world.add_relation(child, ChildOf, parent);
let view = world.filtered_view::<(
Entities,
&mut WorldTransform,
&Transform,
Option<&ResHandle<SceneGraph>>,
), Without<RelationOriginComponent<ChildOf>>>();
system_update_world_transforms(&world, view).unwrap();
let g = world.view_one::<&WorldTransform>(child).unwrap();
assert_eq!(**g, Transform::from_xyz(25.0, 25.0, 25.0));
}
#[test]
fn test_system_many_entities() {
let mut world = World::new();
let parent = world.spawn((
WorldTransform::default(),
Transform::from_xyz(10.0, 10.0, 10.0),
));
let mut children = vec![];
let mut base_offset = 15.0;
for _ in 0..10 {
let en = world.spawn((
WorldTransform::default(),
Transform::from_xyz(base_offset, base_offset, base_offset),
));
world.add_relation(en, ChildOf, parent);
base_offset += 10.0;
children.push(en);
}
let second_child = world.spawn((
WorldTransform::default(),
Transform::from_xyz(5.0, 3.0, 8.0),
));
world.add_relation(second_child, ChildOf, parent);
let view = world.filtered_view::<(
Entities,
&mut WorldTransform,
&Transform,
Option<&ResHandle<SceneGraph>>,
), Without<RelationOriginComponent<ChildOf>>>();
system_update_world_transforms(&world, view).unwrap();
let mut base_offset = 25.0;
for child in children.into_iter() {
let g = world.view_one::<&WorldTransform>(child).unwrap();
println!("Child {:?} at {:?}", child, g.translation);
assert_eq!(
**g,
Transform::from_xyz(base_offset, base_offset, base_offset)
);
base_offset += 10.0;
}
}
#[test]
fn test_system_many_children() {
let mut world = World::new();
let parent = world.spawn((
WorldTransform::default(),
Transform::from_xyz(10.0, 10.0, 10.0),
));
let first_child = world.spawn((
WorldTransform::default(),
Transform::from_xyz(15.0, 15.0, 15.0),
));
world.add_relation(first_child, ChildOf, parent);
let sec_chi = world.spawn((
WorldTransform::default(),
Transform::from_xyz(155.0, 23.0, 6.0),
));
world.add_relation(sec_chi, ChildOf, first_child);
let thir_chi = world.spawn((
WorldTransform::default(),
Transform::from_xyz(51.0, 85.0, 17.0),
));
world.add_relation(thir_chi, ChildOf, sec_chi);
let four_child = world.spawn((
WorldTransform::default(),
Transform::from_xyz(24.0, 61.0, 65.0),
));
world.add_relation(four_child, ChildOf, thir_chi);
let five_child = world.spawn((
WorldTransform::default(),
Transform::from_xyz(356.0, 54.0, 786.0),
));
world.add_relation(five_child, ChildOf, four_child);
let view = world.filtered_view::<(
Entities,
&mut WorldTransform,
&Transform,
Option<&ResHandle<SceneGraph>>,
), Without<RelationOriginComponent<ChildOf>>>();
system_update_world_transforms(&world, view).unwrap();
let g = world.view_one::<&WorldTransform>(five_child).unwrap();
assert_eq!(**g, Transform::from_xyz(611.0, 248.0, 899.0));
}
#[test]
fn test_system_branched_children() {
let mut world = World::new();
let parent = world.spawn((
WorldTransform::default(),
Transform::from_xyz(10.0, 10.0, 10.0),
));
let first_child = world.spawn((
WorldTransform::default(),
Transform::from_xyz(15.0, 15.0, 15.0),
));
world.add_relation(first_child, ChildOf, parent);
let sec_chi = world.spawn((
WorldTransform::default(),
Transform::from_xyz(155.0, 23.0, 6.0),
));
world.add_relation(sec_chi, ChildOf, first_child);
let thir_chi = world.spawn((
WorldTransform::default(),
Transform::from_xyz(51.0, 85.0, 17.0),
));
world.add_relation(thir_chi, ChildOf, first_child);
let four_child = world.spawn((
WorldTransform::default(),
Transform::from_xyz(24.0, 61.0, 65.0),
));
world.add_relation(four_child, ChildOf, thir_chi);
let five_child = world.spawn((
WorldTransform::default(),
Transform::from_xyz(356.0, 54.0, 786.0),
));
world.add_relation(five_child, ChildOf, sec_chi);
let view = world.filtered_view::<(
Entities,
&mut WorldTransform,
&Transform,
Option<&ResHandle<SceneGraph>>,
), Without<RelationOriginComponent<ChildOf>>>();
system_update_world_transforms(&world, view).unwrap();
let g = world.view_one::<&WorldTransform>(five_child).unwrap();
assert_eq!(**g, Transform::from_xyz(536.0, 102.0, 817.0));
let g = world.view_one::<&WorldTransform>(thir_chi).unwrap();
assert_eq!(**g, Transform::from_xyz(76.0, 110.0, 42.0));
let g = world.view_one::<&WorldTransform>(four_child).unwrap();
assert_eq!(**g, Transform::from_xyz(100.0, 171.0, 107.0));
}
}

View file

@ -1,221 +0,0 @@
use quote::quote;
use syn::{parenthesized, token, Token};
pub(crate) enum FieldType {
Unknown,
Type(syn::Path),
Wrapped(syn::Path),
}
impl FieldType {
pub fn is_unknown(&self) -> bool {
matches!(self, FieldType::Unknown)
}
pub fn is_wrapped(&self) -> bool {
matches!(self, FieldType::Wrapped(_))
}
pub fn get_type_path(&self) -> Option<&syn::Path> {
match self {
FieldType::Unknown => None,
FieldType::Type(path) => Some(path),
FieldType::Wrapped(path) => Some(path),
}
}
}
pub(crate) struct Field {
pub field: syn::Ident,
pub field_ty: FieldType,
pub skip_setter: bool,
pub setter: Option<syn::Block>,
pub getter: Option<syn::Block>,
pub wrap_with: Option<syn::Path>,
}
impl Field {
fn parse_extended(input: syn::parse::ParseStream) -> syn::Result<Self> {
let field_name = input.parse()?;
let fty = if input.peek(Token![:]) {
let _col: Token![:] = input.parse()?;
let s: syn::Path = input.parse()?;
let mut fty = FieldType::Type(s.clone());
if let Some(ident) = s.get_ident() {
if ident.to_string() == "wrap" {
let content;
let _parens: token::Paren = parenthesized!(content in input);
fty = FieldType::Wrapped(content.parse()?);
}
}
fty
} else {
FieldType::Unknown
};
let mut s = Self {
field: field_name,
field_ty: fty,
skip_setter: false,
setter: None,
getter: None,
wrap_with: None,
};
while input.peek(Token![,]) {
let _: Token![,] = input.parse()?;
if input.peek(syn::Ident) {
let ident: syn::Ident = input.parse()?;
let ident_str = ident.to_string();
let ident_str = ident_str.as_str();
match ident_str {
"skip_set" => {
s.skip_setter = true;
}
"set" => {
let _eq: Token![=] = input.parse()?;
s.setter = Some(input.parse()?);
}
"get" => {
let _eq: Token![=] = input.parse()?;
s.getter = Some(input.parse()?);
}
"wrap_with" => {
let _eq: Token![=] = input.parse()?;
s.wrap_with = Some(input.parse()?);
}
_ => {
return Err(syn::Error::new_spanned(ident, "unknown wrapper command"));
}
}
}
}
if (s.getter.is_some() || s.setter.is_some()) && s.field_ty.is_wrapped() {
return Err(syn::Error::new(
input.span(),
"cannot specify custom getter or setter \
with wrapped type",
));
}
Ok(s)
}
pub fn table_setter(&self) -> proc_macro2::TokenStream {
if self.skip_setter {
return quote!();
}
let ident = &self.field;
match &self.setter {
Some(set) => quote! {
table.set(stringify!(#ident), #set)?;
},
None => {
if let Some(wrap_with) = &self.wrap_with {
quote! {
let v = #wrap_with::into_lua(lua, &self.#ident)?;
table.set(stringify!(#ident), v)?;
}
} else if let Some(ty) = self.field_ty.get_type_path() {
let arg = if self.field_ty.is_wrapped() {
quote!(#ty(self.#ident.clone()))
} else {
quote!(self.#ident.clone())
};
quote! {
table.set(stringify!(#ident), #arg)?;
}
} else {
syn::Error::new_spanned(
ident,
format!("field type not specified: '{}'", ident.to_string()),
)
.into_compile_error()
}
}
}
}
pub fn table_getter(&self) -> proc_macro2::TokenStream {
let ident = &self.field;
match &self.getter {
Some(get) => {
quote! {
let #ident = #get;
}
}
None => match &self.wrap_with {
Some(wrap_with) => {
quote! {
let #ident = {
let t = table.get(stringify!(#ident))?;
#wrap_with::from_lua(_lua, &t)?
};
}
}
None => {
let ty = self
.field_ty
.get_type_path()
.expect("no field type specified");
quote! {
let #ident: #ty = table.get(stringify!(#ident))?;
}
}
},
}
}
}
impl syn::parse::Parse for Field {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
if input.peek(token::Paren) {
let content;
let _parens: token::Paren = parenthesized!(content in input);
Self::parse_extended(&content)
} else {
let field_name = input.parse()?;
let fty = if input.peek(Token![:]) {
let _col: Token![:] = input.parse()?;
let s: syn::Path = input.parse()?;
let mut fty = FieldType::Type(s.clone());
if let Some(ident) = s.get_ident() {
if ident.to_string() == "wrap" {
let content;
let _parens: token::Paren = parenthesized!(content in input);
fty = FieldType::Wrapped(content.parse()?);
}
}
fty
} else {
FieldType::Unknown
};
Ok(Self {
field: field_name,
field_ty: fty,
skip_setter: false,
setter: None,
getter: None,
wrap_with: None,
})
}
}
}

View file

@ -1,554 +0,0 @@
use quote::{quote, ToTokens};
use syn::{parse_macro_input, spanned::Spanned};
use crate::{
field::{Field, FieldType},
to_lua_macro::{ReflectType, StructType},
};
pub(crate) fn derive_lua_convert_impl(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(item as syn::DeriveInput);
match &input.data {
syn::Data::Struct(s) => lua_convert_struct_impl(&input, s),
_ => todo!(),
}
}
fn lua_convert_struct_impl(
input: &syn::DeriveInput,
struc: &syn::DataStruct,
) -> proc_macro::TokenStream {
let crate_path = quote!(lyra_engine::script);
let mlua = quote!(#crate_path::lua::mlua);
let dattrs = DeriveAttrs::new(&input.attrs);
let ty = &input.ident;
let lua_name = dattrs.rename().cloned().unwrap_or(ty.to_string());
let reflect_type = dattrs.reflect_type();
let struct_type = match struc.fields {
syn::Fields::Named(_) => StructType::Fields,
syn::Fields::Unnamed(_) => StructType::Tuple,
syn::Fields::Unit => todo!("Handle unit structs"),
};
let fields = struc
.fields
.iter()
.map(|f| {
let attrs = FieldAttrs::new(&f.attrs);
let ident = f.ident.clone().expect("TODO: support enum structs");
let ty = if let Some(wrap) = attrs.wrapper() {
FieldType::Wrapped(wrap.clone())
} else {
match &f.ty {
syn::Type::Path(path) => FieldType::Type(path.path.clone()),
_ => todo!("struc.fields.map handle invalid field type"),
}
};
Field {
field: ident,
field_ty: ty,
skip_setter: false,
getter: attrs.get().cloned(),
setter: attrs.set().cloned(),
wrap_with: attrs.wrap_with().cloned(),
}
})
.collect::<Vec<Field>>();
let field_getters_iter = fields.iter().map(Field::table_getter);
let field_setters_iter = fields.iter().map(Field::table_setter);
let lua_functions = dattrs.lua_functions();
let lua_functions = lua_functions.into_iter().flatten().map(|lfn| {
let fn_name = &lfn.name;
let tokens = lfn.as_tokens(mlua.clone(), &ty);
quote! {
table.set(#fn_name, #tokens)?;
}
});
let struct_creator = wrapper_creation(ty, struct_type, None, &fields);
let reflect_fns = if reflect_type == ReflectType::None {
quote!()
} else {
let reflect_fn = get_reflect_lua_functions(&crate_path, &reflect_type, ty, true);
let reflect_type_fn = get_reflect_lua_functions(&crate_path, &reflect_type, ty, false);
quote! {
table.set(
#crate_path::lua::FN_NAME_INTERNAL_REFLECT,
lua.create_function(|_, this: Self| {
#reflect_fn
})?,
)?;
table.set(
#crate_path::lua::FN_NAME_INTERNAL_REFLECT_TYPE,
lua.create_function(|_, ()| {
#reflect_type_fn
})?,
)?;
}
};
quote! {
impl #mlua::FromLua for #ty {
fn from_lua(val: #mlua::Value, _lua: &#mlua::Lua) -> #mlua::Result<Self> {
let ty = val.type_name();
let table = val.as_table().ok_or(#mlua::Error::FromLuaConversionError {
from: ty,
to: "Table".into(),
message: Some("expected Table".into()),
})?;
#(
#field_getters_iter
)*
Ok(#struct_creator)
}
}
impl #mlua::IntoLua for #ty {
fn into_lua(self, lua: &#mlua::Lua) -> #mlua::Result<#mlua::Value> {
use #crate_path::lua::LuaWrapper;
let table = lua.create_table()?;
#(
#field_setters_iter
)*
#reflect_fns
table.set(#mlua::MetaMethod::Type.name(), #lua_name)?;
Self::extend_table(lua, &table)?;
Ok(#mlua::Value::Table(table))
}
}
impl #crate_path::lua::LuaWrapper for #ty {
type Wrap = #ty;
#[inline(always)]
fn wrapped_type_id() -> std::any::TypeId {
std::any::TypeId::of::<#ty>()
}
#[inline(always)]
fn into_wrapped(self) -> Self::Wrap {
self
}
#[inline(always)]
fn extend_table(lua: &#mlua::Lua, table: &#mlua::Table) -> #mlua::Result<()> {
#(
#lua_functions
)*
Ok(())
}
}
}
.into_token_stream()
.into()
}
#[derive(Copy, Clone, PartialEq, Debug)]
enum LuaFuncKind {
Function,
Method,
MethodMut,
#[allow(dead_code)]
MetaMethod,
#[allow(dead_code)]
MetaMethodMut,
}
#[derive(Clone)]
struct LuaFunc {
kind: LuaFuncKind,
name: String,
args: Vec<syn::PatType>,
block: syn::Block,
}
impl LuaFunc {
fn as_tokens(
&self,
mlua: proc_macro2::TokenStream,
self_type: &syn::Ident,
) -> proc_macro2::TokenStream {
let fn_block = &self.block;
let arg_names_iter = self.args.iter().map(|a| &*a.pat);
let arg_types_iter = self.args.iter().map(|a| &*a.ty);
let closure = match self.kind {
LuaFuncKind::Function => quote! {
|_lua: &#mlua::Lua, ( #(#arg_names_iter),* ): ( #(#arg_types_iter),* )| #fn_block
},
LuaFuncKind::Method => quote! {
|_lua: &#mlua::Lua, ( _self, #(#arg_names_iter),* ): ( &#self_type, #(#arg_types_iter),* )| #fn_block
},
LuaFuncKind::MethodMut => quote! {
|_lua: &#mlua::Lua, ( _self, #(#arg_names_iter),* ): ( &mut #self_type, #(#arg_types_iter),* )| #fn_block
},
LuaFuncKind::MetaMethod => todo!(),
LuaFuncKind::MetaMethodMut => todo!(),
};
quote! {
lua.create_function(#closure)?
}
}
}
#[derive(Clone)]
enum DeriveAttr {
Rename(String),
ReflectType(ReflectType),
Functions(Vec<LuaFunc>),
}
impl syn::parse::Parse for DeriveAttr {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let ident: syn::Ident = input.parse()?;
let ident_str = ident.to_string().to_lowercase();
let _eq: syn::Token![=] = input.parse()?;
match ident_str.as_str() {
"rename" => {
let s: syn::LitStr = input.parse()?;
Ok(Self::Rename(s.value()))
}
"reflect" => {
let tyid: syn::Ident = input.parse()?;
let id = tyid.to_string().to_lowercase();
let id = id.as_str();
let ty = match id {
"component" => Ok(ReflectType::Component),
"resource" => Ok(ReflectType::Resource),
"none" => Ok(ReflectType::None),
_ => Err(syn::Error::new(
tyid.span(),
"unknown reflect type, expected 'component' or 'resource'",
)),
}?;
Ok(Self::ReflectType(ty))
}
"functions" => {
let block: syn::Block = input.parse()?;
let mut fns = vec![];
for s in block.stmts {
if let syn::Stmt::Item(syn::Item::Fn(fni)) = s {
// try to find the type of the function
let mut func_type = LuaFuncKind::Function;
let mut typed_args = vec![];
let mut fn_inputs = fni.sig.inputs.iter();
if let Some(first) = fn_inputs.next() {
match first {
syn::FnArg::Receiver(recv) => {
if recv.mutability.is_some() {
func_type = LuaFuncKind::MethodMut;
} else {
func_type = LuaFuncKind::Method;
}
}
syn::FnArg::Typed(ty) => {
typed_args.push(ty.clone());
}
}
}
for arg in fn_inputs {
match arg {
syn::FnArg::Typed(ty) => {
typed_args.push(ty.clone());
}
_ => {}
}
}
fns.push(LuaFunc {
kind: func_type,
name: fni.sig.ident.to_string(),
args: typed_args,
block: (*fni.block).clone(),
});
} else {
return Err(syn::Error::new(s.span(), "expected ItemFn"));
}
}
Ok(Self::Functions(fns))
}
_ => Err(syn::Error::new(
ident.span(),
format!("Unknown derive attribute flag: '{}'", ident_str),
)),
}
}
}
#[derive(Default, Clone)]
struct DeriveAttrs(Vec<DeriveAttr>);
impl DeriveAttrs {
fn new(attrs: &Vec<syn::Attribute>) -> Self {
let mut s = Self::default();
for value in attrs {
if !value.path().is_ident("lua") {
continue;
}
match &value.meta {
syn::Meta::Path(_) => todo!("handle invalid path field attribute"),
syn::Meta::List(list) => {
let f = list
.parse_args_with(
syn::punctuated::Punctuated::<_, syn::Token![,]>::parse_terminated,
)
.unwrap();
s.0.extend(f.into_iter());
}
syn::Meta::NameValue(_) => todo!("handle invalid name value field attribute"),
}
}
s
}
fn rename(&self) -> Option<&String> {
self.0.iter().find_map(|a| {
if let DeriveAttr::Rename(s) = a {
Some(s)
} else {
None
}
})
}
fn reflect_type(&self) -> ReflectType {
self.0
.iter()
.find_map(|a| {
if let DeriveAttr::ReflectType(r) = a {
Some(*r)
} else {
None
}
})
.unwrap_or_default()
}
fn lua_functions(&self) -> Option<&Vec<LuaFunc>> {
self.0.iter().find_map(|a| {
if let DeriveAttr::Functions(f) = a {
Some(f)
} else {
None
}
})
}
}
#[derive(Clone)]
enum FieldAttr {
Wrapper(syn::Path),
Set(syn::Block),
Get(syn::Block),
WrapWith(syn::Path),
}
impl syn::parse::Parse for FieldAttr {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let ident: syn::Ident = input.parse()?;
let ident_str = ident.to_string().to_lowercase();
let _eq: syn::Token![=] = input.parse()?;
match ident_str.as_str() {
"wrap" => Ok(Self::Wrapper(input.parse()?)),
"set" => Ok(Self::Set(input.parse()?)),
"get" => Ok(Self::Get(input.parse()?)),
"wrap_with" => Ok(Self::WrapWith(input.parse()?)),
_ => Err(syn::Error::new(
ident.span(),
format!("Unknown field attribute flag: '{}'", ident_str),
)),
}
}
}
#[derive(Default)]
struct FieldAttrs(Vec<FieldAttr>);
impl FieldAttrs {
fn new(attrs: &Vec<syn::Attribute>) -> Self {
let mut s = Self::default();
for value in attrs {
if !value.path().is_ident("lua") {
continue;
}
match &value.meta {
syn::Meta::Path(_) => todo!("handle invalid path field attribute"),
syn::Meta::List(list) => {
let f = list
.parse_args_with(
syn::punctuated::Punctuated::<_, syn::Token![,]>::parse_terminated,
)
.unwrap();
s.0 = f.into_iter().collect();
}
syn::Meta::NameValue(_) => todo!("handle invalid name value field attribute"),
}
}
s
}
fn wrapper(&self) -> Option<&syn::Path> {
self.0.iter().find_map(|a| {
if let FieldAttr::Wrapper(p) = a {
Some(p)
} else {
None
}
})
}
fn set(&self) -> Option<&syn::Block> {
self.0.iter().find_map(|a| {
if let FieldAttr::Set(b) = a {
Some(b)
} else {
None
}
})
}
fn get(&self) -> Option<&syn::Block> {
self.0.iter().find_map(|a| {
if let FieldAttr::Get(b) = a {
Some(b)
} else {
None
}
})
}
fn wrap_with(&self) -> Option<&syn::Path> {
self.0.iter().find_map(|a| {
if let FieldAttr::WrapWith(p) = a {
Some(p)
} else {
None
}
})
}
}
// impl From<&Vec<syn::Attribute>> for FieldAttrs {
// fn from(attrs: &Vec<syn::Attribute>) -> Self {
// let mut s = Self::default();
// for value in attrs {
// if !value.path().is_ident("lua") {
// continue;
// }
// match &value.meta {
// syn::Meta::Path(_) => todo!("handle invalid path field attribute"),
// syn::Meta::List(list) => {
// let f = list.parse_args_with(
// syn::punctuated::Punctuated::<FieldAttr, syn::Token![,]>::parse_terminated,
// ).unwrap();
// s.0 = f.into_iter().collect();
// }
// syn::Meta::NameValue(_) => todo!("handle invalid name value field attribute"),
// }
// s
// }
// }
pub(crate) fn get_reflect_lua_functions(
crate_path: &proc_macro2::TokenStream,
reflect_ty: &ReflectType,
ty: &syn::Ident,
set_data: bool,
) -> proc_macro2::TokenStream {
let data = if set_data {
quote!(Some(this.clone()))
} else {
quote!(None)
};
match reflect_ty {
ReflectType::Component => {
quote! {
Ok(#crate_path::ScriptBorrow::from_component::<#ty>(#data))
}
}
ReflectType::Resource => {
quote! {
Ok(#crate_path::ScriptBorrow::from_resource::<#ty>(#data))
}
}
ReflectType::None => {
unreachable!(
"to_lua_derive::get_reflect_lua_functions was called with ReflectType::None"
)
}
}
}
pub(crate) fn wrapper_creation(
ty: &syn::Ident,
struct_type: StructType,
create: Option<&syn::Block>,
fields: &Vec<Field>,
) -> proc_macro2::TokenStream {
match create {
Some(b) => quote!(#b),
None => {
let field_iter = fields.iter().map(|f| {
let ident = &f.field;
if f.field_ty.is_wrapped() && struct_type == StructType::Fields {
quote!(#ident: (*#ident).clone())
} else {
quote!(#ident)
}
});
match struct_type {
StructType::Fields => {
quote! {
#ty {
#(
#field_iter
),*
}
}
}
StructType::Tuple => {
quote! {
#ty( #(#field_iter),* )
}
}
}
}
}
}

View file

@ -1,355 +0,0 @@
use proc_macro2::Span;
use quote::{quote, ToTokens};
use syn::{braced, parenthesized, parse_macro_input, punctuated::Punctuated, token, Token};
use crate::{field::Field, FN_NAME_INTERNAL_REFLECT, FN_NAME_INTERNAL_REFLECT_TYPE};
pub(crate) fn wrapper_creation(
wrapper: &syn::Ident,
type_path: &syn::Path,
struct_type: StructType,
create: Option<&syn::Block>,
fields: &Vec<Field>,
) -> proc_macro2::TokenStream {
match create {
Some(b) => quote!(#b),
None => {
/* let field_iter = fields.iter().map(|f| match &f.field_ty {
crate::field::FieldType::Type(path) => quote!(#path),
crate::field::FieldType::Wrapped(path) => quote!(*#path),
_ => todo!()
}); */
let field_iter = fields.iter().map(|f| {
let ident = &f.field;
if f.field_ty.is_wrapped() && struct_type == StructType::Fields {
quote!(#ident: (*#ident).clone())
} else {
quote!(#ident)
}
});
match struct_type {
StructType::Fields => {
quote! {
#wrapper(#type_path {
#(
#field_iter
),*
})
}
}
StructType::Tuple => {
quote! {
#wrapper(#type_path( #(#field_iter),* ))
}
}
}
}
}
}
pub(crate) fn get_reflect_lua_functions(
crate_path: &proc_macro2::TokenStream,
ty: &ReflectType,
type_path: &syn::Path,
set_data: bool,
) -> proc_macro2::TokenStream {
let data = if set_data {
quote!(Some(this.into_wrapped()))
} else {
quote!(None)
};
match ty {
ReflectType::Component => {
quote! {
Ok(#crate_path::ScriptBorrow::from_component::<#type_path>(#data))
}
}
ReflectType::Resource => {
quote! {
Ok(#crate_path::ScriptBorrow::from_component::<#type_path>(#data))
}
}
ReflectType::None => {
unreachable!(
"to_lua_macro::get_reflect_lua_functions was called with ReflectType::None"
)
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ReflectType {
None,
#[default]
Component,
Resource,
}
/// The type of the wrapping struct
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StructType {
#[default]
Fields,
Tuple,
}
struct IntoLuaUsage {
type_path: syn::Path,
struct_type: StructType,
override_name: Option<syn::Ident>,
table_name: String,
derives: Vec<syn::Ident>,
fields: Vec<Field>,
create: Option<syn::Block>,
reflection_type: Option<ReflectType>,
}
impl syn::parse::Parse for IntoLuaUsage {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let type_path: syn::Path = input.parse()?;
let type_ident = &type_path
.segments
.last()
.expect("Failure to find typename in macro usage!")
.ident;
let lua_name = type_ident.to_string();
let mut s = Self {
type_path,
struct_type: StructType::Fields,
override_name: None,
table_name: lua_name,
derives: vec![],
fields: vec![],
create: None,
reflection_type: None,
};
while input.peek(Token![,]) {
let _: Token![,] = input.parse()?;
if input.peek(syn::Ident) {
let ident: syn::Ident = input.parse()?;
let ident_str = ident.to_string();
let ident_str = ident_str.as_str();
match ident_str {
"name" => {
let _eq: Token![=] = input.parse()?;
let name: syn::Ident = input.parse()?;
s.override_name = Some(name);
}
"struct_type" => {
let _eq: Token![=] = input.parse()?;
let st_token = input.parse::<syn::LitStr>()?;
let st_str = st_token.value().to_lowercase();
let st_str = st_str.as_str();
let st = match st_str {
"fields" => StructType::Fields,
"tuple" => StructType::Tuple,
_ => {
return Err(syn::Error::new_spanned(
st_token,
format!(
"unknown struct type: '{}', expected 'fields', or `tuple`",
st_str
),
))
}
};
s.struct_type = st;
}
"lua_name" => {
let _eq: Token![=] = input.parse()?;
s.table_name = input.parse::<syn::LitStr>()?.value();
}
"derives" => {
if input.peek(token::Paren) {
let content;
let _parens: token::Paren = parenthesized!(content in input);
let derives: Punctuated<syn::Ident, Token![,]> =
content.parse_terminated(syn::Ident::parse, Token![,])?;
s.derives = derives.into_iter().collect();
}
}
"fields" => {
let _eq: Token![=] = input.parse()?;
if input.peek(token::Brace) {
let content;
let _braced: token::Brace = braced!(content in input);
let terminated = content.parse_terminated(Field::parse, Token![,])?;
s.fields.extend(terminated.into_iter());
}
}
"create" => {
let _eq: Token![=] = input.parse()?;
s.create = Some(input.parse()?);
}
"reflect" => {
let _eq: Token![=] = input.parse()?;
let ty: syn::Ident = input.parse()?;
let ty_str = ty.to_string();
let ty_str = ty_str.as_str();
let ty = match ty_str {
"component" => ReflectType::Component,
"resource" => ReflectType::Resource,
_ => return Err(syn::Error::new_spanned(
ident,
format!("unknown wrapper type: '{}', expected 'component' or 'resource'", ty_str),
)),
};
s.reflection_type = Some(ty);
}
_ => {
return Err(syn::Error::new_spanned(
ident,
format!("unknown wrapper command: '{}'", ident_str),
));
}
}
}
}
if s.reflection_type.is_none() {
return Err(syn::Error::new(
input.span(),
format!("Wrapper type not specified! Expected 'type=component' or 'type=resource'"),
));
}
if s.table_name.is_empty() {
return Err(syn::Error::new(
input.span(),
format!("No lua table specified. Use 'lua_name=\"Camera\"'"),
));
}
Ok(s)
}
}
pub fn to_lua_struct_impl(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as IntoLuaUsage);
let crate_path = crate::CRATE_PATH.clone();
let mlua = quote!(#crate_path::lua::mlua);
// unwrap is fine since `Some` is ensured in parse impl
let reflect_type = input.reflection_type.as_ref().unwrap();
let type_path = &input.type_path;
let type_name = &type_path
.segments
.last()
.expect("Failure to find typename in macro usage!")
.ident;
let wrapper = input
.override_name
.unwrap_or_else(|| syn::Ident::new(&format!("Lua{}", type_name), Span::call_site()));
let derives_iter = input.derives.into_iter();
let lua_name = &input.table_name;
let field_getters_iter = input.fields.iter().map(Field::table_getter);
let field_setters_iter = input.fields.iter().map(Field::table_setter);
let struct_creator = wrapper_creation(
&wrapper,
type_path,
input.struct_type,
input.create.as_ref(),
&input.fields,
);
let reflect_fn = get_reflect_lua_functions(&crate_path, reflect_type, &input.type_path, true);
let reflect_type_fn =
get_reflect_lua_functions(&crate_path, reflect_type, &input.type_path, false);
quote! {
#[derive(Clone, #(#derives_iter),*)]
pub struct #wrapper(pub(crate) #type_path);
impl std::ops::Deref for #wrapper {
type Target = #type_path;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for #wrapper {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl #mlua::FromLua for #wrapper {
fn from_lua(val: #mlua::Value, _lua: &#mlua::Lua) -> #mlua::Result<Self> {
let ty = val.type_name();
let table = val.as_table().ok_or(#mlua::Error::FromLuaConversionError {
from: ty,
to: "Table".into(),
message: Some("expected Table".into()),
})?;
#(
#field_getters_iter
)*
Ok(#struct_creator)
}
}
impl #mlua::IntoLua for #wrapper {
fn into_lua(self, lua: &#mlua::Lua) -> #mlua::Result<#mlua::Value> {
use #crate_path::lua::LuaWrapper;
let table = lua.create_table()?;
#(
#field_setters_iter
)*
table.set(
#FN_NAME_INTERNAL_REFLECT,
lua.create_function(|_, this: Self| {
#reflect_fn
})?,
)?;
table.set(
#FN_NAME_INTERNAL_REFLECT_TYPE,
lua.create_function(|_, ()| {
#reflect_type_fn
})?,
)?;
table.set(#mlua::MetaMethod::Type.name(), #lua_name)?;
Ok(#mlua::Value::Table(table))
}
}
impl #crate_path::lua::LuaWrapper for #wrapper {
type Wrap = #type_path;
#[inline(always)]
fn wrapped_type_id() -> std::any::TypeId {
std::any::TypeId::of::<#type_path>()
}
#[inline(always)]
fn into_wrapped(self) -> Self::Wrap {
self.0
}
}
}
.into_token_stream()
.into()
}

View file

@ -1,72 +0,0 @@
---Create a Resource query that will return the specific ECS world resource.
---
---@see ResQuery
---@param resource table|userdata
---@return ResQuery
function Res(resource)
return ResQuery.new(resource)
end
---@alias Query function|table|userdata
---Create a `ChangedQuery` query that will return only if the resource or component has changed
---since last tick.
---
---@see ChangedQuery
---@param val table|userdata
---@return ChangedQuery
function Changed(val)
return ChangedQuery.new(val)
end
---Create a `HasQuery` filter that will return only if the entity has a specific component.
---
---@see HasQuery
---@param val table|userdata
---@return HasQuery
function Has(val)
return HasQuery.new(val)
end
---Create a `NotQuery` filter that will allow results if the query returns nothing or
---filter denies.
---
---@see NotQuery
---@param val Query
---@return NotQuery
function Not(val)
return NotQuery.new(val)
end
---Create a `AnyQuery` filter that will allow results if any of the queries return something.
---
---The queries are evaluated in the order they were provided.
---
---@see AnyQuery
---@param ... Query
---@return AnyQuery
function Any(...)
return AnyQuery.new(...)
end
---Create a `TickOfQuery` for retrieving the tick of the resource or component on the entity.
---
---@see TickOfQuery
---@param ... table|userdata
---@return TickOfQuery
function TickOf(...)
return TickOfQuery.new(...)
end
---Create any `OptionalQuery` that allows for a query to return nothing.
---
---If the query is a filter, its result will essentially be ignored. If the query returns `None`
---or `AlwaysNone`, this query will return `Nil`. If the query results in a value, its value
---will be the result of this query.
---
---@see OptionalQuery
---@param q Query
---@return OptionalQuery
function Optional(q)
return OptionalQuery.new(q)
end

View file

@ -1,121 +0,0 @@
---@enum WindowMode
WindowMode = {
WNDOWED = "windowed",
BORDERLESS_FULLSCREEN = "borderless_fullscreen",
SIZED_FULLSCREEN = "sized_fullscreen",
FULLSCREEN = "fullscreen",
}
---@enum CursorGrabMode
CursorGrabMode = {
NONE = "none",
CONFINED = "confined",
LOCKED = "locked",
}
---@enum WindowTheme
WindowTheme = {
LIGHT = "light",
DARK = "dark",
}
---@enum WindowLevel
WindowLevel = {
ALWAYS_ON_BOTTOM = "always_on_bottom",
NORMAL = "normal",
ALWAYS_ON_TOP = "always_on_top",
}
---@enum HandleState
HandleState = {
LOADING = "loading",
READY = "ready",
ERROR = "error",
}
---@enum ActionKind
ActionKind = {
BUTTON = "button",
AXIS = "axis",
}
---@enum ActionState
ActionState = {
IDLE = "idle",
PRESSED = "pressed",
JUST_PRESSED = "just_pressed",
JUST_RELEASED = "just_released",
AXIS = "axis",
OTHER = "other",
}
---@enum FilterMode
FilterMode = {
NEAREST = "nearest",
LINEAR = "linear",
}
---@enum WrappingMode
WrappingMode = {
CLAMP_TO_EDGE = "clamp_to_edge",
MIRRORED_REPEAT = "mirrored_repeat",
REPEAT = "repeat",
}
---@enum CameraProjectionMode
CameraProjectionMode = {
PERSPECTIVE = "perspective",
ORTHOGRAPHIC = "orthographic",
}
---@enum DeviceEventKind
DeviceEventKind = {
ADDED = "added",
REMOVED = "removed",
MOUSE_MOTION = "mouse_motion",
MOUSE_WHEEL = "mouse_wheel",
MOTION = "motion",
BUTTON = "button",
KEY = "key",
}
---@enum NativeKeyCodeKind
NativeKeyCodeKind = {
ANDROID = "android",
MACOS = "macos",
WINDOWS = "windows",
XKB = "xkb",
}
---@enum ElementState
ElementState = {
PRESSED = "pressed",
RELEASED = "released",
}
---@enum Pivot
Pivot = {
CENTER = { type = "center" },
CENTER_LEFT = { type = "center_left" },
CENTER_RIGHT = { type = "center_right" },
TOP_LEFT = { type = "top_left" },
TOP_RIGHT = { type = "top_right" },
TOP_CENTER = { type = "top_center" },
BOTTOM_LEFT = { type = "bottom_left" },
BOTTOM_RIGHT = { type = "bottom_right" },
BOTTOM_CENTER = { type = "bottom_center" },
---A custom pivot point with a `Vec2` as the point
---
---Top left is (-0.5, 0.5), center is (0.0, 0.0).
---
---Example:
---```
---sprite.pivot = Pivot.CUSTOM(Vec2.new(-0.3, 0.3))
---```
CUSTOM = function(val)
return {
type = "custom",
val = val,
}
end,
}

View file

@ -1,23 +0,0 @@
---@meta
---@class GltfHandle: Handle
---
---A handle to a GLTF asset.
GltfHandle = {
}
---Get a list of scenes in the GLTF file.
---
---@return SceneHandle[]
function GltfHandle:scenes() end
---Get a list of materials in the GLTF file.
---
---@return MaterialHandle[]
function GltfHandle:materials() end
---Get a list of meshes in the GLTF file.
---
---@return MeshHandle[]
function GltfHandle:meshes() end

View file

@ -1,43 +0,0 @@
---@meta
---@class Handle: userdata
---
---A handle to an asset. Assets are loaded asynchronously, so you cannot immediately
---use them after you request them from the World.
Handle = {
---The path the asset was loaded from.
---
---@type string
path = nil,
---The version of the resource.
---
---Increments every time a resource is loaded.
---
---@type number
version = nil,
---The unique id of the resource.
---
---This is not changed for the entire lifetime of the handle, it does not change
---when an asset is reloaded.
---
---@type string
uuid = nil,
---Current state of the asset handle.
---@type HandleState
state = nil,
}
---Returns true if the asset is watched for auto-reloading.
---
---@return boolean
function Handle:is_watched() end
---Returns true if the asset is loaded.
---@return boolean
function Handle:is_loaded() end
---Blocks execution until the asset and its dependencies are loaded.
function Handle:wait_until_loaded() end

View file

@ -1,4 +0,0 @@
---@meta
---@class ImageHandle: Handle
ImageHandle = {}

View file

@ -1,102 +0,0 @@
---@meta
---@class MaterialHandle: Handle
---
---A handle to a material asset in a GLTF file.
MaterialHandle = {
---The unique id of the GPU shader.
---@type number?
shader_uuid = nil,
---The name of the material from GLTF.
---@type string?
name = nil,
---@type boolean
double_sided = nil,
---The RGBA base color of the model.
---
---If a texture is supplied with `base_color_texture`, this value will tint the
---texture. If a texture is not provided, this value would be the color of
---the Material.
---
---@type Vec4
base_color = nil,
---The metalness of the material
---
---From 0.0 (non-metal) to 1.0 (metal).
---
---@type number
metallic = nil,
---The roughness of the material
---
---From 0.0 (smooth) to 1.0 (rough)
---
---@type number
roughness = nil,
---The base color texture of the model.
---@type TextureHandle?
base_color_texture = nil,
---The metallic-roughness texture.
---
---The metalness values are sampled from the B channel. The roughness values are sampled from
---the G channel. These values are linear. If other channels are present (R or A), they are
---ignored for metallic-roughness calculations.
---@type TextureHandle?
metallic_roughness_texture = nil,
---A set of parameter values that are used to define the specular-glossiness material model
---from Physically-Based Rendering (PBR) methodology.
---GLTF extension: [KHR_materials_pbrSpecularGlossiness](https://kcoley.github.io/glTF/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness)
---@type TextureHandle?
pbr_glossiness = nil,
---The optional alpha cutoff value of the material.
---
---This will be used instead of the renderer's default.
---
---@type number?
alpha_cutoff = nil,
---The alpha rendering mode of the material.
---
---The material's alpha rendering
---mode enumeration specifying the interpretation of the alpha value of the main
---factor and texture.
---
---* In `Opaque` mode (default) the alpha value is ignored
--- and the rendered output is fully opaque.
---* In `Mask` mode, the rendered
--- output is either fully opaque or fully transparent depending on the alpha
--- value and the specified alpha cutoff value.
---* In `Blend` mode, the alpha value is used to composite the source and
--- destination areas and the rendered output is combined with the background
--- using the normal painting operation (i.e. the Porter and Duff over
--- operator).
---
---@type AlphaMode
alpha_mode = nil,
---@type Specular?
specular = nil,
}
---@enum AlphaMode
AlphaMode = {
OPAQUE = "opaque",
MASK = "mask",
BLEND = "blend",
}
---@class PbrGlossiness
---TODO: implement
PbrGlossiness = {}
---@class Specular
---TODO: implement
Specular = {}

View file

@ -1,14 +0,0 @@
---@meta
---@class MeshHandle: Handle
---
---A handle to a mesh in a GLTF file.
MeshHandle = {
---The material of the mesh
---@type MaterialHandle
material = nil,
}
---Get the indices in the mesh.
---@return number[]
function MeshHandle:indices() end

View file

@ -1,9 +0,0 @@
---@meta
---@class SceneHandle: Handle
---
---A handle to a scene asset in a GLTF file.
SceneHandle = {
}

View file

@ -1,4 +0,0 @@
---@meta
---@class TextureHandle
TextureHandle = {}

View file

@ -1,125 +0,0 @@
---@meta
---@class ActionHandler: userdata
ActionHandler = {}
--- Create a new `ActionHandler`.
---
--- ```lua
--- local handler = ActionHandler.new {
--- -- A list of layout IDs
--- layouts = { 0 },
--- actions = {
--- -- A list of action names and the `ActionKind`s.
--- -- Actions can be buttons or axes.
--- MoveForwardBackward = ActionKind.AXIS,
--- MoveLeftRight = ActionKind.AXIS,
--- MoveUpDown = ActionKind.AXIS,
--- LookLeftRight = ActionKind.AXIS,
--- LookUpDown = ActionKind.AXIS,
--- LookRoll = ActionKind.AXIS,
--- ObjectsMoveUpDown = ActionKind.AXIS
--- },
--- mappings = {
--- -- Each entry here is a mapping of actions for a layout.
--- -- This can be used so that when the current layout is changed,
--- -- the mapping would also change.
--- {
--- -- Specify the layout id that this mapping is for.
--- layout = 0,
--- binds = {
--- -- This is an Action bind. A bind is used to bind an input to an action.
--- -- These actions are defined above in "actions".
--- MoveForwardBackward = {
--- -- This is how you bind a button. In this case the button is a key.
--- -- "key" is the device the bind comes from, then after the colon is the
--- -- input name, in this case a specific key. We specify a modifier to the bind
--- -- after the equal sign.
--- "key:w=1.0",
--- "key:s=-1.0"
--- },
--- MoveLeftRight = {
--- "key:a=-1.0", "key:d=1.0"
--- },
--- MoveUpDown = {
--- "key:c=1.0", "key:z=-1.0"
--- },
--- LookLeftRight = {
--- "key:left=-1.0", "key:right=1.0",
--- -- Here is a bind to an axis.
--- -- We use "mouse", for the device the bind is from, then "axis" to specify
--- -- that we want to bind a specific axis, then we use the name of the axis,
--- -- in this case "x".
--- -- So this binds to the x axis of the mouse.
--- "mouse:axis:x"
--- },
--- LookUpDown = {
--- "key:up=-1.0", "key:down=1.0",
--- -- Here we bind to the y axis of the mouse.
--- "mouse:axis:y",
--- },
--- LookRoll = {
--- "key:e=-1.0", "key:q=1.0",
--- },
--- ObjectsMoveUpDown = {
--- "key:u=1.0", "key:j=-1.0"
--- }
--- }
--- }
--- }
--- }
---
--- -- Add the handler to the world so the host will process it.
--- world:add_resource(handler)
--- ```
---
---@param table table See above example to see the format of this table.
function ActionHandler.new(table) end
---Returns the action's modifier if its an updated axis.
---
---Returns `nil` if the action's state is not `ActionState::Axis`, or if the
---action was not found.
---@param action string
---@return number?
function ActionHandler:get_axis(action) end
---Returns true if the action is pressed (or was just pressed).
---
---Returns `nil` if the action's was not found.
---@param action string
---@return boolean?
function ActionHandler:is_pressed(action) end
---Returns true if the action was **just** pressed.
---
---Returns `nil` if the action was not found
---
---@param action string
---@return boolean?
function ActionHandler:was_just_pressed(action) end
---Returns true if the action was just released.
---
---Returns `nil` if the action was not found
---
---@param action string
---@return boolean?
function ActionHandler:was_just_released(action) end
---Returns the action's modifier if it was just pressed.
---
---Returns `nil` if the action's state is not `ActionState.JUST_PRESSED`,
---or if the action was not found.
---
---@param action string
---@return number?
function ActionHandler:get_just_pressed(action) end
---Returns the current state of the action.
---
---The first element in the returned tuple is the state enum, and the second
---is the state modifier. The modifer will be `nil` if the state is "idle"
---
---@return [ActionState, number?]
function ActionHandler:get_action_state(action) end

View file

@ -1,18 +0,0 @@
---@meta
---@class Camera: userdata
Camera = {
---The position of the camera
---@type Transform
transform = nil,
---The field of view of the camera
---@type Angle
fov = nil,
---The projection mode the camera.
---Can be used to specify if the camera is 2D (orthographic), or 3D (perspective).
---@type CameraProjectionMode
mode = nil,
---Flag to enable some debug rendering stuff.
---@type boolean
debug = nil,
}

View file

@ -1,15 +0,0 @@
---@meta
---@class DeltaTime: userdata
---
---DeltaTime is an ECS world resource. When its requested from the world, a `number`
---is returned.
---
---Example:
---```lua
------@type number
---local dt = world:resource(DeltaTime)
---
---print(type(dt)) --> number
---```
DeltaTime = {}

View file

@ -1,29 +0,0 @@
---@meta
---An entity handle.
---@class Entity: userdata
Entity = {}
---Get the id of the Entity.
---@return number
function Entity:id() end
---Get the generation number of the Entity.
---
---Entity handles are reused by the ECS World, the generation is used to tell reused Entity
---id's apart from previous generations.
---
---@return number
function Entity:generation() end
---A reference to an entity in the world.
---
---Can be used to insert and update components on the entity.
---
---@class EntityRef: userdata
EntityRef = {}
---Update components that are **already** on an Entity.
---
---@param ... any The components to update on the entity.
function EntityRef:update(...) end

View file

@ -1,9 +0,0 @@
---@meta
---@class EventReader<T>: userdata
EventReader = {}
---Get an iterator for reading the event.
---@generic T
---@return fun(): T? iterator An iterator for reading the events.
function EventReader:read() end

View file

@ -1,17 +0,0 @@
---@meta
---@class FreeFlyCamera: userdata
FreeFlyCamera = {
---Movement speed of the camera.
---@type number
speed = nil,
---The speed of the camera rotation.
---@type number
look_speed = nil,
---The sensitivity of the mouse when looking.
---
---This is additional to `look_speed`, but onyl applied to mouse movement.
---
---@type number
mouse_sensitivity = nil,
}

View file

@ -1,9 +0,0 @@
require "action_handler"
require "camera"
require "delta_time"
require "entity"
require "event_reader"
require "free_fly_camera"
require "window"
require "world_transform"
require "world"

View file

@ -1,25 +0,0 @@
---@meta
---An ECS filter that will return if any of the provided queries return.
---
---The queries are evaluated in the order they were provided. When a query or filter returns a value,
---that value will be returned.
---
---Use the utility function `Any(...)` to create a new query since its faster to
---write than this.
---
---@see Any
---@class AnyQuery: userdata
AnyQuery = {}
---Create a new AnyQuery.
---
---Use the utility function `Any(...)` to create a new query since its faster to
---write than this.
---
---@see Any
---@param ... Query The query to invert.
function AnyQuery:new(...) end
---An internal function used by the engine to retrieve the query result.
function AnyQuery:__lyra_internal_ecs_query_result(world, entity) end

View file

@ -1,22 +0,0 @@
---@meta
---An ECS query used for obtaining **changed** resources or components from the world.
---
---Use the utility function `Changed(...)` to create a new query since its faster to
---write than this.
---
---This query will not return if the resource or component has not changed since the last tick.
---
---@class ChangedQuery: userdata
ChangedQuery = {}
---Create a new ChangedQuery.
---
---Use the utility function `Changed(...)` to create a new query since its faster to
---write than this.
---
---@param val table|userdata The component or resource to detect changed of.
function ChangedQuery:new(val) end
---An internal function used by the engine to retrieve the query result.
function ChangedQuery:__lyra_internal_ecs_query_result(world, entity) end

View file

@ -1,22 +0,0 @@
---@meta
---An ECS filter that allows the query if the entity has the Component.
---
---Use the utility function `Has(...)` to create a new query since its faster to
---write than this.
---
---@see Has
---@class HasQuery: userdata
HasQuery = {}
---Create a new HasQuery.
---
---Use the utility function `Has(...)` to create a new query since its faster to
---write than this.
---
---@see Has
---@param val table|userdata The component to look for on the entity.
function HasQuery:new(val) end
---An internal function used by the engine to retrieve the query result.
function HasQuery:__lyra_internal_ecs_query_result(world, entity) end

View file

@ -1,9 +0,0 @@
require "view"
require "view_one"
require "changed"
require "res"
require "has"
require "any"
require "not"
require "optional"
require "tick_of"

View file

@ -1,22 +0,0 @@
---@meta
---An ECS filter that inverts the provided filter/query result.
---
---Use the utility function `Not(...)` to create a new query since its faster to
---write than this.
---
---@see Not
---@class NotQuery: userdata
NotQuery = {}
---Create a new NotQuery.
---
---Use the utility function `Not(...)` to create a new query since its faster to
---write than this.
---
---@see Not
---@param val Query The query to invert.
function NotQuery:new(val) end
---An internal function used by the engine to retrieve the query result.
function NotQuery:__lyra_internal_ecs_query_result(world, entity) end

View file

@ -1,26 +0,0 @@
---@meta
---An ECS query that ignores filters and queries that dont return anything.
---
---If the provided query returns nothing, this query will provide a `nil` value.
---The results of filters are essentially ignored, since it doesn't matter the result, this query
---will return. If the provided query has a result, this query will also return it.
---
---Use the utility function `Optional(...)` to create a new query since its faster to
---write than this.
---
---@see Optional
---@class OptionalQuery: userdata
OptionalQuery = {}
---Create a new OptionalQuery.
---
---Use the utility function `Optional(...)` to create a new query since its faster to
---write than this.
---
---@see Optional
---@param val Query The query to invert.
function OptionalQuery:new(val) end
---An internal function used by the engine to retrieve the query result.
function OptionalQuery:__lyra_internal_ecs_query_result(world, entity) end

View file

@ -1,16 +0,0 @@
---@meta
---An ECS query used for obtaining Resources from the `World`.
---@class ResQuery: userdata
ResQuery = {}
---Create a new ResQuery for getting a Resource from the `World`.
---
---Use the utility function `Res(...)` to create a new query since its faster to
---write than this.
---
---@param val table|userdata The resource type to obtain.
function ResQuery:new(val) end
---An internal function used by the engine to retrieve the query result.
function ResQuery:__lyra_internal_ecs_query_result(world, entity) end

View file

@ -1,22 +0,0 @@
---@meta
---An ECS query that returns the tick of the resource or component provided.
---
---Use the utility function `TickOf(...)` to create a new query since its faster to
---write than this.
---
---@see TickOf
---@class TickOfQuery: userdata
TickOfQuery = {}
---Create a new TickOfQuery.
---
---Use the utility function `TickOf(...)` to create a new query since its faster to
---write than this.
---
---@see TickOf
---@param val table|userdata The component or resource to retrieve the tick of.
function TickOfQuery:new(val) end
---An internal function used by the engine to retrieve the query result.
function TickOfQuery:__lyra_internal_ecs_query_result(world, entity) end

View file

@ -1,23 +0,0 @@
---@meta
---@class View: userdata
View = {}
---Create a new view to query for components and world resources.
---
---Each parameter is a query. If you want to query entities with components, you would just use
---the component names.
---There are other queries, like `Changed` for querying for changed resources and components,
---and `Res` for querying for resources.
---
---@return View
function View.new(...) end
---@class ViewResult: userdata
ViewResult = {}
---Returns an interator over the results of the View.
---
---@generic T...
---@return fun(): EntityRef, T... iterator An iterator over the results. In the same order of the created View.
function ViewResult:iter() end

View file

@ -1,19 +0,0 @@
---@meta
---Results of a View over a single entity.
---@class ViewOneResult: userdata
ViewOneResult = {}
---Returns the results of the view over a single entity.
---
---@see ViewOneResult.__call
---@generic T...
---@return T...
function ViewOneResult:get() end
---Returns the results of the view over a single entity.
---
---@see ViewOneResult.get
---@generic T...
---@return T...
function ViewOneResult:__call() end

View file

@ -1,128 +0,0 @@
---@meta
---@class Window: userdata
Window = {
---Gets or sets the window's focus.
---@type boolean
focused = nil,
---Gets or sets the window mode.
---@type WindowMode
window_mode = nil,
---Gets or sets the position of the top-left corner of the window.
---
---The top-left hand corner of the desktop is not necessarily the same
---as the screen. If the user uses a desktop with multiple monitors, the top-left
---hand corner of the desktop is the top-left hand corner of the monitor at the
---top-left of the desktop.
---
---If this is `nil`, the position will be chosen by the windowing manager at creation,
---then set when the window is created.
---
---@type Vec2?
position = nil,
---@type Vec2
physical_size = nil,
---@type Vec2
size = nil,
---Gets/sets if the window has decorations.
---@type boolean
decorated = nil,
---Gets/sets the window's current maximized state.
---@type boolean
maximized = nil,
---Gets/sets the window's current minimized state.
---
---Is `nil` if the minimized state could not be determined.
---
---@type boolean?
minimized = nil,
---Gets/sets the window's current resizable state
---@type boolean
resizable = nil,
---Gets/sets the window's current visibility state.
---
---Is `nil` when it could not be determined.
---
---@type boolean?
visible = nil,
--TODO: resize_increments
---Gets the scale factor.
---
---You cannot set this field.
---
---@type number
scale_factor = nil,
---Gets/sets the window's blur state.
---@type boolean
blur = nil,
--TODO: cursor appearance
---Gets/sets the window's cursor grab mode.
---@type CursorGrabMode
cursor_grab = nil,
---Gets/sets whether the window catches cursor events.
---@type boolean
cursor_hittest = nil,
---Gets/sets the cursor's visibility.
---@type boolean
cursor_visible = nil,
---Sets whether the window should get IME events.
---
---When IME is allowed, the window will receive Ime events, and during the preedit phase
---the window will NOT get KeyboardInput events. The window should allow IME while
---it is expecting text input.
---
---When IME is not allowed, the window wont receive window ime events, and will receive
---KeyboardInput events for every keypress instead. Not allowing IME is useful for games
---for example. IME is not allowed by default.
---
---@type boolean
ime_allowed = nil,
---Gets/sets the minimum size of the window.
---@type Vec2?
min_size = nil,
---Gets/sets the maximum size of the window.
---@type Vec2?
max_size = nil,
---Gets/sets the current window theme.
---
---Specify `nil` to reset the theme to the system default. May also be `nil` on
---unsupported platforms.
---
---@type WindowTheme?
theme = nil,
---Gets/sets the title of the window.
---@type string
title = nil,
---Gets/sets the window's transparency state.
---@type boolean
transparent = nil,
--TODO: window_icon
---Change the window level.
---@type WindowLevel
window_level = nil,
---Gets the window's occluded state (completely hidden from view).
---@type boolean
occluded = nil,
---Gets/sets the cursor position in the window in logical coordinates.
---
---The value is `nil` when the cursor is not in the window.
---
---@type Vec2?
cursor_position = nil,
---Gets/sets the cursor position in the window in physical coordinates.
---
---The value is `nil` when the cursor is not in the window.
---
---@type Vec2?
physical_cursor_position = nil,
---Checks if the mouse is inside the window
---@param self Window
---@return boolean
is_mouse_inside = function (self) return false end,
}

Some files were not shown because too many files have changed in this diff Show more