diff --git a/.vscode/launch.json b/.vscode/launch.json index a69d883..dbb4757 100755 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,6 +22,24 @@ "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", diff --git a/Cargo.lock b/Cargo.lock index b5960e5..bdf2688 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "377e4c0ba83e4431b10df45c1d4666f178ea9c552cac93e60c3a88bf32785923" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-array" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c92d086290f52938013f6242ac62bf7d401fab8ad36798a609faa65c3fd2c" +dependencies = [ + "generic-array", +] + [[package]] name = "allocator-api2" version = "0.2.16" @@ -122,6 +140,15 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "ash" version = "0.37.3+1.3.251" @@ -351,6 +378,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + [[package]] name = "backtrace" version = "0.3.69" @@ -786,6 +819,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "divrem" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69dde51e8fef5e12c1d65e0929b03d66e4c0c18282bc30ed2ca050ad6f44dd82" + [[package]] name = "dlib" version = "0.5.2" @@ -795,6 +834,12 @@ dependencies = [ "libloading 0.8.1", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "downcast-rs" version = "1.2.0" @@ -807,6 +852,12 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "elapsed" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f4e5af126dafd0741c2ad62d47f68b28602550102e5f0dd45c8a97fc8b49c29" + [[package]] name = "elua" version = "0.1.0" @@ -908,6 +959,18 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fast_poisson" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2472baa9796d2ee497bd61690e3093a26935390d8ce0dd0ddc2db9b47a65898f" +dependencies = [ + "kiddo", + "rand 0.8.5", + "rand_distr", + "rand_xoshiro", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -953,6 +1016,19 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fixed" +version = "1.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc715d38bea7b5bf487fcd79bcf8c209f0b58014f3018a7a19c2b855f472048" +dependencies = [ + "az", + "bytemuck", + "half", + "num-traits", + "typenum", +] + [[package]] name = "fixed-timestep-rotating-model" version = "0.1.0" @@ -1683,6 +1759,26 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "kiddo" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1c5ea778d68eacd5c33f29537ba0b7b6c2595e74ee013a69cedc20ab4d3177" +dependencies = [ + "aligned", + "aligned-array", + "az", + "divrem", + "doc-comment", + "elapsed", + "fixed", + "log", + "min-max-heap", + "num-traits", + "rand 0.8.5", + "rayon", +] + [[package]] name = "kqueue" version = "1.0.8" @@ -1750,6 +1846,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "libredox" version = "0.0.2" @@ -1868,6 +1970,7 @@ dependencies = [ "bind_match", "bytemuck", "cfg-if", + "fast_poisson", "gilrs-core", "glam", "image", @@ -1881,6 +1984,7 @@ dependencies = [ "lyra-scene", "petgraph", "quote", + "round_mult", "rustc-hash", "syn 2.0.51", "thiserror", @@ -2078,6 +2182,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "min-max-heap" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2687e6cf9c00f48e9284cf9fd15f2ef341d03cc7743abf9df4c5f07fdee50b18" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -2285,6 +2395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2726,6 +2837,25 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "range-alloc" version = "0.1.3" @@ -2884,6 +3014,15 @@ dependencies = [ "winreg", ] +[[package]] +name = "round_mult" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74bc7d5286c4d36f09aa6ae93f76acf6aa068cd62bc02970a9deb24763655dee" +dependencies = [ + "rustc_version", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2896,6 +3035,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.37.27" @@ -3010,6 +3158,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "serde" version = "1.0.194" @@ -3075,6 +3229,16 @@ dependencies = [ "digest", ] +[[package]] +name = "shadows" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-std", + "lyra-engine", + "tracing", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -3191,6 +3355,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index a4afe80..1850380 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,6 @@ edition = "2021" [workspace] members = [ - "examples/testbed", "lyra-resource", "lyra-ecs", "lyra-reflect", @@ -14,10 +13,12 @@ members = [ "lyra-math", "lyra-scene", + "examples/testbed", "examples/many-lights", "examples/fixed-timestep-rotating-model", "examples/lua-scripting", - "examples/simple_scene" + "examples/simple_scene", + "examples/shadows", ] [features] @@ -33,4 +34,4 @@ lyra-scripting = { path = "lyra-scripting", optional = true } #opt-level = 1 [profile.release] -debug = true \ No newline at end of file +debug = true diff --git a/examples/assets/shadows-platform-palmtree.glb b/examples/assets/shadows-platform-palmtree.glb new file mode 100644 index 0000000..66a3dfd Binary files /dev/null and b/examples/assets/shadows-platform-palmtree.glb differ diff --git a/examples/assets/wood-platform.glb b/examples/assets/wood-platform.glb new file mode 100644 index 0000000..11624ca Binary files /dev/null and b/examples/assets/wood-platform.glb differ diff --git a/examples/assets/wood-platform/model.bin b/examples/assets/wood-platform/model.bin new file mode 100644 index 0000000..d3bdc51 Binary files /dev/null and b/examples/assets/wood-platform/model.bin differ diff --git a/examples/assets/wood-platform/model.gltf b/examples/assets/wood-platform/model.gltf new file mode 100644 index 0000000..5b1d496 --- /dev/null +++ b/examples/assets/wood-platform/model.gltf @@ -0,0 +1,142 @@ +{ + "asset":{ + "generator":"Khronos glTF Blender I/O v4.1.63", + "version":"2.0" + }, + "scene":0, + "scenes":[ + { + "name":"Scene", + "nodes":[ + 0 + ] + } + ], + "nodes":[ + { + "mesh":0, + "name":"Cube", + "scale":[ + 10, + 0.25, + 10 + ] + } + ], + "materials":[ + { + "doubleSided":true, + "name":"Material.001", + "pbrMetallicRoughness":{ + "baseColorTexture":{ + "index":0 + }, + "metallicFactor":0, + "roughnessFactor":0.5 + } + } + ], + "meshes":[ + { + "name":"Cube.001", + "primitives":[ + { + "attributes":{ + "POSITION":0, + "NORMAL":1, + "TEXCOORD_0":2 + }, + "indices":3, + "material":0 + } + ] + } + ], + "textures":[ + { + "sampler":0, + "source":0 + } + ], + "images":[ + { + "mimeType":"image/jpeg", + "name":"wood1", + "uri":"wood1.jpg" + } + ], + "accessors":[ + { + "bufferView":0, + "componentType":5126, + "count":24, + "max":[ + 1, + 1, + 1 + ], + "min":[ + -1, + -1, + -1 + ], + "type":"VEC3" + }, + { + "bufferView":1, + "componentType":5126, + "count":24, + "type":"VEC3" + }, + { + "bufferView":2, + "componentType":5126, + "count":24, + "type":"VEC2" + }, + { + "bufferView":3, + "componentType":5123, + "count":36, + "type":"SCALAR" + } + ], + "bufferViews":[ + { + "buffer":0, + "byteLength":288, + "byteOffset":0, + "target":34962 + }, + { + "buffer":0, + "byteLength":288, + "byteOffset":288, + "target":34962 + }, + { + "buffer":0, + "byteLength":192, + "byteOffset":576, + "target":34962 + }, + { + "buffer":0, + "byteLength":72, + "byteOffset":768, + "target":34963 + } + ], + "samplers":[ + { + "magFilter":9729, + "minFilter":9987 + } + ], + "buffers":[ + { + "byteLength":840, + "uri":"model.bin" + } + ] +} diff --git a/examples/assets/wood-platform/wood1.jpg b/examples/assets/wood-platform/wood1.jpg new file mode 100644 index 0000000..7fc7cef Binary files /dev/null and b/examples/assets/wood-platform/wood1.jpg differ diff --git a/examples/assets/wood-platform/wood1OLD.jpg b/examples/assets/wood-platform/wood1OLD.jpg new file mode 100644 index 0000000..2cbb3d3 Binary files /dev/null and b/examples/assets/wood-platform/wood1OLD.jpg differ diff --git a/examples/shadows/Cargo.toml b/examples/shadows/Cargo.toml new file mode 100644 index 0000000..d4223db --- /dev/null +++ b/examples/shadows/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "shadows" +version = "0.1.0" +edition = "2021" + +[dependencies] +lyra-engine = { path = "../../", features = ["tracy"] } +anyhow = "1.0.75" +async-std = "1.12.0" +tracing = "0.1.37" \ No newline at end of file diff --git a/examples/shadows/scripts/test.lua b/examples/shadows/scripts/test.lua new file mode 100644 index 0000000..bac6e90 --- /dev/null +++ b/examples/shadows/scripts/test.lua @@ -0,0 +1,59 @@ +---Return the userdata's name from its metatable +---@param val userdata +---@return string +function udname(val) + return getmetatable(val).__name +end + +function on_init() + local cube = world:request_res("../assets/cube-texture-embedded.gltf") + print("Loaded textured cube (" .. udname(cube) .. ")") + + cube:wait_until_loaded() + local scenes = cube:scenes() + local cube_scene = scenes[1] + + local pos = Transform.from_translation(Vec3.new(0, 0, -8.0)) + + local e = world:spawn(pos, cube_scene) + print("spawned entity " .. tostring(e)) +end + +--[[ function on_first() + print("Lua's first function was called") +end + +function on_pre_update() + print("Lua's pre-update function was called") +end ]] + +function on_update() + --[[ ---@type number + local dt = world:resource(DeltaTime) + local act = world:resource(ActionHandler) + ---@type number + local move_objs = act:get_axis("ObjectsMoveUpDown") + + world:view(function (t) + if move_objs ~= nil then + t:translate(0, move_objs * 0.35 * dt, 0) + return t + end + end, Transform) ]] + + ---@type number + local dt = world:resource(DeltaTime) + + world:view(function (t) + t:translate(0, 0.15 * dt, 0) + return t + end, Transform) +end + +--[[ function on_post_update() + print("Lua's post-update function was called") +end + +function on_last() + print("Lua's last function was called") +end ]] \ No newline at end of file diff --git a/examples/shadows/src/main.rs b/examples/shadows/src/main.rs new file mode 100644 index 0000000..1391c6f --- /dev/null +++ b/examples/shadows/src/main.rs @@ -0,0 +1,249 @@ +use lyra_engine::{ + assets::{gltf::Gltf, ResourceManager}, + game::Game, + input::{ + Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource, + InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput, + }, + math::{self, Quat, Transform, Vec3}, + render::{ + graph::{ShadowCasterSettings, ShadowFilteringMode}, + light::{directional::DirectionalLight, PointLight, SpotLight}, + }, + scene::{ + CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform, + ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN, + ACTLBL_MOVE_FORWARD_BACKWARD, ACTLBL_MOVE_LEFT_RIGHT, ACTLBL_MOVE_UP_DOWN, + }, +}; + +#[async_std::main] +async fn main() { + let action_handler_plugin = |game: &mut Game| { + let action_handler = ActionHandler::builder() + .add_layout(LayoutId::from(0)) + .add_action(ACTLBL_MOVE_FORWARD_BACKWARD, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_MOVE_LEFT_RIGHT, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_MOVE_UP_DOWN, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_LOOK_LEFT_RIGHT, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_LOOK_UP_DOWN, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_LOOK_ROLL, Action::new(ActionKind::Axis)) + .add_action("Debug", Action::new(ActionKind::Button)) + .add_mapping( + ActionMapping::builder(LayoutId::from(0), ActionMappingId::from(0)) + .bind( + ACTLBL_MOVE_FORWARD_BACKWARD, + &[ + ActionSource::Keyboard(KeyCode::W).into_binding_modifier(1.0), + ActionSource::Keyboard(KeyCode::S).into_binding_modifier(-1.0), + ], + ) + .bind( + ACTLBL_MOVE_LEFT_RIGHT, + &[ + ActionSource::Keyboard(KeyCode::A).into_binding_modifier(-1.0), + ActionSource::Keyboard(KeyCode::D).into_binding_modifier(1.0), + ], + ) + .bind( + ACTLBL_MOVE_UP_DOWN, + &[ + ActionSource::Keyboard(KeyCode::C).into_binding_modifier(1.0), + ActionSource::Keyboard(KeyCode::Z).into_binding_modifier(-1.0), + ], + ) + .bind( + ACTLBL_LOOK_LEFT_RIGHT, + &[ + ActionSource::Mouse(MouseInput::Axis(MouseAxis::X)).into_binding(), + ActionSource::Keyboard(KeyCode::Left).into_binding_modifier(-1.0), + ActionSource::Keyboard(KeyCode::Right).into_binding_modifier(1.0), + ], + ) + .bind( + ACTLBL_LOOK_UP_DOWN, + &[ + ActionSource::Mouse(MouseInput::Axis(MouseAxis::Y)).into_binding(), + ActionSource::Keyboard(KeyCode::Up).into_binding_modifier(-1.0), + ActionSource::Keyboard(KeyCode::Down).into_binding_modifier(1.0), + ], + ) + .bind( + ACTLBL_LOOK_ROLL, + &[ + ActionSource::Keyboard(KeyCode::E).into_binding_modifier(-1.0), + ActionSource::Keyboard(KeyCode::Q).into_binding_modifier(1.0), + ], + ) + .bind( + "Debug", + &[ActionSource::Keyboard(KeyCode::B).into_binding()], + ) + .finish(), + ) + .finish(); + + let world = game.world_mut(); + world.add_resource(action_handler); + game.with_plugin(InputActionPlugin); + }; + + Game::initialize() + .await + .with_plugin(lyra_engine::DefaultPlugins) + .with_plugin(setup_scene_plugin) + .with_plugin(action_handler_plugin) + //.with_plugin(camera_debug_plugin) + .with_plugin(FreeFlyCameraPlugin) + .run() + .await; +} + +fn setup_scene_plugin(game: &mut Game) { + let world = game.world_mut(); + let resman = world.get_resource_mut::(); + + /* let camera_gltf = resman + .request::("../assets/AntiqueCamera.glb") + .unwrap(); + + camera_gltf.wait_recurse_dependencies_load(); + let camera_mesh = &camera_gltf.data_ref().unwrap().scenes[0]; + drop(resman); + + world.spawn(( + camera_mesh.clone(), + WorldTransform::default(), + Transform::from_xyz(0.0, -5.0, -2.0), + )); */ + + let cube_gltf = resman + .request::("../assets/cube-texture-embedded.gltf") + .unwrap(); + + cube_gltf.wait_recurse_dependencies_load(); + let cube_mesh = &cube_gltf.data_ref().unwrap().scenes[0]; + + let palm_tree_platform_gltf = resman + .request::("../assets/shadows-platform-palmtree.glb") + .unwrap(); + + palm_tree_platform_gltf.wait_recurse_dependencies_load(); + let palm_tree_platform_mesh = &palm_tree_platform_gltf.data_ref().unwrap().scenes[0]; + + drop(resman); + + // cube in the air + /* world.spawn(( + cube_mesh.clone(), + WorldTransform::default(), + Transform::from_xyz(0.0, -2.0, -5.0), + )); + + // cube really high in the air + world.spawn(( + cube_mesh.clone(), + WorldTransform::default(), + Transform::from_xyz(-6.0, 0.0, -5.0), + )); + + // cube on the right, on the ground + world.spawn(( + cube_mesh.clone(), + WorldTransform::default(), + Transform::from_xyz(3.0, -3.75, -5.0), + )); + + world.spawn(( + platform_mesh.clone(), + WorldTransform::default(), + //Transform::from_xyz(0.0, -5.0, -5.0), + Transform::new(math::vec3(0.0, -5.0, -5.0), math::Quat::IDENTITY, math::vec3(5.0, 1.0, 5.0)), + )); */ + + world.spawn(( + palm_tree_platform_mesh.clone(), + WorldTransform::default(), + Transform::from_xyz(5.0, -15.0, 0.0), + //Transform::new(math::vec3(0.0, -5.0, -5.0), math::Quat::IDENTITY, math::vec3(5.0, 1.0, 5.0)), + )); + + //shadows-platform-palmtree.glb + + { + let mut light_tran = Transform::from_xyz(0.0, 0.0, 0.0); + light_tran.scale = Vec3::new(0.5, 0.5, 0.5); + light_tran.rotate_x(math::Angle::Degrees(-45.0)); + light_tran.rotate_y(math::Angle::Degrees(-35.0)); + world.spawn(( + //cube_mesh.clone(), + DirectionalLight { + enabled: true, + color: Vec3::new(1.0, 0.95, 0.9), + intensity: 0.9, + }, + ShadowCasterSettings { + filtering_mode: ShadowFilteringMode::Pcss, + pcf_samples_num: 64, + pcss_blocker_search_samples: 36, + constant_depth_bias_scale: 5.0, + ..Default::default() + }, + light_tran, + )); + + /* world.spawn(( + cube_mesh.clone(), + PointLight { + enabled: true, + color: Vec3::new(0.133, 0.098, 0.91), + intensity: 2.0, + range: 10.0, + ..Default::default() + }, + ShadowCasterSettings { + filtering_mode: ShadowFilteringMode::Pcf, + ..Default::default() + }, + Transform::new( + Vec3::new(4.0 - 1.43, -13.0, 1.53), + Quat::IDENTITY, + Vec3::new(0.5, 0.5, 0.5), + ), + )); */ + + let t = Transform::new( + Vec3::new(4.0 - 1.43, -13.0, 0.0), + //Vec3::new(-5.0, 1.0, -0.28), + //Vec3::new(-10.0, 0.94, -0.28), + + Quat::from_euler(math::EulerRot::XYZ, 0.0, math::Angle::Degrees(-45.0).to_radians(), 0.0), + Vec3::new(0.15, 0.15, 0.15), + ); + + world.spawn(( + SpotLight { + enabled: true, + color: Vec3::new(1.0, 0.0, 0.0), + intensity: 3.0, + range: 4.5, + //cutoff: math::Angle::Degrees(45.0), + ..Default::default() + }, + /* ShadowCasterSettings { + filtering_mode: ShadowFilteringMode::Pcf, + ..Default::default() + }, */ + WorldTransform::from(t), + t, + //cube_mesh.clone(), + )); + } + + let mut camera = CameraComponent::new_3d(); + camera.transform.translation = math::Vec3::new(-1.0, -10.0, -1.5); + camera.transform.rotate_x(math::Angle::Degrees(-27.0)); + camera.transform.rotate_y(math::Angle::Degrees(-90.0)); + + world.spawn((camera, FreeFlyCamera::default())); +} diff --git a/lyra-ecs/src/resource.rs b/lyra-ecs/src/resource.rs index 187f2a9..3497b4d 100644 --- a/lyra-ecs/src/resource.rs +++ b/lyra-ecs/src/resource.rs @@ -2,6 +2,8 @@ use std::{any::{Any, TypeId}, sync::Arc}; use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut}; +use crate::{Tick, TickTracker}; + /// Shorthand for `Send + Sync + 'static`, so it never needs to be implemented manually. pub trait ResourceObject: Send + Sync + Any { fn as_any(&self) -> &dyn Any; @@ -23,14 +25,17 @@ impl ResourceObject for T { pub struct ResourceData { pub(crate) data: Arc>, type_id: TypeId, + // use a tick tracker which has interior mutability + pub(crate) tick: TickTracker, } impl ResourceData { - pub fn new(data: T) -> Self { + pub fn new(data: T, tick: Tick) -> Self { Self { data: Arc::new(AtomicRefCell::new(data)), type_id: TypeId::of::(), + tick: TickTracker::from(*tick), } } @@ -80,4 +85,8 @@ impl ResourceData { .map(|r| AtomicRefMut::map(r, |a| a.as_any_mut().downcast_mut().unwrap())) .ok() } + + pub fn changed(&self, tick: Tick) -> bool { + self.tick.current() >= tick + } } \ No newline at end of file diff --git a/lyra-ecs/src/world.rs b/lyra-ecs/src/world.rs index 4d49f69..746a06e 100644 --- a/lyra-ecs/src/world.rs +++ b/lyra-ecs/src/world.rs @@ -374,33 +374,65 @@ impl World { ViewOne::new(self, entity.id, T::Query::new()) } - //pub fn view_one(&self, entity: EntityId) -> - + /// Add a resource to the world. + /// + /// Ticks the world. pub fn add_resource(&mut self, data: T) { - self.resources.insert(TypeId::of::(), ResourceData::new(data)); + let tick = self.tick(); + self.resources.insert(TypeId::of::(), ResourceData::new(data, tick)); } + /// Add the default value of a resource. + /// + /// Ticks the world. + /// + /// > Note: This will replace existing values. pub fn add_resource_default(&mut self) { - self.resources.insert(TypeId::of::(), ResourceData::new(T::default())); + let tick = self.tick(); + self.resources.insert(TypeId::of::(), ResourceData::new(T::default(), tick)); + } + + /// Add the default value of a resource if it does not already exist. + /// + /// Returns a boolean indicating if the resource was added. Ticks the world if the resource + /// was added. + pub fn add_resource_default_if_absent(&mut self) -> bool { + let id = TypeId::of::(); + if !self.resources.contains_key(&id) { + let tick = self.tick(); + self.resources.insert(id, ResourceData::new(T::default(), tick)); + + true + } else { + false + } } /// Get a resource from the world, or insert it into the world with the provided /// `fn` and return it. + /// + /// Ticks the world. pub fn get_resource_or_else(&mut self, f: F) -> AtomicRefMut where F: Fn() -> T + 'static { - self.resources.entry(TypeId::of::()) - .or_insert_with(|| ResourceData::new(f())) - .get_mut() + let tick = self.tick(); + let res = self.resources.entry(TypeId::of::()) + .or_insert_with(|| ResourceData::new(f(), tick)); + res.tick.tick_to(&tick); + res.get_mut() } /// Get a resource from the world, or insert it into the world as its default. + /// + /// Ticks the world. pub fn get_resource_or_default(&mut self) -> AtomicRefMut { - self.resources.entry(TypeId::of::()) - .or_insert_with(|| ResourceData::new(T::default())) - .get_mut() + let tick = self.tick(); + let res = self.resources.entry(TypeId::of::()) + .or_insert_with(|| ResourceData::new(T::default(), tick)); + res.tick.tick_to(&tick); + res.get_mut() } /// Gets a resource from the World. @@ -413,6 +445,22 @@ impl World { .get() } + /// Returns a boolean indicating if the resource changed. + /// + /// This will return false if the resource doesn't exist. + pub fn has_resource_changed(&self) -> bool { + let tick = self.current_tick(); + self.resources.get(&TypeId::of::()) + .map(|r| r.changed(tick)) + .unwrap_or(false) + } + + /// Returns the [`Tick`] that the resource was last modified at. + pub fn resource_tick(&self) -> Option { + self.resources.get(&TypeId::of::()) + .map(|r| r.tick.current()) + } + /// Returns boolean indicating if the World contains a resource of type `T`. pub fn has_resource(&self) -> bool { self.resources.contains_key(&TypeId::of::()) @@ -430,18 +478,32 @@ impl World { /// /// Will panic if the resource is not in the world. See [`World::try_get_resource_mut`] for /// a function that returns an option. + /// + /// Ticks the world. pub fn get_resource_mut(&self) -> AtomicRefMut { - self.resources.get(&TypeId::of::()) - .expect(&format!("World is missing resource of type '{}'", std::any::type_name::())) - .get_mut() + self.try_get_resource_mut::() + .unwrap_or_else(|| panic!("World is missing resource of type '{}'", std::any::type_name::())) } /// Attempts to get a mutable borrow of a resource from the World. /// - /// Returns `None` if the resource was not found. + /// Returns `None` if the resource was not found. Ticks the world if the resource was found. pub fn try_get_resource_mut(&self) -> Option> { self.resources.get(&TypeId::of::()) - .and_then(|r| r.try_get_mut()) + .and_then(|r| { + // now that the resource was retrieved, tick the world and the resource + let new_tick = self.tick(); + r.tick.tick_to(&new_tick); + r.try_get_mut() + }) + } + + /// Get the corresponding [`ResourceData`]. + /// + /// > Note: If you borrow the resource mutably, the world and the resource will not be ticked. + pub fn try_get_resource_data(&self) -> Option { + self.resources.get(&TypeId::of::()) + .map(|r| r.clone()) } /// Increments the TickTracker which is used for tracking changes to components. @@ -688,4 +750,22 @@ mod tests { let pos = world.view_one::<&mut Vec2>(second).get().unwrap(); assert_eq!(*pos, Vec2::new(5.0, 5.0)); } + + /// Tests resource change checks + #[test] + fn resource_changed() { + let mut world = World::new(); + world.add_resource(SimpleCounter(50)); + + assert!(world.has_resource_changed::()); + + world.spawn(Vec2::new(50.0, 50.0)); + + assert!(!world.has_resource_changed::()); + + let mut counter = world.get_resource_mut::(); + counter.0 += 100; + + assert!(world.has_resource_changed::()); + } } \ No newline at end of file diff --git a/lyra-game/Cargo.toml b/lyra-game/Cargo.toml index 26cc050..621a5f8 100644 --- a/lyra-game/Cargo.toml +++ b/lyra-game/Cargo.toml @@ -38,6 +38,8 @@ unique = "0.9.1" rustc-hash = "1.1.0" petgraph = { version = "0.6.5", features = ["matrix_graph"] } bind_match = "0.1.2" +round_mult = "0.1.3" +fast_poisson = { version = "1.0.0", features = ["single_precision"] } [features] tracy = ["dep:tracing-tracy"] diff --git a/lyra-game/src/render/graph/mod.rs b/lyra-game/src/render/graph/mod.rs index 82ea9ae..1b5ab96 100644 --- a/lyra-game/src/render/graph/mod.rs +++ b/lyra-game/src/render/graph/mod.rs @@ -95,9 +95,9 @@ struct NodeEntry { struct BindGroupEntry { label: RenderGraphLabelValue, /// BindGroup - bg: Rc, + bg: Arc, /// BindGroupLayout - layout: Option>, + layout: Option>, } #[allow(dead_code)] @@ -368,22 +368,23 @@ impl RenderGraph { } #[inline(always)] - pub fn try_bind_group>(&self, label: L) -> Option<&Rc> { + pub fn try_bind_group>(&self, label: L) -> Option<&Arc> { self.bind_groups.get(&label.into()).map(|e| &e.bg) } #[inline(always)] - pub fn bind_group>(&self, label: L) -> &Rc { - self.try_bind_group(label).expect("Unknown id for bind group") + pub fn bind_group>(&self, label: L) -> &Arc { + 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>(&self, label: L) -> Option<&Rc> { + pub fn try_bind_group_layout>(&self, label: L) -> Option<&Arc> { self.bind_groups.get(&label.into()).and_then(|e| e.layout.as_ref()) } #[inline(always)] - pub fn bind_group_layout>(&self, label: L) -> &Rc { + pub fn bind_group_layout>(&self, label: L) -> &Arc { let l = label.into(); self.try_bind_group_layout(l.clone()) .unwrap_or_else(|| panic!("Unknown label '{:?}' for bind group layout", l.clone())) diff --git a/lyra-game/src/render/graph/node.rs b/lyra-game/src/render/graph/node.rs index 48cedd6..7a39a4b 100644 --- a/lyra-game/src/render/graph/node.rs +++ b/lyra-game/src/render/graph/node.rs @@ -1,4 +1,4 @@ -use std::{cell::{Ref, RefCell, RefMut}, num::NonZeroU32, rc::Rc}; +use std::{cell::{Ref, RefCell, RefMut}, num::NonZeroU32, rc::Rc, sync::Arc}; use bind_match::bind_match; use lyra_ecs::World; @@ -54,16 +54,24 @@ pub enum SlotValue { /// The value will be set during a later phase of the render graph. To see the type of value /// this will be set to, see the slots type. Lazy, - TextureView(Rc), + TextureView(Arc), Sampler(Rc), - Texture(Rc), - Buffer(Rc), + Texture(Arc), + Buffer(Arc), RenderTarget(Rc>), Frame(Rc>>), } impl SlotValue { - pub fn as_texture_view(&self) -> Option<&Rc> { + pub fn is_none(&self) -> bool { + matches!(self, Self::None) + } + + pub fn is_lazy(&self) -> bool { + matches!(self, Self::Lazy) + } + + pub fn as_texture_view(&self) -> Option<&Arc> { bind_match!(self, Self::TextureView(v) => v) } @@ -71,11 +79,11 @@ impl SlotValue { bind_match!(self, Self::Sampler(v) => v) } - pub fn as_texture(&self) -> Option<&Rc> { + pub fn as_texture(&self) -> Option<&Arc> { bind_match!(self, Self::Texture(v) => v) } - pub fn as_buffer(&self) -> Option<&Rc> { + pub fn as_buffer(&self) -> Option<&Arc> { bind_match!(self, Self::Buffer(v) => v) } @@ -189,8 +197,8 @@ pub struct NodeDesc { /// This makes the bind groups accessible to other Nodes. pub bind_groups: Vec<( RenderGraphLabelValue, - Rc, - Option>, + Arc, + Option>, )>, } @@ -199,7 +207,7 @@ impl NodeDesc { pub fn new( pass_type: NodeType, pipeline_desc: Option, - bind_groups: Vec<(&dyn RenderGraphLabel, Rc, Option>)>, + bind_groups: Vec<(&dyn RenderGraphLabel, Arc, Option>)>, ) -> Self { Self { ty: pass_type, diff --git a/lyra-game/src/render/graph/passes/base.rs b/lyra-game/src/render/graph/passes/base.rs index d25cb47..9bb45bb 100644 --- a/lyra-game/src/render/graph/passes/base.rs +++ b/lyra-game/src/render/graph/passes/base.rs @@ -1,4 +1,4 @@ -use std::rc::Rc; +use std::sync::Arc; use glam::UVec2; use lyra_game_derive::RenderGraphLabel; @@ -56,8 +56,8 @@ impl Node for BasePass { .buffer_dynamic_offset(false) .contents(&[self.screen_size]) .finish_parts(graph.device()); - let screen_size_bgl = Rc::new(screen_size_bgl); - let screen_size_bg = Rc::new(screen_size_bg); + 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) @@ -66,17 +66,17 @@ impl Node for BasePass { .buffer_dynamic_offset(false) .contents(&[CameraUniform::default()]) .finish_parts(graph.device()); - let camera_bgl = Rc::new(camera_bgl); - let camera_bg = Rc::new(camera_bg); + 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 = Rc::new(dt_bg_pair.bindgroup); + let depth_texture_bg = Arc::new(dt_bg_pair.bindgroup); let depth_texture_bgl = dt_bg_pair.layout; - let depth_texture_view = Rc::new(depth_texture.view); + let depth_texture_view = Arc::new(depth_texture.view); let mut desc = NodeDesc::new( NodeType::Node, @@ -102,12 +102,12 @@ impl Node for BasePass { desc.add_buffer_slot( BasePassSlots::ScreenSize, SlotAttribute::Output, - Some(SlotValue::Buffer(Rc::new(screen_size_buf))), + Some(SlotValue::Buffer(Arc::new(screen_size_buf))), ); desc.add_buffer_slot( BasePassSlots::Camera, SlotAttribute::Output, - Some(SlotValue::Buffer(Rc::new(camera_buf))), + Some(SlotValue::Buffer(Arc::new(camera_buf))), ); desc diff --git a/lyra-game/src/render/graph/passes/fxaa.rs b/lyra-game/src/render/graph/passes/fxaa.rs index 5326937..6eef5bc 100644 --- a/lyra-game/src/render/graph/passes/fxaa.rs +++ b/lyra-game/src/render/graph/passes/fxaa.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, rc::Rc}; +use std::{collections::HashMap, rc::Rc, sync::Arc}; use lyra_game_derive::RenderGraphLabel; @@ -13,7 +13,7 @@ pub struct FxaaPassLabel; #[derive(Debug, Default)] pub struct FxaaPass { target_sampler: Option, - bgl: Option>, + bgl: Option>, /// Store bind groups for the input textures. /// The texture may change due to resizes, or changes to the view target chain /// from other nodes. @@ -54,7 +54,7 @@ impl Node for FxaaPass { }, ], }); - let bgl = Rc::new(bgl); + let bgl = Arc::new(bgl); self.bgl = Some(bgl.clone()); self.target_sampler = Some(device.create_sampler(&wgpu::SamplerDescriptor { label: Some("fxaa sampler"), diff --git a/lyra-game/src/render/graph/passes/light_cull_compute.rs b/lyra-game/src/render/graph/passes/light_cull_compute.rs index d62dca5..ba34741 100644 --- a/lyra-game/src/render/graph/passes/light_cull_compute.rs +++ b/lyra-game/src/render/graph/passes/light_cull_compute.rs @@ -1,4 +1,4 @@ -use std::{mem, rc::Rc}; +use std::{mem, rc::Rc, sync::Arc}; use glam::Vec2Swizzles; use lyra_ecs::World; @@ -63,7 +63,7 @@ impl Node for LightCullComputePass { usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, }); - let light_indices_bg_layout = Rc::new(device.create_bind_group_layout( + let light_indices_bg_layout = Arc::new(device.create_bind_group_layout( &wgpu::BindGroupLayoutDescriptor { entries: &[ wgpu::BindGroupLayoutEntry { @@ -128,7 +128,7 @@ impl Node for LightCullComputePass { array_layer_count: None, }); - let light_indices_bg = Rc::new(device.create_bind_group(&wgpu::BindGroupDescriptor { + let light_indices_bg = Arc::new(device.create_bind_group(&wgpu::BindGroupDescriptor { layout: &light_indices_bg_layout, entries: &[ wgpu::BindGroupEntry { @@ -194,7 +194,7 @@ impl Node for LightCullComputePass { desc.add_buffer_slot( LightCullComputePassSlots::IndexCounterBuffer, SlotAttribute::Output, - Some(SlotValue::Buffer(Rc::new(light_index_counter_buffer))), + Some(SlotValue::Buffer(Arc::new(light_index_counter_buffer))), ); desc diff --git a/lyra-game/src/render/graph/passes/mesh_prepare.rs b/lyra-game/src/render/graph/passes/mesh_prepare.rs new file mode 100644 index 0000000..9058b66 --- /dev/null +++ b/lyra-game/src/render/graph/passes/mesh_prepare.rs @@ -0,0 +1,767 @@ +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::{Has, Not, Or}, + Entities, Res, ResMut, TickOf, + }, + relation::{ChildOf, RelationOriginComponent}, + Component, Entity, ResourceObject, World, +}; +use lyra_game_derive::RenderGraphLabel; +use lyra_math::Transform; +use lyra_resource::{gltf::Mesh, ResHandle}; +use lyra_scene::{SceneGraph, WorldTransform}; +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::{TransformBuffers, TransformGroup}, + vertex::Vertex, + }, + DeltaTime, +}; + +type MeshHandle = ResHandle; +type SceneHandle = ResHandle; + +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>, +} + +#[derive(Clone, Debug, Component)] +struct InterpTransform { + last_transform: Transform, + alpha: f32, +} + +#[derive(Default, Debug, Clone, Copy, Hash, RenderGraphLabel)] +pub struct MeshPrepNodeLabel; + +#[derive(Debug)] +pub struct MeshPrepNode { + pub material_bgl: Arc, +} + +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, + mesh_han: &ResHandle, + ) { + 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::(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_resource::gltf::MeshIndices::U16(v) => { + bytemuck::pod_align_to::(v).1 + } + lyra_resource::gltf::MeshIndices::U32(v) => { + bytemuck::pod_align_to::(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 = 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_resource::gltf::MeshIndices::U16(v) => { + (wgpu::IndexFormat::Uint16, v.len(), bytemuck::cast_slice(v)) + } + lyra_resource::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>, + 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, + material_buffers: &mut RenderAssets>, + entity_meshes: &mut FxHashMap, + 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(world: &mut World) { + if !world.has_resource::() { + world.add_resource_default::(); + } + } +} + +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 render_limits = device.limits(); + + let last_epoch = world.current_tick(); + let mut alive_entities = HashSet::new(); + + { + // prepare the world with resources + if !world.has_resource::() { + let buffers = TransformBuffers::new(device); + world.add_resource(buffers); + } + Self::try_init_resource::(world); + Self::try_init_resource::>(world); + Self::try_init_resource::>>(world); + Self::try_init_resource::>(world); + + let mut render_meshes = world.get_resource_mut::(); + render_meshes.clear(); + } + + let view = world.view_iter::<( + Entities, + &Transform, + TickOf, + Or<(&MeshHandle, TickOf), (&SceneHandle, TickOf)>, + Option<&mut InterpTransform>, + Res, + ResMut, + ResMut, + ResMut>, + ResMut>>, + ResMut>, + )>(); + + // used to store InterpTransform components to add to entities later + let mut component_queue: Vec<(Entity, InterpTransform)> = vec![]; + + for ( + entity, + transform, + _transform_epoch, + (mesh_pair, scene_pair), + interp_tran, + delta_time, + mut transforms, + mut render_meshes, + mut mesh_buffers, + mut material_buffers, + mut entity_meshes, + ) in view + { + alive_entities.insert(entity); + + // Interpolate the transform for this entity using a component. + // If the entity does not have the component then it will be queued to be added + // to it after all the entities are prepared for rendering. + let interp_transform = match interp_tran { + Some(mut interp_transform) => { + // found in https://youtu.be/YJB1QnEmlTs?t=472 + interp_transform.alpha = 1.0 - interp_transform.alpha.powf(**delta_time); + + interp_transform.last_transform = interp_transform + .last_transform + .lerp(*transform, interp_transform.alpha); + interp_transform.last_transform + } + None => { + let interp = InterpTransform { + last_transform: *transform, + alpha: 0.5, + }; + component_queue.push((entity, interp)); + + *transform + } + }; + + { + // expand the transform buffers if they need to be. + // this is done in its own scope to avoid multiple mutable references to self at + // once; aka, make the borrow checker happy + if transforms.needs_expand() { + debug!("Expanding transform buffers"); + transforms.expand_buffers(device); + } + } + + 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 group = TransformGroup::EntityRes(entity, mesh_han.uuid()); + let transform_id = transforms.update_or_push( + device, + queue, + &render_limits, + group, + interp_transform.calculate_mat4(), + glam::Mat3::from_quat(interp_transform.rotation), + ); + + 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_id); + render_meshes.push_back(job); + } + } + + if let Some((scene_han, scene_epoch)) = scene_pair { + if let Some(scene) = scene_han.data_ref() { + if scene_epoch == last_epoch { + let view = scene.world().view::<( + Entities, + &mut WorldTransform, + &Transform, + Not>>, + )>(); + lyra_scene::system_update_world_transforms(scene.world(), view).unwrap(); + } + + for (mesh_han, pos) in + scene.world().view_iter::<(&MeshHandle, &WorldTransform)>() + { + if let Some(mesh) = mesh_han.data_ref() { + let mesh_interpo = interp_transform + **pos; + + // 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 scene_mesh_group = + TransformGroup::Res(scene_han.uuid(), mesh_han.uuid()); + let group = TransformGroup::OwnedGroup(entity, scene_mesh_group.into()); + let transform_id = transforms.update_or_push( + device, + queue, + &render_limits, + group, + mesh_interpo.calculate_mat4(), + glam::Mat3::from_quat(mesh_interpo.rotation), + ); + + 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_id); + render_meshes.push_back(job); + } + } + } + } + } + + for (en, interp) in component_queue { + world.insert(en, interp); + } + + let mut transforms = world.get_resource_mut::(); + transforms.send_to_gpu(queue); + } + + fn execute( + &mut self, + _: &mut crate::render::graph::RenderGraph, + _: &crate::render::graph::NodeDesc, + _: &mut crate::render::graph::RenderGraphContext, + ) { + } +} + +#[repr(transparent)] +pub struct RenderAssets(FxHashMap); + +impl Deref for RenderAssets { + type Target = FxHashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for RenderAssets { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Default for RenderAssets { + fn default() -> Self { + Self(Default::default()) + } +} + +impl RenderAssets { + pub fn new() -> Self { + Self::default() + } +} + +#[allow(dead_code)] +pub struct GpuMaterial { + pub bind_group: Arc, + bind_group_layout: Arc, + 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 { + 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::() 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: std::num::NonZeroU32::new(4 * dim.0), + rows_per_image: std::num::NonZeroU32::new(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, + mat: &lyra_resource::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); + +impl Deref for RenderMeshes { + type Target = VecDeque; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for RenderMeshes { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/lyra-game/src/render/graph/passes/meshes.rs b/lyra-game/src/render/graph/passes/meshes.rs index ebb75ba..ba2858a 100644 --- a/lyra-game/src/render/graph/passes/meshes.rs +++ b/lyra-game/src/render/graph/passes/meshes.rs @@ -1,497 +1,363 @@ -use std::{collections::{HashSet, VecDeque}, rc::Rc}; +use std::{rc::Rc, sync::Arc}; -use glam::Vec3; -use itertools::izip; -use lyra_ecs::{query::{filter::{Has, Not, Or}, Entities, Res, TickOf}, relation::{ChildOf, RelationOriginComponent}, Component, Entity}; +use lyra_ecs::{AtomicRef, ResourceData}; use lyra_game_derive::RenderGraphLabel; -use lyra_math::Transform; -use lyra_resource::{gltf::Mesh, ResHandle}; -use lyra_scene::{SceneGraph, WorldTransform}; -use rustc_hash::FxHashMap; -use tracing::{debug, instrument, warn}; -use uuid::Uuid; -use wgpu::util::DeviceExt; +use tracing::{instrument, warn}; -use crate::{ - render::{ - desc_buf_lay::DescVertexBufferLayout, graph::{ - Node, NodeDesc, NodeType, RenderGraph, RenderGraphContext - }, material::{Material, MaterialUniform}, render_buffer::{BufferStorage, BufferWrapper}, render_job::RenderJob, resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, texture::RenderTexture, transform_buffer_storage::{TransformBuffers, TransformGroup}, vertex::Vertex - }, - DeltaTime, +use crate::render::{ + desc_buf_lay::DescVertexBufferLayout, + graph::{Node, NodeDesc, NodeType, RenderGraph, RenderGraphContext}, + resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, + texture::RenderTexture, + transform_buffer_storage::TransformBuffers, + vertex::Vertex, }; -use super::{BasePassSlots, LightBasePassSlots, LightCullComputePassSlots}; - -type MeshHandle = ResHandle; -type SceneHandle = ResHandle; +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 + Material, } -struct MeshBufferStorage { - buffer_vertex: BufferStorage, - 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? - material: Option>, +/// Stores the bind group and bind group layout for the shadow atlas texture +struct ShadowsAtlasBgPair { + layout: Arc, + bg: Arc, } -#[derive(Clone, Debug, Component)] -struct InterpTransform { - last_transform: Transform, - alpha: f32, -} - -#[derive(Default)] +//#[derive(Default)] +#[allow(dead_code)] pub struct MeshPass { - transforms: Option, - mesh_buffers: FxHashMap, - render_jobs: VecDeque, - - texture_bind_group_layout: Option>, - material_buffer: Option, - material_buffers: FxHashMap>, - entity_meshes: FxHashMap, - default_texture: Option, pipeline: Option, - material_bgl: Option>, + material_bgl: Arc, + + // TODO: find a better way to extract these resources from the main world to be used in the + // render stage. + transform_buffers: Option, + render_meshes: Option, + mesh_buffers: Option, + + shadows_atlas: Option, } impl MeshPass { - pub fn new() -> Self { - Self::default() - } + pub fn new(material_bgl: Arc) -> Self { + Self { + default_texture: None, + pipeline: None, + material_bgl, - /// Checks if the mesh buffers in the GPU need to be updated. - #[instrument(skip(self, device, queue, mesh_han))] - fn check_mesh_buffers(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, mesh_han: &ResHandle) { - let mesh_uuid = mesh_han.uuid(); + transform_buffers: None, + render_meshes: None, + mesh_buffers: None, - if let (Some(mesh), Some(buffers)) = (mesh_han.data_ref(), self.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 = self.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::(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_resource::gltf::MeshIndices::U16(v) => bytemuck::pod_align_to::(v).1, - lyra_resource::gltf::MeshIndices::U32(v) => bytemuck::pod_align_to::(v).1, - }; - - let index_buffer = index_buffer.1.buffer(); - queue.write_buffer(index_buffer, 0, bytemuck::cast_slice(aligned_indices)); - } + shadows_atlas: None, } } - #[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 = 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_resource::gltf::MeshIndices::U16(v) => (wgpu::IndexFormat::Uint16, v.len(), bytemuck::cast_slice(v)), - lyra_resource::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 ) + fn transform_buffers(&self) -> AtomicRef { + self.transform_buffers.as_ref().unwrap().get() } - #[instrument(skip(self, device, queue, mesh))] - fn create_mesh_buffers(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, 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 = self.material_buffers.entry(material.uuid()) - .or_insert_with(|| { - debug!(uuid=material.uuid().to_string(), "Sending material to gpu"); - Rc::new(Material::from_resource(device, queue, self.texture_bind_group_layout.clone().unwrap(), &material_ref)) - }); - - // TODO: support material uniforms from multiple uniforms - let uni = MaterialUniform::from(&**material); - queue.write_buffer(self.material_buffer.as_ref().unwrap(), 0, bytemuck::bytes_of(&uni)); - - MeshBufferStorage { - buffer_vertex: vertex_buffer, - buffer_indices, - material: Some(material.clone()), - } + fn render_meshes(&self) -> AtomicRef { + self.render_meshes.as_ref().unwrap().get() } - /// 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, entity))] - fn process_mesh(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, entity: Entity, mesh: &Mesh, mesh_uuid: Uuid) -> bool { - #[allow(clippy::map_entry)] - if !self.mesh_buffers.contains_key(&mesh_uuid) { - // create the mesh's buffers - let buffers = self.create_mesh_buffers(device, queue, mesh); - self.mesh_buffers.insert(mesh_uuid, buffers); - self.entity_meshes.insert(entity, mesh_uuid); - - true - } else { false } + fn mesh_buffers(&self) -> AtomicRef> { + self.mesh_buffers.as_ref().unwrap().get() } } impl Node for MeshPass { fn desc( &mut self, - graph: &mut crate::render::graph::RenderGraph, + _: &mut crate::render::graph::RenderGraph, ) -> crate::render::graph::NodeDesc { - - let device = graph.device(); - - let transforms = TransformBuffers::new(device); - //let transform_bgl = transforms.bindgroup_layout.clone(); - self.transforms = Some(transforms); - - let texture_bind_group_layout = Rc::new(RenderTexture::create_layout(device)); - self.texture_bind_group_layout = Some(texture_bind_group_layout.clone()); - - let (material_bgl, material_bg, material_buf, _) = BufferWrapper::builder() - .label_prefix("material") - .visibility(wgpu::ShaderStages::FRAGMENT) - .buffer_usage(wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST) - .contents(&[MaterialUniform::default()]) - .finish_parts(device); - let material_bgl = Rc::new(material_bgl); - self.material_bgl = Some(material_bgl.clone()); - let material_bg = Rc::new(material_bg); - - self.material_buffer = Some(material_buf); - // 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()); - - // get surface config format - /* let main_rt = graph.slot_value(BasePassSlots::MainRenderTarget) - .and_then(|s| s.as_render_target()) - .expect("missing main render target"); - let surface_config_format = main_rt.format(); - drop(main_rt); */ - - /* 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 shader = Rc::new(Shader { - label: Some("base_shader".into()), - source: include_str!("../../shaders/base.wgsl").to_string(), - }); */ - - + //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, - /* Some(PipelineDescriptor::Render(RenderPipelineDescriptor { - label: Some("meshes".into()), - layouts: vec![ - texture_bind_group_layout.clone(), - transform_bgl, - camera_bgl.clone(), - lights_bgl.clone(), - material_bgl.clone(), - texture_bind_group_layout, - light_grid_bgl.clone(), - ], - 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::default(), - multisample: wgpu::MultisampleState::default(), - multiview: None, - })), */ vec![ - (&MeshesPassSlots::Material, material_bg, Some(material_bgl)), + //(&MeshesPassSlots::Material, material_bg, Some(material_bgl)), ], ) } - #[instrument(skip(self, graph, world, context))] - fn prepare(&mut self, graph: &mut RenderGraph, world: &mut lyra_ecs::World, context: &mut RenderGraphContext) { - let device = &context.device; - let queue = &context.queue; - let render_limits = device.limits(); - - let last_epoch = world.current_tick(); - let mut alive_entities = HashSet::new(); - - let view = world.view_iter::<( - Entities, - &Transform, - TickOf, - Or< - (&MeshHandle, TickOf), - (&SceneHandle, TickOf) - >, - Option<&mut InterpTransform>, - Res, - )>(); - - // used to store InterpTransform components to add to entities later - let mut component_queue: Vec<(Entity, InterpTransform)> = vec![]; - - for ( - entity, - transform, - _transform_epoch, - ( - mesh_pair, - scene_pair - ), - interp_tran, - delta_time, - ) in view - { - alive_entities.insert(entity); - - // Interpolate the transform for this entity using a component. - // If the entity does not have the component then it will be queued to be added - // to it after all the entities are prepared for rendering. - let interp_transform = match interp_tran { - Some(mut interp_transform) => { - // found in https://youtu.be/YJB1QnEmlTs?t=472 - interp_transform.alpha = 1.0 - interp_transform.alpha.powf(**delta_time); - - interp_transform.last_transform = interp_transform.last_transform.lerp(*transform, interp_transform.alpha); - interp_transform.last_transform - }, - None => { - let interp = InterpTransform { - last_transform: *transform, - alpha: 0.5, - }; - component_queue.push((entity, interp)); - - *transform - } - }; - - { - // expand the transform buffers if they need to be. - // this is done in its own scope to avoid multiple mutable references to self at - // once; aka, make the borrow checker happy - let transforms = self.transforms.as_mut().unwrap(); - if transforms.needs_expand() { - debug!("Expanding transform buffers"); - transforms.expand_buffers(device); - } - } - - 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, entity, &mesh, mesh_han.uuid()) - && mesh_epoch == last_epoch { - self.check_mesh_buffers(device, queue, &mesh_han); - } - - let transforms = self.transforms.as_mut().unwrap(); - let group = TransformGroup::EntityRes(entity, mesh_han.uuid()); - let transform_id = transforms.update_or_push(device, queue, &render_limits, - group, interp_transform.calculate_mat4(), glam::Mat3::from_quat(interp_transform.rotation)); - - 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_id); - self.render_jobs.push_back(job); - } - } - - if let Some((scene_han, scene_epoch)) = scene_pair { - if let Some(scene) = scene_han.data_ref() { - if scene_epoch == last_epoch { - let view = scene.world().view::<(Entities, &mut WorldTransform, &Transform, Not>>)>(); - lyra_scene::system_update_world_transforms(scene.world(), view).unwrap(); - } - - for (mesh_han, pos) in scene.world().view_iter::<(&MeshHandle, &WorldTransform)>() { - if let Some(mesh) = mesh_han.data_ref() { - let mesh_interpo = interp_transform + **pos; - - // 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, entity, &mesh, mesh_han.uuid()) - && scene_epoch == last_epoch { - self.check_mesh_buffers(device, queue, &mesh_han); - } - - let transforms = self.transforms.as_mut().unwrap(); - let scene_mesh_group = TransformGroup::Res(scene_han.uuid(), mesh_han.uuid()); - let group = TransformGroup::OwnedGroup(entity, scene_mesh_group.into()); - let transform_id = transforms.update_or_push(device, queue, &render_limits, - group, mesh_interpo.calculate_mat4(), glam::Mat3::from_quat(mesh_interpo.rotation) ); - - 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_id); - self.render_jobs.push_back(job); - } - } - } - } - } - - for (en, interp) in component_queue { - world.insert(en, interp); - } - - let transforms = self.transforms.as_mut().unwrap(); - transforms.send_to_gpu(queue); - + #[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 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 light_grid_bgl = + graph.bind_group_layout(LightCullComputePassSlots::LightIndicesGridGroup); + let atlas_bgl = self.shadows_atlas.as_ref().unwrap().layout.clone(); let shader = Rc::new(Shader { label: Some("base_shader".into()), source: include_str!("../../shaders/base.wgsl").to_string(), }); - self.pipeline = Some(RenderPipeline::create(device, &RenderPipelineDescriptor { - label: Some("meshes".into()), - layouts: vec![ - self.texture_bind_group_layout.as_ref().unwrap().clone(), - //transform_bgl - self.transforms.as_ref().unwrap().bindgroup_layout.clone(), - camera_bgl.clone(), - lights_bgl.clone(), - self.material_bgl.as_ref().unwrap().clone(), - self.texture_bind_group_layout.as_ref().unwrap().clone(), - light_grid_bgl.clone(), - ], - push_constant_ranges: vec![], - vertex: VertexState { - module: shader.clone(), - entry_point: "vs_main".into(), - buffers: vec![ - Vertex::desc().into(), + let transforms = world + .try_get_resource_data::() + .expect("Missing transform buffers"); + self.transform_buffers = Some(transforms.clone()); + + let render_meshes = world + .try_get_resource_data::() + .expect("Missing transform buffers"); + self.render_meshes = Some(render_meshes.clone()); + + let mesh_buffers = world + .try_get_resource_data::>() + .expect("Missing render meshes"); + self.mesh_buffers = Some(mesh_buffers.clone()); + + let transforms = transforms.get::(); + + 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, }, - 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::default(), - multisample: wgpu::MultisampleState::default(), - multiview: None, - })); + )); } } @@ -504,10 +370,10 @@ impl Node for MeshPass { 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"); */ + .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(); @@ -518,102 +384,117 @@ impl Node for MeshPass { .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 camera_bg = graph.bind_group(BasePassSlots::Camera); - let light_grid_bg = graph - .bind_group(LightCullComputePassSlots::LightIndicesGridGroup); + let lights_bg = graph.bind_group(LightBasePassSlots::Lights); - let material_bg = graph - .bind_group(MeshesPassSlots::Material); + 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"); */ + .expect("Failed to find pipeline for MeshPass"); */ let pipeline = self.pipeline.as_ref().unwrap(); - 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::Clear(wgpu::Color { - r: 0.1, - g: 0.2, - b: 0.3, - a: 1.0, + 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::Clear(wgpu::Color { + r: 0.1, + g: 0.2, + b: 0.3, + a: 1.0, + }), + store: true, + }, + })], + // enable depth buffer + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: depth_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: true, }), - store: true, - }, - })], - // enable depth buffer - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: depth_view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Clear(1.0), - store: true, + stencil_ops: None, }), - stencil_ops: None, - }), - }); + }); - pass.set_pipeline(pipeline); + pass.set_pipeline(pipeline); - //let material_buffer_bg = self.material_buffer.as_ref().unwrap().bindgroup(); - let default_texture = self.default_texture.as_ref().unwrap(); - let transforms = self.transforms.as_mut().unwrap(); + //let default_texture = self.default_texture.as_ref().unwrap(); - while let Some(job) = self.render_jobs.pop_front() { - // get the mesh (containing vertices) and the buffers from storage - let buffers = self.mesh_buffers.get(&job.mesh_uuid); - if buffers.is_none() { - warn!("Skipping job since its mesh is missing {:?}", job.mesh_uuid); - continue; - } - let buffers = buffers.unwrap(); + for job in render_meshes.iter() { + // get the mesh (containing vertices) and the buffers from storage + let buffers = mesh_buffers.get(&job.mesh_uuid); + if buffers.is_none() { + warn!("Skipping job since its mesh is missing {:?}", job.mesh_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(), &[]); - } + // 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(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, ]); + // 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(2, camera_bg, &[]); + pass.set_bind_group(3, lights_bg, &[]); + //pass.set_bind_group(4, material_bg, &[]); - pass.set_bind_group(6, light_grid_bg, &[]); + pass.set_bind_group(4, light_grid_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_bind_group(5, shadows_atlas_bg, &[]); - 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(); + // 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.draw(0..vertex_count as u32, 0..1); + 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); + } } } } diff --git a/lyra-game/src/render/graph/passes/mod.rs b/lyra-game/src/render/graph/passes/mod.rs index 8e7a2d9..230c5ae 100644 --- a/lyra-game/src/render/graph/passes/mod.rs +++ b/lyra-game/src/render/graph/passes/mod.rs @@ -20,4 +20,10 @@ mod tint; pub use tint::*; mod fxaa; -pub use fxaa::*; \ No newline at end of file +pub use fxaa::*; + +mod shadows; +pub use shadows::*; + +mod mesh_prepare; +pub use mesh_prepare::*; \ No newline at end of file diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs new file mode 100644 index 0000000..3df7011 --- /dev/null +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -0,0 +1,1150 @@ +use std::{ + collections::VecDeque, + mem, + rc::Rc, + sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}, +}; + +use fast_poisson::{Poisson2D, Poisson3D}; +use glam::Vec2; +use itertools::Itertools; +use lyra_ecs::{ + query::{filter::Has, Entities}, + AtomicRef, Component, Entity, ResourceData, +}; +use lyra_game_derive::RenderGraphLabel; +use lyra_math::{Angle, Transform}; +use rustc_hash::FxHashMap; +use tracing::{debug, warn}; +use wgpu::util::DeviceExt; + +use crate::render::{ + graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, + light::{directional::DirectionalLight, LightType, PointLight, SpotLight}, + resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, + transform_buffer_storage::TransformBuffers, + vertex::Vertex, + AtlasFrame, GpuSlotBuffer, TextureAtlas, +}; + +use super::{MeshBufferStorage, RenderAssets, RenderMeshes}; + +const SHADOW_SIZE: glam::UVec2 = glam::uvec2(1024, 1024); + +#[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] +pub enum ShadowMapsPassSlots { + ShadowAtlasTexture, + ShadowAtlasTextureView, + ShadowAtlasSampler, + ShadowAtlasSamplerComparison, + ShadowAtlasSizeBuffer, + ShadowLightUniformsBuffer, + ShadowSettingsUniform, + PcfPoissonDiscBuffer, + PcfPoissonDiscBuffer3d, + PcssPoissonDiscBuffer, +} + +#[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] +pub struct ShadowMapsPassLabel; + +#[derive(Clone, Copy)] +struct LightDepthMap { + /// The type of the light that this map is created for. + light_type: LightType, + /// The index of the first shadow depth map. + /// + /// If the light is a point light, this is the index of the FIRST depth map in the atlas with + /// the maps of the other sides following the index. + atlas_index: u64, + /// The index of the uniform for the light in the uniform array. + uniform_index: [u64; 6], +} + +pub struct ShadowMapsPass { + bgl: Arc, + atlas_size_buffer: Arc, + light_uniforms_buffer: GpuSlotBuffer, + uniforms_bg: Arc, + /// depth maps for a light owned by an entity. + depth_maps: FxHashMap, + + // TODO: find a better way to extract these resources from the main world to be used in the + // render stage. + transform_buffers: Option, + render_meshes: Option, + mesh_buffers: Option, + pipeline: Option, + point_light_pipeline: Option, + + atlas: LightShadowMapAtlas, + /// The depth map atlas sampler + atlas_sampler: Rc, + atlas_sampler_compare: Rc, +} + +impl ShadowMapsPass { + pub fn new(device: &wgpu::Device) -> Self { + let bgl = Arc::new( + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("bgl_shadow_maps_lights"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + 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 = TextureAtlas::new( + device, + wgpu::TextureFormat::Depth32Float, + wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, + SHADOW_SIZE * 8, + ); + + let atlas_size_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("buffer_shadow_maps_atlas_size"), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + contents: bytemuck::bytes_of(&atlas.atlas_size()), + }); + + let sampler_compare = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("compare_sampler_shadow_map_atlas"), + address_mode_u: wgpu::AddressMode::ClampToBorder, + address_mode_v: wgpu::AddressMode::ClampToBorder, + address_mode_w: wgpu::AddressMode::ClampToBorder, + mag_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + border_color: Some(wgpu::SamplerBorderColor::OpaqueWhite), + compare: Some(wgpu::CompareFunction::LessEqual), + ..Default::default() + }); + + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("sampler_shadow_map_atlas"), + address_mode_u: wgpu::AddressMode::ClampToBorder, + address_mode_v: wgpu::AddressMode::ClampToBorder, + address_mode_w: wgpu::AddressMode::ClampToBorder, + mag_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + border_color: Some(wgpu::SamplerBorderColor::OpaqueWhite), + ..Default::default() + }); + + let cap = device.limits().max_storage_buffer_binding_size as u64 + / mem::size_of::() as u64; + let uniforms_buffer = GpuSlotBuffer::new( + device, + Some("buffer_shadow_maps_light"), + wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + cap, + ); + + let uniforms_bg = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("bind_group_shadows"), + layout: &bgl, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: uniforms_buffer.buffer(), + offset: 0, + size: None, + }), + }], + }); + + Self { + bgl, + light_uniforms_buffer: uniforms_buffer, + uniforms_bg: Arc::new(uniforms_bg), + atlas_size_buffer: Arc::new(atlas_size_buffer), + depth_maps: Default::default(), + transform_buffers: None, + render_meshes: None, + mesh_buffers: None, + pipeline: None, + point_light_pipeline: None, + + atlas_sampler: Rc::new(sampler), + atlas_sampler_compare: Rc::new(sampler_compare), + atlas: LightShadowMapAtlas(Arc::new(RwLock::new(atlas))), + } + } + + /// Create a depth map and return the id of the depth map in the texture atlas. + fn create_depth_map( + &mut self, + queue: &wgpu::Queue, + light_type: LightType, + entity: Entity, + light_pos: Transform, + light_half_outer_angle: Option, + are_settings_custom: bool, + shadow_settings: ShadowCasterSettings, + ) -> LightDepthMap { + let mut atlas = self.atlas.get_mut(); + + let u = ShadowSettingsUniform::new( + shadow_settings.filtering_mode, + shadow_settings.pcf_samples_num, + shadow_settings.pcss_blocker_search_samples, + ); + + let has_shadow_settings = if are_settings_custom { + 1 + } else { 0 }; + /* let (has_shadow_settings, pcf_samples_num, pcss_samples_num) = if are_settings_custom { + + (1, u.pcf_samples_num, u.pcss_blocker_search_samples) + } else { + (0, , 0) + }; */ + + /* shadow_settings.map(|ss| { + let u = ShadowSettingsUniform::new(ss.filtering_mode, ss.pcf_samples_num, ss.pcss_blocker_search_samples); + (1, u.pcf_samples_num, u.pcss_blocker_search_samples) + }).unwrap_or((0, 0, 0)); */ + + let (start_atlas_idx, uniform_indices) = match light_type { + LightType::Directional => { + let directional_size = SHADOW_SIZE * 4; + // directional lights require a single map, so allocate that in the atlas. + let atlas_index = atlas + .pack(directional_size.x as _, directional_size.y as _) + .expect("failed to pack new shadow map into texture atlas"); + let atlas_frame = atlas.texture_frame(atlas_index).expect("Frame missing"); + + let projection = glam::Mat4::orthographic_rh( + -10.0, + 10.0, + -10.0, + 10.0, + shadow_settings.near_plane, + shadow_settings.far_plane, + ); + + // honestly no clue why this works, but I got it from here and the results are good + // https://github.com/asylum2010/Asylum_Tutorials/blob/423e5edfaee7b5ea450a450e65f2eabf641b2482/ShaderTutors/43_ShadowMapFiltering/main.cpp#L323 + let frustum_size = Vec2::new(0.5 * projection.col(0).x, 0.5 * projection.col(1).y); + // maybe its better to make this a vec2 on the gpu? + let size_avg = (frustum_size.x + frustum_size.y) / 2.0; + let light_size_uv = 0.2 * size_avg; + + let look_view = glam::Mat4::look_to_rh( + light_pos.translation, + light_pos.forward(), + light_pos.up(), + ); + + let light_proj = projection * look_view; + + let u = LightShadowUniform { + space_mat: light_proj, + atlas_frame, + near_plane: shadow_settings.near_plane, + far_plane: shadow_settings.far_plane, + light_size_uv, + _padding1: 0, + light_pos: light_pos.translation, + has_shadow_settings, + pcf_samples_num: u.pcf_samples_num, + pcss_blocker_search_samples: u.pcss_blocker_search_samples, + constant_depth_bias: DEFAULT_CONSTANT_DEPTH_BIAS * shadow_settings.constant_depth_bias_scale, + _padding2: 0, + }; + + let uniform_index = self.light_uniforms_buffer.insert(queue, &u); + let mut indices = [0; 6]; + indices[0] = uniform_index; + (atlas_index, indices) + } + LightType::Spotlight => { + let directional_size = SHADOW_SIZE * 4; + // directional lights require a single map, so allocate that in the atlas. + let atlas_index = atlas + .pack(directional_size.x as _, directional_size.y as _) + .expect("failed to pack new shadow map into texture atlas"); + let atlas_frame = atlas.texture_frame(atlas_index).expect("Frame missing"); + + let aspect = SHADOW_SIZE.x as f32 / SHADOW_SIZE.y as f32; + let projection = glam::Mat4::perspective_rh( + (light_half_outer_angle.unwrap() * 2.0).to_radians(), + aspect, + shadow_settings.near_plane, + shadow_settings.far_plane, + ); + + // honestly no clue why this works, but I got it from here and the results are good + // https://github.com/asylum2010/Asylum_Tutorials/blob/423e5edfaee7b5ea450a450e65f2eabf641b2482/ShaderTutors/43_ShadowMapFiltering/main.cpp#L323 + /* let frustum_size = Vec2::new(0.5 * projection.col(0).x, 0.5 * projection.col(1).y); + // maybe its better to make this a vec2 on the gpu? + let size_avg = (frustum_size.x + frustum_size.y) / 2.0; + let light_size_uv = 0.2 * size_avg; */ + + let light_trans = light_pos.translation; + let look_view = glam::Mat4::look_at_rh( + light_trans, + light_trans + glam::vec3(1.0, 0.0, 0.0), + glam::vec3(0.0, -1.0, 0.0), + ); + + let light_proj = projection * look_view; + + let u = LightShadowUniform { + space_mat: light_proj, + atlas_frame, + near_plane: shadow_settings.near_plane, + far_plane: shadow_settings.far_plane, + light_size_uv: 0.0, + _padding1: 0, + light_pos: light_pos.translation, + has_shadow_settings, + pcf_samples_num: u.pcf_samples_num, + pcss_blocker_search_samples: u.pcss_blocker_search_samples, + constant_depth_bias: DEFAULT_CONSTANT_DEPTH_BIAS * shadow_settings.constant_depth_bias_scale, + _padding2: 0, + }; + + let uniform_index = self.light_uniforms_buffer.insert(queue, &u); + let mut indices = [0; 6]; + indices[0] = uniform_index; + (atlas_index, indices) + }, + LightType::Point => { + let aspect = SHADOW_SIZE.x as f32 / SHADOW_SIZE.y as f32; + let projection = glam::Mat4::perspective_rh( + Angle::Degrees(90.0).to_radians(), + aspect, + shadow_settings.near_plane, + shadow_settings.far_plane, + ); + + let light_trans = light_pos.translation; + // right, left, top, bottom, near, and far + let views = [ + projection + * glam::Mat4::look_at_rh( + light_trans, + light_trans + glam::vec3(1.0, 0.0, 0.0), + glam::vec3(0.0, -1.0, 0.0), + ), + projection + * glam::Mat4::look_at_rh( + light_trans, + light_trans + glam::vec3(-1.0, 0.0, 0.0), + glam::vec3(0.0, -1.0, 0.0), + ), + projection + * glam::Mat4::look_at_rh( + light_trans, + light_trans + glam::vec3(0.0, 1.0, 0.0), + glam::vec3(0.0, 0.0, 1.0), + ), + projection + * glam::Mat4::look_at_rh( + light_trans, + light_trans + glam::vec3(0.0, -1.0, 0.0), + glam::vec3(0.0, 0.0, -1.0), + ), + projection + * glam::Mat4::look_at_rh( + light_trans, + light_trans + glam::vec3(0.0, 0.0, 1.0), + glam::vec3(0.0, -1.0, 0.0), + ), + projection + * glam::Mat4::look_at_rh( + light_trans, + light_trans + glam::vec3(0.0, 0.0, -1.0), + glam::vec3(0.0, -1.0, 0.0), + ), + ]; + + let atlas_idx_1 = atlas.pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _).unwrap(); + let atlas_idx_2 = atlas.pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _).unwrap(); + let atlas_idx_3 = atlas.pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _).unwrap(); + let atlas_idx_4 = atlas.pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _).unwrap(); + let atlas_idx_5 = atlas.pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _).unwrap(); + let atlas_idx_6 = atlas.pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _).unwrap(); + + let frames = [ + atlas.texture_frame(atlas_idx_1).unwrap(), + atlas.texture_frame(atlas_idx_2).unwrap(), + atlas.texture_frame(atlas_idx_3).unwrap(), + atlas.texture_frame(atlas_idx_4).unwrap(), + atlas.texture_frame(atlas_idx_5).unwrap(), + atlas.texture_frame(atlas_idx_6).unwrap(), + ]; + + // create the uniforms of the light, storing them in the gpu buffer, and + // collecting the indices in the buffer they're at. + let mut indices = [0; 6]; + for i in 0..6 { + let uniform_i = self.light_uniforms_buffer.insert( + queue, + &LightShadowUniform { + space_mat: views[i], + atlas_frame: frames[i], + near_plane: shadow_settings.near_plane, + far_plane: shadow_settings.far_plane, + light_size_uv: 0.0, + _padding1: 0, + light_pos: light_trans, + has_shadow_settings, + pcf_samples_num: u.pcf_samples_num, + pcss_blocker_search_samples: u.pcss_blocker_search_samples, + constant_depth_bias: DEFAULT_CONSTANT_DEPTH_BIAS * shadow_settings.constant_depth_bias_scale, + _padding2: 0, + }, + ); + indices[i] = uniform_i; + } + + (atlas_idx_1, indices) + } + }; + + let v = LightDepthMap { + light_type, + atlas_index: start_atlas_idx, + uniform_index: uniform_indices, + }; + self.depth_maps.insert(entity, v); + + v + } + + fn transform_buffers(&self) -> AtomicRef { + self.transform_buffers.as_ref().unwrap().get() + } + + fn render_meshes(&self) -> AtomicRef { + self.render_meshes.as_ref().unwrap().get() + } + + fn mesh_buffers(&self) -> AtomicRef> { + self.mesh_buffers.as_ref().unwrap().get() + } + + /// Create the gpu buffer for a poisson disc + fn create_poisson_disc_buffer( + &self, + device: &wgpu::Device, + label: &str, + dimension: u32, + num_samples: u32, + ) -> wgpu::Buffer { + debug_assert!( + dimension == 2 || dimension == 3, + "unknown dimension {dimension}, expected 2 (2d) or 3 (3d)" + ); + + device.create_buffer(&wgpu::BufferDescriptor { + label: Some(label), + size: mem::size_of::() as u64 * (num_samples * dimension) as u64, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }) + } + + /// Generate and write a Poisson disc to `buffer` with `num_pcf_samples.pow(2)` amount of points. + fn write_poisson_disc( + &self, + queue: &wgpu::Queue, + buffer: &wgpu::Buffer, + dimension: u32, + num_samples: u32, + ) { + debug_assert!( + dimension == 2 || dimension == 3, + "unknown dimension {dimension}, expected 2 (2d) or 3 (3d)" + ); + + let num_floats = num_samples * dimension; // points are vec2f + let min_dist = (num_floats as f32).sqrt() / num_floats as f32; + let mut points = vec![]; + + // use a while loop to ensure that the correct number of floats is created + while points.len() < num_floats as usize { + if dimension == 2 { + let poisson = Poisson2D::new() + .with_dimensions([1.0, 1.0], min_dist) + .with_samples(num_samples); + + points = poisson + .iter() + .flatten() + .map(|p| p * 2.0 - 1.0) + .collect_vec(); + } else if dimension == 3 { + let poisson = Poisson3D::new() + .with_dimensions([1.0, 1.0, 1.0], min_dist) + .with_samples(num_samples); + + points = poisson + .iter() + .flatten() + .map(|p| p * 2.0 - 1.0) + .collect_vec(); + } + } + points.truncate(num_floats as _); + + queue.write_buffer(buffer, 0, bytemuck::cast_slice(points.as_slice())); + } +} + +impl Node for ShadowMapsPass { + fn desc( + &mut self, + graph: &mut crate::render::graph::RenderGraph, + ) -> crate::render::graph::NodeDesc { + let mut node = NodeDesc::new(NodeType::Render, None, vec![]); + + let atlas = self.atlas.get(); + + node.add_texture_slot( + ShadowMapsPassSlots::ShadowAtlasTexture, + SlotAttribute::Output, + Some(SlotValue::Texture(atlas.texture().clone())), + ); + + node.add_texture_view_slot( + ShadowMapsPassSlots::ShadowAtlasTextureView, + SlotAttribute::Output, + Some(SlotValue::TextureView(atlas.view().clone())), + ); + + node.add_sampler_slot( + ShadowMapsPassSlots::ShadowAtlasSampler, + SlotAttribute::Output, + Some(SlotValue::Sampler(self.atlas_sampler.clone())), + ); + + node.add_sampler_slot( + ShadowMapsPassSlots::ShadowAtlasSamplerComparison, + SlotAttribute::Output, + Some(SlotValue::Sampler(self.atlas_sampler_compare.clone())), + ); + + node.add_buffer_slot( + ShadowMapsPassSlots::ShadowLightUniformsBuffer, + SlotAttribute::Output, + Some(SlotValue::Buffer( + self.light_uniforms_buffer.buffer().clone(), + )), + ); + + node.add_buffer_slot( + ShadowMapsPassSlots::ShadowAtlasSizeBuffer, + SlotAttribute::Output, + Some(SlotValue::Buffer(self.atlas_size_buffer.clone())), + ); + + let device = graph.device(); + let settings_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("buffer_shadow_settings"), + size: mem::size_of::() as _, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + node.add_buffer_slot( + ShadowMapsPassSlots::ShadowSettingsUniform, + SlotAttribute::Output, + Some(SlotValue::Buffer(Arc::new(settings_buffer))), + ); + + node.add_buffer_slot( + ShadowMapsPassSlots::PcfPoissonDiscBuffer, + SlotAttribute::Output, + Some(SlotValue::Buffer(Arc::new( + self.create_poisson_disc_buffer( + device, + "buffer_poisson_disc_pcf", + 2, + PCF_SAMPLES_NUM_MAX, + ), + ))), + ); + + node.add_buffer_slot( + ShadowMapsPassSlots::PcfPoissonDiscBuffer3d, + SlotAttribute::Output, + Some(SlotValue::Buffer(Arc::new( + self.create_poisson_disc_buffer( + device, + "buffer_poisson_disc_pcf_3d", + 3, + PCF_SAMPLES_NUM_MAX, + ), + ))), + ); + + node.add_buffer_slot( + ShadowMapsPassSlots::PcssPoissonDiscBuffer, + SlotAttribute::Output, + Some(SlotValue::Buffer(Arc::new( + self.create_poisson_disc_buffer( + device, + "buffer_poisson_disc_pcss", + 2, + PCSS_SAMPLES_NUM_MAX, + ), + ))), + ); + + node + } + + fn prepare( + &mut self, + graph: &mut crate::render::graph::RenderGraph, + world: &mut lyra_ecs::World, + context: &mut crate::render::graph::RenderGraphContext, + ) { + world.add_resource_default_if_absent::(); + if world.has_resource_changed::() { + debug!("Detected change in ShadowSettings, recreating poisson disks"); + + let settings = world.get_resource::(); + // convert to uniform now since the from impl limits to max values + let uniform = ShadowSettingsUniform::from(*settings); + + let buffer = graph + .slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer) + .unwrap() + .as_buffer() + .unwrap(); + self.write_poisson_disc(&context.queue, &buffer, 2, uniform.pcf_samples_num); + + let buffer = graph + .slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer3d) + .unwrap() + .as_buffer() + .unwrap(); + self.write_poisson_disc(&context.queue, &buffer, 3, uniform.pcf_samples_num); + + let buffer = graph + .slot_value(ShadowMapsPassSlots::PcssPoissonDiscBuffer) + .unwrap() + .as_buffer() + .unwrap(); + self.write_poisson_disc( + &context.queue, + &buffer, + 2, + uniform.pcss_blocker_search_samples, + ); + + context.queue_buffer_write_with(ShadowMapsPassSlots::ShadowSettingsUniform, 0, uniform); + } + let settings = *world.get_resource::(); + + if settings.use_back_faces { + // TODO: shadow maps rendering with back faces + todo!("render with back faces"); + } + + self.render_meshes = world.try_get_resource_data::(); + self.transform_buffers = world.try_get_resource_data::(); + self.mesh_buffers = world.try_get_resource_data::>(); + + world.add_resource(self.atlas.clone()); + + // use a queue for storing atlas ids to add to entities after the entities are iterated + let mut index_components_queue = VecDeque::new(); + + for (entity, pos, shadow_settings, _) in world.view_iter::<( + Entities, + &Transform, + Option<&ShadowCasterSettings>, + Has, + )>() { + if !self.depth_maps.contains_key(&entity) { + let (custom_settings, shadow_settings) = shadow_settings + .map(|ss| (true, ss.clone())) + .unwrap_or((false, settings)); + + let atlas_index = self.create_depth_map( + &context.queue, + LightType::Directional, + entity, + *pos, + None, + custom_settings, + shadow_settings, + ); + index_components_queue.push_back((entity, atlas_index)); + } + } + + for (entity, pos, shadow_settings, _) in world.view_iter::<( + Entities, + &Transform, + Option<&ShadowCasterSettings>, + Has, + )>() { + if !self.depth_maps.contains_key(&entity) { + let (custom_settings, shadow_settings) = shadow_settings + .map(|ss| (true, ss.clone())) + .unwrap_or((false, settings)); + + let atlas_index = self.create_depth_map( + &context.queue, + LightType::Point, + entity, + *pos, + None, + custom_settings, + shadow_settings, + ); + index_components_queue.push_back((entity, atlas_index)); + } + } + + for (entity, pos, shadow_settings, spot) in world.view_iter::<( + Entities, + &Transform, + Option<&ShadowCasterSettings>, + &SpotLight, + )>() { + if !self.depth_maps.contains_key(&entity) { + let (custom_settings, shadow_settings) = shadow_settings + .map(|ss| (true, ss.clone())) + .unwrap_or((false, settings)); + + let atlas_index = self.create_depth_map( + &context.queue, + LightType::Spotlight, + entity, + *pos, + Some(spot.outer_cutoff), + custom_settings, + shadow_settings, + ); + index_components_queue.push_back((entity, atlas_index)); + } + } + + // now consume from the queue adding the components to the entities + while let Some((entity, depth)) = index_components_queue.pop_front() { + world.insert( + entity, + LightShadowMapId { + atlas_index: depth.atlas_index, + uniform_indices: depth.uniform_index, + }, + ); + } + + if self.pipeline.is_none() { + let shader = Rc::new(Shader { + label: Some("shader_shadows".into()), + source: include_str!("../../shaders/shadows.wgsl").to_string(), + }); + + let bgl = self.bgl.clone(); + let transforms = self.transform_buffers().bindgroup_layout.clone(); + + self.pipeline = Some(RenderPipeline::create( + &graph.device, + &RenderPipelineDescriptor { + label: Some("pipeline_shadows".into()), + layouts: vec![bgl.clone(), transforms.clone()], + push_constant_ranges: vec![], + vertex: VertexState { + module: shader.clone(), + entry_point: "vs_main".into(), + buffers: vec![Vertex::position_desc().into()], + }, + fragment: None, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + primitive: wgpu::PrimitiveState { + //cull_mode: Some(wgpu::Face::Front), + cull_mode: Some(wgpu::Face::Back), + ..Default::default() + }, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }, + )); + + self.point_light_pipeline = Some(RenderPipeline::create( + &graph.device, + &RenderPipelineDescriptor { + label: Some("pipeline_point_light_shadows".into()), + layouts: vec![bgl, transforms], + push_constant_ranges: vec![], + vertex: VertexState { + module: shader.clone(), + entry_point: "vs_main".into(), + buffers: vec![Vertex::position_desc().into()], + }, + fragment: Some(FragmentState { + module: shader, + entry_point: "fs_point_light_main".into(), + targets: vec![], + }), + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + primitive: wgpu::PrimitiveState { + //cull_mode: Some(wgpu::Face::Front), + cull_mode: Some(wgpu::Face::Back), + ..Default::default() + }, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }, + )); + } + } + + fn execute( + &mut self, + _: &mut crate::render::graph::RenderGraph, + _: &crate::render::graph::NodeDesc, + context: &mut crate::render::graph::RenderGraphContext, + ) { + let encoder = context.encoder.as_mut().unwrap(); + let pipeline = self.pipeline.as_ref().unwrap(); + let point_light_pipeline = self.point_light_pipeline.as_ref().unwrap(); + + let render_meshes = self.render_meshes(); + let mesh_buffers = self.mesh_buffers(); + let transforms = self.transform_buffers(); + + let atlas = self.atlas.get(); + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("pass_shadow_map"), + color_attachments: &[], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: atlas.view(), + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: true, + }), + stencil_ops: None, + }), + }); + + for light_depth_map in self.depth_maps.values() { + match light_depth_map.light_type { + LightType::Directional => { + pass.set_pipeline(&pipeline); + + let frame = atlas + .texture_frame(light_depth_map.atlas_index) + .expect("missing atlas frame for light"); + + light_shadow_pass_impl( + &mut pass, + &self.uniforms_bg, + &render_meshes, + &mesh_buffers, + &transforms, + &frame, + light_depth_map.uniform_index[0] as _, + ); + } + LightType::Point => { + pass.set_pipeline(&point_light_pipeline); + + for side in 0..6 { + let frame = atlas + .texture_frame(light_depth_map.atlas_index + side) + .expect("missing atlas frame of light"); + let ui = light_depth_map.uniform_index[side as usize]; + + light_shadow_pass_impl( + &mut pass, + &self.uniforms_bg, + &render_meshes, + &mesh_buffers, + &transforms, + &frame, + ui as _, + ); + } + } + LightType::Spotlight => { + pass.set_pipeline(&pipeline); + + let frame = atlas + .texture_frame(light_depth_map.atlas_index) + .expect("missing atlas frame for light"); + + light_shadow_pass_impl( + &mut pass, + &self.uniforms_bg, + &render_meshes, + &mesh_buffers, + &transforms, + &frame, + light_depth_map.uniform_index[0] as _, + ); + }, + } + } + } +} + +fn light_shadow_pass_impl<'a>( + pass: &mut wgpu::RenderPass<'a>, + uniforms_bind_group: &'a wgpu::BindGroup, + render_meshes: &RenderMeshes, + mesh_buffers: &'a RenderAssets, + transforms: &'a TransformBuffers, + shadow_atlas_viewport: &AtlasFrame, + uniform_index: u32, +) { + // only render to the light's map in the atlas + pass.set_viewport( + shadow_atlas_viewport.x as _, + shadow_atlas_viewport.y as _, + shadow_atlas_viewport.width as _, + shadow_atlas_viewport.height as _, + 0.0, + 1.0, + ); + // only clear the light map in the atlas + pass.set_scissor_rect( + shadow_atlas_viewport.x as _, + shadow_atlas_viewport.y as _, + shadow_atlas_viewport.width as _, + shadow_atlas_viewport.height as _, + ); + + for job in render_meshes.iter() { + // get the mesh (containing vertices) and the buffers from storage + let buffers = mesh_buffers.get(&job.mesh_uuid); + if buffers.is_none() { + warn!("Skipping job since its mesh is missing {:?}", job.mesh_uuid); + continue; + } + let buffers = buffers.unwrap(); + + pass.set_bind_group(0, &uniforms_bind_group, &[]); + + // 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]); + + // 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, uniform_index..uniform_index + 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, uniform_index..uniform_index + 1); + } + } +} + +/// Shadow casting settings for a light caster. +/// +/// Put this on an entity with a light source to override the global shadow +/// settings, the [`ShadowSettings`] resource. +#[derive(Debug, Copy, Clone, Component)] +pub struct ShadowCasterSettings { + pub filtering_mode: ShadowFilteringMode, + /// How many PCF filtering samples are used per dimension. + /// + /// A value of 25 is common, this is maxed to 128. + pub pcf_samples_num: u32, + /// How many samples are used for the PCSS blocker search step. + /// + /// Multiple samples are required to avoid holes int he penumbra due to missing blockers. + /// A value of 25 is common, this is maxed to 128. + pub pcss_blocker_search_samples: u32, + pub near_plane: f32, + pub far_plane: f32, + /// The scale of the constant shadow depth bias. + /// + /// This scale will be multiplied by the default constant depth bias value of 0.001. + pub constant_depth_bias_scale: f32, + pub use_back_faces: bool, +} + +const DEFAULT_CONSTANT_DEPTH_BIAS: f32 = 0.001; + +impl Default for ShadowCasterSettings { + fn default() -> Self { + Self { + filtering_mode: ShadowFilteringMode::default(), + pcf_samples_num: 25, + pcss_blocker_search_samples: 25, + near_plane: 0.1, + far_plane: 45.0, + constant_depth_bias_scale: 1.0, + use_back_faces: false, + } + } +} + +#[repr(C)] +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct LightShadowUniform { + space_mat: glam::Mat4, + atlas_frame: AtlasFrame, // 2xUVec2 (4xf32), so no padding needed + near_plane: f32, + far_plane: f32, + /// Light size in UV space (light_size / frustum_size) + light_size_uv: f32, + _padding1: u32, + light_pos: glam::Vec3, + /// Boolean casted as integer + has_shadow_settings: u32, + pcf_samples_num: u32, + pcss_blocker_search_samples: u32, + constant_depth_bias: f32, + _padding2: u32, +} + +/// A component that stores the ID of a shadow map in the shadow map atlas for the entities. +/// +/// An entity owns a light. If that light casts shadows, this will contain the ID of the shadow +/// map inside of the [`TextureAtlas`]. +#[derive(Debug, Default, Copy, Clone, Component)] +pub struct LightShadowMapId { + atlas_index: u64, + uniform_indices: [u64; 6], +} + +impl LightShadowMapId { + pub fn atlas_index(&self) -> u64 { + self.atlas_index + } + + pub fn uniform_index(&self, side: usize) -> u64 { + self.uniform_indices[side] + } +} + +/// An ecs resource storing the [`TextureAtlas`] of shadow maps. +#[derive(Clone)] +pub struct LightShadowMapAtlas(Arc>); + +impl LightShadowMapAtlas { + pub fn get(&self) -> RwLockReadGuard { + self.0.read().unwrap() + } + + pub fn get_mut(&self) -> RwLockWriteGuard { + self.0.write().unwrap() + } +} + +#[derive(Default, Debug, Copy, Clone)] +pub enum ShadowFilteringMode { + None, + /// Uses hardware features for 2x2 PCF. + Pcf2x2, + Pcf, + #[default] + Pcss, +} + +/* #[derive(Debug, Copy, Clone)] +pub struct ShadowSettings { + pub filtering_mode: ShadowFilteringMode, + /// How many PCF filtering samples are used per dimension. + /// + /// A value of 25 is common, this is maxed to 128. + pub pcf_samples_num: u32, + /// How many samples are used for the PCSS blocker search step. + /// + /// Multiple samples are required to avoid holes int he penumbra due to missing blockers. + /// A value of 25 is common, this is maxed to 128. + pub pcss_blocker_search_samples: u32, +} + +impl Default for ShadowSettings { + fn default() -> Self { + Self { + filtering_mode: ShadowFilteringMode::default(), + pcf_samples_num: 25, + pcss_blocker_search_samples: 25, + } + } +} */ + +const PCF_SAMPLES_NUM_MAX: u32 = 128; +const PCSS_SAMPLES_NUM_MAX: u32 = 128; + +/// Uniform version of [`ShadowSettings`]. +/// +/// If `pcf_samples_num` is set to zero, PCF and PCSS will be disabled. +/// If `pcf_samples_num` is set to 2, ONLY hardware 2x2 PCF will be used. +/// If `pcss_blocker_search_samples` is set to zero, PCSS will be disabled. +#[repr(C)] +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct ShadowSettingsUniform { + //use_pcf_hardware_2x2: u32, + pcf_samples_num: u32, + pcss_blocker_search_samples: u32, +} + +impl From for ShadowSettingsUniform { + fn from(value: ShadowCasterSettings) -> Self { + Self::new( + value.filtering_mode, + value.pcf_samples_num, + value.pcss_blocker_search_samples, + ) + } +} + +impl ShadowSettingsUniform { + /// Create a new shadow settings uniform. + pub fn new( + filter_mode: ShadowFilteringMode, + pcf_samples_num: u32, + pcss_blocker_search_samples: u32, + ) -> Self { + let raw_pcf_samples = pcf_samples_num.min(PCF_SAMPLES_NUM_MAX); + let raw_pcss_samples = pcss_blocker_search_samples.min(PCSS_SAMPLES_NUM_MAX); + + let (pcf_samples, pcss_samples) = match filter_mode { + ShadowFilteringMode::None => (0, 0), + ShadowFilteringMode::Pcf2x2 => (2, 0), + ShadowFilteringMode::Pcf => (raw_pcf_samples, 0), + ShadowFilteringMode::Pcss => (raw_pcf_samples, raw_pcss_samples), + }; + + Self { + pcf_samples_num: pcf_samples, + pcss_blocker_search_samples: pcss_samples, + } + } +} diff --git a/lyra-game/src/render/graph/passes/tint.rs b/lyra-game/src/render/graph/passes/tint.rs index 228a381..1b05f43 100644 --- a/lyra-game/src/render/graph/passes/tint.rs +++ b/lyra-game/src/render/graph/passes/tint.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, rc::Rc}; +use std::{collections::HashMap, rc::Rc, sync::Arc}; use lyra_game_derive::RenderGraphLabel; @@ -13,7 +13,7 @@ pub struct TintPassLabel; #[derive(Debug, Default)] pub struct TintPass { target_sampler: Option, - bgl: Option>, + bgl: Option>, /// Store bind groups for the input textures. /// The texture may change due to resizes, or changes to the view target chain /// from other nodes. @@ -54,7 +54,7 @@ impl Node for TintPass { }, ], }); - let bgl = Rc::new(bgl); + let bgl = Arc::new(bgl); self.bgl = Some(bgl.clone()); self.target_sampler = Some(device.create_sampler(&wgpu::SamplerDescriptor::default())); diff --git a/lyra-game/src/render/light/mod.rs b/lyra-game/src/render/light/mod.rs index bd0af89..545d4eb 100644 --- a/lyra-game/src/render/light/mod.rs +++ b/lyra-game/src/render/light/mod.rs @@ -1,17 +1,24 @@ -pub mod point; pub mod directional; +pub mod point; pub mod spotlight; use lyra_ecs::{Entity, Tick, World}; pub use point::*; pub use spotlight::*; -use std::{collections::{HashMap, VecDeque}, marker::PhantomData, mem, rc::Rc}; +use std::{ + collections::{HashMap, VecDeque}, + marker::PhantomData, + mem, + sync::Arc, +}; use crate::math::Transform; use self::directional::DirectionalLight; +use super::graph::LightShadowMapId; + const MAX_LIGHT_COUNT: usize = 16; /// A struct that stores a list of lights in a wgpu::Buffer. @@ -20,7 +27,7 @@ pub struct LightBuffer { /// The max amount of light casters that could fit in this buffer. pub max_count: usize, /// The amount of light casters that are taking up space in the buffer. - /// + /// /// This means that a light may be inactive in the buffer, by being replaced /// with a default caster as to not affect lighting. Its easier this way than /// to recreate the array and remove the gaps. @@ -47,15 +54,27 @@ impl LightBuffer { } /// Update an existing light in the light buffer. - pub fn update_light(&mut self, lights_buffer: &mut [U; MAX_LIGHT_COUNT], entity: Entity, light: U) { - let buffer_idx = *self.used_indexes.get(&entity) + pub fn update_light( + &mut self, + lights_buffer: &mut [U; MAX_LIGHT_COUNT], + entity: Entity, + light: U, + ) { + let buffer_idx = *self + .used_indexes + .get(&entity) .expect("Entity for Light is not in buffer!"); lights_buffer[buffer_idx] = light; } /// Add a new light to the light buffer. - pub fn add_light(&mut self, lights_buffer: &mut [U; MAX_LIGHT_COUNT], entity: Entity, light: U) { + pub fn add_light( + &mut self, + lights_buffer: &mut [U; MAX_LIGHT_COUNT], + entity: Entity, + light: U, + ) { let buffer_idx = match self.dead_indexes.pop_front() { Some(i) => i, None => { @@ -67,15 +86,20 @@ impl LightBuffer { assert!(self.buffer_count <= self.max_count); i - }, + } }; - + self.used_indexes.insert(entity, buffer_idx); self.update_light(lights_buffer, entity, light); } /// Update, or add a new caster, to the light buffer. - pub fn update_or_add(&mut self, lights_buffer: &mut [U; MAX_LIGHT_COUNT], entity: Entity, light: U) { + pub fn update_or_add( + &mut self, + lights_buffer: &mut [U; MAX_LIGHT_COUNT], + entity: Entity, + light: U, + ) { if self.used_indexes.contains_key(&entity) { self.update_light(lights_buffer, entity, light); } else { @@ -84,7 +108,11 @@ impl LightBuffer { } /// Remove a caster from the buffer, returns true if it was removed. - pub fn remove_light(&mut self, lights_buffer: &mut [U; MAX_LIGHT_COUNT], entity: Entity) -> bool { + pub fn remove_light( + &mut self, + lights_buffer: &mut [U; MAX_LIGHT_COUNT], + entity: Entity, + ) -> bool { if let Some(removed_idx) = self.used_indexes.remove(&entity) { self.dead_indexes.push_back(removed_idx); //self.current_count -= 1; @@ -98,9 +126,9 @@ impl LightBuffer { } pub(crate) struct LightUniformBuffers { - pub buffer: Rc, - pub bind_group: Rc, - pub bind_group_layout: Rc, + pub buffer: Arc, + pub bind_group: Arc, + pub bind_group_layout: Arc, max_light_count: u64, } @@ -110,54 +138,44 @@ impl LightUniformBuffers { // TODO: ensure we dont write over this limit let max_buffer_sizes = (limits.max_uniform_buffer_binding_size as u64) / 2; - let buffer = device.create_buffer( - &wgpu::BufferDescriptor { - label: Some("UBO_Lights"), - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - size: max_buffer_sizes, - mapped_at_creation: false, - } - ); + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("UBO_Lights"), + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + size: max_buffer_sizes, + mapped_at_creation: false, + }); let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT | wgpu::ShaderStages::COMPUTE, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Storage { - read_only: true - }, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT | wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, }, - ], + count: None, + }], label: Some("BGL_Lights"), }); let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { layout: &bindgroup_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer( - wgpu::BufferBinding { - buffer: &buffer, - offset: 0, - size: None, // use the full buffer - } - ) - }, - ], + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &buffer, + offset: 0, + size: None, // use the full buffer + }), + }], label: Some("BG_Lights"), }); Self { - buffer: Rc::new(buffer), - bind_group: Rc::new(bindgroup), - bind_group_layout: Rc::new(bindgroup_layout), + buffer: Arc::new(buffer), + bind_group: Arc::new(bindgroup), + bind_group_layout: Arc::new(bindgroup_layout), max_light_count: max_buffer_sizes / mem::size_of::() as u64, } } @@ -166,27 +184,43 @@ impl LightUniformBuffers { let _ = world_tick; let mut lights = vec![]; - for (point_light, transform) in world.view_iter::<(&PointLight, &Transform)>() { - let uniform = LightUniform::from_point_light_bundle(&point_light, &transform); + for (point_light, transform, shadow_map_id) in + world.view_iter::<(&PointLight, &Transform, Option<&LightShadowMapId>)>() + { + let shadow_map_id = shadow_map_id.map(|m| m.clone()); + let uniform = + LightUniform::from_point_light_bundle(&point_light, &transform, shadow_map_id); lights.push(uniform); } - for (spot_light, transform) in world.view_iter::<(&SpotLight, &Transform)>() { - let uniform = LightUniform::from_spot_light_bundle(&spot_light, &transform); + for (spot_light, transform, shadow_map_id) in + world.view_iter::<(&SpotLight, &Transform, Option<&LightShadowMapId>)>() + { + let shadow_map_id = shadow_map_id.map(|m| m.clone()); + let uniform = + LightUniform::from_spot_light_bundle(&spot_light, &transform, shadow_map_id); lights.push(uniform); } - for (dir_light, transform) in world.view_iter::<(&DirectionalLight, &Transform)>() { - let uniform = LightUniform::from_directional_bundle(&dir_light, &transform); + for (dir_light, transform, shadow_map_id) in + world.view_iter::<(&DirectionalLight, &Transform, Option<&LightShadowMapId>)>() + { + let shadow_map_id = shadow_map_id.map(|m| m.clone()); + let uniform = + LightUniform::from_directional_bundle(&dir_light, &transform, shadow_map_id); lights.push(uniform); } - assert!(lights.len() < self.max_light_count as _); // ensure we dont overwrite the buffer + assert!(lights.len() < self.max_light_count as usize); // ensure we dont overwrite the buffer // write the amount of lights to the buffer, and right after that the list of lights. queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(&[lights.len()])); // the size of u32 is multiplied by 4 because of gpu alignment requirements - queue.write_buffer(&self.buffer, mem::size_of::() as u64 * 4, bytemuck::cast_slice(lights.as_slice())); + queue.write_buffer( + &self.buffer, + mem::size_of::() as u64 * 4, + bytemuck::cast_slice(lights.as_slice()), + ); } } @@ -209,17 +243,22 @@ pub(crate) struct LightUniform { pub color: glam::Vec3, // no padding is needed here since range acts as the padding // that would usually be needed for the vec3 - pub range: f32, pub intensity: f32, pub smoothness: f32, pub spot_cutoff_rad: f32, pub spot_outer_cutoff_rad: f32, + pub light_shadow_uniform_index: [i32; 6], + _padding: [u32; 2], } impl LightUniform { - pub fn from_point_light_bundle(light: &PointLight, transform: &Transform) -> Self { + pub fn from_point_light_bundle( + light: &PointLight, + transform: &Transform, + map_id: Option, + ) -> Self { Self { light_type: LightType::Point as u32, enabled: light.enabled as u32, @@ -233,11 +272,27 @@ impl LightUniform { spot_cutoff_rad: 0.0, spot_outer_cutoff_rad: 0.0, - + light_shadow_uniform_index: map_id + .map(|m| { + [ + m.uniform_index(0) as i32, + m.uniform_index(1) as i32, + m.uniform_index(2) as i32, + m.uniform_index(3) as i32, + m.uniform_index(4) as i32, + m.uniform_index(5) as i32, + ] + }) + .unwrap_or([-1; 6]), + _padding: [0; 2], } } - pub fn from_directional_bundle(light: &DirectionalLight, transform: &Transform) -> Self { + pub fn from_directional_bundle( + light: &DirectionalLight, + transform: &Transform, + map_id: Option, + ) -> Self { Self { light_type: LightType::Directional as u32, enabled: light.enabled as u32, @@ -251,11 +306,28 @@ impl LightUniform { spot_cutoff_rad: 0.0, spot_outer_cutoff_rad: 0.0, + light_shadow_uniform_index: map_id + .map(|m| { + [ + m.uniform_index(0) as i32, + m.uniform_index(1) as i32, + m.uniform_index(2) as i32, + m.uniform_index(3) as i32, + m.uniform_index(4) as i32, + m.uniform_index(5) as i32, + ] + }) + .unwrap_or([-1; 6]), + _padding: [0; 2], } } // Create the SpotLightUniform from an ECS bundle - pub fn from_spot_light_bundle(light: &SpotLight, transform: &Transform) -> Self { + pub fn from_spot_light_bundle( + light: &SpotLight, + transform: &Transform, + map_id: Option, + ) -> Self { Self { light_type: LightType::Spotlight as u32, enabled: light.enabled as u32, @@ -269,7 +341,19 @@ impl LightUniform { spot_cutoff_rad: light.cutoff.to_radians(), spot_outer_cutoff_rad: light.outer_cutoff.to_radians(), + light_shadow_uniform_index: map_id + .map(|m| { + [ + m.uniform_index(0) as i32, + m.uniform_index(1) as i32, + m.uniform_index(2) as i32, + m.uniform_index(3) as i32, + m.uniform_index(4) as i32, + m.uniform_index(5) as i32, + ] + }) + .unwrap_or([-1; 6]), + _padding: [0; 2], } } } - diff --git a/lyra-game/src/render/material.rs b/lyra-game/src/render/material.rs index b505ffc..9fafa40 100755 --- a/lyra-game/src/render/material.rs +++ b/lyra-game/src/render/material.rs @@ -1,9 +1,10 @@ -use std::rc::Rc; +use std::sync::Arc; use lyra_resource::{ResHandle, Texture}; use super::texture::RenderTexture; +#[derive(Default)] pub struct MaterialSpecular { pub factor: f32, pub color_factor: glam::Vec3, @@ -11,7 +12,7 @@ pub struct MaterialSpecular { pub color_texture: Option, } -fn texture_to_render(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: &Rc, i: &Option>) -> Option { +fn texture_to_render(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: &Arc, i: &Option>) -> Option { if let Some(tex) = i { RenderTexture::from_resource(device, queue, bg_layout.clone(), tex, None).ok() } else { @@ -20,7 +21,7 @@ fn texture_to_render(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: &Rc< } impl MaterialSpecular { - pub fn from_resource(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: Rc, value: &lyra_resource::gltf::Specular) -> Self { + pub fn from_resource(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: Arc, value: &lyra_resource::gltf::Specular) -> Self { let tex = texture_to_render(device, queue, &bg_layout, &value.texture); let color_tex = texture_to_render(device, queue, &bg_layout, &value.color_texture); @@ -45,7 +46,7 @@ pub struct Material { } impl Material { - pub fn from_resource(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: Rc, value: &lyra_resource::gltf::Material) -> Self { + pub fn from_resource(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: Arc, value: &lyra_resource::gltf::Material) -> Self { let diffuse_texture = texture_to_render(device, queue, &bg_layout, &value.base_color_texture); let specular = value.specular.as_ref().map(|s| MaterialSpecular::from_resource(device, queue, bg_layout.clone(), s)); @@ -57,7 +58,7 @@ impl Material { //diffuse: glam::Vec3::new(value.base_color.x, value.base_color.y, value.base_color.z), //diffuse: glam::Vec3::new(1.0, 0.5, 0.31), //specular: glam::Vec3::new(0.5, 0.5, 0.5), - ambient: glam::Vec3::new(1.0, 1.0, 1.0), + ambient: glam::Vec3::new(1.0, 1.0, 1.0) * 0.5, diffuse: glam::Vec3::new(1.0, 1.0, 1.0), shininess: 32.0, diff --git a/lyra-game/src/render/mod.rs b/lyra-game/src/render/mod.rs index d1e39d5..11341f9 100755 --- a/lyra-game/src/render/mod.rs +++ b/lyra-game/src/render/mod.rs @@ -14,4 +14,10 @@ pub mod transform_buffer_storage; pub mod light; //pub mod light_cull_compute; pub mod avec; -pub mod graph; \ No newline at end of file +pub mod graph; + +mod texture_atlas; +pub use texture_atlas::*; + +mod slot_buffer; +pub use slot_buffer::*; \ No newline at end of file diff --git a/lyra-game/src/render/render_buffer.rs b/lyra-game/src/render/render_buffer.rs index 2789b5f..1e4d8b9 100755 --- a/lyra-game/src/render/render_buffer.rs +++ b/lyra-game/src/render/render_buffer.rs @@ -1,4 +1,4 @@ -use std::{num::NonZeroU32, rc::Rc}; +use std::{num::NonZeroU32, sync::Arc}; use wgpu::util::DeviceExt; @@ -23,11 +23,11 @@ impl RenderBuffer { pub struct BindGroupPair { pub bindgroup: wgpu::BindGroup, - pub layout: Rc, + pub layout: Arc, } impl BindGroupPair { - pub fn create_bind_group(device: &wgpu::Device, layout: Rc, entries: &[wgpu::BindGroupEntry<'_>]) -> Self { + pub fn create_bind_group(device: &wgpu::Device, layout: Arc, entries: &[wgpu::BindGroupEntry<'_>]) -> Self { let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { layout: &layout, entries, @@ -43,7 +43,7 @@ impl BindGroupPair { pub fn new(bindgroup: wgpu::BindGroup, layout: wgpu::BindGroupLayout) -> Self { Self { bindgroup, - layout: Rc::new(layout), + layout: Arc::new(layout), } } } @@ -136,7 +136,7 @@ impl BufferWrapper { } /// Take the bind group layout, the bind group, and the buffer out of the wrapper. - pub fn parts(self) -> (Option>, Option, wgpu::Buffer) { + pub fn parts(self) -> (Option>, Option, wgpu::Buffer) { if let Some(pair) = self.bindgroup_pair { (Some(pair.layout), Some(pair.bindgroup), self.inner_buf) } else { @@ -297,7 +297,7 @@ impl BufferWrapperBuilder { BindGroupPair { bindgroup: bg, - layout: Rc::new(bg_layout), + layout: Arc::new(bg_layout), } } }; @@ -308,7 +308,7 @@ impl BufferWrapperBuilder { len: Some(self.count.unwrap_or_default() as usize), } */ - (Rc::try_unwrap(bg_pair.layout).unwrap(), bg_pair.bindgroup, buffer, self.count.unwrap_or_default() as usize) + (Arc::try_unwrap(bg_pair.layout).unwrap(), bg_pair.bindgroup, buffer, self.count.unwrap_or_default() as usize) } pub fn finish(self, device: &wgpu::Device) -> BufferWrapper { @@ -316,7 +316,7 @@ impl BufferWrapperBuilder { BufferWrapper { bindgroup_pair: Some(BindGroupPair { - layout: Rc::new(bgl), + layout: Arc::new(bgl), bindgroup: bg }), inner_buf: buff, diff --git a/lyra-game/src/render/renderer.rs b/lyra-game/src/render/renderer.rs index a08e60d..592f541 100755 --- a/lyra-game/src/render/renderer.rs +++ b/lyra-game/src/render/renderer.rs @@ -9,7 +9,7 @@ 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, MeshesPassLabel, PresentPass, PresentPassLabel, RenderGraphLabelValue, RenderTarget, SubGraphNode, ViewTarget}; +use crate::render::graph::{BasePass, BasePassLabel, BasePassSlots, FxaaPass, FxaaPassLabel, LightBasePass, LightBasePassLabel, LightCullComputePass, LightCullComputePassLabel, MeshPass, MeshPrepNode, MeshPrepNodeLabel, MeshesPassLabel, PresentPass, PresentPassLabel, RenderGraphLabelValue, RenderTarget, ShadowMapsPass, ShadowMapsPassLabel, SubGraphNode, ViewTarget}; use super::graph::RenderGraph; use super::{resource::RenderPipeline, render_job::RenderJob}; @@ -90,7 +90,7 @@ impl BasicRenderer { let (device, queue) = adapter.request_device( &wgpu::DeviceDescriptor { - features: wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES, + 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 @@ -147,9 +147,21 @@ impl BasicRenderer { forward_plus_graph.add_node(LightCullComputePassLabel, LightCullComputePass::new(size)); debug!("Adding mesh pass"); - forward_plus_graph.add_node(MeshesPassLabel, MeshPass::new()); + forward_plus_graph.add_node(ShadowMapsPassLabel, ShadowMapsPass::new(&device)); + + let mesh_prep = MeshPrepNode::new(&device); + let material_bgl = mesh_prep.material_bgl.clone(); + forward_plus_graph.add_node(MeshPrepNodeLabel, mesh_prep); + forward_plus_graph.add_node(MeshesPassLabel, MeshPass::new(material_bgl)); + 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, diff --git a/lyra-game/src/render/resource/compute_pipeline.rs b/lyra-game/src/render/resource/compute_pipeline.rs index 54ee812..8145ab0 100644 --- a/lyra-game/src/render/resource/compute_pipeline.rs +++ b/lyra-game/src/render/resource/compute_pipeline.rs @@ -1,4 +1,4 @@ -use std::{ops::Deref, rc::Rc}; +use std::{ops::Deref, rc::Rc, sync::Arc}; use wgpu::PipelineLayout; @@ -7,7 +7,7 @@ use super::Shader; //#[derive(Debug, Clone)] pub struct ComputePipelineDescriptor { pub label: Option, - pub layouts: Vec>, + pub layouts: Vec>, pub push_constant_ranges: Vec, // TODO: make this a ResHandle /// The compiled shader module for the stage. diff --git a/lyra-game/src/render/resource/render_pipeline.rs b/lyra-game/src/render/resource/render_pipeline.rs index 4558c4d..c149b09 100755 --- a/lyra-game/src/render/resource/render_pipeline.rs +++ b/lyra-game/src/render/resource/render_pipeline.rs @@ -1,4 +1,4 @@ -use std::{num::NonZeroU32, ops::Deref, rc::Rc}; +use std::{num::NonZeroU32, ops::Deref, sync::Arc}; use wgpu::PipelineLayout; @@ -7,7 +7,7 @@ use super::{FragmentState, VertexState}; //#[derive(Debug, Clone)] pub struct RenderPipelineDescriptor { pub label: Option, - pub layouts: Vec>, + pub layouts: Vec>, pub push_constant_ranges: Vec, pub vertex: VertexState, pub fragment: Option, @@ -87,7 +87,7 @@ impl RenderPipeline { // an Rc was used here so that this shader could be reused by the fragment stage if // they share the same shader. I tried to do it without an Rc but couldn't get past // the borrow checker - let vrtx_shad = Rc::new(device.create_shader_module(wgpu::ShaderModuleDescriptor { + let vrtx_shad = Arc::new(device.create_shader_module(wgpu::ShaderModuleDescriptor { label: desc.vertex.module.label.as_deref(), source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( &desc.vertex.module.source, @@ -103,7 +103,7 @@ impl RenderPipeline { if f.module == desc.vertex.module { vrtx_shad.clone() } else { - Rc::new(device.create_shader_module(wgpu::ShaderModuleDescriptor { + Arc::new(device.create_shader_module(wgpu::ShaderModuleDescriptor { label: f.module.label.as_deref(), source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(&f.module.source)), })) diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 4ec9a3f..0b2f233 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -19,6 +19,16 @@ struct VertexOutput { @location(0) tex_coords: vec2, @location(1) world_position: vec3, @location(2) world_normal: vec3, + @location(3) frag_pos_light_space: vec4, +} + +struct TextureAtlasFrame { + /*offset: vec2, + size: vec2,*/ + x: u32, + y: u32, + width: u32, + height: u32, } struct TransformData { @@ -48,6 +58,7 @@ struct Light { spot_cutoff: f32, spot_outer_cutoff: f32, + light_shadow_uniform_index: array, }; struct Lights { @@ -70,16 +81,16 @@ fn vs_main( ) -> VertexOutput { var out: VertexOutput; + var world_position: vec4 = u_model_transform_data.transform * vec4(model.position, 1.0); + out.world_position = world_position.xyz; + out.tex_coords = model.tex_coords; - out.clip_position = u_camera.view_projection * u_model_transform_data.transform * vec4(model.position, 1.0); + out.clip_position = u_camera.view_projection * world_position; // the normal mat is actually only a mat3x3, but there's a bug in wgpu: https://github.com/gfx-rs/wgpu-rs/issues/36 let normal_mat4 = u_model_transform_data.normal_matrix; let normal_mat = mat3x3(normal_mat4[0].xyz, normal_mat4[1].xyz, normal_mat4[2].xyz); - out.world_normal = normalize(normal_mat * model.normal, ); - - var world_position: vec4 = u_model_transform_data.transform * vec4(model.position, 1.0); - out.world_position = world_position.xyz; + out.world_normal = normalize(normal_mat * model.normal); return out; } @@ -87,29 +98,60 @@ fn vs_main( // Fragment shader struct Material { - ambient: vec4, - diffuse: vec4, - specular: vec4, + ambient: vec3, + diffuse: vec3, shininess: f32, + specular_factor: f32, + specular_color: vec3, } @group(0) @binding(0) -var t_diffuse: texture_2d; +var u_material: Material; @group(0) @binding(1) +var t_diffuse: texture_2d; +@group(0) @binding(2) var s_diffuse: sampler; +struct LightShadowMapUniform { + light_space_matrix: mat4x4, + atlas_frame: TextureAtlasFrame, + near_plane: f32, + far_plane: f32, + light_size_uv: f32, + light_pos: vec3, + /// 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, +} + @group(4) @binding(0) -var u_material: Material; +var u_light_indices: array; +@group(4) @binding(1) +var t_light_grid: texture_storage_2d; // rg32uint = vec2 @group(5) @binding(0) -var t_specular: texture_2d; +var t_shadow_maps_atlas: texture_depth_2d; @group(5) @binding(1) -var s_specular: sampler; - -@group(6) @binding(0) -var u_light_indices: array; -@group(6) @binding(1) -var t_light_grid: texture_storage_2d; // vec2 +var s_shadow_maps_atlas: sampler; +@group(5) @binding(2) +var s_shadow_maps_atlas_compare: sampler_comparison; +@group(5) @binding(3) +var u_shadow_settings: ShadowSettingsUniform; +@group(5) @binding(4) +var u_light_shadow: array; +@group(5) @binding(5) +var u_pcf_poisson_disc: array>; +@group(5) @binding(6) +var u_pcf_poisson_disc_3d: array>; +@group(5) @binding(7) +var u_pcss_poisson_disc: array>; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { @@ -118,7 +160,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { } let object_color: vec4 = textureSample(t_diffuse, s_diffuse, in.tex_coords); - let specular_color: vec3 = textureSample(t_specular, s_specular, in.tex_coords).xyz; + let specular_color: vec3 = vec3(0.0); //textureSample(t_specular, s_specular, in.tex_coords).xyz; var light_res = vec3(0.0); if (object_color.a < ALPHA_CUTOFF) { @@ -131,16 +173,25 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let light_offset = tile.x; let light_count = tile.y; + let atlas_dimensions = textureDimensions(t_shadow_maps_atlas); + for (var i = 0u; i < light_count; i++) { let light_index = u_light_indices[light_offset + i]; let light: Light = u_lights.data[light_index]; + let light_dir = normalize(-light.direction); if (light.light_ty == LIGHT_TY_DIRECTIONAL) { - light_res += blinn_phong_dir_light(in.world_position, in.world_normal, light, u_material, specular_color); + let shadow_u: LightShadowMapUniform = u_light_shadow[light.light_shadow_uniform_index[0]]; + let frag_pos_light_space = shadow_u.light_space_matrix * vec4(in.world_position, 1.0); + + let shadow = calc_shadow_dir_light(in.world_position, in.world_normal, light_dir, light); + light_res += blinn_phong_dir_light(in.world_position, in.world_normal, light, u_material, specular_color, shadow); } else if (light.light_ty == LIGHT_TY_POINT) { - light_res += blinn_phong_point_light(in.world_position, in.world_normal, light, u_material, specular_color); + let shadow = calc_shadow_point_light(in.world_position, in.world_normal, light_dir, light, atlas_dimensions); + light_res += blinn_phong_point_light(in.world_position, in.world_normal, light, u_material, specular_color, shadow); } else if (light.light_ty == LIGHT_TY_SPOT) { - light_res += blinn_phong_spot_light(in.world_position, in.world_normal, light, u_material, specular_color); + let shadow = calc_shadow_spot_light(in.world_position, in.world_normal, light_dir, light, atlas_dimensions); + light_res += blinn_phong_spot_light(in.world_position, in.world_normal, light, u_material, specular_color, shadow); } } @@ -148,6 +199,355 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { return vec4(light_object_res, object_color.a); } +/// 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) -> vec3 { + 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(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(-tex_coord.z, -tex_coord.y); + } else { + cube_idx = 2; + res = vec2(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(tex_coord.x, tex_coord.z); + } else { + cube_idx = 4; + res = vec2(tex_coord.x, -tex_coord.z); + } + } else { + major_axis = tex_coord.z; + if (tex_coord.z > 0.0) { + cube_idx = 5; + res = vec2(tex_coord.x, -tex_coord.y); + } else { + cube_idx = 6; + res = vec2(-tex_coord.x, -tex_coord.y); + } + } + + res = (res / abs(major_axis) + 1.0) * 0.5; + res.y = 1.0 - res.y; + + return vec3(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 { + if shadow_u.has_shadow_settings == 1u { + return vec2(shadow_u.pcf_samples_num, shadow_u.pcss_blocker_search_samples); + } else { + return vec2(u_shadow_settings.pcf_samples_num, u_shadow_settings.pcss_blocker_search_samples); + } +} + +fn calc_shadow_dir_light(world_pos: vec3, world_normal: vec3, light_dir: vec3, 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(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, safety_offset: bool) -> vec2 { + let atlas_dimensions = textureDimensions(t_shadow_maps_atlas); + + // get the rect of the frame as a vec4 + var region_rect = vec4(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( + 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, receiver_depth: f32, bias: f32, shadow_u: LightShadowMapUniform) -> vec2 { + 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(avg_dist / b, b); +} + +fn pcss_dir_light(tex_coords: vec2, 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, 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, world_normal: vec3, light_dir: vec3, light: Light, atlas_dimensions: vec2) -> 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( + 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, test_depth: f32, shadow_us: array, 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, world_normal: vec3, light_dir: vec3, light: Light, atlas_dimensions: vec2) -> 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(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, 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); +} + fn debug_grid(in: VertexOutput) -> vec4 { let tile_index_float: vec2 = in.clip_position.xy / 16.0; let tile_index = vec2(floor(tile_index_float)); @@ -163,7 +563,7 @@ fn debug_grid(in: VertexOutput) -> vec4 { return vec4(ratio, ratio, ratio, 1.0); } -fn blinn_phong_dir_light(world_pos: vec3, world_norm: vec3, dir_light: Light, material: Material, specular_factor: vec3) -> vec3 { +fn blinn_phong_dir_light(world_pos: vec3, world_norm: vec3, dir_light: Light, material: Material, specular_factor: vec3, shadow: f32) -> vec3 { let light_color = dir_light.color.xyz; let camera_view_pos = u_camera.position; @@ -189,10 +589,10 @@ fn blinn_phong_dir_light(world_pos: vec3, world_norm: vec3, dir_light: diffuse_color *= dir_light.diffuse; specular_color *= dir_light.specular;*/ - return (ambient_color + diffuse_color + specular_color) * dir_light.intensity; + return (ambient_color + (shadow) * (diffuse_color + specular_color)) * dir_light.intensity; } -fn blinn_phong_point_light(world_pos: vec3, world_norm: vec3, point_light: Light, material: Material, specular_factor: vec3) -> vec3 { +fn blinn_phong_point_light(world_pos: vec3, world_norm: vec3, point_light: Light, material: Material, specular_factor: vec3, shadow: f32) -> vec3 { let light_color = point_light.color.xyz; let light_pos = point_light.position.xyz; let camera_view_pos = u_camera.position; @@ -222,10 +622,11 @@ fn blinn_phong_point_light(world_pos: vec3, world_norm: vec3, point_li diffuse_color *= attenuation; specular_color *= attenuation; - return (ambient_color + diffuse_color + specular_color) * point_light.intensity; + //return (ambient_color + shadow * (diffuse_color + specular_color)) * point_light.intensity; + return (shadow * (ambient_color + diffuse_color + specular_color)) * point_light.intensity; } -fn blinn_phong_spot_light(world_pos: vec3, world_norm: vec3, spot_light: Light, material: Material, specular_factor: vec3) -> vec3 { +fn blinn_phong_spot_light(world_pos: vec3, world_norm: vec3, spot_light: Light, material: Material, specular_factor: vec3, shadow: f32) -> vec3 { let light_color = spot_light.color; let light_pos = spot_light.position; let camera_view_pos = u_camera.position; @@ -260,13 +661,13 @@ fn blinn_phong_spot_light(world_pos: vec3, world_norm: vec3, spot_ligh let distance = length(light_pos - world_pos); let attenuation = calc_attenuation(spot_light, distance); - ambient_color *= attenuation * spot_light.intensity * cone; - diffuse_color *= attenuation * spot_light.intensity * cone; - specular_color *= attenuation * spot_light.intensity * cone; + ambient_color *= attenuation * cone; + diffuse_color *= attenuation * cone; + specular_color *= attenuation * cone; //// end of spot light attenuation //// - - return /*ambient_color +*/ diffuse_color + specular_color; + //return /*ambient_color +*/ diffuse_color + specular_color; + return (shadow * (diffuse_color + specular_color)) * spot_light.intensity; } fn calc_attenuation(light: Light, distance: f32) -> f32 { diff --git a/lyra-game/src/render/shaders/light_cull.comp.wgsl b/lyra-game/src/render/shaders/light_cull.comp.wgsl index fd3552d..7897095 100644 --- a/lyra-game/src/render/shaders/light_cull.comp.wgsl +++ b/lyra-game/src/render/shaders/light_cull.comp.wgsl @@ -31,6 +31,7 @@ struct Light { spot_cutoff: f32, spot_outer_cutoff: f32, + light_shadow_uniform_index: array, }; struct Lights { diff --git a/lyra-game/src/render/shaders/shadows.wgsl b/lyra-game/src/render/shaders/shadows.wgsl new file mode 100644 index 0000000..58be2c5 --- /dev/null +++ b/lyra-game/src/render/shaders/shadows.wgsl @@ -0,0 +1,70 @@ +struct TransformData { + transform: mat4x4, + normal_matrix: mat4x4, +} + +struct TextureAtlasFrame { + offset: vec2, + size: vec2, +} + +struct LightShadowMapUniform { + light_space_matrix: mat4x4, + atlas_frame: TextureAtlasFrame, + near_plane: f32, + far_plane: f32, + light_size_uv: f32, + light_pos: vec3, + /// boolean casted as u32 + has_shadow_settings: u32, + pcf_samples_num: u32, + pcss_blocker_search_samples: u32, + constant_depth_bias: f32, +} + +@group(0) @binding(0) +var u_light_shadow: array; +/*@group(0) @binding(1) +var u_light_pos: vec3; +@group(0) @binding(2) +var u_light_far_plane: f32;*/ + +@group(1) @binding(0) +var u_model_transform_data: TransformData; + + +struct VertexOutput { + @builtin(position) + clip_position: vec4, + @location(0) world_pos: vec3, + @location(1) instance_index: u32, +} + +@vertex +fn vs_main( + @location(0) position: vec3, + @builtin(instance_index) instance_index: u32, +) -> VertexOutput { + let world_pos = u_model_transform_data.transform * vec4(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); +} \ No newline at end of file diff --git a/lyra-game/src/render/slot_buffer.rs b/lyra-game/src/render/slot_buffer.rs new file mode 100644 index 0000000..67e5a82 --- /dev/null +++ b/lyra-game/src/render/slot_buffer.rs @@ -0,0 +1,149 @@ +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 { + /// 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, + /// The optional alignment of elements in the buffer. + alignment: Option, + /// The actual gpu buffer + buffer: Arc, + _marker: PhantomData, +} + +impl GpuSlotBuffer { + /// 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) -> Self { + let buffer = Arc::new(device.create_buffer(&wgpu::BufferDescriptor { + label, + size: capacity * mem::size_of::() 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::() 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 { + // 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::(), 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 { + &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 + } +} \ No newline at end of file diff --git a/lyra-game/src/render/texture.rs b/lyra-game/src/render/texture.rs index b51ccef..716b566 100755 --- a/lyra-game/src/render/texture.rs +++ b/lyra-game/src/render/texture.rs @@ -1,4 +1,4 @@ -use std::rc::Rc; +use std::sync::Arc; use image::GenericImageView; use lyra_resource::{FilterMode, ResHandle, Texture, WrappingMode}; @@ -44,7 +44,7 @@ impl RenderTexture { }) } - fn create_bind_group_pair(device: &wgpu::Device, layout: Rc, view: &wgpu::TextureView, sampler: &wgpu::Sampler) -> BindGroupPair { + fn create_bind_group_pair(device: &wgpu::Device, layout: Arc, view: &wgpu::TextureView, sampler: &wgpu::Sampler) -> BindGroupPair { let bg = device.create_bind_group( &wgpu::BindGroupDescriptor { layout: &layout, @@ -68,12 +68,12 @@ impl RenderTexture { } } - pub fn from_bytes(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: Rc, bytes: &[u8], label: &str) -> anyhow::Result { + pub fn from_bytes(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: Arc, bytes: &[u8], label: &str) -> anyhow::Result { let img = image::load_from_memory(bytes)?; Self::from_image(device, queue, bg_layout, &img, Some(label)) } - pub fn from_image(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: Rc, img: &image::DynamicImage, label: Option<&str>) -> anyhow::Result { + pub fn from_image(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: Arc, img: &image::DynamicImage, label: Option<&str>) -> anyhow::Result { let rgba = img.to_rgba8(); let dimensions = img.dimensions(); @@ -134,7 +134,7 @@ impl RenderTexture { }) } - pub fn from_resource(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: Rc, texture_res: &ResHandle, label: Option<&str>) -> anyhow::Result { + pub fn from_resource(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: Arc, texture_res: &ResHandle, label: Option<&str>) -> anyhow::Result { let texture_ref = texture_res.data_ref().unwrap(); let img = texture_ref.image.data_ref().unwrap(); @@ -371,7 +371,7 @@ impl RenderTexture { /// Convert [`lyra_resource::WrappingMode`] to [`wgpu::AddressMode`] #[inline(always)] -fn res_wrap_to_wgpu(wmode: WrappingMode) -> wgpu::AddressMode { +pub(crate) fn res_wrap_to_wgpu(wmode: WrappingMode) -> wgpu::AddressMode { match wmode { WrappingMode::ClampToEdge => wgpu::AddressMode::ClampToEdge, WrappingMode::MirroredRepeat => wgpu::AddressMode::MirrorRepeat, @@ -381,7 +381,7 @@ fn res_wrap_to_wgpu(wmode: WrappingMode) -> wgpu::AddressMode { /// Convert [`lyra_resource::FilterMode`] to [`wgpu::FilterMode`] #[inline(always)] -fn res_filter_to_wgpu(fmode: FilterMode) -> wgpu::FilterMode { +pub(crate) fn res_filter_to_wgpu(fmode: FilterMode) -> wgpu::FilterMode { match fmode { FilterMode::Nearest => wgpu::FilterMode::Nearest, FilterMode::Linear => wgpu::FilterMode::Linear, diff --git a/lyra-game/src/render/texture_atlas.rs b/lyra-game/src/render/texture_atlas.rs new file mode 100644 index 0000000..3dec36e --- /dev/null +++ b/lyra-game/src/render/texture_atlas.rs @@ -0,0 +1,297 @@ +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 TextureAtlas { + atlas_size: UVec2, + + texture_format: wgpu::TextureFormat, + texture: Arc, + view: Arc, + + packer: P, +} + +impl TextureAtlas

{ + 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 { + 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 { + self.packer.frame(atlas_index as _) + } + + pub fn view(&self) -> &Arc { + &self.view + } + + pub fn texture(&self) -> &Arc { + &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; + + /// Get all [`AtlasFrame`]s in the atlas. + fn frames(&self) -> &HashMap; + + /// Pack a new rect into the atlas. + fn pack(&mut self, width: u32, height: u32) -> Result; +} + +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, + frame_idx: usize, + frames: HashMap, +} + +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 { + 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 { + self.frames.get(&id).cloned() + } + + fn frames(&self) -> &HashMap { + &self.frames + } + + fn pack(&mut self, width: u32, height: u32) -> Result { + 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) + } + } +} diff --git a/lyra-game/src/render/transform_buffer_storage.rs b/lyra-game/src/render/transform_buffer_storage.rs index 31c5dda..a8839c0 100644 --- a/lyra-game/src/render/transform_buffer_storage.rs +++ b/lyra-game/src/render/transform_buffer_storage.rs @@ -1,4 +1,4 @@ -use std::{collections::{HashMap, VecDeque}, hash::{BuildHasher, DefaultHasher, Hash, Hasher, RandomState}, num::NonZeroU64, rc::Rc}; +use std::{collections::{HashMap, VecDeque}, hash::{BuildHasher, DefaultHasher, Hash, Hasher, RandomState}, num::NonZeroU64, sync::Arc}; use lyra_ecs::Entity; use tracing::instrument; @@ -165,7 +165,7 @@ impl CachedValMap, + pub bindgroup_layout: Arc, //groups: CachedValMap, //groups: SlotMap, entries: Vec, @@ -195,7 +195,7 @@ impl TransformBuffers { }); let mut s = Self { - bindgroup_layout: Rc::new(bindgroup_layout), + bindgroup_layout: Arc::new(bindgroup_layout), entries: Default::default(), max_transform_count: (limits.max_uniform_buffer_binding_size) as usize / (limits.min_uniform_buffer_offset_alignment as usize), //(mem::size_of::()), limits, @@ -345,9 +345,6 @@ impl TransformBuffers { /// Returns a boolean indicating if the buffers need to be expanded pub fn needs_expand(&self) -> bool { false - /* self.entries.last() - .map(|entry| entry.len >= self.max_transform_count) - .unwrap_or(false) */ } } diff --git a/lyra-game/src/render/vertex.rs b/lyra-game/src/render/vertex.rs index 57a7432..1f9be15 100755 --- a/lyra-game/src/render/vertex.rs +++ b/lyra-game/src/render/vertex.rs @@ -15,6 +15,23 @@ impl Vertex { position, tex_coords, normals } } + + /// Returns a [`wgpu::VertexBufferLayout`] with only the position as a vertex attribute. + /// + /// The stride is still `std::mem::size_of::()`, but only position is included. + pub fn position_desc<'a>() -> wgpu::VertexBufferLayout<'a> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() 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 { diff --git a/lyra-math/src/angle.rs b/lyra-math/src/angle.rs index 30d541c..71ef507 100755 --- a/lyra-math/src/angle.rs +++ b/lyra-math/src/angle.rs @@ -10,7 +10,7 @@ pub fn radians_to_degrees(radians: f32) -> f32 { radians * 180.0 / PI } -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub enum Angle { Degrees(f32), Radians(f32), @@ -68,4 +68,18 @@ impl std::ops::SubAssign for Angle { Angle::Radians(r) => *r -= rhs.to_radians(), } } +} + +impl std::ops::Mul for Angle { + type Output = Angle; + + fn mul(self, rhs: f32) -> Self::Output { + Angle::Radians(self.to_radians() * rhs) + } +} + +impl std::ops::MulAssign for Angle { + fn mul_assign(&mut self, rhs: f32) { + *self = *self * rhs; + } } \ No newline at end of file diff --git a/lyra-resource/src/gltf/material.rs b/lyra-resource/src/gltf/material.rs index 372d7ee..c4bb4de 100644 --- a/lyra-resource/src/gltf/material.rs +++ b/lyra-resource/src/gltf/material.rs @@ -95,7 +95,7 @@ impl From for AlphaMode { } } -#[derive(Clone, Reflect)] +#[derive(Clone, Reflect, Default)] pub struct Specular { /// The strength of the specular reflection, default of 1.0 pub factor: f32,