From 7ff67a194b2c6d44f55b2ce63aa43f423217c2d5 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Fri, 28 Jun 2024 16:15:21 -0400 Subject: [PATCH 01/28] create an example for testing shadow maps --- Cargo.lock | 10 ++ Cargo.toml | 5 +- examples/assets/wood-platform.glb | Bin 0 -> 50576 bytes examples/shadows/Cargo.toml | 10 ++ examples/shadows/scripts/test.lua | 59 +++++++++++ examples/shadows/src/main.rs | 165 ++++++++++++++++++++++++++++++ 6 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 examples/assets/wood-platform.glb create mode 100644 examples/shadows/Cargo.toml create mode 100644 examples/shadows/scripts/test.lua create mode 100644 examples/shadows/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index b5960e5..c5c7786 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3075,6 +3075,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" diff --git a/Cargo.toml b/Cargo.toml index a4afe80..737e1ef 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] diff --git a/examples/assets/wood-platform.glb b/examples/assets/wood-platform.glb new file mode 100644 index 0000000000000000000000000000000000000000..11624ca09c26b3227692246b92ad0e089b08503e GIT binary patch literal 50576 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

{RkJzpY>rvO$+U4nzu`!T~Uh^tMmmemvq0 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 a!toKd54f-GRLL|${ZDAEKtc0A|JeXbyuc^` literal 0 HcmV?d00001 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..5e3a9fc --- /dev/null +++ b/examples/shadows/src/main.rs @@ -0,0 +1,165 @@ +use lyra_engine::{ + assets::{gltf::Gltf, ResourceManager}, + game::Game, + input::{ + Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource, + InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput, + }, + math::{self, Transform, Vec3}, + render::light::directional::DirectionalLight, + 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 platform_gltf = resman + .request::("../assets/wood-platform.glb") + .unwrap(); + + platform_gltf.wait_recurse_dependencies_load(); + let platform_mesh = &platform_gltf.data_ref().unwrap().scenes[0]; + + drop(resman); + + world.spawn(( + cube_mesh.clone(), + WorldTransform::default(), + Transform::from_xyz(0.0, -2.0, -5.0), + )); + + world.spawn(( + platform_mesh.clone(), + WorldTransform::default(), + Transform::from_xyz(0.0, -5.0, -5.0), + )); + + { + let mut light_tran = Transform::from_xyz(-5.5, 2.5, -3.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: 1.0, + }, + light_tran, + )); + } + + let mut camera = CameraComponent::new_3d(); + camera.transform.translation += math::Vec3::new(0.0, 2.0, 10.5); + camera.transform.rotate_x(math::Angle::Degrees(-17.0)); + world.spawn((camera, FreeFlyCamera::default())); +} \ No newline at end of file From 3a80c069c991bbcda8a0249133bed5784b78fd01 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sat, 29 Jun 2024 22:23:49 -0400 Subject: [PATCH 02/28] render: move most of the mesh processing to a MeshPrepare node Moving that out of the MeshesPass makes the rendering meshes accessible to other passes/nodes. The shadow pass will need access to them which is why this was done now --- lyra-ecs/src/world.rs | 5 + lyra-game/src/render/graph/mod.rs | 15 +- lyra-game/src/render/graph/node.rs | 16 +- lyra-game/src/render/graph/passes/base.rs | 18 +- lyra-game/src/render/graph/passes/fxaa.rs | 6 +- .../render/graph/passes/light_cull_compute.rs | 8 +- .../src/render/graph/passes/mesh_prepare.rs | 767 ++++++++++++++++++ lyra-game/src/render/graph/passes/meshes.rs | 723 +++++------------ lyra-game/src/render/graph/passes/mod.rs | 8 +- lyra-game/src/render/graph/passes/tint.rs | 6 +- lyra-game/src/render/light/mod.rs | 14 +- lyra-game/src/render/material.rs | 9 +- lyra-game/src/render/render_buffer.rs | 16 +- lyra-game/src/render/renderer.rs | 8 +- .../src/render/resource/compute_pipeline.rs | 4 +- .../src/render/resource/render_pipeline.rs | 8 +- lyra-game/src/render/shaders/base.wgsl | 21 +- lyra-game/src/render/texture.rs | 14 +- .../src/render/transform_buffer_storage.rs | 9 +- lyra-resource/src/gltf/material.rs | 2 +- 20 files changed, 1075 insertions(+), 602 deletions(-) create mode 100644 lyra-game/src/render/graph/passes/mesh_prepare.rs diff --git a/lyra-ecs/src/world.rs b/lyra-ecs/src/world.rs index 4d49f69..41e16e9 100644 --- a/lyra-ecs/src/world.rs +++ b/lyra-ecs/src/world.rs @@ -444,6 +444,11 @@ impl World { .and_then(|r| r.try_get_mut()) } + 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. /// /// Most users wont need to call this manually, its done for you through queries and views. 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..780b669 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,16 @@ 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), + Buffer(Arc), RenderTarget(Rc>), Frame(Rc>>), } impl SlotValue { - pub fn as_texture_view(&self) -> Option<&Rc> { + pub fn as_texture_view(&self) -> Option<&Arc> { bind_match!(self, Self::TextureView(v) => v) } @@ -75,7 +75,7 @@ impl SlotValue { 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 +189,8 @@ pub struct NodeDesc { /// This makes the bind groups accessible to other Nodes. pub bind_groups: Vec<( RenderGraphLabelValue, - Rc, - Option>, + Arc, + Option>, )>, } @@ -199,7 +199,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..5264b14 100644 --- a/lyra-game/src/render/graph/passes/meshes.rs +++ b/lyra-game/src/render/graph/passes/meshes.rs @@ -1,497 +1,175 @@ -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, +}; #[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>, -} - -#[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, + transform_buffers: Option, + // TODO: find a better way to extract these resources from the main world to be used in the + // render stage. + render_meshes: Option, + mesh_buffers: Option, } impl MeshPass { - pub fn new() -> Self { - Self::default() - } - - /// 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(); - - 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)); - } + pub fn new(material_bgl: Arc) -> Self { + Self { + default_texture: None, + pipeline: None, + material_bgl, + transform_buffers: None, + render_meshes: None, + mesh_buffers: 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 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 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(), ], + 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, }, - 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 +182,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 +196,113 @@ 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 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; + // if this mesh uses indices, use them to draw the mesh + if let Some((idx_type, indices)) = buffers.buffer_indices.as_ref() { + let indices_len = indices.count() as u32; - pass.set_vertex_buffer(buffers.buffer_vertex.slot(), buffers.buffer_vertex.buffer().slice(..)); - pass.set_index_buffer(indices.buffer().slice(..), *idx_type); - pass.draw_indexed(0..indices_len, 0, 0..1); - } else { - let vertex_count = buffers.buffer_vertex.count(); + pass.set_vertex_buffer( + buffers.buffer_vertex.slot(), + buffers.buffer_vertex.buffer().slice(..), + ); + pass.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); + 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..4386b9c 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 shadow_maps; +pub use shadow_maps::*; */ + +mod mesh_prepare; +pub use mesh_prepare::*; \ No newline at end of file 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..94377ed 100644 --- a/lyra-game/src/render/light/mod.rs +++ b/lyra-game/src/render/light/mod.rs @@ -6,7 +6,7 @@ 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; @@ -98,9 +98,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, } @@ -155,9 +155,9 @@ impl LightUniformBuffers { }); 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, } } diff --git a/lyra-game/src/render/material.rs b/lyra-game/src/render/material.rs index b505ffc..cdbccbe 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)); 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..5d40fdb 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, SubGraphNode, ViewTarget}; use super::graph::RenderGraph; use super::{resource::RenderPipeline, render_job::RenderJob}; @@ -147,9 +147,13 @@ impl BasicRenderer { forward_plus_graph.add_node(LightCullComputePassLabel, LightCullComputePass::new(size)); debug!("Adding mesh pass"); - forward_plus_graph.add_node(MeshesPassLabel, MeshPass::new()); + 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(MeshPrepNodeLabel, 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..8fd9c34 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -87,28 +87,31 @@ 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; -@group(4) @binding(0) +/*@group(4) @binding(0) var u_material: Material; @group(5) @binding(0) var t_specular: texture_2d; @group(5) @binding(1) -var s_specular: sampler; +var s_specular: sampler;*/ -@group(6) @binding(0) +@group(4) @binding(0) var u_light_indices: array; -@group(6) @binding(1) +@group(4) @binding(1) var t_light_grid: texture_storage_2d; // vec2 @fragment @@ -118,7 +121,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) { 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/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-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, From e8974bbd445a5ed44003fcf27dcd7298a37a6c1a Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 30 Jun 2024 19:33:51 -0400 Subject: [PATCH 03/28] render: create a depth map for the directional light --- lyra-game/src/render/graph/passes/mod.rs | 4 +- lyra-game/src/render/graph/passes/shadows.rs | 312 +++++++++++++++++++ lyra-game/src/render/renderer.rs | 8 +- lyra-game/src/render/shaders/shadows.wgsl | 23 ++ lyra-game/src/render/vertex.rs | 17 + 5 files changed, 361 insertions(+), 3 deletions(-) create mode 100644 lyra-game/src/render/graph/passes/shadows.rs create mode 100644 lyra-game/src/render/shaders/shadows.wgsl diff --git a/lyra-game/src/render/graph/passes/mod.rs b/lyra-game/src/render/graph/passes/mod.rs index 4386b9c..230c5ae 100644 --- a/lyra-game/src/render/graph/passes/mod.rs +++ b/lyra-game/src/render/graph/passes/mod.rs @@ -22,8 +22,8 @@ pub use tint::*; mod fxaa; pub use fxaa::*; -/* mod shadow_maps; -pub use shadow_maps::*; */ +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..c3578df --- /dev/null +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -0,0 +1,312 @@ +use std::{mem, num::NonZeroU64, rc::Rc, sync::Arc}; + +use lyra_ecs::{query::Entities, AtomicRef, Entity, ResourceData}; +use lyra_game_derive::RenderGraphLabel; +use lyra_math::{Transform, OPENGL_TO_WGPU_MATRIX}; +use rustc_hash::FxHashMap; +use tracing::{debug, warn}; +use wgpu::util::DeviceExt; + +use crate::render::{ + graph::{Node, NodeDesc, NodeType}, + light::directional::DirectionalLight, + resource::{FragmentState, PipelineDescriptor, RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, + transform_buffer_storage::TransformBuffers, + vertex::Vertex, +}; + +use super::{MeshBufferStorage, RenderAssets, RenderMeshes}; + +const SHADOW_SIZE: glam::UVec2 = glam::UVec2::new(1024, 1024); + +#[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] +pub struct ShadowMapsPassLabel; + +struct LightDepthMap { + light_projection_buffer: wgpu::Buffer, + texture: wgpu::Texture, + view: wgpu::TextureView, + sampler: wgpu::Sampler, + bindgroup: wgpu::BindGroup, +} + +pub struct ShadowMapsPass { + bgl: 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, +} + +impl ShadowMapsPass { + pub fn new(device: &wgpu::Device) -> Self { + let bgl = Arc::new(device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("shadows_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some( + NonZeroU64::new(mem::size_of::() as _).unwrap(), + ), + }, + count: None, + }], + })); + + Self { + bgl, + depth_maps: Default::default(), + transform_buffers: None, + render_meshes: None, + mesh_buffers: None, + pipeline: None, + } + } + + fn create_depth_map(&mut self, device: &wgpu::Device, entity: Entity, light_pos: Transform) { + let tex = device.create_texture(&wgpu::TextureDescriptor { + label: Some("texture_shadow_map_directional_light"), + size: wgpu::Extent3d { + width: SHADOW_SIZE.x, + height: SHADOW_SIZE.y, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Depth32Float, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + let view = tex.create_view(&wgpu::TextureViewDescriptor { + label: Some("shadows_map_view"), + ..Default::default() + }); + + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("sampler_light_depth_map"), + 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::Linear, + mipmap_filter: wgpu::FilterMode::Linear, + border_color: Some(wgpu::SamplerBorderColor::OpaqueWhite), + ..Default::default() + }); + + const NEAR_PLANE: f32 = 0.1; + const FAR_PLANE: f32 = 80.0; + + let ortho_proj = + glam::Mat4::orthographic_rh_gl(-20.0, 20.0, -20.0, 20.0, NEAR_PLANE, FAR_PLANE); + + let look_view = glam::Mat4::look_to_rh( + light_pos.translation, + light_pos.forward(), + light_pos.up() + ); + + let light_proj = OPENGL_TO_WGPU_MATRIX * (ortho_proj * look_view); + + let light_projection_buffer = + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("shadows_light_view_mat_buffer"), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + contents: bytemuck::bytes_of(&light_proj), + }); + + let bg = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("shadows_bind_group"), + layout: &self.bgl, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &light_projection_buffer, + offset: 0, + size: None, + }), + }], + }); + + self.depth_maps.insert( + entity, + LightDepthMap { + light_projection_buffer, + texture: tex, + view, + sampler, + bindgroup: bg, + }, + ); + } + + 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() + } +} + +impl Node for ShadowMapsPass { + fn desc( + &mut self, + graph: &mut crate::render::graph::RenderGraph, + ) -> crate::render::graph::NodeDesc { + NodeDesc::new(NodeType::Render, None, vec![]) + } + + fn prepare( + &mut self, + graph: &mut crate::render::graph::RenderGraph, + world: &mut lyra_ecs::World, + context: &mut crate::render::graph::RenderGraphContext, + ) { + self.render_meshes = world.try_get_resource_data::(); + self.transform_buffers = world.try_get_resource_data::(); + self.mesh_buffers = world.try_get_resource_data::>(); + + for (entity, pos, light) in world.view_iter::<(Entities, &Transform, &DirectionalLight)>() { + if !self.depth_maps.contains_key(&entity) { + self.create_depth_map(graph.device(), entity, *pos); + debug!("Created depth map for {:?} light entity", entity); + } + } + + 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, + transforms, + ], + push_constant_ranges: vec![], + vertex: VertexState { + module: shader.clone(), + entry_point: "vs_main".into(), + buffers: vec![Vertex::position_desc().into()], + }, + fragment: None, /* Some(FragmentState { + module: shader, + entry_point: "fs_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::default(), + multisample: wgpu::MultisampleState::default(), + multiview: None, + } + )); + /* */ + } + } + + fn execute( + &mut self, + graph: &mut crate::render::graph::RenderGraph, + desc: &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 render_meshes = self.render_meshes(); + let mesh_buffers = self.mesh_buffers(); + let transforms = self.transform_buffers(); + + debug_assert_eq!( + self.depth_maps.len(), + 1, + "shadows map pass only supports 1 light" + ); + let (_, dir_depth_map) = self + .depth_maps + .iter() + .next() + .expect("missing directional light in scene"); + + { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("pass_shadow_map"), + color_attachments: &[], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &dir_depth_map.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: true, + }), + stencil_ops: None, + }), + }); + pass.set_pipeline(&pipeline); + + 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, &dir_depth_map.bindgroup, &[]); + + // 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, 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/renderer.rs b/lyra-game/src/render/renderer.rs index 5d40fdb..73c9910 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, MeshPrepNode, MeshPrepNodeLabel, 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}; @@ -152,8 +152,14 @@ impl BasicRenderer { forward_plus_graph.add_node(MeshPrepNodeLabel, mesh_prep); forward_plus_graph.add_node(MeshesPassLabel, MeshPass::new(material_bgl)); + forward_plus_graph.add_node(ShadowMapsPassLabel, ShadowMapsPass::new(&device)); + forward_plus_graph.add_edge(LightBasePassLabel, LightCullComputePassLabel); 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/shaders/shadows.wgsl b/lyra-game/src/render/shaders/shadows.wgsl new file mode 100644 index 0000000..fa2291c --- /dev/null +++ b/lyra-game/src/render/shaders/shadows.wgsl @@ -0,0 +1,23 @@ +struct TransformData { + transform: mat4x4, + normal_matrix: mat4x4, +} + +@group(0) @binding(0) +var u_light_space_matrix: mat4x4; + +@group(1) @binding(0) +var u_model_transform_data: TransformData; + +struct VertexOutput { + @builtin(position) + clip_position: vec4, +} + +@vertex +fn vs_main( + @location(0) position: vec3 +) -> VertexOutput { + let pos = u_light_space_matrix * u_model_transform_data.transform * vec4(position, 1.0); + return VertexOutput(pos); +} \ No newline at end of file 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 { From 7b2d2424a36d97ec6710a1640e89f3aeca9f330f Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 30 Jun 2024 20:56:41 -0400 Subject: [PATCH 04/28] render: start moving to a shadow map atlas texture and expose the resources as slots --- lyra-game/src/render/graph/passes/shadows.rs | 127 ++++++++++++------- 1 file changed, 79 insertions(+), 48 deletions(-) diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index c3578df..dbbfe36 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -8,9 +8,12 @@ use tracing::{debug, warn}; use wgpu::util::DeviceExt; use crate::render::{ - graph::{Node, NodeDesc, NodeType}, + graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, light::directional::DirectionalLight, - resource::{FragmentState, PipelineDescriptor, RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, + resource::{ + FragmentState, PipelineDescriptor, RenderPipeline, RenderPipelineDescriptor, Shader, + VertexState, + }, transform_buffer_storage::TransformBuffers, vertex::Vertex, }; @@ -19,14 +22,18 @@ use super::{MeshBufferStorage, RenderAssets, RenderMeshes}; const SHADOW_SIZE: glam::UVec2 = glam::UVec2::new(1024, 1024); +#[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] +pub enum ShadowMapsPassSlots { + ShadowAtlasTexture, + ShadowAtlasTextureView, + ShadowAtlasSampler, +} + #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] pub struct ShadowMapsPassLabel; struct LightDepthMap { light_projection_buffer: wgpu::Buffer, - texture: wgpu::Texture, - view: wgpu::TextureView, - sampler: wgpu::Sampler, bindgroup: wgpu::BindGroup, } @@ -41,39 +48,37 @@ pub struct ShadowMapsPass { render_meshes: Option, mesh_buffers: Option, pipeline: Option, + + /// The depth map atlas texture + atlas_texture: Rc, + /// The depth map atlas texture view + atlas_view: Arc, + /// The depth map atlas sampler + atlas_sampler: Rc, } impl ShadowMapsPass { pub fn new(device: &wgpu::Device) -> Self { - let bgl = Arc::new(device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("shadows_bgl"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some( - NonZeroU64::new(mem::size_of::() as _).unwrap(), - ), - }, - count: None, - }], - })); + let bgl = Arc::new( + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("bgl_shadows_light_projection"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some( + NonZeroU64::new(mem::size_of::() as _).unwrap(), + ), + }, + count: None, + }], + }), + ); - Self { - bgl, - depth_maps: Default::default(), - transform_buffers: None, - render_meshes: None, - mesh_buffers: None, - pipeline: None, - } - } - - fn create_depth_map(&mut self, device: &wgpu::Device, entity: Entity, light_pos: Transform) { let tex = device.create_texture(&wgpu::TextureDescriptor { - label: Some("texture_shadow_map_directional_light"), + label: Some("texture_shadow_map_atlas"), size: wgpu::Extent3d { width: SHADOW_SIZE.x, height: SHADOW_SIZE.y, @@ -93,7 +98,7 @@ impl ShadowMapsPass { }); let sampler = device.create_sampler(&wgpu::SamplerDescriptor { - label: Some("sampler_light_depth_map"), + label: Some("sampler_shadow_map_atlas"), address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, address_mode_w: wgpu::AddressMode::ClampToEdge, @@ -104,17 +109,29 @@ impl ShadowMapsPass { ..Default::default() }); + Self { + bgl, + depth_maps: Default::default(), + transform_buffers: None, + render_meshes: None, + mesh_buffers: None, + pipeline: None, + + atlas_sampler: Rc::new(sampler), + atlas_texture: Rc::new(tex), + atlas_view: Arc::new(view), + } + } + + fn create_depth_map(&mut self, device: &wgpu::Device, entity: Entity, light_pos: Transform) { const NEAR_PLANE: f32 = 0.1; const FAR_PLANE: f32 = 80.0; let ortho_proj = glam::Mat4::orthographic_rh_gl(-20.0, 20.0, -20.0, 20.0, NEAR_PLANE, FAR_PLANE); - let look_view = glam::Mat4::look_to_rh( - light_pos.translation, - light_pos.forward(), - light_pos.up() - ); + let look_view = + glam::Mat4::look_to_rh(light_pos.translation, light_pos.forward(), light_pos.up()); let light_proj = OPENGL_TO_WGPU_MATRIX * (ortho_proj * look_view); @@ -142,9 +159,6 @@ impl ShadowMapsPass { entity, LightDepthMap { light_projection_buffer, - texture: tex, - view, - sampler, bindgroup: bg, }, ); @@ -168,7 +182,27 @@ impl Node for ShadowMapsPass { &mut self, graph: &mut crate::render::graph::RenderGraph, ) -> crate::render::graph::NodeDesc { - NodeDesc::new(NodeType::Render, None, vec![]) + let mut node = NodeDesc::new(NodeType::Render, None, vec![]); + + node.add_texture_slot( + ShadowMapsPassSlots::ShadowAtlasTexture, + SlotAttribute::Output, + Some(SlotValue::Texture(self.atlas_texture.clone())), + ); + + node.add_texture_view_slot( + ShadowMapsPassSlots::ShadowAtlasTextureView, + SlotAttribute::Output, + Some(SlotValue::TextureView(self.atlas_view.clone())), + ); + + node.add_sampler_slot( + ShadowMapsPassSlots::ShadowAtlasSampler, + SlotAttribute::Output, + Some(SlotValue::Sampler(self.atlas_sampler.clone())), + ); + + node } fn prepare( @@ -201,10 +235,7 @@ impl Node for ShadowMapsPass { &graph.device, &RenderPipelineDescriptor { label: Some("pipeline_shadows".into()), - layouts: vec![ - bgl, - transforms, - ], + layouts: vec![bgl, transforms], push_constant_ranges: vec![], vertex: VertexState { module: shader.clone(), @@ -226,7 +257,7 @@ impl Node for ShadowMapsPass { primitive: wgpu::PrimitiveState::default(), multisample: wgpu::MultisampleState::default(), multiview: None, - } + }, )); /* */ } @@ -261,7 +292,7 @@ impl Node for ShadowMapsPass { label: Some("pass_shadow_map"), color_attachments: &[], depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &dir_depth_map.view, + view: &self.atlas_view, depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: true, From 1c649b2eb676a3d1ac2e750ab1c73246e7452b2c Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 30 Jun 2024 21:42:08 -0400 Subject: [PATCH 05/28] render: bind the shadow map atlas to the meshes shaders --- lyra-game/src/render/graph/passes/meshes.rs | 95 +++++++++++++++++---- lyra-game/src/render/renderer.rs | 3 +- lyra-game/src/render/shaders/base.wgsl | 5 ++ 3 files changed, 84 insertions(+), 19 deletions(-) diff --git a/lyra-game/src/render/graph/passes/meshes.rs b/lyra-game/src/render/graph/passes/meshes.rs index 5264b14..46f67ed 100644 --- a/lyra-game/src/render/graph/passes/meshes.rs +++ b/lyra-game/src/render/graph/passes/meshes.rs @@ -15,7 +15,7 @@ use crate::render::{ use super::{ BasePassSlots, LightBasePassSlots, LightCullComputePassSlots, MeshBufferStorage, RenderAssets, - RenderMeshes, + RenderMeshes, ShadowMapsPassSlots, }; #[derive(Debug, Hash, Clone, Default, PartialEq, RenderGraphLabel)] @@ -26,6 +26,12 @@ pub enum MeshesPassSlots { Material, } +/// Stores the bind group and bind group layout for the shadow atlas texture +struct ShadowsAtlasBgPair { + layout: Arc, + bg: Arc, +} + //#[derive(Default)] #[allow(dead_code)] pub struct MeshPass { @@ -33,11 +39,14 @@ pub struct MeshPass { pipeline: Option, material_bgl: Arc, - transform_buffers: Option, + // 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 { @@ -46,43 +55,89 @@ impl MeshPass { default_texture: None, pipeline: None, material_bgl, + transform_buffers: None, render_meshes: None, mesh_buffers: None, + + shadows_atlas: None, } } fn transform_buffers(&self) -> AtomicRef { - self.transform_buffers - .as_ref() - .unwrap() - .get() + self.transform_buffers.as_ref().unwrap().get() } fn render_meshes(&self) -> AtomicRef { - self.render_meshes - .as_ref() - .unwrap() - .get() + self.render_meshes.as_ref().unwrap().get() } fn mesh_buffers(&self) -> AtomicRef> { - self.mesh_buffers - .as_ref() - .unwrap() - .get() + self.mesh_buffers.as_ref().unwrap().get() } } impl Node for MeshPass { fn desc( &mut self, - _: &mut crate::render::graph::RenderGraph, + graph: &mut crate::render::graph::RenderGraph, ) -> crate::render::graph::NodeDesc { // load the default texture //let bytes = include_bytes!("../../default_texture.png"); //self.default_texture = Some(RenderTexture::from_bytes(device, &graph.queue, texture_bind_group_layout.clone(), bytes, "default_texture").unwrap()); + 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 device = graph.device(); + 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, + }, + ], + }); + + 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), + } + ], + }); + + self.shadows_atlas = Some(ShadowsAtlasBgPair { + layout: Arc::new(atlas_layout), + bg: Arc::new(atlas_bg), + }); + NodeDesc::new( NodeType::Render, None, @@ -107,18 +162,18 @@ impl Node for MeshPass { let lights_bgl = graph.bind_group_layout(LightBasePassSlots::Lights); let light_grid_bgl = graph.bind_group_layout(LightCullComputePassSlots::LightIndicesGridGroup); + let atlas_bgl = self.shadows_atlas.as_ref().unwrap().layout.clone(); let shader = Rc::new(Shader { label: Some("base_shader".into()), source: include_str!("../../shaders/base.wgsl").to_string(), }); - 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"); @@ -128,7 +183,6 @@ impl Node for MeshPass { .try_get_resource_data::>() .expect("Missing render meshes"); self.mesh_buffers = Some(mesh_buffers.clone()); - let transforms = transforms.get::(); @@ -142,6 +196,7 @@ impl Node for MeshPass { camera_bgl.clone(), lights_bgl.clone(), light_grid_bgl.clone(), + atlas_bgl, ], push_constant_ranges: vec![], vertex: VertexState { @@ -202,6 +257,8 @@ impl Node for MeshPass { 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()) @@ -284,6 +341,8 @@ impl Node for MeshPass { pass.set_bind_group(4, light_grid_bg, &[]); + pass.set_bind_group(5, shadows_atlas_bg, &[]); + // if this mesh uses indices, use them to draw the mesh if let Some((idx_type, indices)) = buffers.buffer_indices.as_ref() { let indices_len = indices.count() as u32; diff --git a/lyra-game/src/render/renderer.rs b/lyra-game/src/render/renderer.rs index 73c9910..da9dd1d 100755 --- a/lyra-game/src/render/renderer.rs +++ b/lyra-game/src/render/renderer.rs @@ -147,12 +147,13 @@ impl BasicRenderer { forward_plus_graph.add_node(LightCullComputePassLabel, LightCullComputePass::new(size)); debug!("Adding mesh pass"); + 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_node(ShadowMapsPassLabel, ShadowMapsPass::new(&device)); forward_plus_graph.add_edge(LightBasePassLabel, LightCullComputePassLabel); forward_plus_graph.add_edge(MeshPrepNodeLabel, MeshesPassLabel); diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 8fd9c34..3eefbf0 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -114,6 +114,11 @@ var u_light_indices: array; @group(4) @binding(1) var t_light_grid: texture_storage_2d; // vec2 +@group(5) @binding(0) +var t_shadow_maps_atlas: texture_2d; +@group(5) @binding(1) +var s_shadow_maps_atlas: sampler; + @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { if (u_camera.tile_debug == 1u) { From 6c6893149a4b58e60bcb0b80da21f5c4e4ab5503 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 30 Jun 2024 21:58:08 -0400 Subject: [PATCH 06/28] render: bind direction light projection matrix to meshes shader --- lyra-game/src/render/graph/passes/meshes.rs | 128 +++++++++++-------- lyra-game/src/render/graph/passes/shadows.rs | 17 ++- lyra-game/src/render/renderer.rs | 1 + lyra-game/src/render/shaders/base.wgsl | 2 + 4 files changed, 94 insertions(+), 54 deletions(-) diff --git a/lyra-game/src/render/graph/passes/meshes.rs b/lyra-game/src/render/graph/passes/meshes.rs index 46f67ed..75e190d 100644 --- a/lyra-game/src/render/graph/passes/meshes.rs +++ b/lyra-game/src/render/graph/passes/meshes.rs @@ -86,58 +86,6 @@ impl Node for MeshPass { //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()); - 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 device = graph.device(); - 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, - }, - ], - }); - - 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), - } - ], - }); - - self.shadows_atlas = Some(ShadowsAtlasBgPair { - layout: Arc::new(atlas_layout), - bg: Arc::new(atlas_bg), - }); - NodeDesc::new( NodeType::Render, None, @@ -158,6 +106,82 @@ impl Node for MeshPass { 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 dir_light_projection_buf = graph + .slot_value(ShadowMapsPassSlots::DirLightProjectionBuffer) + .expect("missing ShadowMapsPassSlots::DirLightProjectionBuffer") + .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::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + 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::Buffer(wgpu::BufferBinding { + buffer: dir_light_projection_buf, + 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 = diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index dbbfe36..cb2cc55 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -27,13 +27,14 @@ pub enum ShadowMapsPassSlots { ShadowAtlasTexture, ShadowAtlasTextureView, ShadowAtlasSampler, + DirLightProjectionBuffer, } #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] pub struct ShadowMapsPassLabel; struct LightDepthMap { - light_projection_buffer: wgpu::Buffer, + light_projection_buffer: Arc, bindgroup: wgpu::BindGroup, } @@ -158,7 +159,7 @@ impl ShadowMapsPass { self.depth_maps.insert( entity, LightDepthMap { - light_projection_buffer, + light_projection_buffer: Arc::new(light_projection_buffer), bindgroup: bg, }, ); @@ -202,6 +203,12 @@ impl Node for ShadowMapsPass { Some(SlotValue::Sampler(self.atlas_sampler.clone())), ); + node.add_sampler_slot( + ShadowMapsPassSlots::DirLightProjectionBuffer, + SlotAttribute::Output, + Some(SlotValue::Lazy), + ); + node } @@ -222,6 +229,12 @@ impl Node for ShadowMapsPass { } } + // update the light projection buffer slot + let (_, dir_depth_map) = self.depth_maps.iter().next().unwrap(); + let val = graph.slot_value_mut(ShadowMapsPassSlots::DirLightProjectionBuffer) + .unwrap(); + *val = SlotValue::Buffer(dir_depth_map.light_projection_buffer.clone()); + if self.pipeline.is_none() { let shader = Rc::new(Shader { label: Some("shader_shadows".into()), diff --git a/lyra-game/src/render/renderer.rs b/lyra-game/src/render/renderer.rs index da9dd1d..e162f55 100755 --- a/lyra-game/src/render/renderer.rs +++ b/lyra-game/src/render/renderer.rs @@ -156,6 +156,7 @@ impl BasicRenderer { 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 diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 3eefbf0..b695844 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -118,6 +118,8 @@ var t_light_grid: texture_storage_2d; // vec2 var t_shadow_maps_atlas: texture_2d; @group(5) @binding(1) var s_shadow_maps_atlas: sampler; +@group(5) @binding(2) +var u_light_space_matrix: mat4x4; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { From fd65f754cfb4958a40a6de8f60055d7f0f058d13 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Thu, 4 Jul 2024 13:43:36 -0400 Subject: [PATCH 07/28] render: get simple directional shadow maps working --- examples/shadows/src/main.rs | 7 ++- lyra-game/src/render/graph/passes/shadows.rs | 12 ++-- lyra-game/src/render/renderer.rs | 2 +- lyra-game/src/render/shaders/base.wgsl | 65 +++++++++++++++++--- 4 files changed, 69 insertions(+), 17 deletions(-) diff --git a/examples/shadows/src/main.rs b/examples/shadows/src/main.rs index 5e3a9fc..036f360 100644 --- a/examples/shadows/src/main.rs +++ b/examples/shadows/src/main.rs @@ -139,16 +139,17 @@ fn setup_scene_plugin(game: &mut Game) { world.spawn(( platform_mesh.clone(), WorldTransform::default(), - Transform::from_xyz(0.0, -5.0, -5.0), + //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)), )); { - let mut light_tran = Transform::from_xyz(-5.5, 2.5, -3.0); + let mut light_tran = Transform::from_xyz(0.0, 2.5, 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(), + //cube_mesh.clone(), DirectionalLight { enabled: true, color: Vec3::new(1.0, 0.95, 0.9), diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index cb2cc55..fc75b4a 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -100,9 +100,9 @@ impl ShadowMapsPass { let sampler = device.create_sampler(&wgpu::SamplerDescriptor { label: Some("sampler_shadow_map_atlas"), - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, + address_mode_u: wgpu::AddressMode::ClampToBorder, + address_mode_v: wgpu::AddressMode::ClampToBorder, + address_mode_w: wgpu::AddressMode::ClampToBorder, mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Linear, mipmap_filter: wgpu::FilterMode::Linear, @@ -126,15 +126,15 @@ impl ShadowMapsPass { fn create_depth_map(&mut self, device: &wgpu::Device, entity: Entity, light_pos: Transform) { const NEAR_PLANE: f32 = 0.1; - const FAR_PLANE: f32 = 80.0; + const FAR_PLANE: f32 = 25.5; let ortho_proj = - glam::Mat4::orthographic_rh_gl(-20.0, 20.0, -20.0, 20.0, NEAR_PLANE, FAR_PLANE); + glam::Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, NEAR_PLANE, FAR_PLANE); let look_view = glam::Mat4::look_to_rh(light_pos.translation, light_pos.forward(), light_pos.up()); - let light_proj = OPENGL_TO_WGPU_MATRIX * (ortho_proj * look_view); + let light_proj = ortho_proj * look_view; let light_projection_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { diff --git a/lyra-game/src/render/renderer.rs b/lyra-game/src/render/renderer.rs index e162f55..592f541 100755 --- a/lyra-game/src/render/renderer.rs +++ b/lyra-game/src/render/renderer.rs @@ -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 diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index b695844..79cf8f1 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -19,6 +19,7 @@ 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 TransformData { @@ -70,16 +71,18 @@ 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.frag_pos_light_space = u_light_space_matrix * world_position; return out; } @@ -115,7 +118,7 @@ var u_light_indices: array; var t_light_grid: texture_storage_2d; // vec2 @group(5) @binding(0) -var t_shadow_maps_atlas: texture_2d; +var t_shadow_maps_atlas: texture_depth_2d; @group(5) @binding(1) var s_shadow_maps_atlas: sampler; @group(5) @binding(2) @@ -146,7 +149,27 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let light: Light = u_lights.data[light_index]; 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 = calc_shadow(in.frag_pos_light_space); + return vec4(vec3(shadow), 1.0);*/ + + /*var proj_coords = in.frag_pos_light_space / in.frag_pos_light_space.w; + proj_coords = proj_coords * 0.5 + 0.5; + + let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, proj_coords.xy, 0.0); + let current_depth = proj_coords.z; + + if current_depth > closest_depth { + return vec4(vec3(current_depth), 1.0); + } else { + return vec4(vec3(closest_depth), 1.0); + }*/ + + //return vec4(vec3(closest_depth), 1.0); + //let shadow = select(0.0, 1.0, current_depth > closest_depth); + let light_dir = normalize(-light.direction); + + let shadow = calc_shadow(in.world_normal, light_dir, in.frag_pos_light_space); + 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); } else if (light.light_ty == LIGHT_TY_SPOT) { @@ -158,6 +181,34 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { return vec4(light_object_res, object_color.a); } +fn calc_shadow(normal: vec3, light_dir: vec3, frag_pos_light_space: vec4) -> f32 { + var proj_coords = frag_pos_light_space.xyz / frag_pos_light_space.w; + // for some reason the y component is clipped 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; + proj_coords.x = xy_remapped.x; + proj_coords.y = xy_remapped.y; + + let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, proj_coords.xy, 0.0); + let current_depth = proj_coords.z; + + // use a bias to avoid shadow acne + let bias = max(0.05 * (1.0 - dot(normal, light_dir)), 0.005); + var shadow = 0.0; + if current_depth - bias > closest_depth { + shadow = 1.0; + } + + // dont cast shadows outside the light's far plane + if (proj_coords.z > 1.0) { + shadow = 0.0; + } + + return 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)); @@ -173,7 +224,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; @@ -199,7 +250,7 @@ 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 + (1.0 - 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 { From 6d57b40629b3ba3caebfa271cc5503379f7a20c2 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Thu, 4 Jul 2024 23:28:21 -0400 Subject: [PATCH 08/28] render: cull back faces, code cleanup to fix warnings --- examples/shadows/src/main.rs | 6 ++++++ lyra-game/src/render/graph/passes/meshes.rs | 7 +++++-- lyra-game/src/render/graph/passes/shadows.rs | 21 +++++++++++--------- lyra-game/src/render/shaders/base.wgsl | 18 ----------------- 4 files changed, 23 insertions(+), 29 deletions(-) diff --git a/examples/shadows/src/main.rs b/examples/shadows/src/main.rs index 036f360..3e5c478 100644 --- a/examples/shadows/src/main.rs +++ b/examples/shadows/src/main.rs @@ -136,6 +136,12 @@ fn setup_scene_plugin(game: &mut Game) { Transform::from_xyz(0.0, -2.0, -5.0), )); + world.spawn(( + cube_mesh.clone(), + WorldTransform::default(), + Transform::from_xyz(3.0, -3.75, -5.0), + )); + world.spawn(( platform_mesh.clone(), WorldTransform::default(), diff --git a/lyra-game/src/render/graph/passes/meshes.rs b/lyra-game/src/render/graph/passes/meshes.rs index 75e190d..61b5c05 100644 --- a/lyra-game/src/render/graph/passes/meshes.rs +++ b/lyra-game/src/render/graph/passes/meshes.rs @@ -80,7 +80,7 @@ impl MeshPass { impl Node for MeshPass { fn desc( &mut self, - graph: &mut crate::render::graph::RenderGraph, + _: &mut crate::render::graph::RenderGraph, ) -> crate::render::graph::NodeDesc { // load the default texture //let bytes = include_bytes!("../../default_texture.png"); @@ -244,7 +244,10 @@ impl Node for MeshPass { stencil: wgpu::StencilState::default(), // TODO: stencil buffer bias: wgpu::DepthBiasState::default(), }), - primitive: wgpu::PrimitiveState::default(), + primitive: wgpu::PrimitiveState { + cull_mode: Some(wgpu::Face::Back), + ..Default::default() + }, multisample: wgpu::MultisampleState::default(), multiview: None, }, diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index fc75b4a..d074503 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -1,8 +1,8 @@ use std::{mem, num::NonZeroU64, rc::Rc, sync::Arc}; -use lyra_ecs::{query::Entities, AtomicRef, Entity, ResourceData}; +use lyra_ecs::{query::{filter::Has, Entities}, AtomicRef, Entity, ResourceData}; use lyra_game_derive::RenderGraphLabel; -use lyra_math::{Transform, OPENGL_TO_WGPU_MATRIX}; +use lyra_math::Transform; use rustc_hash::FxHashMap; use tracing::{debug, warn}; use wgpu::util::DeviceExt; @@ -11,7 +11,7 @@ use crate::render::{ graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, light::directional::DirectionalLight, resource::{ - FragmentState, PipelineDescriptor, RenderPipeline, RenderPipelineDescriptor, Shader, + RenderPipeline, RenderPipelineDescriptor, Shader, VertexState, }, transform_buffer_storage::TransformBuffers, @@ -181,7 +181,7 @@ impl ShadowMapsPass { impl Node for ShadowMapsPass { fn desc( &mut self, - graph: &mut crate::render::graph::RenderGraph, + _: &mut crate::render::graph::RenderGraph, ) -> crate::render::graph::NodeDesc { let mut node = NodeDesc::new(NodeType::Render, None, vec![]); @@ -216,13 +216,13 @@ impl Node for ShadowMapsPass { &mut self, graph: &mut crate::render::graph::RenderGraph, world: &mut lyra_ecs::World, - context: &mut crate::render::graph::RenderGraphContext, + _: &mut crate::render::graph::RenderGraphContext, ) { self.render_meshes = world.try_get_resource_data::(); self.transform_buffers = world.try_get_resource_data::(); self.mesh_buffers = world.try_get_resource_data::>(); - for (entity, pos, light) in world.view_iter::<(Entities, &Transform, &DirectionalLight)>() { + for (entity, pos, _) in world.view_iter::<(Entities, &Transform, Has)>() { if !self.depth_maps.contains_key(&entity) { self.create_depth_map(graph.device(), entity, *pos); debug!("Created depth map for {:?} light entity", entity); @@ -267,7 +267,10 @@ impl Node for ShadowMapsPass { stencil: wgpu::StencilState::default(), bias: wgpu::DepthBiasState::default(), }), - primitive: wgpu::PrimitiveState::default(), + primitive: wgpu::PrimitiveState { + cull_mode: Some(wgpu::Face::Back), + ..Default::default() + }, multisample: wgpu::MultisampleState::default(), multiview: None, }, @@ -278,8 +281,8 @@ impl Node for ShadowMapsPass { fn execute( &mut self, - graph: &mut crate::render::graph::RenderGraph, - desc: &crate::render::graph::NodeDesc, + _: &mut crate::render::graph::RenderGraph, + _: &crate::render::graph::NodeDesc, context: &mut crate::render::graph::RenderGraphContext, ) { let encoder = context.encoder.as_mut().unwrap(); diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 79cf8f1..e00b065 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -149,25 +149,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let light: Light = u_lights.data[light_index]; if (light.light_ty == LIGHT_TY_DIRECTIONAL) { - /*let shadow = calc_shadow(in.frag_pos_light_space); - return vec4(vec3(shadow), 1.0);*/ - - /*var proj_coords = in.frag_pos_light_space / in.frag_pos_light_space.w; - proj_coords = proj_coords * 0.5 + 0.5; - - let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, proj_coords.xy, 0.0); - let current_depth = proj_coords.z; - - if current_depth > closest_depth { - return vec4(vec3(current_depth), 1.0); - } else { - return vec4(vec3(closest_depth), 1.0); - }*/ - - //return vec4(vec3(closest_depth), 1.0); - //let shadow = select(0.0, 1.0, current_depth > closest_depth); let light_dir = normalize(-light.direction); - let shadow = calc_shadow(in.world_normal, light_dir, in.frag_pos_light_space); 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) { From e2b554b4ef44d27af0c18433efe2f4d055b184c5 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Fri, 5 Jul 2024 17:29:38 -0400 Subject: [PATCH 09/28] render: implement simple texture atlas for the shadow maps --- lyra-game/src/render/graph/node.rs | 4 +- lyra-game/src/render/graph/passes/shadows.rs | 50 ++++++++----- lyra-game/src/render/mod.rs | 5 +- lyra-game/src/render/shaders/base.wgsl | 54 ++++++++++++-- lyra-game/src/render/texture_atlas.rs | 78 ++++++++++++++++++++ 5 files changed, 162 insertions(+), 29 deletions(-) create mode 100644 lyra-game/src/render/texture_atlas.rs diff --git a/lyra-game/src/render/graph/node.rs b/lyra-game/src/render/graph/node.rs index 780b669..f0cdcfe 100644 --- a/lyra-game/src/render/graph/node.rs +++ b/lyra-game/src/render/graph/node.rs @@ -56,7 +56,7 @@ pub enum SlotValue { Lazy, TextureView(Arc), Sampler(Rc), - Texture(Rc), + Texture(Arc), Buffer(Arc), RenderTarget(Rc>), Frame(Rc>>), @@ -71,7 +71,7 @@ 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) } diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index d074503..8dab5cf 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -1,6 +1,10 @@ use std::{mem, num::NonZeroU64, rc::Rc, sync::Arc}; -use lyra_ecs::{query::{filter::Has, Entities}, AtomicRef, Entity, ResourceData}; +use glam::UVec2; +use lyra_ecs::{ + query::{filter::Has, Entities}, + AtomicRef, Entity, ResourceData, +}; use lyra_game_derive::RenderGraphLabel; use lyra_math::Transform; use rustc_hash::FxHashMap; @@ -10,12 +14,10 @@ use wgpu::util::DeviceExt; use crate::render::{ graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, light::directional::DirectionalLight, - resource::{ - RenderPipeline, RenderPipelineDescriptor, Shader, - VertexState, - }, + resource::{RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, transform_buffer_storage::TransformBuffers, vertex::Vertex, + TextureAtlas, }; use super::{MeshBufferStorage, RenderAssets, RenderMeshes}; @@ -50,10 +52,7 @@ pub struct ShadowMapsPass { mesh_buffers: Option, pipeline: Option, - /// The depth map atlas texture - atlas_texture: Rc, - /// The depth map atlas texture view - atlas_view: Arc, + atlas: Arc, /// The depth map atlas sampler atlas_sampler: Rc, } @@ -78,7 +77,7 @@ impl ShadowMapsPass { }), ); - let tex = device.create_texture(&wgpu::TextureDescriptor { + /* let tex = device.create_texture(&wgpu::TextureDescriptor { label: Some("texture_shadow_map_atlas"), size: wgpu::Extent3d { width: SHADOW_SIZE.x, @@ -96,7 +95,15 @@ impl ShadowMapsPass { let view = tex.create_view(&wgpu::TextureViewDescriptor { label: Some("shadows_map_view"), ..Default::default() - }); + }); */ + + let atlas = TextureAtlas::new( + device, + wgpu::TextureFormat::Depth32Float, + wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, + SHADOW_SIZE, + UVec2::new(4, 4), + ); let sampler = device.create_sampler(&wgpu::SamplerDescriptor { label: Some("sampler_shadow_map_atlas"), @@ -119,14 +126,13 @@ impl ShadowMapsPass { pipeline: None, atlas_sampler: Rc::new(sampler), - atlas_texture: Rc::new(tex), - atlas_view: Arc::new(view), + atlas: Arc::new(atlas), } } fn create_depth_map(&mut self, device: &wgpu::Device, entity: Entity, light_pos: Transform) { const NEAR_PLANE: f32 = 0.1; - const FAR_PLANE: f32 = 25.5; + const FAR_PLANE: f32 = 45.0; let ortho_proj = glam::Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, NEAR_PLANE, FAR_PLANE); @@ -188,13 +194,13 @@ impl Node for ShadowMapsPass { node.add_texture_slot( ShadowMapsPassSlots::ShadowAtlasTexture, SlotAttribute::Output, - Some(SlotValue::Texture(self.atlas_texture.clone())), + Some(SlotValue::Texture(self.atlas.texture().clone())), ); node.add_texture_view_slot( ShadowMapsPassSlots::ShadowAtlasTextureView, SlotAttribute::Output, - Some(SlotValue::TextureView(self.atlas_view.clone())), + Some(SlotValue::TextureView(self.atlas.view().clone())), ); node.add_sampler_slot( @@ -222,6 +228,8 @@ impl Node for ShadowMapsPass { self.transform_buffers = world.try_get_resource_data::(); self.mesh_buffers = world.try_get_resource_data::>(); + world.add_resource(self.atlas.clone()); + for (entity, pos, _) in world.view_iter::<(Entities, &Transform, Has)>() { if !self.depth_maps.contains_key(&entity) { self.create_depth_map(graph.device(), entity, *pos); @@ -231,7 +239,8 @@ impl Node for ShadowMapsPass { // update the light projection buffer slot let (_, dir_depth_map) = self.depth_maps.iter().next().unwrap(); - let val = graph.slot_value_mut(ShadowMapsPassSlots::DirLightProjectionBuffer) + let val = graph + .slot_value_mut(ShadowMapsPassSlots::DirLightProjectionBuffer) .unwrap(); *val = SlotValue::Buffer(dir_depth_map.light_projection_buffer.clone()); @@ -308,7 +317,7 @@ impl Node for ShadowMapsPass { label: Some("pass_shadow_map"), color_attachments: &[], depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &self.atlas_view, + view: self.atlas.view(), depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: true, @@ -317,6 +326,11 @@ impl Node for ShadowMapsPass { }), }); pass.set_pipeline(&pipeline); + let viewport = self.atlas.texture_viewport(0); + // only render to the light's map in the atlas + pass.set_viewport(viewport.offset.x as _, viewport.offset.y as _, viewport.size.x as _, viewport.size.y as _, 0.0, 1.0); + // only clear the light map in the atlas + pass.set_scissor_rect(viewport.offset.x, viewport.offset.y, viewport.size.x, viewport.size.y); for job in render_meshes.iter() { // get the mesh (containing vertices) and the buffers from storage diff --git a/lyra-game/src/render/mod.rs b/lyra-game/src/render/mod.rs index d1e39d5..641a6e0 100755 --- a/lyra-game/src/render/mod.rs +++ b/lyra-game/src/render/mod.rs @@ -14,4 +14,7 @@ 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::*; \ No newline at end of file diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index e00b065..eb3c321 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -149,6 +149,34 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let light: Light = u_lights.data[light_index]; if (light.light_ty == LIGHT_TY_DIRECTIONAL) { + /*var proj_coords = in.frag_pos_light_space.xyz / in.frag_pos_light_space.w; + // for some reason the y component is clipped 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; + proj_coords.x = mix(0.0, 1024.0 / 4096.0, xy_remapped.x); + proj_coords.y = mix(0.0, 1024.0 / 4096.0, xy_remapped.y); + + let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, proj_coords.xy, 0.0, vec2(0, 0)); + let current_depth = proj_coords.z; + + // use a bias to avoid shadow acne + let light_dir = normalize(-light.direction); + let bias = max(0.05 * (1.0 - dot(in.world_normal, light_dir)), 0.005); + var shadow = 0.0; + if current_depth - bias > closest_depth { + shadow = 1.0; + } + + // dont cast shadows outside the light's far plane + if (proj_coords.z > 1.0) { + shadow = 0.0; + } + + return vec4(vec3(closest_depth), 1.0);*/ + + let light_dir = normalize(-light.direction); let shadow = calc_shadow(in.world_normal, light_dir, in.frag_pos_light_space); light_res += blinn_phong_dir_light(in.world_position, in.world_normal, light, u_material, specular_color, shadow); @@ -168,12 +196,27 @@ fn calc_shadow(normal: vec3, light_dir: vec3, frag_pos_light_space: ve // for some reason the y component is clipped after transforming proj_coords.y = -proj_coords.y; + // dont cast shadows outside the light's far plane + if (proj_coords.z > 1.0) { + return 0.0; + } + // Remap xy to [0.0, 1.0] let xy_remapped = proj_coords.xy * 0.5 + 0.5; - proj_coords.x = xy_remapped.x; - proj_coords.y = xy_remapped.y; + // TODO: when more lights are added, change the index, and the atlas sizes + let shadow_map_index = 0; + let shadow_map_region = vec2( (f32(shadow_map_index) * 1024.0) / 4096.0, (f32(shadow_map_index + 1) * 1024.0) / 4096.0); + // lerp the tex coords to the shadow map for this light. + proj_coords.x = mix(shadow_map_region.x, shadow_map_region.y, xy_remapped.x); + proj_coords.y = mix(shadow_map_region.x, shadow_map_region.y, xy_remapped.y); - let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, proj_coords.xy, 0.0); + // simulate `ClampToBorder`, not creating shadows past the shadow map regions + if (proj_coords.x > shadow_map_region.y && proj_coords.y > shadow_map_region.y) + || (proj_coords.x < shadow_map_region.x && proj_coords.y < shadow_map_region.x) { + return 0.0; + } + + let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, proj_coords.xy, 0.0, vec2(0, 0)); let current_depth = proj_coords.z; // use a bias to avoid shadow acne @@ -183,11 +226,6 @@ fn calc_shadow(normal: vec3, light_dir: vec3, frag_pos_light_space: ve shadow = 1.0; } - // dont cast shadows outside the light's far plane - if (proj_coords.z > 1.0) { - shadow = 0.0; - } - return shadow; } diff --git a/lyra-game/src/render/texture_atlas.rs b/lyra-game/src/render/texture_atlas.rs new file mode 100644 index 0000000..6808d74 --- /dev/null +++ b/lyra-game/src/render/texture_atlas.rs @@ -0,0 +1,78 @@ +use std::sync::Arc; + +use glam::UVec2; + +#[derive(Debug, Clone, Copy)] +pub struct AtlasViewport { + pub offset: UVec2, + pub size: UVec2, +} + +pub struct TextureAtlas { + /// The size of each texture in the atlas. + texture_size: UVec2, + /// The amount of textures in the atlas. + texture_count: UVec2, + + texture_format: wgpu::TextureFormat, + texture: Arc, + view: Arc, +} + +impl TextureAtlas { + pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat, usages: wgpu::TextureUsages, texture_size: UVec2, texture_count: UVec2) -> Self { + let total_size = texture_size * texture_count; + + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("texture_atlas"), + size: wgpu::Extent3d { width: total_size.x, height: total_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 { + texture_size, + texture_count, + texture_format: format, + texture: Arc::new(texture), + view: Arc::new(view), + } + } + + /// Get the viewport of a texture index in the atlas. + pub fn texture_viewport(&self, atlas_index: u32) -> AtlasViewport { + let x = (atlas_index % self.texture_count.x) * self.texture_size.x; + let y = (atlas_index / self.texture_count.y) * self.texture_size.y; + + AtlasViewport { offset: UVec2::new(x, y), size: self.texture_size } + } + + pub fn view(&self) -> &Arc { + &self.view + } + + pub fn texture(&self) -> &Arc { + &self.texture + } + + pub fn texture_format(&self) -> &wgpu::TextureFormat { + &self.texture_format + } + + pub fn texture_size(&self) -> UVec2 { + self.texture_size + } + + pub fn texture_count(&self) -> UVec2 { + self.texture_count + } + + pub fn total_texture_count(&self) -> u32 { + self.texture_count.x * self.texture_count.y + } +} \ No newline at end of file From a4ce4cb432d2290d613af2cd3408e1eda9948632 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Wed, 10 Jul 2024 20:16:21 -0400 Subject: [PATCH 10/28] render: implement packed texture atlas for shadow maps --- Cargo.lock | 7 + lyra-game/Cargo.toml | 1 + lyra-game/src/render/graph/passes/meshes.rs | 31 ++- lyra-game/src/render/graph/passes/shadows.rs | 193 ++++++++++++------- lyra-game/src/render/shaders/base.wgsl | 74 +++---- lyra-game/src/render/shaders/shadows.wgsl | 14 +- lyra-game/src/render/texture_atlas.rs | 149 +++++++++++--- 7 files changed, 322 insertions(+), 147 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c5c7786..b453320 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1881,6 +1881,7 @@ dependencies = [ "lyra-scene", "petgraph", "quote", + "rectangle-pack", "rustc-hash", "syn 2.0.51", "thiserror", @@ -2767,6 +2768,12 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "rectangle-pack" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d463f2884048e7153449a55166f91028d5b0ea53c79377099ce4e8cf0cf9bb" + [[package]] name = "redox_syscall" version = "0.3.5" diff --git a/lyra-game/Cargo.toml b/lyra-game/Cargo.toml index 26cc050..5420015 100644 --- a/lyra-game/Cargo.toml +++ b/lyra-game/Cargo.toml @@ -38,6 +38,7 @@ unique = "0.9.1" rustc-hash = "1.1.0" petgraph = { version = "0.6.5", features = ["matrix_graph"] } bind_match = "0.1.2" +rectangle-pack = "0.4.2" [features] tracy = ["dep:tracing-tracy"] diff --git a/lyra-game/src/render/graph/passes/meshes.rs b/lyra-game/src/render/graph/passes/meshes.rs index 61b5c05..b00df97 100644 --- a/lyra-game/src/render/graph/passes/meshes.rs +++ b/lyra-game/src/render/graph/passes/meshes.rs @@ -116,9 +116,14 @@ impl Node for MeshPass { .expect("missing ShadowMapsPassSlots::ShadowAtlasSampler") .as_sampler() .unwrap(); - let dir_light_projection_buf = graph - .slot_value(ShadowMapsPassSlots::DirLightProjectionBuffer) - .expect("missing ShadowMapsPassSlots::DirLightProjectionBuffer") + let atlas_size_buf = graph + .slot_value(ShadowMapsPassSlots::ShadowAtlasSizeBuffer) + .expect("missing ShadowMapsPassSlots::ShadowAtlasSizeBuffer") + .as_buffer() + .unwrap(); + let light_uniform_buf = graph + .slot_value(ShadowMapsPassSlots::ShadowLightUniformsBuffer) + .expect("missing ShadowMapsPassSlots::ShadowLightUniformsBuffer") .as_buffer() .unwrap(); @@ -151,6 +156,16 @@ impl Node for MeshPass { }, 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, + }, ], }); @@ -169,7 +184,15 @@ impl Node for MeshPass { wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: dir_light_projection_buf, + buffer: atlas_size_buf, + offset: 0, + size: None, + }), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: light_uniform_buf, offset: 0, size: None, }), diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index 8dab5cf..b4de5fb 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -1,9 +1,7 @@ -use std::{mem, num::NonZeroU64, rc::Rc, sync::Arc}; +use std::{collections::VecDeque, mem, num::NonZeroU64, ops::Deref, rc::Rc, sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}}; -use glam::UVec2; use lyra_ecs::{ - query::{filter::Has, Entities}, - AtomicRef, Entity, ResourceData, + query::{filter::Has, Entities}, AtomicRef, Component, Entity, ResourceData }; use lyra_game_derive::RenderGraphLabel; use lyra_math::Transform; @@ -12,24 +10,20 @@ use tracing::{debug, warn}; use wgpu::util::DeviceExt; use crate::render::{ - graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, - light::directional::DirectionalLight, - resource::{RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, - transform_buffer_storage::TransformBuffers, - vertex::Vertex, - TextureAtlas, + graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, light::directional::DirectionalLight, resource::{RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, transform_buffer_storage::TransformBuffers, vertex::Vertex, AtlasViewport, TextureAtlas }; use super::{MeshBufferStorage, RenderAssets, RenderMeshes}; -const SHADOW_SIZE: glam::UVec2 = glam::UVec2::new(1024, 1024); +const SHADOW_SIZE: glam::UVec2 = glam::uvec2(1024, 1024); #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] pub enum ShadowMapsPassSlots { ShadowAtlasTexture, ShadowAtlasTextureView, ShadowAtlasSampler, - DirLightProjectionBuffer, + ShadowAtlasSizeBuffer, + ShadowLightUniformsBuffer, } #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] @@ -38,10 +32,12 @@ pub struct ShadowMapsPassLabel; struct LightDepthMap { light_projection_buffer: Arc, bindgroup: wgpu::BindGroup, + atlas_index: u64, } pub struct ShadowMapsPass { bgl: Arc, + atlas_size_buffer: Arc, /// depth maps for a light owned by an entity. depth_maps: FxHashMap, @@ -52,7 +48,7 @@ pub struct ShadowMapsPass { mesh_buffers: Option, pipeline: Option, - atlas: Arc, + atlas: LightShadowMapAtlas, /// The depth map atlas sampler atlas_sampler: Rc, } @@ -61,50 +57,38 @@ impl ShadowMapsPass { pub fn new(device: &wgpu::Device) -> Self { let bgl = Arc::new( device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("bgl_shadows_light_projection"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some( - NonZeroU64::new(mem::size_of::() as _).unwrap(), - ), - }, - count: None, - }], + label: Some("bgl_shadow_maps_lights"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some( + NonZeroU64::new(mem::size_of::() as _).unwrap(), + ), + }, + count: None, + } + ], }), ); - /* let tex = device.create_texture(&wgpu::TextureDescriptor { - label: Some("texture_shadow_map_atlas"), - size: wgpu::Extent3d { - width: SHADOW_SIZE.x, - height: SHADOW_SIZE.y, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Depth32Float, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }); - - let view = tex.create_view(&wgpu::TextureViewDescriptor { - label: Some("shadows_map_view"), - ..Default::default() - }); */ - let atlas = TextureAtlas::new( device, wgpu::TextureFormat::Depth32Float, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, - SHADOW_SIZE, - UVec2::new(4, 4), + SHADOW_SIZE * 4, ); + 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 = device.create_sampler(&wgpu::SamplerDescriptor { label: Some("sampler_shadow_map_atlas"), address_mode_u: wgpu::AddressMode::ClampToBorder, @@ -119,6 +103,7 @@ impl ShadowMapsPass { Self { bgl, + atlas_size_buffer: Arc::new(atlas_size_buffer), depth_maps: Default::default(), transform_buffers: None, render_meshes: None, @@ -126,14 +111,20 @@ impl ShadowMapsPass { pipeline: None, atlas_sampler: Rc::new(sampler), - atlas: Arc::new(atlas), + atlas: LightShadowMapAtlas(Arc::new(RwLock::new(atlas))), } } - fn create_depth_map(&mut self, device: &wgpu::Device, entity: Entity, light_pos: Transform) { + /// Create a depth map and return the id of the depth map in the texture atlas. + fn create_depth_map(&mut self, device: &wgpu::Device, entity: Entity, light_pos: Transform) -> u64 { const NEAR_PLANE: f32 = 0.1; const FAR_PLANE: f32 = 45.0; + let mut atlas = self.atlas.get_mut(); + let atlas_index = atlas.pack_new_texture(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _) + .expect("failed to pack new shadow map into texture atlas"); + let atlas_frame = atlas.texture_viewport(atlas_index); + let ortho_proj = glam::Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, NEAR_PLANE, FAR_PLANE); @@ -141,25 +132,31 @@ impl ShadowMapsPass { glam::Mat4::look_to_rh(light_pos.translation, light_pos.forward(), light_pos.up()); let light_proj = ortho_proj * look_view; + let uniform = LightShadowUniform { + space_mat: light_proj, + atlas_frame, + }; let light_projection_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("shadows_light_view_mat_buffer"), + label: Some("buffer_shadow_maps_light"), usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - contents: bytemuck::bytes_of(&light_proj), + contents: bytemuck::bytes_of(&uniform), }); let bg = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("shadows_bind_group"), + label: Some("shadow_maps_bind_group"), layout: &self.bgl, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &light_projection_buffer, - offset: 0, - size: None, - }), - }], + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &light_projection_buffer, + offset: 0, + size: None, + }), + } + ], }); self.depth_maps.insert( @@ -167,8 +164,11 @@ impl ShadowMapsPass { LightDepthMap { light_projection_buffer: Arc::new(light_projection_buffer), bindgroup: bg, + atlas_index }, ); + + atlas_index } fn transform_buffers(&self) -> AtomicRef { @@ -191,16 +191,18 @@ impl Node for ShadowMapsPass { ) -> 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(self.atlas.texture().clone())), + Some(SlotValue::Texture(atlas.texture().clone())), ); node.add_texture_view_slot( ShadowMapsPassSlots::ShadowAtlasTextureView, SlotAttribute::Output, - Some(SlotValue::TextureView(self.atlas.view().clone())), + Some(SlotValue::TextureView(atlas.view().clone())), ); node.add_sampler_slot( @@ -210,11 +212,17 @@ impl Node for ShadowMapsPass { ); node.add_sampler_slot( - ShadowMapsPassSlots::DirLightProjectionBuffer, + ShadowMapsPassSlots::ShadowLightUniformsBuffer, SlotAttribute::Output, Some(SlotValue::Lazy), ); + node.add_buffer_slot( + ShadowMapsPassSlots::ShadowAtlasSizeBuffer, + SlotAttribute::Output, + Some(SlotValue::Buffer(self.atlas_size_buffer.clone())), + ); + node } @@ -230,17 +238,29 @@ impl Node for ShadowMapsPass { 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, _) in world.view_iter::<(Entities, &Transform, Has)>() { if !self.depth_maps.contains_key(&entity) { - self.create_depth_map(graph.device(), entity, *pos); + + // TODO: dont pack the textures as they're added + let atlas_index = self.create_depth_map(graph.device(), entity, *pos); + index_components_queue.push_back((entity, atlas_index)); + debug!("Created depth map for {:?} light entity", entity); } } + + // now consume from the queue adding the components to the entities + while let Some((entity, atlas_id)) = index_components_queue.pop_front() { + world.insert(entity, LightShadowMapId(atlas_id)); + } // update the light projection buffer slot let (_, dir_depth_map) = self.depth_maps.iter().next().unwrap(); let val = graph - .slot_value_mut(ShadowMapsPassSlots::DirLightProjectionBuffer) + .slot_value_mut(ShadowMapsPassSlots::ShadowLightUniformsBuffer) .unwrap(); *val = SlotValue::Buffer(dir_depth_map.light_projection_buffer.clone()); @@ -313,11 +333,12 @@ impl Node for ShadowMapsPass { .expect("missing directional light in scene"); { + 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: self.atlas.view(), + view: atlas.view(), depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: true, @@ -326,7 +347,7 @@ impl Node for ShadowMapsPass { }), }); pass.set_pipeline(&pipeline); - let viewport = self.atlas.texture_viewport(0); + let viewport = atlas.texture_viewport(dir_depth_map.atlas_index); // only render to the light's map in the atlas pass.set_viewport(viewport.offset.x as _, viewport.offset.y as _, viewport.size.x as _, viewport.size.y as _, 0.0, 1.0); // only clear the light map in the atlas @@ -371,3 +392,39 @@ impl Node for ShadowMapsPass { } } } + +#[repr(C)] +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct LightShadowUniform { + space_mat: glam::Mat4, + atlas_frame: AtlasViewport, // 2xUVec2 (4xf32), so no padding needed +} + +/// 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(u64); + +impl Deref for LightShadowMapId { + type Target = u64; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// 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() + } +} \ No newline at end of file diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index eb3c321..8d3bb19 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -22,6 +22,11 @@ struct VertexOutput { @location(3) frag_pos_light_space: vec4, } +struct TextureAtlasFrame { + offset: vec2, + size: vec2, +} + struct TransformData { transform: mat4x4, normal_matrix: mat4x4, @@ -82,7 +87,7 @@ fn vs_main( let normal_mat = mat3x3(normal_mat4[0].xyz, normal_mat4[1].xyz, normal_mat4[2].xyz); out.world_normal = normalize(normal_mat * model.normal, ); - out.frag_pos_light_space = u_light_space_matrix * world_position; + out.frag_pos_light_space = u_light_shadow.light_space_matrix * world_position; return out; } @@ -104,13 +109,10 @@ var t_diffuse: texture_2d; @group(0) @binding(2) var s_diffuse: sampler; -/*@group(4) @binding(0) -var u_material: Material; - -@group(5) @binding(0) -var t_specular: texture_2d; -@group(5) @binding(1) -var s_specular: sampler;*/ +struct LightShadowMapUniform { + light_space_matrix: mat4x4, + atlas_frame: TextureAtlasFrame, +} @group(4) @binding(0) var u_light_indices: array; @@ -122,7 +124,9 @@ var t_shadow_maps_atlas: texture_depth_2d; @group(5) @binding(1) var s_shadow_maps_atlas: sampler; @group(5) @binding(2) -var u_light_space_matrix: mat4x4; +var u_shadow_maps_atlas_size: vec2; +@group(5) @binding(3) +var u_light_shadow: LightShadowMapUniform; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { @@ -149,36 +153,8 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let light: Light = u_lights.data[light_index]; if (light.light_ty == LIGHT_TY_DIRECTIONAL) { - /*var proj_coords = in.frag_pos_light_space.xyz / in.frag_pos_light_space.w; - // for some reason the y component is clipped 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; - proj_coords.x = mix(0.0, 1024.0 / 4096.0, xy_remapped.x); - proj_coords.y = mix(0.0, 1024.0 / 4096.0, xy_remapped.y); - - let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, proj_coords.xy, 0.0, vec2(0, 0)); - let current_depth = proj_coords.z; - - // use a bias to avoid shadow acne let light_dir = normalize(-light.direction); - let bias = max(0.05 * (1.0 - dot(in.world_normal, light_dir)), 0.005); - var shadow = 0.0; - if current_depth - bias > closest_depth { - shadow = 1.0; - } - - // dont cast shadows outside the light's far plane - if (proj_coords.z > 1.0) { - shadow = 0.0; - } - - return vec4(vec3(closest_depth), 1.0);*/ - - - let light_dir = normalize(-light.direction); - let shadow = calc_shadow(in.world_normal, light_dir, in.frag_pos_light_space); + let shadow = calc_shadow(in.world_normal, light_dir, in.frag_pos_light_space, u_light_shadow.atlas_frame); 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); @@ -191,7 +167,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { return vec4(light_object_res, object_color.a); } -fn calc_shadow(normal: vec3, light_dir: vec3, frag_pos_light_space: vec4) -> f32 { +fn calc_shadow(normal: vec3, light_dir: vec3, frag_pos_light_space: vec4, atlas_region: TextureAtlasFrame) -> f32 { var proj_coords = frag_pos_light_space.xyz / frag_pos_light_space.w; // for some reason the y component is clipped after transforming proj_coords.y = -proj_coords.y; @@ -203,20 +179,24 @@ fn calc_shadow(normal: vec3, light_dir: vec3, frag_pos_light_space: ve // Remap xy to [0.0, 1.0] let xy_remapped = proj_coords.xy * 0.5 + 0.5; - // TODO: when more lights are added, change the index, and the atlas sizes - let shadow_map_index = 0; - let shadow_map_region = vec2( (f32(shadow_map_index) * 1024.0) / 4096.0, (f32(shadow_map_index + 1) * 1024.0) / 4096.0); + + // no need to get the y since the maps are square + let atlas_start = f32(atlas_region.offset.x) / f32(u_shadow_maps_atlas_size.x); + let atlas_end = f32(atlas_region.offset.x + atlas_region.size.x) / f32(u_shadow_maps_atlas_size.x); // lerp the tex coords to the shadow map for this light. - proj_coords.x = mix(shadow_map_region.x, shadow_map_region.y, xy_remapped.x); - proj_coords.y = mix(shadow_map_region.x, shadow_map_region.y, xy_remapped.y); + proj_coords.x = mix(atlas_start, atlas_end, xy_remapped.x); + proj_coords.y = mix(atlas_start, atlas_end, xy_remapped.y); // simulate `ClampToBorder`, not creating shadows past the shadow map regions - if (proj_coords.x > shadow_map_region.y && proj_coords.y > shadow_map_region.y) - || (proj_coords.x < shadow_map_region.x && proj_coords.y < shadow_map_region.x) { + if (proj_coords.x > atlas_end && proj_coords.y > atlas_end) + || (proj_coords.x < atlas_start && proj_coords.y < atlas_start) { return 0.0; } - let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, proj_coords.xy, 0.0, vec2(0, 0)); + // must manually apply offset to the texture coords since `textureSampleLevel` requires a + // const value. + let offset_coords = proj_coords.xy + (vec2(atlas_region.offset) / vec2(u_shadow_maps_atlas_size)); + let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, offset_coords, 0.0); let current_depth = proj_coords.z; // use a bias to avoid shadow acne diff --git a/lyra-game/src/render/shaders/shadows.wgsl b/lyra-game/src/render/shaders/shadows.wgsl index fa2291c..5f1a0c5 100644 --- a/lyra-game/src/render/shaders/shadows.wgsl +++ b/lyra-game/src/render/shaders/shadows.wgsl @@ -3,8 +3,18 @@ struct TransformData { normal_matrix: mat4x4, } +struct TextureAtlasFrame { + offset: vec2, + size: vec2, +} + +struct LightShadowMapUniform { + light_space_matrix: mat4x4, + atlas_frame: TextureAtlasFrame, +} + @group(0) @binding(0) -var u_light_space_matrix: mat4x4; +var u_light_shadow: LightShadowMapUniform; @group(1) @binding(0) var u_model_transform_data: TransformData; @@ -18,6 +28,6 @@ struct VertexOutput { fn vs_main( @location(0) position: vec3 ) -> VertexOutput { - let pos = u_light_space_matrix * u_model_transform_data.transform * vec4(position, 1.0); + let pos = u_light_shadow.light_space_matrix * u_model_transform_data.transform * vec4(position, 1.0); return VertexOutput(pos); } \ No newline at end of file diff --git a/lyra-game/src/render/texture_atlas.rs b/lyra-game/src/render/texture_atlas.rs index 6808d74..65b0989 100644 --- a/lyra-game/src/render/texture_atlas.rs +++ b/lyra-game/src/render/texture_atlas.rs @@ -1,31 +1,54 @@ -use std::sync::Arc; +use std::{ + collections::BTreeMap, + sync::Arc, +}; use glam::UVec2; +use rectangle_pack::{pack_rects, GroupedRectsToPlace, RectToInsert, RectanglePackOk, TargetBin}; -#[derive(Debug, Clone, Copy)] +#[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, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] pub struct AtlasViewport { pub offset: UVec2, pub size: UVec2, } pub struct TextureAtlas { - /// The size of each texture in the atlas. - texture_size: UVec2, - /// The amount of textures in the atlas. - texture_count: UVec2, + atlas_size: UVec2, texture_format: wgpu::TextureFormat, texture: Arc, view: Arc, + + /// The next id of the next texture that will be added to the atlas. + next_texture_id: u64, + + rects: GroupedRectsToPlace, + bins: BTreeMap, + placement: Option>, } impl TextureAtlas { - pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat, usages: wgpu::TextureUsages, texture_size: UVec2, texture_count: UVec2) -> Self { - let total_size = texture_size * texture_count; - + 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: total_size.x, height: total_size.y, depth_or_array_layers: 1 }, + 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, @@ -35,21 +58,98 @@ impl TextureAtlas { }); let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let mut bins = BTreeMap::new(); + // max_depth=1 for 2d + bins.insert(0, TargetBin::new(atlas_size.x, atlas_size.y, 1)); + Self { - texture_size, - texture_count, + atlas_size, texture_format: format, texture: Arc::new(texture), view: Arc::new(view), + next_texture_id: 0, + rects: GroupedRectsToPlace::new(), + bins, + placement: None, } } - /// Get the viewport of a texture index in the atlas. - pub fn texture_viewport(&self, atlas_index: u32) -> AtlasViewport { - let x = (atlas_index % self.texture_count.x) * self.texture_size.x; - let y = (atlas_index / self.texture_count.y) * self.texture_size.y; + /// 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_new_texture(&mut self, width: u32, height: u32) -> Result { + let id = self.next_texture_id; + self.next_texture_id += 1; - AtlasViewport { offset: UVec2::new(x, y), size: self.texture_size } + // for 2d rects, set depth to 1 + let r = RectToInsert::new(width, height, 1); + self.rects.push_rect(id, None, r); + + self.pack_atlas()?; + + Ok(id) + } + + /// Add a new texture and **DO NOT** pack it into the atlas. + /// + ///

+ /// + /// The texture will not be packed into the atlas meaning + /// [`TextureAtlas::texture_viewport`] will return `None`. To pack the texture, + /// use [`TextureAtlas::pack_atlas`] or use [`TextureAtlas::pack_new_texture`] + /// when only adding a single texture. + /// + ///
+ pub fn add_texture_unpacked(&mut self, width: u32, height: u32) -> Result { + let id = self.next_texture_id; + self.next_texture_id += 1; + + // for 2d rects, set depth to 1 + let r = RectToInsert::new(width, height, 1); + self.rects.push_rect(id, None, r); + + self.pack_atlas()?; + + Ok(id) + } + + /// Pack the textures into the atlas. + pub fn pack_atlas(&mut self) -> Result<(), AtlasPackError> { + let placement = pack_rects( + &self.rects, + &mut self.bins, + &rectangle_pack::volume_heuristic, + &rectangle_pack::contains_smallest_box, + ) + .map_err(|e| match e { + rectangle_pack::RectanglePackError::NotEnoughBinSpace => AtlasPackError::NotEnoughSpace, + })?; + self.placement = Some(placement); + + Ok(()) + } + + /// Get the viewport of a texture index in the atlas. + pub fn texture_viewport(&self, atlas_index: u64) -> AtlasViewport { + let locations = self.placement.as_ref().unwrap().packed_locations(); + let (bin_id, loc) = locations + .get(&atlas_index) + .expect("atlas index is incorrect"); + debug_assert_eq!(*bin_id, 0, "somehow the texture was put in some other bin"); + + AtlasViewport { + offset: UVec2 { + x: loc.x(), + y: loc.y(), + }, + size: UVec2 { + x: loc.width(), + y: loc.height(), + }, + } } pub fn view(&self) -> &Arc { @@ -64,15 +164,12 @@ impl TextureAtlas { &self.texture_format } - pub fn texture_size(&self) -> UVec2 { - self.texture_size + pub fn total_texture_count(&self) -> u64 { + self.next_texture_id // starts at zero, so no need to increment } - pub fn texture_count(&self) -> UVec2 { - self.texture_count + /// Returns the size of the entire texture atlas. + pub fn atlas_size(&self) -> UVec2 { + self.atlas_size } - - pub fn total_texture_count(&self) -> u32 { - self.texture_count.x * self.texture_count.y - } -} \ No newline at end of file +} From cc1c482c404d463c3ac54f0b0a8e68d98378e289 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Thu, 11 Jul 2024 18:27:26 -0400 Subject: [PATCH 11/28] render: provide shadow texture atlas frame for each shadow casting light --- lyra-game/src/render/graph/passes/meshes.rs | 5 +- lyra-game/src/render/graph/passes/shadows.rs | 127 +++++++++++-------- lyra-game/src/render/light/mod.rs | 27 ++-- lyra-game/src/render/shaders/base.wgsl | 29 +++-- lyra-game/src/render/shaders/shadows.wgsl | 2 +- 5 files changed, 115 insertions(+), 75 deletions(-) diff --git a/lyra-game/src/render/graph/passes/meshes.rs b/lyra-game/src/render/graph/passes/meshes.rs index b00df97..981e7c9 100644 --- a/lyra-game/src/render/graph/passes/meshes.rs +++ b/lyra-game/src/render/graph/passes/meshes.rs @@ -14,8 +14,7 @@ use crate::render::{ }; use super::{ - BasePassSlots, LightBasePassSlots, LightCullComputePassSlots, MeshBufferStorage, RenderAssets, - RenderMeshes, ShadowMapsPassSlots, + BasePassSlots, LightBasePassSlots, LightCullComputePassSlots, MeshBufferStorage, RenderAssets, RenderMeshes, ShadowMapsPassSlots }; #[derive(Debug, Hash, Clone, Default, PartialEq, RenderGraphLabel)] @@ -160,7 +159,7 @@ impl Node for MeshPass { binding: 3, visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, + ty: wgpu::BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, min_binding_size: None, }, diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index b4de5fb..4dcdafa 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -1,4 +1,4 @@ -use std::{collections::VecDeque, mem, num::NonZeroU64, ops::Deref, rc::Rc, sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}}; +use std::{collections::VecDeque, mem, num::NonZeroU64, rc::Rc, sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}}; use lyra_ecs::{ query::{filter::Has, Entities}, AtomicRef, Component, Entity, ResourceData @@ -29,15 +29,20 @@ pub enum ShadowMapsPassSlots { #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] pub struct ShadowMapsPassLabel; +#[derive(Clone, Copy)] struct LightDepthMap { - light_projection_buffer: Arc, - bindgroup: wgpu::BindGroup, + //light_projection_buffer: Arc, + //bindgroup: wgpu::BindGroup, atlas_index: u64, + uniform_index: u64, } pub struct ShadowMapsPass { bgl: Arc, atlas_size_buffer: Arc, + light_uniforms_buffer: Arc, + light_uniforms_index: u64, + uniforms_bg: Arc, /// depth maps for a light owned by an entity. depth_maps: FxHashMap, @@ -63,8 +68,8 @@ impl ShadowMapsPass { binding: 0, visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: true, min_binding_size: Some( NonZeroU64::new(mem::size_of::() as _).unwrap(), ), @@ -101,8 +106,32 @@ impl ShadowMapsPass { ..Default::default() }); + let uniforms_buffer = + device.create_buffer(&wgpu::BufferDescriptor { + label: Some("buffer_shadow_maps_light"), + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + size: device.limits().max_storage_buffer_binding_size as u64, + mapped_at_creation: false, + }); + + 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, + offset: 0, + size: Some(NonZeroU64::new(mem::size_of::() as _).unwrap()), + }), + }], + }); + Self { bgl, + light_uniforms_buffer: Arc::new(uniforms_buffer), + light_uniforms_index: 0, + uniforms_bg: Arc::new(uniforms_bg), atlas_size_buffer: Arc::new(atlas_size_buffer), depth_maps: Default::default(), transform_buffers: None, @@ -116,7 +145,7 @@ impl ShadowMapsPass { } /// Create a depth map and return the id of the depth map in the texture atlas. - fn create_depth_map(&mut self, device: &wgpu::Device, entity: Entity, light_pos: Transform) -> u64 { + fn create_depth_map(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, entity: Entity, light_pos: Transform) -> LightDepthMap { const NEAR_PLANE: f32 = 0.1; const FAR_PLANE: f32 = 45.0; @@ -137,38 +166,23 @@ impl ShadowMapsPass { atlas_frame, }; - let light_projection_buffer = - device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("buffer_shadow_maps_light"), - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - contents: bytemuck::bytes_of(&uniform), - }); + let uniform_index = self.light_uniforms_index; + self.light_uniforms_index += 1; - let bg = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("shadow_maps_bind_group"), - layout: &self.bgl, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &light_projection_buffer, - offset: 0, - size: None, - }), - } - ], - }); + //self.light_uniforms_buffer + let offset = uniform_index_offset(&device.limits(), uniform_index); + queue.write_buffer(&self.light_uniforms_buffer, offset as u64, bytemuck::bytes_of(&uniform)); + let v = LightDepthMap { + atlas_index, + uniform_index, + }; self.depth_maps.insert( entity, - LightDepthMap { - light_projection_buffer: Arc::new(light_projection_buffer), - bindgroup: bg, - atlas_index - }, + v, ); - atlas_index + v } fn transform_buffers(&self) -> AtomicRef { @@ -211,10 +225,10 @@ impl Node for ShadowMapsPass { Some(SlotValue::Sampler(self.atlas_sampler.clone())), ); - node.add_sampler_slot( + node.add_buffer_slot( ShadowMapsPassSlots::ShadowLightUniformsBuffer, SlotAttribute::Output, - Some(SlotValue::Lazy), + Some(SlotValue::Buffer(self.light_uniforms_buffer.clone())), ); node.add_buffer_slot( @@ -230,7 +244,7 @@ impl Node for ShadowMapsPass { &mut self, graph: &mut crate::render::graph::RenderGraph, world: &mut lyra_ecs::World, - _: &mut crate::render::graph::RenderGraphContext, + context: &mut crate::render::graph::RenderGraphContext, ) { self.render_meshes = world.try_get_resource_data::(); self.transform_buffers = world.try_get_resource_data::(); @@ -245,7 +259,7 @@ impl Node for ShadowMapsPass { if !self.depth_maps.contains_key(&entity) { // TODO: dont pack the textures as they're added - let atlas_index = self.create_depth_map(graph.device(), entity, *pos); + let atlas_index = self.create_depth_map(graph.device(), &context.queue, entity, *pos); index_components_queue.push_back((entity, atlas_index)); debug!("Created depth map for {:?} light entity", entity); @@ -253,17 +267,13 @@ impl Node for ShadowMapsPass { } // now consume from the queue adding the components to the entities - while let Some((entity, atlas_id)) = index_components_queue.pop_front() { - world.insert(entity, LightShadowMapId(atlas_id)); + while let Some((entity, depth)) = index_components_queue.pop_front() { + world.insert(entity, LightShadowMapId { + atlas_index: depth.atlas_index, + uniform_index: depth.uniform_index, + }); } - // update the light projection buffer slot - let (_, dir_depth_map) = self.depth_maps.iter().next().unwrap(); - let val = graph - .slot_value_mut(ShadowMapsPassSlots::ShadowLightUniformsBuffer) - .unwrap(); - *val = SlotValue::Buffer(dir_depth_map.light_projection_buffer.clone()); - if self.pipeline.is_none() { let shader = Rc::new(Shader { label: Some("shader_shadows".into()), @@ -348,6 +358,7 @@ impl Node for ShadowMapsPass { }); pass.set_pipeline(&pipeline); let viewport = atlas.texture_viewport(dir_depth_map.atlas_index); + debug!("Rendering shadow map to viewport: {viewport:?}, uniform index: {}", dir_depth_map.uniform_index); // only render to the light's map in the atlas pass.set_viewport(viewport.offset.x as _, viewport.offset.y as _, viewport.size.x as _, viewport.size.y as _, 0.0, 1.0); // only clear the light map in the atlas @@ -362,7 +373,9 @@ impl Node for ShadowMapsPass { } let buffers = buffers.unwrap(); - pass.set_bind_group(0, &dir_depth_map.bindgroup, &[]); + let uniform_index = uniform_index_offset(&context.device.limits(), dir_depth_map.uniform_index); + //debug!("Uniform offset: {uniform_index}"); + pass.set_bind_group(0, &self.uniforms_bg, &[uniform_index]); // Get the bindgroup for job's transform and bind to it using an offset. let bindgroup = transforms.bind_group(job.transform_id); @@ -395,7 +408,7 @@ impl Node for ShadowMapsPass { #[repr(C)] #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct LightShadowUniform { +pub struct LightShadowUniform { space_mat: glam::Mat4, atlas_frame: AtlasViewport, // 2xUVec2 (4xf32), so no padding needed } @@ -405,13 +418,18 @@ struct LightShadowUniform { /// 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(u64); +pub struct LightShadowMapId { + atlas_index: u64, + uniform_index: u64, +} -impl Deref for LightShadowMapId { - type Target = u64; +impl LightShadowMapId { + pub fn atlas_index(&self) -> u64 { + self.atlas_index + } - fn deref(&self) -> &Self::Target { - &self.0 + pub fn uniform_index(&self) -> u64 { + self.uniform_index } } @@ -427,4 +445,9 @@ impl LightShadowMapAtlas { pub fn get_mut(&self) -> RwLockWriteGuard { self.0.write().unwrap() } +} + +fn uniform_index_offset(limits: &wgpu::Limits, uniform_idx: u64) -> u32 { + let t = uniform_idx as u32 % (limits.max_storage_buffer_binding_size / mem::size_of::() as u32); + t * limits.min_uniform_buffer_offset_alignment } \ No newline at end of file diff --git a/lyra-game/src/render/light/mod.rs b/lyra-game/src/render/light/mod.rs index 94377ed..cc82f24 100644 --- a/lyra-game/src/render/light/mod.rs +++ b/lyra-game/src/render/light/mod.rs @@ -12,6 +12,8 @@ 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. @@ -166,18 +168,21 @@ 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); } @@ -216,10 +221,11 @@ pub(crate) struct LightUniform { pub spot_cutoff_rad: f32, pub spot_outer_cutoff_rad: f32, + pub light_shadow_uniform_index: i32, } 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 +239,12 @@ impl LightUniform { spot_cutoff_rad: 0.0, spot_outer_cutoff_rad: 0.0, + light_shadow_uniform_index: map_id.map(|m| m.uniform_index() as i32).unwrap_or(-1), } } - 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 +258,12 @@ impl LightUniform { spot_cutoff_rad: 0.0, spot_outer_cutoff_rad: 0.0, + light_shadow_uniform_index: map_id.map(|m| m.uniform_index() as i32).unwrap_or(-1), } } // 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,6 +277,7 @@ 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() as i32).unwrap_or(-1), } } } diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 8d3bb19..0618934 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -54,6 +54,7 @@ struct Light { spot_cutoff: f32, spot_outer_cutoff: f32, + light_shadow_uniform_index: i32, }; struct Lights { @@ -85,9 +86,7 @@ fn vs_main( // 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, ); - - out.frag_pos_light_space = u_light_shadow.light_space_matrix * world_position; + out.world_normal = normalize(normal_mat * model.normal); return out; } @@ -114,10 +113,15 @@ struct LightShadowMapUniform { atlas_frame: TextureAtlasFrame, } +struct LightShadowMapUniformAligned { + @size(256) + inner: LightShadowMapUniform +} + @group(4) @binding(0) var u_light_indices: array; @group(4) @binding(1) -var t_light_grid: texture_storage_2d; // vec2 +var t_light_grid: texture_storage_2d; // rg32uint = vec2 @group(5) @binding(0) var t_shadow_maps_atlas: texture_depth_2d; @@ -126,7 +130,7 @@ var s_shadow_maps_atlas: sampler; @group(5) @binding(2) var u_shadow_maps_atlas_size: vec2; @group(5) @binding(3) -var u_light_shadow: LightShadowMapUniform; +var u_light_shadow: array; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { @@ -148,13 +152,18 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let light_offset = tile.x; let light_count = tile.y; + let atlas_dimensions: vec2 = 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]; if (light.light_ty == LIGHT_TY_DIRECTIONAL) { let light_dir = normalize(-light.direction); - let shadow = calc_shadow(in.world_normal, light_dir, in.frag_pos_light_space, u_light_shadow.atlas_frame); + let shadow_u: LightShadowMapUniform = u_light_shadow[light.light_shadow_uniform_index].inner; + let frag_pos_light_space = shadow_u.light_space_matrix * vec4(in.world_position, 1.0); + + let shadow = calc_shadow(in.world_normal, light_dir, frag_pos_light_space, atlas_dimensions, shadow_u.atlas_frame); 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); @@ -167,7 +176,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { return vec4(light_object_res, object_color.a); } -fn calc_shadow(normal: vec3, light_dir: vec3, frag_pos_light_space: vec4, atlas_region: TextureAtlasFrame) -> f32 { +fn calc_shadow(normal: vec3, light_dir: vec3, frag_pos_light_space: vec4, atlas_dimensions: vec2, atlas_region: TextureAtlasFrame) -> f32 { var proj_coords = frag_pos_light_space.xyz / frag_pos_light_space.w; // for some reason the y component is clipped after transforming proj_coords.y = -proj_coords.y; @@ -181,8 +190,8 @@ fn calc_shadow(normal: vec3, light_dir: vec3, frag_pos_light_space: ve let xy_remapped = proj_coords.xy * 0.5 + 0.5; // no need to get the y since the maps are square - let atlas_start = f32(atlas_region.offset.x) / f32(u_shadow_maps_atlas_size.x); - let atlas_end = f32(atlas_region.offset.x + atlas_region.size.x) / f32(u_shadow_maps_atlas_size.x); + let atlas_start = f32(atlas_region.offset.x) / f32(atlas_dimensions.x); + let atlas_end = f32(atlas_region.offset.x + atlas_region.size.x) / f32(atlas_dimensions.x); // lerp the tex coords to the shadow map for this light. proj_coords.x = mix(atlas_start, atlas_end, xy_remapped.x); proj_coords.y = mix(atlas_start, atlas_end, xy_remapped.y); @@ -195,7 +204,7 @@ fn calc_shadow(normal: vec3, light_dir: vec3, frag_pos_light_space: ve // must manually apply offset to the texture coords since `textureSampleLevel` requires a // const value. - let offset_coords = proj_coords.xy + (vec2(atlas_region.offset) / vec2(u_shadow_maps_atlas_size)); + let offset_coords = proj_coords.xy + (vec2(atlas_region.offset) / vec2(atlas_dimensions)); let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, offset_coords, 0.0); let current_depth = proj_coords.z; diff --git a/lyra-game/src/render/shaders/shadows.wgsl b/lyra-game/src/render/shaders/shadows.wgsl index 5f1a0c5..8ea73d3 100644 --- a/lyra-game/src/render/shaders/shadows.wgsl +++ b/lyra-game/src/render/shaders/shadows.wgsl @@ -14,7 +14,7 @@ struct LightShadowMapUniform { } @group(0) @binding(0) -var u_light_shadow: LightShadowMapUniform; +var u_light_shadow: LightShadowMapUniform; @group(1) @binding(0) var u_model_transform_data: TransformData; From 87aa4406910bdd782ab97011ce5735e9f4ecce88 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Thu, 11 Jul 2024 20:00:46 -0400 Subject: [PATCH 12/28] render: create a GpuSlotBuffer for stable indices in a gpu buffer --- Cargo.lock | 25 +++ lyra-game/Cargo.toml | 1 + lyra-game/src/render/graph/passes/shadows.rs | 152 ++++++++++++------- lyra-game/src/render/mod.rs | 5 +- lyra-game/src/render/slot_buffer.rs | 150 ++++++++++++++++++ 5 files changed, 273 insertions(+), 60 deletions(-) create mode 100644 lyra-game/src/render/slot_buffer.rs diff --git a/Cargo.lock b/Cargo.lock index b453320..c0ae382 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1882,6 +1882,7 @@ dependencies = [ "petgraph", "quote", "rectangle-pack", + "round_mult", "rustc-hash", "syn 2.0.51", "thiserror", @@ -2891,6 +2892,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" @@ -2903,6 +2913,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" @@ -3017,6 +3036,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" diff --git a/lyra-game/Cargo.toml b/lyra-game/Cargo.toml index 5420015..c5bd26f 100644 --- a/lyra-game/Cargo.toml +++ b/lyra-game/Cargo.toml @@ -39,6 +39,7 @@ rustc-hash = "1.1.0" petgraph = { version = "0.6.5", features = ["matrix_graph"] } bind_match = "0.1.2" rectangle-pack = "0.4.2" +round_mult = "0.1.3" [features] tracy = ["dep:tracing-tracy"] diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index 4dcdafa..9fe0e6f 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -1,7 +1,14 @@ -use std::{collections::VecDeque, mem, num::NonZeroU64, rc::Rc, sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}}; +use std::{ + collections::VecDeque, + mem, + num::NonZeroU64, + rc::Rc, + sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}, +}; use lyra_ecs::{ - query::{filter::Has, Entities}, AtomicRef, Component, Entity, ResourceData + query::{filter::Has, Entities}, + AtomicRef, Component, Entity, ResourceData, }; use lyra_game_derive::RenderGraphLabel; use lyra_math::Transform; @@ -10,7 +17,12 @@ use tracing::{debug, warn}; use wgpu::util::DeviceExt; use crate::render::{ - graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, light::directional::DirectionalLight, resource::{RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, transform_buffer_storage::TransformBuffers, vertex::Vertex, AtlasViewport, TextureAtlas + graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, + light::directional::DirectionalLight, + resource::{RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, + transform_buffer_storage::TransformBuffers, + vertex::Vertex, + AtlasViewport, GpuSlotBuffer, TextureAtlas, }; use super::{MeshBufferStorage, RenderAssets, RenderMeshes}; @@ -40,8 +52,7 @@ struct LightDepthMap { pub struct ShadowMapsPass { bgl: Arc, atlas_size_buffer: Arc, - light_uniforms_buffer: Arc, - light_uniforms_index: u64, + light_uniforms_buffer: GpuSlotBuffer, uniforms_bg: Arc, /// depth maps for a light owned by an entity. depth_maps: FxHashMap, @@ -63,20 +74,18 @@ impl ShadowMapsPass { 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: true, - min_binding_size: Some( - NonZeroU64::new(mem::size_of::() as _).unwrap(), - ), - }, - count: None, - } - ], + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: true, + min_binding_size: Some( + NonZeroU64::new(mem::size_of::() as _).unwrap(), + ), + }, + count: None, + }], }), ); @@ -87,12 +96,11 @@ impl ShadowMapsPass { SHADOW_SIZE * 4, ); - 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 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 = device.create_sampler(&wgpu::SamplerDescriptor { label: Some("sampler_shadow_map_atlas"), @@ -106,13 +114,15 @@ impl ShadowMapsPass { ..Default::default() }); - let uniforms_buffer = - device.create_buffer(&wgpu::BufferDescriptor { - label: Some("buffer_shadow_maps_light"), - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - size: device.limits().max_storage_buffer_binding_size as u64, - mapped_at_creation: false, - }); + let cap = device.limits().max_storage_buffer_binding_size as u64 + / mem::size_of::() as u64; + let uniforms_buffer = GpuSlotBuffer::new_aligned( + device, + Some("buffer_shadow_maps_light"), + wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + cap, + 256, + ); let uniforms_bg = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("bind_group_shadows"), @@ -120,7 +130,7 @@ impl ShadowMapsPass { entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &uniforms_buffer, + buffer: uniforms_buffer.buffer(), offset: 0, size: Some(NonZeroU64::new(mem::size_of::() as _).unwrap()), }), @@ -129,8 +139,7 @@ impl ShadowMapsPass { Self { bgl, - light_uniforms_buffer: Arc::new(uniforms_buffer), - light_uniforms_index: 0, + light_uniforms_buffer: uniforms_buffer, uniforms_bg: Arc::new(uniforms_bg), atlas_size_buffer: Arc::new(atlas_size_buffer), depth_maps: Default::default(), @@ -145,12 +154,18 @@ impl ShadowMapsPass { } /// Create a depth map and return the id of the depth map in the texture atlas. - fn create_depth_map(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, entity: Entity, light_pos: Transform) -> LightDepthMap { + fn create_depth_map( + &mut self, + queue: &wgpu::Queue, + entity: Entity, + light_pos: Transform, + ) -> LightDepthMap { const NEAR_PLANE: f32 = 0.1; const FAR_PLANE: f32 = 45.0; let mut atlas = self.atlas.get_mut(); - let atlas_index = atlas.pack_new_texture(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _) + let atlas_index = atlas + .pack_new_texture(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _) .expect("failed to pack new shadow map into texture atlas"); let atlas_frame = atlas.texture_viewport(atlas_index); @@ -166,21 +181,19 @@ impl ShadowMapsPass { atlas_frame, }; - let uniform_index = self.light_uniforms_index; + /* let uniform_index = self.light_uniforms_index; self.light_uniforms_index += 1; //self.light_uniforms_buffer let offset = uniform_index_offset(&device.limits(), uniform_index); - queue.write_buffer(&self.light_uniforms_buffer, offset as u64, bytemuck::bytes_of(&uniform)); + queue.write_buffer(&self.light_uniforms_buffer, offset as u64, bytemuck::bytes_of(&uniform)); */ + let uniform_index = self.light_uniforms_buffer.insert(queue, &uniform); let v = LightDepthMap { atlas_index, uniform_index, }; - self.depth_maps.insert( - entity, - v, - ); + self.depth_maps.insert(entity, v); v } @@ -228,7 +241,9 @@ impl Node for ShadowMapsPass { node.add_buffer_slot( ShadowMapsPassSlots::ShadowLightUniformsBuffer, SlotAttribute::Output, - Some(SlotValue::Buffer(self.light_uniforms_buffer.clone())), + Some(SlotValue::Buffer( + self.light_uniforms_buffer.buffer().clone(), + )), ); node.add_buffer_slot( @@ -257,21 +272,24 @@ impl Node for ShadowMapsPass { for (entity, pos, _) in world.view_iter::<(Entities, &Transform, Has)>() { if !self.depth_maps.contains_key(&entity) { - // TODO: dont pack the textures as they're added - let atlas_index = self.create_depth_map(graph.device(), &context.queue, entity, *pos); + let atlas_index = + self.create_depth_map(&context.queue, entity, *pos); index_components_queue.push_back((entity, atlas_index)); debug!("Created depth map for {:?} light entity", entity); } } - + // 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_index: depth.uniform_index, - }); + world.insert( + entity, + LightShadowMapId { + atlas_index: depth.atlas_index, + uniform_index: depth.uniform_index, + }, + ); } if self.pipeline.is_none() { @@ -358,11 +376,26 @@ impl Node for ShadowMapsPass { }); pass.set_pipeline(&pipeline); let viewport = atlas.texture_viewport(dir_depth_map.atlas_index); - debug!("Rendering shadow map to viewport: {viewport:?}, uniform index: {}", dir_depth_map.uniform_index); + debug!( + "Rendering shadow map to viewport: {viewport:?}, uniform index: {}", + dir_depth_map.uniform_index + ); // only render to the light's map in the atlas - pass.set_viewport(viewport.offset.x as _, viewport.offset.y as _, viewport.size.x as _, viewport.size.y as _, 0.0, 1.0); + pass.set_viewport( + viewport.offset.x as _, + viewport.offset.y as _, + viewport.size.x as _, + viewport.size.y as _, + 0.0, + 1.0, + ); // only clear the light map in the atlas - pass.set_scissor_rect(viewport.offset.x, viewport.offset.y, viewport.size.x, viewport.size.y); + pass.set_scissor_rect( + viewport.offset.x, + viewport.offset.y, + viewport.size.x, + viewport.size.y, + ); for job in render_meshes.iter() { // get the mesh (containing vertices) and the buffers from storage @@ -373,8 +406,9 @@ impl Node for ShadowMapsPass { } let buffers = buffers.unwrap(); - let uniform_index = uniform_index_offset(&context.device.limits(), dir_depth_map.uniform_index); - //debug!("Uniform offset: {uniform_index}"); + let uniform_index = + self.light_uniforms_buffer + .offset_of(dir_depth_map.uniform_index) as u32; pass.set_bind_group(0, &self.uniforms_bg, &[uniform_index]); // Get the bindgroup for job's transform and bind to it using an offset. @@ -414,7 +448,7 @@ pub struct LightShadowUniform { } /// 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)] @@ -447,7 +481,7 @@ impl LightShadowMapAtlas { } } -fn uniform_index_offset(limits: &wgpu::Limits, uniform_idx: u64) -> u32 { +/* fn uniform_index_offset(limits: &wgpu::Limits, uniform_idx: u64) -> u32 { let t = uniform_idx as u32 % (limits.max_storage_buffer_binding_size / mem::size_of::() as u32); t * limits.min_uniform_buffer_offset_alignment -} \ No newline at end of file +} */ diff --git a/lyra-game/src/render/mod.rs b/lyra-game/src/render/mod.rs index 641a6e0..11341f9 100755 --- a/lyra-game/src/render/mod.rs +++ b/lyra-game/src/render/mod.rs @@ -17,4 +17,7 @@ pub mod avec; pub mod graph; mod texture_atlas; -pub use texture_atlas::*; \ No newline at end of file +pub use texture_atlas::*; + +mod slot_buffer; +pub use slot_buffer::*; \ 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..9bc1264 --- /dev/null +++ b/lyra-game/src/render/slot_buffer.rs @@ -0,0 +1,150 @@ +use std::{collections::VecDeque, marker::PhantomData, mem, num::NonZeroU64, 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 { + let offset = i * mem::size_of::() as u64; + + if let Some(align) = self.alignment { + round_mult::up(offset, NonZeroU64::new(align).unwrap()).unwrap() + } else { + offset + } + } + + /// 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 From 40fa9c09da8e21a8ad2678f81f0257e92fcbe9be Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Fri, 12 Jul 2024 14:58:18 -0400 Subject: [PATCH 13/28] render: fix shadow map atlas packing by writing my own skyline packer --- .vscode/launch.json | 18 + Cargo.lock | 7 - Cargo.toml | 2 +- examples/shadows/src/main.rs | 16 +- lyra-game/Cargo.toml | 1 - lyra-game/src/render/graph/passes/shadows.rs | 398 +++++++++++++----- lyra-game/src/render/light/mod.rs | 195 ++++++--- lyra-game/src/render/shaders/base.wgsl | 14 +- .../src/render/shaders/light_cull.comp.wgsl | 1 + lyra-game/src/render/slot_buffer.rs | 11 +- lyra-game/src/render/texture_atlas.rs | 310 +++++++++----- 11 files changed, 687 insertions(+), 286 deletions(-) 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 c0ae382..c988b5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1881,7 +1881,6 @@ dependencies = [ "lyra-scene", "petgraph", "quote", - "rectangle-pack", "round_mult", "rustc-hash", "syn 2.0.51", @@ -2769,12 +2768,6 @@ dependencies = [ "rand_core 0.3.1", ] -[[package]] -name = "rectangle-pack" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d463f2884048e7153449a55166f91028d5b0ea53c79377099ce4e8cf0cf9bb" - [[package]] name = "redox_syscall" version = "0.3.5" diff --git a/Cargo.toml b/Cargo.toml index 737e1ef..1850380 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,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/shadows/src/main.rs b/examples/shadows/src/main.rs index 3e5c478..70650a8 100644 --- a/examples/shadows/src/main.rs +++ b/examples/shadows/src/main.rs @@ -6,7 +6,7 @@ use lyra_engine::{ InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput, }, math::{self, Transform, Vec3}, - render::light::directional::DirectionalLight, + render::light::{directional::DirectionalLight, PointLight}, scene::{ CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform, ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN, @@ -130,12 +130,14 @@ fn setup_scene_plugin(game: &mut Game) { drop(resman); + // cube in the air world.spawn(( cube_mesh.clone(), WorldTransform::default(), Transform::from_xyz(0.0, -2.0, -5.0), )); + // cube on the right, on the ground world.spawn(( cube_mesh.clone(), WorldTransform::default(), @@ -163,6 +165,18 @@ fn setup_scene_plugin(game: &mut Game) { }, light_tran, )); + + world.spawn(( + cube_mesh.clone(), + PointLight { + enabled: true, + color: Vec3::new(0.133, 0.098, 0.91), + intensity: 2.0, + range: 9.0, + ..Default::default() + }, + Transform::from_xyz(5.0, -2.5, -3.3), + )); } let mut camera = CameraComponent::new_3d(); diff --git a/lyra-game/Cargo.toml b/lyra-game/Cargo.toml index c5bd26f..7c0d0d2 100644 --- a/lyra-game/Cargo.toml +++ b/lyra-game/Cargo.toml @@ -38,7 +38,6 @@ unique = "0.9.1" rustc-hash = "1.1.0" petgraph = { version = "0.6.5", features = ["matrix_graph"] } bind_match = "0.1.2" -rectangle-pack = "0.4.2" round_mult = "0.1.3" [features] diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index 9fe0e6f..fdd6a4c 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -11,18 +11,18 @@ use lyra_ecs::{ AtomicRef, Component, Entity, ResourceData, }; use lyra_game_derive::RenderGraphLabel; -use lyra_math::Transform; +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, + light::{directional::DirectionalLight, LightType, PointLight}, resource::{RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, transform_buffer_storage::TransformBuffers, vertex::Vertex, - AtlasViewport, GpuSlotBuffer, TextureAtlas, + AtlasFrame, GpuSlotBuffer, TextureAtlas, }; use super::{MeshBufferStorage, RenderAssets, RenderMeshes}; @@ -43,10 +43,15 @@ pub struct ShadowMapsPassLabel; #[derive(Clone, Copy)] struct LightDepthMap { - //light_projection_buffer: Arc, - //bindgroup: wgpu::BindGroup, + /// 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, - uniform_index: u64, + /// The index of the uniform for the light in the uniform array. + uniform_index: [u64; 6], } pub struct ShadowMapsPass { @@ -157,6 +162,7 @@ impl ShadowMapsPass { fn create_depth_map( &mut self, queue: &wgpu::Queue, + light_type: LightType, entity: Entity, light_pos: Transform, ) -> LightDepthMap { @@ -164,34 +170,136 @@ impl ShadowMapsPass { const FAR_PLANE: f32 = 45.0; let mut atlas = self.atlas.get_mut(); - let atlas_index = atlas - .pack_new_texture(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _) - .expect("failed to pack new shadow map into texture atlas"); - let atlas_frame = atlas.texture_viewport(atlas_index); - let ortho_proj = - glam::Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, NEAR_PLANE, FAR_PLANE); + let (start_atlas_idx, uniform_indices) = match light_type { + LightType::Directional => { + // directional lights require a single map, so allocate that in the atlas. + let atlas_index = atlas + .pack(SHADOW_SIZE.x as _, SHADOW_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 look_view = - glam::Mat4::look_to_rh(light_pos.translation, light_pos.forward(), light_pos.up()); + let projection = + glam::Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, NEAR_PLANE, FAR_PLANE); + let look_view = glam::Mat4::look_to_rh( + light_pos.translation, + light_pos.forward(), + light_pos.up(), + ); - let light_proj = ortho_proj * look_view; - let uniform = LightShadowUniform { - space_mat: light_proj, - atlas_frame, + let light_proj = projection * look_view; + + let u = LightShadowUniform { + space_mat: light_proj, + atlas_frame, + }; + + let uniform_index = self.light_uniforms_buffer.insert(queue, &u); + let mut indices = [0; 6]; + indices[0] = uniform_index; + (atlas_index, indices) + } + LightType::Spotlight => todo!(), + 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, + NEAR_PLANE, + FAR_PLANE, + ); + + let light_trans = light_pos.translation; + 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], + }, + ); + indices[i] = uniform_i; + } + + (atlas_idx_1, indices) + } }; - /* let uniform_index = self.light_uniforms_index; - self.light_uniforms_index += 1; - - //self.light_uniforms_buffer - let offset = uniform_index_offset(&device.limits(), uniform_index); - queue.write_buffer(&self.light_uniforms_buffer, offset as u64, bytemuck::bytes_of(&uniform)); */ - let uniform_index = self.light_uniforms_buffer.insert(queue, &uniform); - let v = LightDepthMap { - atlas_index, - uniform_index, + light_type, + atlas_index: start_atlas_idx, + uniform_index: uniform_indices, }; self.depth_maps.insert(entity, v); @@ -270,14 +378,40 @@ impl Node for ShadowMapsPass { // 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, (has_dir, has_point)) in world.view_iter::<(Entities, &Transform, Or, Has>)>() { + if !self.depth_maps.contains_key(&entity) { + let light_type = if has_dir.is_some() { + LightType::Directional + } else if has_point.is_some() { + LightType::Point + } else { + todo!("Spot lights") + }; + + debug!("Creating depth map for {light_type:?}"); + + // TODO: dont pack the textures as they're added + let atlas_index = + self.create_depth_map(&context.queue, light_type, entity, *pos); + index_components_queue.push_back((entity, atlas_index)); + } + } */ + for (entity, pos, _) in world.view_iter::<(Entities, &Transform, Has)>() { if !self.depth_maps.contains_key(&entity) { // TODO: dont pack the textures as they're added let atlas_index = - self.create_depth_map(&context.queue, entity, *pos); + self.create_depth_map(&context.queue, LightType::Directional, entity, *pos); index_components_queue.push_back((entity, atlas_index)); + } + } - debug!("Created depth map for {:?} light entity", entity); + for (entity, pos, _) in world.view_iter::<(Entities, &Transform, Has)>() { + if !self.depth_maps.contains_key(&entity) { + // TODO: dont pack the textures as they're added + let atlas_index = + self.create_depth_map(&context.queue, LightType::Point, entity, *pos); + index_components_queue.push_back((entity, atlas_index)); } } @@ -287,7 +421,7 @@ impl Node for ShadowMapsPass { entity, LightShadowMapId { atlas_index: depth.atlas_index, - uniform_index: depth.uniform_index, + uniform_indices: depth.uniform_index, }, ); } @@ -332,7 +466,6 @@ impl Node for ShadowMapsPass { multiview: None, }, )); - /* */ } } @@ -349,102 +482,137 @@ impl Node for ShadowMapsPass { let mesh_buffers = self.mesh_buffers(); let transforms = self.transform_buffers(); - debug_assert_eq!( - self.depth_maps.len(), - 1, - "shadows map pass only supports 1 light" - ); - let (_, dir_depth_map) = self - .depth_maps - .iter() - .next() - .expect("missing directional light in scene"); - - { - 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, + 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, }), - }); - pass.set_pipeline(&pipeline); - let viewport = atlas.texture_viewport(dir_depth_map.atlas_index); - debug!( - "Rendering shadow map to viewport: {viewport:?}, uniform index: {}", - dir_depth_map.uniform_index - ); - // only render to the light's map in the atlas - pass.set_viewport( - viewport.offset.x as _, - viewport.offset.y as _, - viewport.size.x as _, - viewport.size.y as _, - 0.0, - 1.0, - ); - // only clear the light map in the atlas - pass.set_scissor_rect( - viewport.offset.x, - viewport.offset.y, - viewport.size.x, - viewport.size.y, - ); + stencil_ops: None, + }), + }); + pass.set_pipeline(&pipeline); - 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(); + for light_depth_map in self.depth_maps.values() { - let uniform_index = - self.light_uniforms_buffer - .offset_of(dir_depth_map.uniform_index) as u32; - pass.set_bind_group(0, &self.uniforms_bg, &[uniform_index]); + match light_depth_map.light_type { + LightType::Directional => { + let frame = atlas.texture_frame(light_depth_map.atlas_index) + .expect("missing atlas frame of light"); + let u_offset = self.light_uniforms_buffer.offset_of(light_depth_map.uniform_index[0]) as u32; - // 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]); + //debug!("Rendering directional light with atlas {} uniform index {} and offset {}, in viewport {:?}", light_depth_map.atlas_index, light_depth_map.uniform_index[0], u_offset, frame); - // 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(..), + light_shadow_pass_impl( + &mut pass, + &self.uniforms_bg, + &render_meshes, + &mesh_buffers, + &transforms, + &frame, + u_offset, ); - 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(); + }, + LightType::Point => { + 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]; + let u_offset = self.light_uniforms_buffer.offset_of(ui) as u32; + + //debug!("Rendering point light side {side} with atlas {} uniform index {ui} and offset {u_offset} and viewport {:?}", light_depth_map.atlas_index + side, frame); - pass.set_vertex_buffer( - buffers.buffer_vertex.slot(), - buffers.buffer_vertex.buffer().slice(..), - ); - pass.draw(0..vertex_count as u32, 0..1); - } + light_shadow_pass_impl( + &mut pass, + &self.uniforms_bg, + &render_meshes, + &mesh_buffers, + &transforms, + &frame, + u_offset, + ); + } + }, + LightType::Spotlight => todo!(), } } } } +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_offset: 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(); + + //let uniform_index = light_uniforms_buffer.offset_of(light_depth_map.uniform_index[0]) as u32; + pass.set_bind_group(0, &uniforms_bind_group, &[uniform_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]); + + // if this mesh uses indices, use them to draw the mesh + if let Some((idx_type, indices)) = buffers.buffer_indices.as_ref() { + let indices_len = indices.count() as u32; + + pass.set_vertex_buffer( + buffers.buffer_vertex.slot(), + buffers.buffer_vertex.buffer().slice(..), + ); + pass.set_index_buffer(indices.buffer().slice(..), *idx_type); + pass.draw_indexed(0..indices_len, 0, 0..1); + } else { + let vertex_count = buffers.buffer_vertex.count(); + + pass.set_vertex_buffer( + buffers.buffer_vertex.slot(), + buffers.buffer_vertex.buffer().slice(..), + ); + pass.draw(0..vertex_count as u32, 0..1); + } + } +} + #[repr(C)] #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] pub struct LightShadowUniform { space_mat: glam::Mat4, - atlas_frame: AtlasViewport, // 2xUVec2 (4xf32), so no padding needed + atlas_frame: AtlasFrame, // 2xUVec2 (4xf32), so no padding needed } /// A component that stores the ID of a shadow map in the shadow map atlas for the entities. @@ -454,7 +622,7 @@ pub struct LightShadowUniform { #[derive(Debug, Default, Copy, Clone, Component)] pub struct LightShadowMapId { atlas_index: u64, - uniform_index: u64, + uniform_indices: [u64; 6], } impl LightShadowMapId { @@ -462,8 +630,8 @@ impl LightShadowMapId { self.atlas_index } - pub fn uniform_index(&self) -> u64 { - self.uniform_index + pub fn uniform_index(&self, side: usize) -> u64 { + self.uniform_indices[side] } } diff --git a/lyra-game/src/render/light/mod.rs b/lyra-game/src/render/light/mod.rs index cc82f24..b744a8a 100644 --- a/lyra-game/src/render/light/mod.rs +++ b/lyra-game/src/render/light/mod.rs @@ -1,12 +1,17 @@ -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, sync::Arc}; +use std::{ + collections::{HashMap, VecDeque}, + marker::PhantomData, + mem, + sync::Arc, +}; use crate::math::Transform; @@ -22,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. @@ -49,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 => { @@ -69,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 { @@ -86,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; @@ -112,47 +138,37 @@ 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"), }); @@ -168,21 +184,30 @@ impl LightUniformBuffers { let _ = world_tick; let mut lights = vec![]; - for (point_light, transform, shadow_map_id) in world.view_iter::<(&PointLight, &Transform, Option<&LightShadowMapId>)>() { + 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); + let uniform = + LightUniform::from_point_light_bundle(&point_light, &transform, shadow_map_id); lights.push(uniform); } - for (spot_light, transform, shadow_map_id) in world.view_iter::<(&SpotLight, &Transform, Option<&LightShadowMapId>)>() { + 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); + let uniform = + LightUniform::from_spot_light_bundle(&spot_light, &transform, shadow_map_id); lights.push(uniform); } - for (dir_light, transform, shadow_map_id) in world.view_iter::<(&DirectionalLight, &Transform, Option<&LightShadowMapId>)>() { + 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); + let uniform = + LightUniform::from_directional_bundle(&dir_light, &transform, shadow_map_id); lights.push(uniform); } @@ -191,7 +216,11 @@ impl LightUniformBuffers { // 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()), + ); } } @@ -214,18 +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, + pub light_shadow_uniform_index: [i32; 6], + _padding: [u32; 2], } impl LightUniform { - pub fn from_point_light_bundle(light: &PointLight, transform: &Transform, map_id: Option) -> 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, @@ -239,12 +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() as i32).unwrap_or(-1), - + 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, map_id: Option) -> 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, @@ -258,12 +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() as i32).unwrap_or(-1), + 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, map_id: Option) -> 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, @@ -277,8 +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() as i32).unwrap_or(-1), + 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/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 0618934..9e441ec 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -54,7 +54,7 @@ struct Light { spot_cutoff: f32, spot_outer_cutoff: f32, - light_shadow_uniform_index: i32, + light_shadow_uniform_index: array, }; struct Lights { @@ -114,7 +114,7 @@ struct LightShadowMapUniform { } struct LightShadowMapUniformAligned { - @size(256) + @align(256) inner: LightShadowMapUniform } @@ -160,10 +160,10 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { if (light.light_ty == LIGHT_TY_DIRECTIONAL) { let light_dir = normalize(-light.direction); - let shadow_u: LightShadowMapUniform = u_light_shadow[light.light_shadow_uniform_index].inner; + let shadow_u: LightShadowMapUniform = u_light_shadow[light.light_shadow_uniform_index[0]].inner; let frag_pos_light_space = shadow_u.light_space_matrix * vec4(in.world_position, 1.0); - let shadow = calc_shadow(in.world_normal, light_dir, frag_pos_light_space, atlas_dimensions, shadow_u.atlas_frame); + let shadow = calc_shadow_dir_light(in.world_normal, light_dir, frag_pos_light_space, atlas_dimensions, shadow_u.atlas_frame); 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); @@ -176,7 +176,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { return vec4(light_object_res, object_color.a); } -fn calc_shadow(normal: vec3, light_dir: vec3, frag_pos_light_space: vec4, atlas_dimensions: vec2, atlas_region: TextureAtlasFrame) -> f32 { +fn calc_shadow_dir_light(normal: vec3, light_dir: vec3, frag_pos_light_space: vec4, atlas_dimensions: vec2, atlas_region: TextureAtlasFrame) -> f32 { var proj_coords = frag_pos_light_space.xyz / frag_pos_light_space.w; // for some reason the y component is clipped after transforming proj_coords.y = -proj_coords.y; @@ -218,6 +218,10 @@ fn calc_shadow(normal: vec3, light_dir: vec3, frag_pos_light_space: ve return shadow; } +fn calc_shadow_point(world_pos: vec3, atlas_dimensions: vec2, atlas_regions: array) -> f32 { + return 0.0; +} + 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)); 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/slot_buffer.rs b/lyra-game/src/render/slot_buffer.rs index 9bc1264..be830ce 100644 --- a/lyra-game/src/render/slot_buffer.rs +++ b/lyra-game/src/render/slot_buffer.rs @@ -1,4 +1,4 @@ -use std::{collections::VecDeque, marker::PhantomData, mem, num::NonZeroU64, sync::Arc}; +use std::{collections::VecDeque, marker::PhantomData, mem, sync::Arc}; /// A buffer on the GPU that has persistent indices. /// @@ -54,12 +54,19 @@ impl GpuSlotBuffer { /// Calculates the byte offset in the buffer of the element at `i`. pub fn offset_of(&self, i: u64) -> u64 { - let offset = i * mem::size_of::() as u64; + /* let offset = i * mem::size_of::() as u64; if let Some(align) = self.alignment { round_mult::up(offset, NonZeroU64::new(align).unwrap()).unwrap() } else { offset + } */ + + if let Some(align) = self.alignment { + let transform_index = i % self.capacity; + transform_index * align + } else { + mem::size_of::() as u64 } } diff --git a/lyra-game/src/render/texture_atlas.rs b/lyra-game/src/render/texture_atlas.rs index 65b0989..f312ac9 100644 --- a/lyra-game/src/render/texture_atlas.rs +++ b/lyra-game/src/render/texture_atlas.rs @@ -1,10 +1,8 @@ use std::{ - collections::BTreeMap, - sync::Arc, + cmp::max, collections::HashMap, sync::Arc }; use glam::UVec2; -use rectangle_pack::{pack_rects, GroupedRectsToPlace, RectToInsert, RectanglePackOk, TargetBin}; #[derive(Debug, thiserror::Error)] pub enum AtlasPackError { @@ -14,28 +12,33 @@ pub enum AtlasPackError { } #[repr(C)] -#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -pub struct AtlasViewport { - pub offset: UVec2, - pub size: UVec2, +#[derive(Debug, Default, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct AtlasFrame { + pub x: u32, + pub y: u32, + pub width: u32, + pub height: u32, } -pub struct TextureAtlas { +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, - /// The next id of the next texture that will be added to the atlas. - next_texture_id: u64, - - rects: GroupedRectsToPlace, - bins: BTreeMap, - placement: Option>, + packer: P, } -impl TextureAtlas { +impl TextureAtlas

{ pub fn new( device: &wgpu::Device, format: wgpu::TextureFormat, @@ -58,98 +61,30 @@ impl TextureAtlas { }); let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - let mut bins = BTreeMap::new(); - // max_depth=1 for 2d - bins.insert(0, TargetBin::new(atlas_size.x, atlas_size.y, 1)); - Self { atlas_size, texture_format: format, texture: Arc::new(texture), view: Arc::new(view), - next_texture_id: 0, - rects: GroupedRectsToPlace::new(), - bins, - placement: None, + 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_new_texture(&mut self, width: u32, height: u32) -> Result { - let id = self.next_texture_id; - self.next_texture_id += 1; + pub fn pack(&mut self, width: u32, height: u32) -> Result { + let id = self.packer.pack(width, height)?; - // for 2d rects, set depth to 1 - let r = RectToInsert::new(width, height, 1); - self.rects.push_rect(id, None, r); - - self.pack_atlas()?; - - Ok(id) - } - - /// Add a new texture and **DO NOT** pack it into the atlas. - /// - ///

- /// - /// The texture will not be packed into the atlas meaning - /// [`TextureAtlas::texture_viewport`] will return `None`. To pack the texture, - /// use [`TextureAtlas::pack_atlas`] or use [`TextureAtlas::pack_new_texture`] - /// when only adding a single texture. - /// - ///
- pub fn add_texture_unpacked(&mut self, width: u32, height: u32) -> Result { - let id = self.next_texture_id; - self.next_texture_id += 1; - - // for 2d rects, set depth to 1 - let r = RectToInsert::new(width, height, 1); - self.rects.push_rect(id, None, r); - - self.pack_atlas()?; - - Ok(id) - } - - /// Pack the textures into the atlas. - pub fn pack_atlas(&mut self) -> Result<(), AtlasPackError> { - let placement = pack_rects( - &self.rects, - &mut self.bins, - &rectangle_pack::volume_heuristic, - &rectangle_pack::contains_smallest_box, - ) - .map_err(|e| match e { - rectangle_pack::RectanglePackError::NotEnoughBinSpace => AtlasPackError::NotEnoughSpace, - })?; - self.placement = Some(placement); - - Ok(()) + Ok(id as u64) } /// Get the viewport of a texture index in the atlas. - pub fn texture_viewport(&self, atlas_index: u64) -> AtlasViewport { - let locations = self.placement.as_ref().unwrap().packed_locations(); - let (bin_id, loc) = locations - .get(&atlas_index) - .expect("atlas index is incorrect"); - debug_assert_eq!(*bin_id, 0, "somehow the texture was put in some other bin"); - - AtlasViewport { - offset: UVec2 { - x: loc.x(), - y: loc.y(), - }, - size: UVec2 { - x: loc.width(), - y: loc.height(), - }, - } + pub fn texture_frame(&self, atlas_index: u64) -> Option { + self.packer.frame(atlas_index as _) } pub fn view(&self) -> &Arc { @@ -164,12 +99,199 @@ impl TextureAtlas { &self.texture_format } - pub fn total_texture_count(&self) -> u64 { - self.next_texture_id // starts at zero, so no need to increment - } - /// 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 _) + { + 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 _); + assert!(skyline.y <= self.size.y as _); + + 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) + } + } +} From b45c2f4fab832d1592ebfa8f716cc561f85e4016 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sat, 13 Jul 2024 00:56:09 -0400 Subject: [PATCH 14/28] render: point light shadows in texture atlas, fix bug with unaligned GpuSlotBuffer --- examples/shadows/src/main.rs | 16 +- lyra-game/src/render/graph/passes/shadows.rs | 127 ++++++++++------ lyra-game/src/render/shaders/base.wgsl | 146 ++++++++++++++++--- lyra-game/src/render/shaders/shadows.wgsl | 39 ++++- lyra-game/src/render/slot_buffer.rs | 10 +- 5 files changed, 258 insertions(+), 80 deletions(-) diff --git a/examples/shadows/src/main.rs b/examples/shadows/src/main.rs index 70650a8..49aa82a 100644 --- a/examples/shadows/src/main.rs +++ b/examples/shadows/src/main.rs @@ -161,13 +161,13 @@ fn setup_scene_plugin(game: &mut Game) { DirectionalLight { enabled: true, color: Vec3::new(1.0, 0.95, 0.9), - intensity: 1.0, + intensity: 0.5, }, light_tran, )); world.spawn(( - cube_mesh.clone(), + //cube_mesh.clone(), PointLight { enabled: true, color: Vec3::new(0.133, 0.098, 0.91), @@ -177,6 +177,18 @@ fn setup_scene_plugin(game: &mut Game) { }, Transform::from_xyz(5.0, -2.5, -3.3), )); + + world.spawn(( + //cube_mesh.clone(), + PointLight { + enabled: true, + color: Vec3::new(0.278, 0.984, 0.0), + intensity: 2.0, + range: 9.0, + ..Default::default() + }, + Transform::from_xyz(-0.5, 2.0, -5.0), + )); } let mut camera = CameraComponent::new_3d(); diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index fdd6a4c..f11b8c1 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -1,7 +1,6 @@ use std::{ collections::VecDeque, mem, - num::NonZeroU64, rc::Rc, sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}, }; @@ -13,13 +12,13 @@ use lyra_ecs::{ use lyra_game_derive::RenderGraphLabel; use lyra_math::{Angle, Transform}; use rustc_hash::FxHashMap; -use tracing::{debug, warn}; +use tracing::warn; use wgpu::util::DeviceExt; use crate::render::{ graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, light::{directional::DirectionalLight, LightType, PointLight}, - resource::{RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, + resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, transform_buffer_storage::TransformBuffers, vertex::Vertex, AtlasFrame, GpuSlotBuffer, TextureAtlas, @@ -68,6 +67,7 @@ pub struct ShadowMapsPass { render_meshes: Option, mesh_buffers: Option, pipeline: Option, + point_light_pipeline: Option, atlas: LightShadowMapAtlas, /// The depth map atlas sampler @@ -84,10 +84,8 @@ impl ShadowMapsPass { visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only: true }, - has_dynamic_offset: true, - min_binding_size: Some( - NonZeroU64::new(mem::size_of::() as _).unwrap(), - ), + has_dynamic_offset: false, + min_binding_size: None, }, count: None, }], @@ -98,7 +96,7 @@ impl ShadowMapsPass { device, wgpu::TextureFormat::Depth32Float, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, - SHADOW_SIZE * 4, + SHADOW_SIZE * 8, ); let atlas_size_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { @@ -121,12 +119,11 @@ impl ShadowMapsPass { let cap = device.limits().max_storage_buffer_binding_size as u64 / mem::size_of::() as u64; - let uniforms_buffer = GpuSlotBuffer::new_aligned( + let uniforms_buffer = GpuSlotBuffer::new( device, Some("buffer_shadow_maps_light"), wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, cap, - 256, ); let uniforms_bg = device.create_bind_group(&wgpu::BindGroupDescriptor { @@ -137,7 +134,7 @@ impl ShadowMapsPass { resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: uniforms_buffer.buffer(), offset: 0, - size: Some(NonZeroU64::new(mem::size_of::() as _).unwrap()), + size: None, }), }], }); @@ -152,6 +149,7 @@ impl ShadowMapsPass { render_meshes: None, mesh_buffers: None, pipeline: None, + point_light_pipeline: None, atlas_sampler: Rc::new(sampler), atlas: LightShadowMapAtlas(Arc::new(RwLock::new(atlas))), @@ -165,6 +163,7 @@ impl ShadowMapsPass { light_type: LightType, entity: Entity, light_pos: Transform, + far_plane: f32, ) -> LightDepthMap { const NEAR_PLANE: f32 = 0.1; const FAR_PLANE: f32 = 45.0; @@ -193,6 +192,11 @@ impl ShadowMapsPass { let u = LightShadowUniform { space_mat: light_proj, atlas_frame, + near_plane: NEAR_PLANE, + far_plane, + _padding1: [0; 2], + light_pos: light_pos.translation, + _padding2: 0, }; let uniform_index = self.light_uniforms_buffer.insert(queue, &u); @@ -207,10 +211,11 @@ impl ShadowMapsPass { Angle::Degrees(90.0).to_radians(), aspect, NEAR_PLANE, - FAR_PLANE, + far_plane, ); let light_trans = light_pos.translation; + // right, left, top, bottom, near, and far let views = [ projection * glam::Mat4::look_at_rh( @@ -287,6 +292,11 @@ impl ShadowMapsPass { &LightShadowUniform { space_mat: views[i], atlas_frame: frames[i], + near_plane: NEAR_PLANE, + far_plane, + _padding1: [0; 2], + light_pos: light_trans, + _padding2: 0, }, ); indices[i] = uniform_i; @@ -380,19 +390,18 @@ impl Node for ShadowMapsPass { /* for (entity, pos, (has_dir, has_point)) in world.view_iter::<(Entities, &Transform, Or, Has>)>() { if !self.depth_maps.contains_key(&entity) { - let light_type = if has_dir.is_some() { - LightType::Directional + // TODO: calculate far plane + let (light_type, far_plane) = if has_dir.is_some() { + (LightType::Directional, 45.0) } else if has_point.is_some() { - LightType::Point + (LightType::Point, 45.0) } else { todo!("Spot lights") }; - debug!("Creating depth map for {light_type:?}"); - // TODO: dont pack the textures as they're added let atlas_index = - self.create_depth_map(&context.queue, light_type, entity, *pos); + self.create_depth_map(&context.queue, light_type, entity, *pos, far_plane); index_components_queue.push_back((entity, atlas_index)); } } */ @@ -401,7 +410,7 @@ impl Node for ShadowMapsPass { if !self.depth_maps.contains_key(&entity) { // TODO: dont pack the textures as they're added let atlas_index = - self.create_depth_map(&context.queue, LightType::Directional, entity, *pos); + self.create_depth_map(&context.queue, LightType::Directional, entity, *pos, 45.0); index_components_queue.push_back((entity, atlas_index)); } } @@ -410,7 +419,7 @@ impl Node for ShadowMapsPass { if !self.depth_maps.contains_key(&entity) { // TODO: dont pack the textures as they're added let atlas_index = - self.create_depth_map(&context.queue, LightType::Point, entity, *pos); + self.create_depth_map(&context.queue, LightType::Point, entity, *pos, 30.0); index_components_queue.push_back((entity, atlas_index)); } } @@ -439,18 +448,14 @@ impl Node for ShadowMapsPass { &graph.device, &RenderPipelineDescriptor { label: Some("pipeline_shadows".into()), - layouts: vec![bgl, transforms], + 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, /* Some(FragmentState { - module: shader, - entry_point: "fs_main".into(), - targets: vec![], - }), */ + fragment: None, depth_stencil: Some(wgpu::DepthStencilState { format: wgpu::TextureFormat::Depth32Float, depth_write_enabled: true, @@ -459,6 +464,40 @@ impl Node for ShadowMapsPass { 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() }, @@ -477,6 +516,7 @@ impl Node for ShadowMapsPass { ) { 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(); @@ -495,17 +535,15 @@ impl Node for ShadowMapsPass { stencil_ops: None, }), }); - pass.set_pipeline(&pipeline); for light_depth_map in self.depth_maps.values() { match light_depth_map.light_type { LightType::Directional => { - let frame = atlas.texture_frame(light_depth_map.atlas_index) - .expect("missing atlas frame of light"); - let u_offset = self.light_uniforms_buffer.offset_of(light_depth_map.uniform_index[0]) as u32; + pass.set_pipeline(&pipeline); - //debug!("Rendering directional light with atlas {} uniform index {} and offset {}, in viewport {:?}", light_depth_map.atlas_index, light_depth_map.uniform_index[0], u_offset, frame); + let frame = atlas.texture_frame(light_depth_map.atlas_index) + .expect("missing atlas frame for light"); light_shadow_pass_impl( &mut pass, @@ -514,17 +552,16 @@ impl Node for ShadowMapsPass { &mesh_buffers, &transforms, &frame, - u_offset, + 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]; - let u_offset = self.light_uniforms_buffer.offset_of(ui) as u32; - - //debug!("Rendering point light side {side} with atlas {} uniform index {ui} and offset {u_offset} and viewport {:?}", light_depth_map.atlas_index + side, frame); light_shadow_pass_impl( &mut pass, @@ -533,7 +570,7 @@ impl Node for ShadowMapsPass { &mesh_buffers, &transforms, &frame, - u_offset, + ui as _, ); } }, @@ -550,7 +587,7 @@ fn light_shadow_pass_impl<'a>( mesh_buffers: &'a RenderAssets, transforms: &'a TransformBuffers, shadow_atlas_viewport: &AtlasFrame, - uniform_offset: u32, + uniform_index: u32, ) { // only render to the light's map in the atlas pass.set_viewport( @@ -579,7 +616,7 @@ fn light_shadow_pass_impl<'a>( let buffers = buffers.unwrap(); //let uniform_index = light_uniforms_buffer.offset_of(light_depth_map.uniform_index[0]) as u32; - pass.set_bind_group(0, &uniforms_bind_group, &[uniform_offset]); + 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); @@ -595,7 +632,7 @@ fn light_shadow_pass_impl<'a>( buffers.buffer_vertex.buffer().slice(..), ); pass.set_index_buffer(indices.buffer().slice(..), *idx_type); - pass.draw_indexed(0..indices_len, 0, 0..1); + pass.draw_indexed(0..indices_len, 0, uniform_index..uniform_index + 1); } else { let vertex_count = buffers.buffer_vertex.count(); @@ -603,7 +640,7 @@ fn light_shadow_pass_impl<'a>( buffers.buffer_vertex.slot(), buffers.buffer_vertex.buffer().slice(..), ); - pass.draw(0..vertex_count as u32, 0..1); + pass.draw(0..vertex_count as u32, uniform_index..uniform_index + 1); } } } @@ -613,6 +650,11 @@ fn light_shadow_pass_impl<'a>( pub struct LightShadowUniform { space_mat: glam::Mat4, atlas_frame: AtlasFrame, // 2xUVec2 (4xf32), so no padding needed + near_plane: f32, + far_plane: f32, + _padding1: [u32; 2], + light_pos: glam::Vec3, + _padding2: u32, } /// A component that stores the ID of a shadow map in the shadow map atlas for the entities. @@ -648,8 +690,3 @@ impl LightShadowMapAtlas { self.0.write().unwrap() } } - -/* fn uniform_index_offset(limits: &wgpu::Limits, uniform_idx: u64) -> u32 { - let t = uniform_idx as u32 % (limits.max_storage_buffer_binding_size / mem::size_of::() as u32); - t * limits.min_uniform_buffer_offset_alignment -} */ diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 9e441ec..d6f15db 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -23,8 +23,12 @@ struct VertexOutput { } struct TextureAtlasFrame { - offset: vec2, - size: vec2, + /*offset: vec2, + size: vec2,*/ + x: u32, + y: u32, + width: u32, + height: u32, } struct TransformData { @@ -111,11 +115,9 @@ var s_diffuse: sampler; struct LightShadowMapUniform { light_space_matrix: mat4x4, atlas_frame: TextureAtlasFrame, -} - -struct LightShadowMapUniformAligned { - @align(256) - inner: LightShadowMapUniform + near_plane: f32, + far_plane: f32, + light_pos: vec3, } @group(4) @binding(0) @@ -130,7 +132,7 @@ var s_shadow_maps_atlas: sampler; @group(5) @binding(2) var u_shadow_maps_atlas_size: vec2; @group(5) @binding(3) -var u_light_shadow: array; +var u_light_shadow: array; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { @@ -152,21 +154,22 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let light_offset = tile.x; let light_count = tile.y; - let atlas_dimensions: vec2 = textureDimensions(t_shadow_maps_atlas); + 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) { - let light_dir = normalize(-light.direction); - let shadow_u: LightShadowMapUniform = u_light_shadow[light.light_shadow_uniform_index[0]].inner; + 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_normal, light_dir, frag_pos_light_space, atlas_dimensions, shadow_u.atlas_frame); 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(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); } @@ -176,9 +179,70 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { return vec4(light_object_res, object_color.a); } +/// Get the cube map side index of a 3d texture coord +/// +/// 0 -> UNKNOWN +/// 1 -> right +/// 2 -> left +/// 3 -> top +/// 4 -> bottom +/// 5 -> near +/// 6 -> far +fn get_side_idx(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 = normalize(res); + //res.y = 1.0-res.y; // invert y because wgsl + //let t = res.x; + //res.x = res.y; + + //res.y = 1.0 - t; + res.y = 1.0 - res.y; + //res.x = 1.0 - res.x; + + return vec3(res, f32(cube_idx)); +} + fn calc_shadow_dir_light(normal: vec3, light_dir: vec3, frag_pos_light_space: vec4, atlas_dimensions: vec2, atlas_region: TextureAtlasFrame) -> f32 { var proj_coords = frag_pos_light_space.xyz / frag_pos_light_space.w; - // for some reason the y component is clipped after transforming + // for some reason the y component is flipped after transforming proj_coords.y = -proj_coords.y; // dont cast shadows outside the light's far plane @@ -190,8 +254,8 @@ fn calc_shadow_dir_light(normal: vec3, light_dir: vec3, frag_pos_light let xy_remapped = proj_coords.xy * 0.5 + 0.5; // no need to get the y since the maps are square - let atlas_start = f32(atlas_region.offset.x) / f32(atlas_dimensions.x); - let atlas_end = f32(atlas_region.offset.x + atlas_region.size.x) / f32(atlas_dimensions.x); + let atlas_start = f32(atlas_region.x) / f32(atlas_dimensions.x); + let atlas_end = f32(atlas_region.x + atlas_region.width) / f32(atlas_dimensions.x); // lerp the tex coords to the shadow map for this light. proj_coords.x = mix(atlas_start, atlas_end, xy_remapped.x); proj_coords.y = mix(atlas_start, atlas_end, xy_remapped.y); @@ -204,7 +268,7 @@ fn calc_shadow_dir_light(normal: vec3, light_dir: vec3, frag_pos_light // must manually apply offset to the texture coords since `textureSampleLevel` requires a // const value. - let offset_coords = proj_coords.xy + (vec2(atlas_region.offset) / vec2(atlas_dimensions)); + let offset_coords = proj_coords.xy + (vec2(f32(atlas_region.x), f32(atlas_region.y)) / vec2(atlas_dimensions)); let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, offset_coords, 0.0); let current_depth = proj_coords.z; @@ -218,8 +282,50 @@ fn calc_shadow_dir_light(normal: vec3, light_dir: vec3, frag_pos_light return shadow; } -fn calc_shadow_point(world_pos: vec3, atlas_dimensions: vec2, atlas_regions: array) -> f32 { - return 0.0; +fn calc_shadow_point(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 = get_side_idx(normalize(frag_to_light)); + var coords_2d = temp.xy; + let cube_idx = i32(temp.z); + + /// if an unknown cube side was returned, something is broken + if cube_idx == 0 { + return 0.0; + } + + var indices = light.light_shadow_uniform_index; + let i = indices[cube_idx - 1]; + let u: LightShadowMapUniform = u_light_shadow[i]; + + // get the atlas frame in [0; 1] in the atlas texture + // z is width, w is height + var region_coords = vec4(f32(u.atlas_frame.x), f32(u.atlas_frame.y), f32(u.atlas_frame.width), f32(u.atlas_frame.height)); + region_coords /= f32(atlas_dimensions.x); + + // simulate `ClampToBorder`, not creating shadows past the shadow map regions + if (coords_2d.x >= 1.0 || coords_2d.y >= 1.0) { + return 0.0; + } + + // get the coords inside of the region + coords_2d.x = mix(region_coords.x, region_coords.x + region_coords.z, coords_2d.x); + coords_2d.y = mix(region_coords.y, region_coords.y + region_coords.w, coords_2d.y); + + var closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, coords_2d, 0.0); + let current_depth = length(frag_to_light); + + // convert depth from [0; 1] to the original depth value + closest_depth *= u.far_plane; + + // use a bias to avoid shadow acne + let bias = max(0.05 * (1.0 - dot(world_normal, light_dir)), 0.005); + + var shadow = 0.0; + if current_depth - bias > closest_depth { + shadow = 1.0; + } + + return shadow; } fn debug_grid(in: VertexOutput) -> vec4 { @@ -266,7 +372,7 @@ fn blinn_phong_dir_light(world_pos: vec3, world_norm: vec3, dir_light: return (ambient_color + (1.0 - 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; @@ -296,7 +402,7 @@ 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 + (1.0 - shadow) * (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 { diff --git a/lyra-game/src/render/shaders/shadows.wgsl b/lyra-game/src/render/shaders/shadows.wgsl index 8ea73d3..b8e17fc 100644 --- a/lyra-game/src/render/shaders/shadows.wgsl +++ b/lyra-game/src/render/shaders/shadows.wgsl @@ -11,23 +11,54 @@ struct TextureAtlasFrame { struct LightShadowMapUniform { light_space_matrix: mat4x4, atlas_frame: TextureAtlasFrame, + near_plane: f32, + far_plane: f32, + light_pos: vec3, } @group(0) @binding(0) -var u_light_shadow: LightShadowMapUniform; +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 + @location(0) position: vec3, + @builtin(instance_index) instance_index: u32, ) -> VertexOutput { - let pos = u_light_shadow.light_space_matrix * u_model_transform_data.transform * vec4(position, 1.0); - return VertexOutput(pos); + 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 index be830ce..67e5a82 100644 --- a/lyra-game/src/render/slot_buffer.rs +++ b/lyra-game/src/render/slot_buffer.rs @@ -54,19 +54,11 @@ impl GpuSlotBuffer { /// Calculates the byte offset in the buffer of the element at `i`. pub fn offset_of(&self, i: u64) -> u64 { - /* let offset = i * mem::size_of::() as u64; - - if let Some(align) = self.alignment { - round_mult::up(offset, NonZeroU64::new(align).unwrap()).unwrap() - } else { - offset - } */ - if let Some(align) = self.alignment { let transform_index = i % self.capacity; transform_index * align } else { - mem::size_of::() as u64 + i * mem::size_of::() as u64 } } From d02258224a992c0c8dbc559b26323093c6bc0189 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 14 Jul 2024 12:24:13 -0400 Subject: [PATCH 15/28] render: fix bug with texture atlas not packing textures in last column --- lyra-game/src/render/texture_atlas.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lyra-game/src/render/texture_atlas.rs b/lyra-game/src/render/texture_atlas.rs index f312ac9..34955c9 100644 --- a/lyra-game/src/render/texture_atlas.rs +++ b/lyra-game/src/render/texture_atlas.rs @@ -172,7 +172,7 @@ impl SkylinePacker { return None; } - if self.skylines[i].width as u32 > width_left { + if self.skylines[i].width as u32 >= width_left { return Some(y as usize); } From ff06bd55f3cf26d03ed96fcc12397d14fd1d68c6 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 14 Jul 2024 19:06:38 -0400 Subject: [PATCH 16/28] render: simple PCF --- examples/shadows/src/main.rs | 4 +- lyra-game/src/render/graph/passes/meshes.rs | 2 +- lyra-game/src/render/graph/passes/shadows.rs | 1 + lyra-game/src/render/material.rs | 2 +- lyra-game/src/render/shaders/base.wgsl | 98 +++++++++++--------- 5 files changed, 59 insertions(+), 48 deletions(-) diff --git a/examples/shadows/src/main.rs b/examples/shadows/src/main.rs index 49aa82a..5cef2a6 100644 --- a/examples/shadows/src/main.rs +++ b/examples/shadows/src/main.rs @@ -166,7 +166,7 @@ fn setup_scene_plugin(game: &mut Game) { light_tran, )); - world.spawn(( + /* world.spawn(( //cube_mesh.clone(), PointLight { enabled: true, @@ -188,7 +188,7 @@ fn setup_scene_plugin(game: &mut Game) { ..Default::default() }, Transform::from_xyz(-0.5, 2.0, -5.0), - )); + )); */ } let mut camera = CameraComponent::new_3d(); diff --git a/lyra-game/src/render/graph/passes/meshes.rs b/lyra-game/src/render/graph/passes/meshes.rs index 981e7c9..c0a315f 100644 --- a/lyra-game/src/render/graph/passes/meshes.rs +++ b/lyra-game/src/render/graph/passes/meshes.rs @@ -142,7 +142,7 @@ impl Node for MeshPass { wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison), count: None, }, wgpu::BindGroupLayoutEntry { diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index f11b8c1..15621bf 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -114,6 +114,7 @@ impl ShadowMapsPass { min_filter: wgpu::FilterMode::Linear, mipmap_filter: wgpu::FilterMode::Linear, border_color: Some(wgpu::SamplerBorderColor::OpaqueWhite), + compare: Some(wgpu::CompareFunction::LessEqual), ..Default::default() }); diff --git a/lyra-game/src/render/material.rs b/lyra-game/src/render/material.rs index cdbccbe..9fafa40 100755 --- a/lyra-game/src/render/material.rs +++ b/lyra-game/src/render/material.rs @@ -58,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/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index d6f15db..1862ba0 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -8,6 +8,8 @@ const LIGHT_TY_SPOT = 2u; const ALPHA_CUTOFF = 0.1; +const SHADOW_MAP_PCF_SIZE = 4.0; + struct VertexInput { @location(0) position: vec3, @location(1) tex_coords: vec2, @@ -128,7 +130,7 @@ var t_light_grid: texture_storage_2d; // rg32uint = vec2 u_shadow_maps_atlas_size: vec2; @group(5) @binding(3) @@ -165,7 +167,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { 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_normal, light_dir, frag_pos_light_space, atlas_dimensions, shadow_u.atlas_frame); + let shadow = calc_shadow_dir_light(in.world_normal, light_dir, frag_pos_light_space, atlas_dimensions, shadow_u); 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) { let shadow = calc_shadow_point(in.world_position, in.world_normal, light_dir, light, atlas_dimensions); @@ -240,45 +242,60 @@ fn get_side_idx(tex_coord: vec3) -> vec3 { return vec3(res, f32(cube_idx)); } -fn calc_shadow_dir_light(normal: vec3, light_dir: vec3, frag_pos_light_space: vec4, atlas_dimensions: vec2, atlas_region: TextureAtlasFrame) -> f32 { +fn calc_shadow_dir_light(normal: vec3, light_dir: vec3, frag_pos_light_space: vec4, atlas_dimensions: vec2, shadow_u: LightShadowMapUniform) -> f32 { 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; - // dont cast shadows outside the light's far plane - if (proj_coords.z > 1.0) { - return 0.0; - } - // Remap xy to [0.0, 1.0] let xy_remapped = proj_coords.xy * 0.5 + 0.5; - // no need to get the y since the maps are square - let atlas_start = f32(atlas_region.x) / f32(atlas_dimensions.x); - let atlas_end = f32(atlas_region.x + atlas_region.width) / f32(atlas_dimensions.x); - // lerp the tex coords to the shadow map for this light. - proj_coords.x = mix(atlas_start, atlas_end, xy_remapped.x); - proj_coords.y = mix(atlas_start, atlas_end, xy_remapped.y); - - // simulate `ClampToBorder`, not creating shadows past the shadow map regions - if (proj_coords.x > atlas_end && proj_coords.y > atlas_end) - || (proj_coords.x < atlas_start && proj_coords.y < atlas_start) { - return 0.0; - } - - // must manually apply offset to the texture coords since `textureSampleLevel` requires a - // const value. - let offset_coords = proj_coords.xy + (vec2(f32(atlas_region.x), f32(atlas_region.y)) / vec2(atlas_dimensions)); - let closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, offset_coords, 0.0); - let current_depth = proj_coords.z; - + // get the atlas frame in [0; 1] in the atlas texture + // z is width, w is height + 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)); + region_rect /= f32(atlas_dimensions.x); + let region_coords = vec2( + mix(region_rect.x, region_rect.x + region_rect.z, xy_remapped.x), + mix(region_rect.y, region_rect.y + region_rect.w, xy_remapped.y) + ); + // use a bias to avoid shadow acne let bias = max(0.05 * (1.0 - dot(normal, light_dir)), 0.005); - var shadow = 0.0; - if current_depth - bias > closest_depth { + let current_depth = proj_coords.z - bias; + + var shadow = pcf_dir_light(region_coords, current_depth, shadow_u); + + // 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_dir_light(tex_coords: vec2, test_depth: f32, shadow_u: LightShadowMapUniform) -> f32 { + let half_filter_size = SHADOW_MAP_PCF_SIZE / 2.0; + let texel_size = 1.0 / vec2(f32(shadow_u.atlas_frame.width), f32(shadow_u.atlas_frame.height)); + + // Sample PCF + var shadow = 0.0; + for (var x = -half_filter_size; x <= half_filter_size; x += 1.0) { + for (var y = -half_filter_size; y <= half_filter_size; y += 1.0) { + let offset = tex_coords + vec2(x, y) * texel_size; + let pcf_depth = textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas, offset, test_depth); + shadow += pcf_depth; + } + } + shadow /= pow(SHADOW_MAP_PCF_SIZE, 2.0); + // ensure the shadow value does not go above 1.0 + shadow = min(shadow, 1.0); + return shadow; } @@ -289,9 +306,9 @@ fn calc_shadow_point(world_pos: vec3, world_normal: vec3, light_dir: v let cube_idx = i32(temp.z); /// if an unknown cube side was returned, something is broken - if cube_idx == 0 { + /*if cube_idx == 0 { return 0.0; - } + }*/ var indices = light.light_shadow_uniform_index; let i = indices[cube_idx - 1]; @@ -303,27 +320,20 @@ fn calc_shadow_point(world_pos: vec3, world_normal: vec3, light_dir: v region_coords /= f32(atlas_dimensions.x); // simulate `ClampToBorder`, not creating shadows past the shadow map regions - if (coords_2d.x >= 1.0 || coords_2d.y >= 1.0) { + /*if (coords_2d.x >= 1.0 || coords_2d.y >= 1.0) { return 0.0; - } + }*/ // get the coords inside of the region coords_2d.x = mix(region_coords.x, region_coords.x + region_coords.z, coords_2d.x); coords_2d.y = mix(region_coords.y, region_coords.y + region_coords.w, coords_2d.y); - var closest_depth = textureSampleLevel(t_shadow_maps_atlas, s_shadow_maps_atlas, coords_2d, 0.0); - let current_depth = length(frag_to_light); - - // convert depth from [0; 1] to the original depth value - closest_depth *= u.far_plane; - // use a bias to avoid shadow acne let bias = max(0.05 * (1.0 - dot(world_normal, light_dir)), 0.005); + var current_depth = length(frag_to_light) - bias; + current_depth /= u.far_plane; - var shadow = 0.0; - if current_depth - bias > closest_depth { - shadow = 1.0; - } + var shadow = textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas, coords_2d, current_depth); return shadow; } @@ -369,7 +379,7 @@ 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 + (1.0 - shadow) * (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, shadow: f32) -> vec3 { From 27bc88c5a7ad52d56c0da078ffadd783baf544c1 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 14 Jul 2024 19:46:15 -0400 Subject: [PATCH 17/28] render: pass shadow settings to gpu --- lyra-game/src/render/graph/passes/meshes.rs | 8 +-- lyra-game/src/render/graph/passes/shadows.rs | 55 +++++++++++++++++--- lyra-game/src/render/shaders/base.wgsl | 12 +++-- 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/lyra-game/src/render/graph/passes/meshes.rs b/lyra-game/src/render/graph/passes/meshes.rs index c0a315f..08aef81 100644 --- a/lyra-game/src/render/graph/passes/meshes.rs +++ b/lyra-game/src/render/graph/passes/meshes.rs @@ -115,9 +115,9 @@ impl Node for MeshPass { .expect("missing ShadowMapsPassSlots::ShadowAtlasSampler") .as_sampler() .unwrap(); - let atlas_size_buf = graph - .slot_value(ShadowMapsPassSlots::ShadowAtlasSizeBuffer) - .expect("missing ShadowMapsPassSlots::ShadowAtlasSizeBuffer") + let shadow_settings_buf = graph + .slot_value(ShadowMapsPassSlots::ShadowSettingsUniform) + .expect("missing ShadowMapsPassSlots::ShadowSettingsUniform") .as_buffer() .unwrap(); let light_uniform_buf = graph @@ -183,7 +183,7 @@ impl Node for MeshPass { wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: atlas_size_buf, + buffer: shadow_settings_buf, offset: 0, size: None, }), diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index 15621bf..220d890 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -16,16 +16,12 @@ use tracing::warn; use wgpu::util::DeviceExt; use crate::render::{ - graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, - light::{directional::DirectionalLight, LightType, PointLight}, - resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, - transform_buffer_storage::TransformBuffers, - vertex::Vertex, - AtlasFrame, GpuSlotBuffer, TextureAtlas, + graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, light::{directional::DirectionalLight, LightType, PointLight}, resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, transform_buffer_storage::TransformBuffers, vertex::Vertex, AtlasFrame, GpuSlotBuffer, TextureAtlas }; use super::{MeshBufferStorage, RenderAssets, RenderMeshes}; +const PCF_SAMPLES_NUM: u32 = 4; const SHADOW_SIZE: glam::UVec2 = glam::uvec2(1024, 1024); #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] @@ -35,6 +31,7 @@ pub enum ShadowMapsPassSlots { ShadowAtlasSampler, ShadowAtlasSizeBuffer, ShadowLightUniformsBuffer, + ShadowSettingsUniform, } #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] @@ -333,7 +330,7 @@ impl ShadowMapsPass { impl Node for ShadowMapsPass { fn desc( &mut self, - _: &mut crate::render::graph::RenderGraph, + graph: &mut crate::render::graph::RenderGraph, ) -> crate::render::graph::NodeDesc { let mut node = NodeDesc::new(NodeType::Render, None, vec![]); @@ -371,6 +368,18 @@ impl Node for ShadowMapsPass { Some(SlotValue::Buffer(self.atlas_size_buffer.clone())), ); + let settings_buffer = graph.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 } @@ -380,6 +389,12 @@ impl Node for ShadowMapsPass { world: &mut lyra_ecs::World, context: &mut crate::render::graph::RenderGraphContext, ) { + { + // TODO: only write buffer on changes to resource + let shadow_settings = world.get_resource_or_default::(); + context.queue_buffer_write_with(ShadowMapsPassSlots::ShadowSettingsUniform, 0, ShadowSettingsUniform::from(*shadow_settings)); + } + self.render_meshes = world.try_get_resource_data::(); self.transform_buffers = world.try_get_resource_data::(); self.mesh_buffers = world.try_get_resource_data::>(); @@ -691,3 +706,29 @@ impl LightShadowMapAtlas { self.0.write().unwrap() } } + +#[derive(Debug, Copy, Clone)] +pub struct ShadowSettings { + pub pcf_samples_num: u32, +} + +impl Default for ShadowSettings { + fn default() -> Self { + Self { pcf_samples_num: PCF_SAMPLES_NUM } + } +} + +/// Uniform version of [`ShadowSettings`] +#[repr(C)] +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct ShadowSettingsUniform { + pcf_samples_num: u32, +} + +impl From for ShadowSettingsUniform { + fn from(value: ShadowSettings) -> Self { + Self { + pcf_samples_num: value.pcf_samples_num, + } + } +} \ No newline at end of file diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 1862ba0..4a61fa1 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -8,8 +8,6 @@ const LIGHT_TY_SPOT = 2u; const ALPHA_CUTOFF = 0.1; -const SHADOW_MAP_PCF_SIZE = 4.0; - struct VertexInput { @location(0) position: vec3, @location(1) tex_coords: vec2, @@ -122,6 +120,10 @@ struct LightShadowMapUniform { light_pos: vec3, } +struct ShadowSettingsUniform { + pcf_samples_num: u32, +} + @group(4) @binding(0) var u_light_indices: array; @group(4) @binding(1) @@ -132,7 +134,7 @@ var t_shadow_maps_atlas: texture_depth_2d; @group(5) @binding(1) var s_shadow_maps_atlas: sampler_comparison; @group(5) @binding(2) -var u_shadow_maps_atlas_size: vec2; +var u_shadow_settings: ShadowSettingsUniform; @group(5) @binding(3) var u_light_shadow: array; @@ -280,7 +282,7 @@ fn calc_shadow_dir_light(normal: vec3, light_dir: vec3, frag_pos_light /// Calculate the shadow coefficient using PCF of a directional light fn pcf_dir_light(tex_coords: vec2, test_depth: f32, shadow_u: LightShadowMapUniform) -> f32 { - let half_filter_size = SHADOW_MAP_PCF_SIZE / 2.0; + let half_filter_size = f32(u_shadow_settings.pcf_samples_num) / 2.0; let texel_size = 1.0 / vec2(f32(shadow_u.atlas_frame.width), f32(shadow_u.atlas_frame.height)); // Sample PCF @@ -292,7 +294,7 @@ fn pcf_dir_light(tex_coords: vec2, test_depth: f32, shadow_u: LightShadowMa shadow += pcf_depth; } } - shadow /= pow(SHADOW_MAP_PCF_SIZE, 2.0); + shadow /= pow(f32(u_shadow_settings.pcf_samples_num), 2.0); // ensure the shadow value does not go above 1.0 shadow = min(shadow, 1.0); From 4c6c6c4dd5886746ace98f0a42c705aa9cd07aae Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 14 Jul 2024 22:14:08 -0400 Subject: [PATCH 18/28] render: PCF with poisson disc on directional lights --- Cargo.lock | 135 +++++++++++++++++++ examples/shadows/src/main.rs | 4 +- lyra-game/Cargo.toml | 1 + lyra-game/src/render/graph/passes/meshes.rs | 23 ++++ lyra-game/src/render/graph/passes/shadows.rs | 125 ++++++++++++----- lyra-game/src/render/light/mod.rs | 2 +- lyra-game/src/render/shaders/base.wgsl | 9 +- lyra-game/src/render/texture_atlas.rs | 6 +- 8 files changed, 261 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c988b5c..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", @@ -2079,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" @@ -2286,6 +2395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2727,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" @@ -3226,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/examples/shadows/src/main.rs b/examples/shadows/src/main.rs index 5cef2a6..56a31c0 100644 --- a/examples/shadows/src/main.rs +++ b/examples/shadows/src/main.rs @@ -6,7 +6,7 @@ use lyra_engine::{ InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput, }, math::{self, Transform, Vec3}, - render::light::{directional::DirectionalLight, PointLight}, + render::light::directional::DirectionalLight, scene::{ CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform, ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN, @@ -161,7 +161,7 @@ fn setup_scene_plugin(game: &mut Game) { DirectionalLight { enabled: true, color: Vec3::new(1.0, 0.95, 0.9), - intensity: 0.5, + intensity: 0.9, }, light_tran, )); diff --git a/lyra-game/Cargo.toml b/lyra-game/Cargo.toml index 7c0d0d2..621a5f8 100644 --- a/lyra-game/Cargo.toml +++ b/lyra-game/Cargo.toml @@ -39,6 +39,7 @@ 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/passes/meshes.rs b/lyra-game/src/render/graph/passes/meshes.rs index 08aef81..99d127d 100644 --- a/lyra-game/src/render/graph/passes/meshes.rs +++ b/lyra-game/src/render/graph/passes/meshes.rs @@ -125,6 +125,11 @@ impl Node for MeshPass { .expect("missing ShadowMapsPassSlots::ShadowLightUniformsBuffer") .as_buffer() .unwrap(); + let pcf_poisson_disc = graph + .slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer) + .expect("missing ShadowMapsPassSlots::PcfPoissonDiscBuffer") + .as_buffer() + .unwrap(); let atlas_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("bgl_shadows_atlas"), @@ -165,6 +170,16 @@ impl Node for MeshPass { }, 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, + }, ], }); @@ -196,6 +211,14 @@ impl Node for MeshPass { size: None, }), }, + wgpu::BindGroupEntry { + binding: 4, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: pcf_poisson_disc, + offset: 0, + size: None, + }), + }, ], }); diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index 220d890..b9b418f 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -1,10 +1,9 @@ use std::{ - collections::VecDeque, - mem, - rc::Rc, - sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}, + collections::VecDeque, mem, rc::Rc, sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard} }; +use fast_poisson::Poisson2D; +use itertools::Itertools; use lyra_ecs::{ query::{filter::Has, Entities}, AtomicRef, Component, Entity, ResourceData, @@ -16,12 +15,17 @@ use tracing::warn; use wgpu::util::DeviceExt; use crate::render::{ - graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, light::{directional::DirectionalLight, LightType, PointLight}, resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, transform_buffer_storage::TransformBuffers, vertex::Vertex, AtlasFrame, GpuSlotBuffer, TextureAtlas + graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, + light::{directional::DirectionalLight, LightType, PointLight}, + resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, + transform_buffer_storage::TransformBuffers, + vertex::Vertex, + AtlasFrame, GpuSlotBuffer, TextureAtlas, }; use super::{MeshBufferStorage, RenderAssets, RenderMeshes}; -const PCF_SAMPLES_NUM: u32 = 4; +const PCF_SAMPLES_NUM: u32 = 6; const SHADOW_SIZE: glam::UVec2 = glam::uvec2(1024, 1024); #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] @@ -32,6 +36,7 @@ pub enum ShadowMapsPassSlots { ShadowAtlasSizeBuffer, ShadowLightUniformsBuffer, ShadowSettingsUniform, + PcfPoissonDiscBuffer, } #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] @@ -174,8 +179,7 @@ impl ShadowMapsPass { let atlas_index = atlas .pack(SHADOW_SIZE.x as _, SHADOW_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 atlas_frame = atlas.texture_frame(atlas_index).expect("Frame missing"); let projection = glam::Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, NEAR_PLANE, FAR_PLANE); @@ -253,24 +257,12 @@ impl ShadowMapsPass { ), ]; - 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 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(), @@ -325,6 +317,37 @@ impl ShadowMapsPass { 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, num_samples: u32) -> wgpu::Buffer { + device.create_buffer(&wgpu::BufferDescriptor { + label: Some(label), + size: mem::size_of::() as u64 * (num_samples.pow(2)) 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, num_pcf_samples: u32) { + let num_points = num_pcf_samples.pow(2); + let num_floats = num_points * 2; // 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 { + let poisson = Poisson2D::new() + .with_dimensions([1.0, 1.0], min_dist) + .with_samples(num_pcf_samples); + + points = poisson.iter().flatten().collect_vec(); + + } + points.truncate(num_floats as _); + + queue.write_buffer(buffer, 0, bytemuck::cast_slice(points.as_slice())); + } } impl Node for ShadowMapsPass { @@ -368,7 +391,8 @@ impl Node for ShadowMapsPass { Some(SlotValue::Buffer(self.atlas_size_buffer.clone())), ); - let settings_buffer = graph.device().create_buffer(&wgpu::BufferDescriptor { + 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, @@ -380,6 +404,14 @@ impl Node for ShadowMapsPass { 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", PCF_SAMPLES_NUM), + ))), + ); + node } @@ -390,9 +422,20 @@ impl Node for ShadowMapsPass { context: &mut crate::render::graph::RenderGraphContext, ) { { + // TODO: Update the poisson disc every time the PCF sampling point number changed + if !world.has_resource::() { + let buffer = graph.slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer) + .unwrap().as_buffer().unwrap(); + self.write_poisson_disc(&context.queue, &buffer, ShadowSettings::default().pcf_samples_num); + } + // TODO: only write buffer on changes to resource let shadow_settings = world.get_resource_or_default::(); - context.queue_buffer_write_with(ShadowMapsPassSlots::ShadowSettingsUniform, 0, ShadowSettingsUniform::from(*shadow_settings)); + context.queue_buffer_write_with( + ShadowMapsPassSlots::ShadowSettingsUniform, + 0, + ShadowSettingsUniform::from(*shadow_settings), + ); } self.render_meshes = world.try_get_resource_data::(); @@ -425,8 +468,13 @@ impl Node for ShadowMapsPass { for (entity, pos, _) in world.view_iter::<(Entities, &Transform, Has)>() { if !self.depth_maps.contains_key(&entity) { // TODO: dont pack the textures as they're added - let atlas_index = - self.create_depth_map(&context.queue, LightType::Directional, entity, *pos, 45.0); + let atlas_index = self.create_depth_map( + &context.queue, + LightType::Directional, + entity, + *pos, + 45.0, + ); index_components_queue.push_back((entity, atlas_index)); } } @@ -553,12 +601,12 @@ impl Node for ShadowMapsPass { }); 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) + let frame = atlas + .texture_frame(light_depth_map.atlas_index) .expect("missing atlas frame for light"); light_shadow_pass_impl( @@ -570,12 +618,13 @@ impl Node for ShadowMapsPass { &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) + 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]; @@ -589,7 +638,7 @@ impl Node for ShadowMapsPass { ui as _, ); } - }, + } LightType::Spotlight => todo!(), } } @@ -714,7 +763,9 @@ pub struct ShadowSettings { impl Default for ShadowSettings { fn default() -> Self { - Self { pcf_samples_num: PCF_SAMPLES_NUM } + Self { + pcf_samples_num: PCF_SAMPLES_NUM, + } } } @@ -731,4 +782,4 @@ impl From for ShadowSettingsUniform { pcf_samples_num: value.pcf_samples_num, } } -} \ No newline at end of file +} diff --git a/lyra-game/src/render/light/mod.rs b/lyra-game/src/render/light/mod.rs index b744a8a..545d4eb 100644 --- a/lyra-game/src/render/light/mod.rs +++ b/lyra-game/src/render/light/mod.rs @@ -211,7 +211,7 @@ impl LightUniformBuffers { 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()])); diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 4a61fa1..0e026f2 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -137,6 +137,8 @@ var s_shadow_maps_atlas: sampler_comparison; var u_shadow_settings: ShadowSettingsUniform; @group(5) @binding(3) var u_light_shadow: array; +@group(5) @binding(4) +var u_pcf_poisson_disc: array>; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { @@ -287,11 +289,16 @@ fn pcf_dir_light(tex_coords: vec2, test_depth: f32, shadow_u: LightShadowMa // Sample PCF var shadow = 0.0; + var i = 0; for (var x = -half_filter_size; x <= half_filter_size; x += 1.0) { for (var y = -half_filter_size; y <= half_filter_size; y += 1.0) { - let offset = tex_coords + vec2(x, y) * texel_size; + //let random = u_pcf_poisson_disc[i] * texel_size; + let offset = tex_coords + (u_pcf_poisson_disc[i] + vec2(x, y)) * texel_size; + let pcf_depth = textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas, offset, test_depth); shadow += pcf_depth; + + i++; } } shadow /= pow(f32(u_shadow_settings.pcf_samples_num), 2.0); diff --git a/lyra-game/src/render/texture_atlas.rs b/lyra-game/src/render/texture_atlas.rs index 34955c9..3dec36e 100644 --- a/lyra-game/src/render/texture_atlas.rs +++ b/lyra-game/src/render/texture_atlas.rs @@ -198,7 +198,7 @@ impl SkylinePacker { /* 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 _) + (y + height == min_height && self.skylines[i].width < min_width as usize) { min_height = y + height; min_width = self.skylines[i].width as _; @@ -224,8 +224,8 @@ impl SkylinePacker { width: frame.width as _ }; - assert!(skyline.right() <= self.size.x as _); - assert!(skyline.y <= self.size.y as _); + assert!(skyline.right() <= self.size.x as usize); + assert!(skyline.y <= self.size.y as usize); self.skylines.insert(i, skyline); From 4449172c2b30340ac51a5f36ef2402ba91357d81 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Thu, 18 Jul 2024 23:43:08 -0400 Subject: [PATCH 19/28] render: implement PCSS for directional lights --- examples/assets/shadows-platform-palmtree.glb | Bin 0 -> 97604 bytes examples/assets/wood-platform/model.bin | Bin 0 -> 840 bytes examples/assets/wood-platform/model.gltf | 142 ++++++++++++++++++ examples/assets/wood-platform/wood1.jpg | Bin 0 -> 4257 bytes examples/assets/wood-platform/wood1OLD.jpg | Bin 0 -> 48455 bytes examples/shadows/src/main.rs | 33 +++- lyra-game/src/render/graph/node.rs | 8 + lyra-game/src/render/graph/passes/meshes.rs | 48 +++++- lyra-game/src/render/graph/passes/shadows.rs | 100 +++++++++--- lyra-game/src/render/shaders/base.wgsl | 126 +++++++++++----- 10 files changed, 394 insertions(+), 63 deletions(-) create mode 100644 examples/assets/shadows-platform-palmtree.glb create mode 100644 examples/assets/wood-platform/model.bin create mode 100644 examples/assets/wood-platform/model.gltf create mode 100644 examples/assets/wood-platform/wood1.jpg create mode 100644 examples/assets/wood-platform/wood1OLD.jpg diff --git a/examples/assets/shadows-platform-palmtree.glb b/examples/assets/shadows-platform-palmtree.glb new file mode 100644 index 0000000000000000000000000000000000000000..66a3dfd5e0e16768e5c820685f20b9122b809a5e GIT binary patch literal 97604 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 z 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 H5?%ZQGE$s> literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7fc7ceff2e246f57a7b9cdfb8ff38b229228d7a5 GIT binary patch literal 4257 zcmeIu%@M*N5C!0u2nGX*5Fw_U9GuCFR$&zuWfdMQ!k>$stmGI=aNe>9c*E}DlZSlZ zPt$QiQIxpk19{_sRawrROXb!^Yo&BuRcqsF=WK0l({{dTLu;+?eb~j6lH2ZwJ`P<> zQEsqCLFKgpa!1M+ENheF=zmkl3ugX1;(A^V@I?U%P=Epypa2CZKmiI+fC3btz#j^X G^85m$=@rfZ literal 0 HcmV?d00001 diff --git a/examples/assets/wood-platform/wood1OLD.jpg b/examples/assets/wood-platform/wood1OLD.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2cbb3d327b1d44f46b766b712113b47b253f34b0 GIT binary patch literal 48455 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 literal 0 HcmV?d00001 diff --git a/examples/shadows/src/main.rs b/examples/shadows/src/main.rs index 56a31c0..ec37a89 100644 --- a/examples/shadows/src/main.rs +++ b/examples/shadows/src/main.rs @@ -128,15 +128,29 @@ fn setup_scene_plugin(game: &mut Game) { platform_gltf.wait_recurse_dependencies_load(); let platform_mesh = &platform_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(( + /* 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(), @@ -149,10 +163,19 @@ fn setup_scene_plugin(game: &mut Game) { 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, 2.5, 0.0); + 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)); @@ -192,7 +215,9 @@ fn setup_scene_plugin(game: &mut Game) { } let mut camera = CameraComponent::new_3d(); - camera.transform.translation += math::Vec3::new(0.0, 2.0, 10.5); - camera.transform.rotate_x(math::Angle::Degrees(-17.0)); + //camera.transform.translation += math::Vec3::new(0.0, 2.0, 10.5); + camera.transform.translation = math::Vec3::new(-3.0, -8.0, -3.0); + camera.transform.rotate_x(math::Angle::Degrees(-27.0)); + camera.transform.rotate_y(math::Angle::Degrees(-55.0)); world.spawn((camera, FreeFlyCamera::default())); } \ No newline at end of file diff --git a/lyra-game/src/render/graph/node.rs b/lyra-game/src/render/graph/node.rs index f0cdcfe..7a39a4b 100644 --- a/lyra-game/src/render/graph/node.rs +++ b/lyra-game/src/render/graph/node.rs @@ -63,6 +63,14 @@ pub enum SlotValue { } impl SlotValue { + 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) } diff --git a/lyra-game/src/render/graph/passes/meshes.rs b/lyra-game/src/render/graph/passes/meshes.rs index 99d127d..73edf51 100644 --- a/lyra-game/src/render/graph/passes/meshes.rs +++ b/lyra-game/src/render/graph/passes/meshes.rs @@ -115,6 +115,11 @@ impl Node for MeshPass { .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") @@ -130,6 +135,11 @@ impl Node for MeshPass { .expect("missing ShadowMapsPassSlots::PcfPoissonDiscBuffer") .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"), @@ -147,11 +157,17 @@ impl Node for MeshPass { wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison), + 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, @@ -161,7 +177,7 @@ impl Node for MeshPass { count: None, }, wgpu::BindGroupLayoutEntry { - binding: 3, + binding: 4, visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only: true }, @@ -171,7 +187,17 @@ impl Node for MeshPass { count: None, }, wgpu::BindGroupLayoutEntry { - binding: 4, + 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 }, @@ -197,6 +223,10 @@ impl Node for MeshPass { }, 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, @@ -204,7 +234,7 @@ impl Node for MeshPass { }), }, wgpu::BindGroupEntry { - binding: 3, + binding: 4, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: light_uniform_buf, offset: 0, @@ -212,13 +242,21 @@ impl Node for MeshPass { }), }, wgpu::BindGroupEntry { - binding: 4, + 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: pcss_poisson_disc, + offset: 0, + size: None, + }), + }, ], }); diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index b9b418f..e4e1b61 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -3,6 +3,7 @@ use std::{ }; use fast_poisson::Poisson2D; +use glam::Vec2; use itertools::Itertools; use lyra_ecs::{ query::{filter::Has, Entities}, @@ -25,18 +26,19 @@ use crate::render::{ use super::{MeshBufferStorage, RenderAssets, RenderMeshes}; -const PCF_SAMPLES_NUM: u32 = 6; -const SHADOW_SIZE: glam::UVec2 = glam::uvec2(1024, 1024); +const SHADOW_SIZE: glam::UVec2 = glam::uvec2(4096, 4096); #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] pub enum ShadowMapsPassSlots { ShadowAtlasTexture, ShadowAtlasTextureView, ShadowAtlasSampler, + ShadowAtlasSamplerComparison, ShadowAtlasSizeBuffer, ShadowLightUniformsBuffer, ShadowSettingsUniform, PcfPoissonDiscBuffer, + PcssPoissonDiscBuffer, } #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] @@ -74,6 +76,7 @@ pub struct ShadowMapsPass { atlas: LightShadowMapAtlas, /// The depth map atlas sampler atlas_sampler: Rc, + atlas_sampler_compare: Rc, } impl ShadowMapsPass { @@ -98,7 +101,7 @@ impl ShadowMapsPass { device, wgpu::TextureFormat::Depth32Float, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, - SHADOW_SIZE * 8, + SHADOW_SIZE * 2, ); let atlas_size_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { @@ -107,16 +110,28 @@ impl ShadowMapsPass { 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::Linear, - min_filter: wgpu::FilterMode::Linear, - mipmap_filter: wgpu::FilterMode::Linear, + 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() }); @@ -155,6 +170,7 @@ impl ShadowMapsPass { point_light_pipeline: None, atlas_sampler: Rc::new(sampler), + atlas_sampler_compare: Rc::new(sampler_compare), atlas: LightShadowMapAtlas(Arc::new(RwLock::new(atlas))), } } @@ -183,6 +199,14 @@ impl ShadowMapsPass { let projection = glam::Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, NEAR_PLANE, 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(), @@ -196,7 +220,8 @@ impl ShadowMapsPass { atlas_frame, near_plane: NEAR_PLANE, far_plane, - _padding1: [0; 2], + light_size_uv, + _padding1: 0, light_pos: light_pos.translation, _padding2: 0, }; @@ -284,7 +309,8 @@ impl ShadowMapsPass { atlas_frame: frames[i], near_plane: NEAR_PLANE, far_plane, - _padding1: [0; 2], + light_size_uv: 0.0, + _padding1: 0, light_pos: light_trans, _padding2: 0, }, @@ -322,26 +348,29 @@ impl ShadowMapsPass { fn create_poisson_disc_buffer(&self, device: &wgpu::Device, label: &str, num_samples: u32) -> wgpu::Buffer { device.create_buffer(&wgpu::BufferDescriptor { label: Some(label), - size: mem::size_of::() as u64 * (num_samples.pow(2)) as u64, + size: mem::size_of::() as u64 * (num_samples * 2) 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, num_pcf_samples: u32) { - let num_points = num_pcf_samples.pow(2); - let num_floats = num_points * 2; // points are vec2f + fn write_poisson_disc(&self, queue: &wgpu::Queue, buffer: &wgpu::Buffer, num_samples: u32) { + //let num_points = num_samples.pow(2); + let num_floats = num_samples * 2; // points are vec2f let min_dist = (num_floats as f32).sqrt() / num_floats as f32; + //let min_dist = (num_samples as f32).sqrt() / num_samples 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 { let poisson = Poisson2D::new() .with_dimensions([1.0, 1.0], min_dist) - .with_samples(num_pcf_samples); + .with_samples(num_samples); - points = poisson.iter().flatten().collect_vec(); + points = poisson.iter().flatten() + .map(|p| p * 2.0 - 1.0) + .collect_vec(); } points.truncate(num_floats as _); @@ -377,6 +406,12 @@ impl Node for ShadowMapsPass { 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, @@ -404,11 +439,20 @@ impl Node for ShadowMapsPass { Some(SlotValue::Buffer(Arc::new(settings_buffer))), ); + let def_settings = ShadowSettings::default(); node.add_buffer_slot( ShadowMapsPassSlots::PcfPoissonDiscBuffer, SlotAttribute::Output, Some(SlotValue::Buffer(Arc::new( - self.create_poisson_disc_buffer(device, "buffer_poisson_disc_pcf", PCF_SAMPLES_NUM), + self.create_poisson_disc_buffer(device, "buffer_poisson_disc_pcf", def_settings.pcf_samples_num), + ))), + ); + + node.add_buffer_slot( + ShadowMapsPassSlots::PcssPoissonDiscBuffer, + SlotAttribute::Output, + Some(SlotValue::Buffer(Arc::new( + self.create_poisson_disc_buffer(device, "buffer_poisson_disc_pcss", def_settings.pcss_blocker_search_samples), ))), ); @@ -424,9 +468,14 @@ impl Node for ShadowMapsPass { { // TODO: Update the poisson disc every time the PCF sampling point number changed if !world.has_resource::() { + let def_settings = ShadowSettings::default(); let buffer = graph.slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer) .unwrap().as_buffer().unwrap(); - self.write_poisson_disc(&context.queue, &buffer, ShadowSettings::default().pcf_samples_num); + self.write_poisson_disc(&context.queue, &buffer, def_settings.pcf_samples_num); + + let buffer = graph.slot_value(ShadowMapsPassSlots::PcssPoissonDiscBuffer) + .unwrap().as_buffer().unwrap(); + self.write_poisson_disc(&context.queue, &buffer, def_settings.pcss_blocker_search_samples); } // TODO: only write buffer on changes to resource @@ -717,7 +766,9 @@ pub struct LightShadowUniform { atlas_frame: AtlasFrame, // 2xUVec2 (4xf32), so no padding needed near_plane: f32, far_plane: f32, - _padding1: [u32; 2], + /// Light size in UV space (light_size / frustum_size) + light_size_uv: f32, + _padding1: u32, light_pos: glam::Vec3, _padding2: u32, } @@ -758,13 +809,22 @@ impl LightShadowMapAtlas { #[derive(Debug, Copy, Clone)] pub struct ShadowSettings { + /// How many PCF filtering samples are used per dimension. + /// + /// A value of 16 is common. 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 16 is common. + pub pcss_blocker_search_samples: u32, } impl Default for ShadowSettings { fn default() -> Self { Self { - pcf_samples_num: PCF_SAMPLES_NUM, + pcf_samples_num: 64, + pcss_blocker_search_samples: 36, } } } @@ -774,12 +834,14 @@ impl Default for ShadowSettings { #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] struct ShadowSettingsUniform { pcf_samples_num: u32, + pcss_blocker_search_samples: u32, } impl From for ShadowSettingsUniform { fn from(value: ShadowSettings) -> Self { Self { pcf_samples_num: value.pcf_samples_num, + pcss_blocker_search_samples: value.pcss_blocker_search_samples, } } } diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 0e026f2..3d8a627 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -117,11 +117,13 @@ struct LightShadowMapUniform { atlas_frame: TextureAtlasFrame, near_plane: f32, far_plane: f32, + light_size_uv: f32, light_pos: vec3, } struct ShadowSettingsUniform { pcf_samples_num: u32, + pcss_blocker_search_samples: u32, } @group(4) @binding(0) @@ -132,13 +134,17 @@ var t_light_grid: texture_storage_2d; // rg32uint = vec2 u_shadow_settings: ShadowSettingsUniform; +var s_shadow_maps_atlas_compare: sampler_comparison; @group(5) @binding(3) -var u_light_shadow: array; +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_pcss_poisson_disc: array>; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { @@ -185,8 +191,12 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { return vec4(light_object_res, object_color.a); } -/// Get the cube map side index of a 3d texture coord +/// 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 @@ -194,7 +204,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { /// 4 -> bottom /// 5 -> near /// 6 -> far -fn get_side_idx(tex_coord: vec3) -> vec3 { +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); @@ -234,14 +244,7 @@ fn get_side_idx(tex_coord: vec3) -> vec3 { } res = (res / abs(major_axis) + 1.0) * 0.5; - //res = normalize(res); - //res.y = 1.0-res.y; // invert y because wgsl - //let t = res.x; - //res.x = res.y; - - //res.y = 1.0 - t; res.y = 1.0 - res.y; - //res.x = 1.0 - res.x; return vec3(res, f32(cube_idx)); } @@ -264,10 +267,11 @@ fn calc_shadow_dir_light(normal: vec3, light_dir: vec3, frag_pos_light ); // use a bias to avoid shadow acne - let bias = max(0.05 * (1.0 - dot(normal, light_dir)), 0.005); + let bias = 0.005;//max(0.05 * (1.0 - dot(normal, light_dir)), 0.005); let current_depth = proj_coords.z - bias; - var shadow = pcf_dir_light(region_coords, current_depth, shadow_u); + //var shadow = pcf_dir_light(region_coords, current_depth, shadow_u, 1.0); + var shadow = pcss_dir_light(xy_remapped, current_depth, shadow_u); // dont cast shadows outside the light's far plane if (proj_coords.z > 1.0) { @@ -282,35 +286,87 @@ fn calc_shadow_dir_light(normal: vec3, light_dir: vec3, frag_pos_light return shadow; } -/// Calculate the shadow coefficient using PCF of a directional light -fn pcf_dir_light(tex_coords: vec2, test_depth: f32, shadow_u: LightShadowMapUniform) -> f32 { - let half_filter_size = f32(u_shadow_settings.pcf_samples_num) / 2.0; - let texel_size = 1.0 / vec2(f32(shadow_u.atlas_frame.width), f32(shadow_u.atlas_frame.height)); +// 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; +} - // Sample PCF - var shadow = 0.0; - var i = 0; - for (var x = -half_filter_size; x <= half_filter_size; x += 1.0) { - for (var y = -half_filter_size; y <= half_filter_size; y += 1.0) { - //let random = u_pcf_poisson_disc[i] * texel_size; - let offset = tex_coords + (u_pcf_poisson_disc[i] + vec2(x, y)) * texel_size; - - let pcf_depth = textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas, offset, test_depth); - shadow += pcf_depth; +/// Convert texture coords to be texture coords of an atlas frame. +fn to_atlas_frame_coords(shadow_u: LightShadowMapUniform, coords: vec2) -> 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); + + // lerp input coords + let region_coords = vec2( + mix(region_rect.x, region_rect.x + region_rect.z, coords.x), + mix(region_rect.y, region_rect.y + region_rect.w, coords.y) + ); + + return region_coords; +} - i++; +/// 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); + 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; } } - shadow /= pow(f32(u_shadow_settings.pcf_samples_num), 2.0); - // ensure the shadow value does not go above 1.0 - shadow = min(shadow, 1.0); - return shadow; + 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); + + 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(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 = get_side_idx(normalize(frag_to_light)); + let temp = coords_to_cube_atlas(normalize(frag_to_light)); var coords_2d = temp.xy; let cube_idx = i32(temp.z); @@ -342,7 +398,7 @@ fn calc_shadow_point(world_pos: vec3, world_normal: vec3, light_dir: v var current_depth = length(frag_to_light) - bias; current_depth /= u.far_plane; - var shadow = textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas, coords_2d, current_depth); + var shadow = textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, coords_2d, current_depth); return shadow; } From 54b47c21781246f0cd63f8d2e7141e3edf261daf Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Fri, 19 Jul 2024 16:07:03 -0400 Subject: [PATCH 20/28] ecs: implement change tracking for world resources --- lyra-ecs/src/resource.rs | 11 +++- lyra-ecs/src/world.rs | 105 +++++++++++++++++++++++++++++++++------ 2 files changed, 100 insertions(+), 16 deletions(-) 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 41e16e9..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,20 +478,29 @@ 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()) @@ -693,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 From c961568b969dc138e912b844b4d9949b05558fa4 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Fri, 19 Jul 2024 16:07:40 -0400 Subject: [PATCH 21/28] render: update the shadow filting poisson disc when shadow settings are modified --- lyra-game/src/render/graph/passes/shadows.rs | 49 ++++++++++---------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index e4e1b61..cd217e7 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -12,7 +12,7 @@ use lyra_ecs::{ use lyra_game_derive::RenderGraphLabel; use lyra_math::{Angle, Transform}; use rustc_hash::FxHashMap; -use tracing::warn; +use tracing::{debug, warn}; use wgpu::util::DeviceExt; use crate::render::{ @@ -356,10 +356,8 @@ impl ShadowMapsPass { /// 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, num_samples: u32) { - //let num_points = num_samples.pow(2); let num_floats = num_samples * 2; // points are vec2f let min_dist = (num_floats as f32).sqrt() / num_floats as f32; - //let min_dist = (num_samples as f32).sqrt() / num_samples as f32; let mut points = vec![]; // use a while loop to ensure that the correct number of floats is created @@ -439,12 +437,11 @@ impl Node for ShadowMapsPass { Some(SlotValue::Buffer(Arc::new(settings_buffer))), ); - let def_settings = ShadowSettings::default(); node.add_buffer_slot( ShadowMapsPassSlots::PcfPoissonDiscBuffer, SlotAttribute::Output, Some(SlotValue::Buffer(Arc::new( - self.create_poisson_disc_buffer(device, "buffer_poisson_disc_pcf", def_settings.pcf_samples_num), + self.create_poisson_disc_buffer(device, "buffer_poisson_disc_pcf", PCF_SAMPLES_NUM_MAX), ))), ); @@ -452,7 +449,7 @@ impl Node for ShadowMapsPass { ShadowMapsPassSlots::PcssPoissonDiscBuffer, SlotAttribute::Output, Some(SlotValue::Buffer(Arc::new( - self.create_poisson_disc_buffer(device, "buffer_poisson_disc_pcss", def_settings.pcss_blocker_search_samples), + self.create_poisson_disc_buffer(device, "buffer_poisson_disc_pcss", PCSS_SAMPLES_NUM_MAX), ))), ); @@ -465,25 +462,26 @@ impl Node for ShadowMapsPass { world: &mut lyra_ecs::World, context: &mut crate::render::graph::RenderGraphContext, ) { - { - // TODO: Update the poisson disc every time the PCF sampling point number changed - if !world.has_resource::() { - let def_settings = ShadowSettings::default(); - let buffer = graph.slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer) - .unwrap().as_buffer().unwrap(); - self.write_poisson_disc(&context.queue, &buffer, def_settings.pcf_samples_num); + world.add_resource_default_if_absent::(); + if world.has_resource_changed::() { + debug!("Detected change in ShadowSettings, recreating poisson disks"); - let buffer = graph.slot_value(ShadowMapsPassSlots::PcssPoissonDiscBuffer) - .unwrap().as_buffer().unwrap(); - self.write_poisson_disc(&context.queue, &buffer, def_settings.pcss_blocker_search_samples); - } + 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, uniform.pcf_samples_num); + + let buffer = graph.slot_value(ShadowMapsPassSlots::PcssPoissonDiscBuffer) + .unwrap().as_buffer().unwrap(); + self.write_poisson_disc(&context.queue, &buffer, uniform.pcss_blocker_search_samples); - // TODO: only write buffer on changes to resource - let shadow_settings = world.get_resource_or_default::(); context.queue_buffer_write_with( ShadowMapsPassSlots::ShadowSettingsUniform, 0, - ShadowSettingsUniform::from(*shadow_settings), + uniform, ); } @@ -811,12 +809,12 @@ impl LightShadowMapAtlas { pub struct ShadowSettings { /// How many PCF filtering samples are used per dimension. /// - /// A value of 16 is common. + /// 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 16 is common. + /// A value of 25 is common, this is maxed to 128. pub pcss_blocker_search_samples: u32, } @@ -829,6 +827,9 @@ impl Default for ShadowSettings { } } +const PCF_SAMPLES_NUM_MAX: u32 = 128; +const PCSS_SAMPLES_NUM_MAX: u32 = 128; + /// Uniform version of [`ShadowSettings`] #[repr(C)] #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] @@ -840,8 +841,8 @@ struct ShadowSettingsUniform { impl From for ShadowSettingsUniform { fn from(value: ShadowSettings) -> Self { Self { - pcf_samples_num: value.pcf_samples_num, - pcss_blocker_search_samples: value.pcss_blocker_search_samples, + pcf_samples_num: value.pcf_samples_num.min(PCF_SAMPLES_NUM_MAX), + pcss_blocker_search_samples: value.pcss_blocker_search_samples.min(PCSS_SAMPLES_NUM_MAX), } } } From c91ee67961a4f4998c32961aa57f9867a8b52521 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Fri, 19 Jul 2024 17:56:27 -0400 Subject: [PATCH 22/28] render: improve shadow settings to make it possible to switch between PCF, PCSS, hardware 2x2 PCF, or disable filtering all together --- lyra-game/src/render/graph/passes/shadows.rs | 58 +++++++++++--------- lyra-game/src/render/shaders/base.wgsl | 30 ++++++---- 2 files changed, 51 insertions(+), 37 deletions(-) diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index cd217e7..8ab48f4 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -494,27 +494,8 @@ impl Node for ShadowMapsPass { // 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, (has_dir, has_point)) in world.view_iter::<(Entities, &Transform, Or, Has>)>() { - if !self.depth_maps.contains_key(&entity) { - // TODO: calculate far plane - let (light_type, far_plane) = if has_dir.is_some() { - (LightType::Directional, 45.0) - } else if has_point.is_some() { - (LightType::Point, 45.0) - } else { - todo!("Spot lights") - }; - - // TODO: dont pack the textures as they're added - let atlas_index = - self.create_depth_map(&context.queue, light_type, entity, *pos, far_plane); - index_components_queue.push_back((entity, atlas_index)); - } - } */ - for (entity, pos, _) in world.view_iter::<(Entities, &Transform, Has)>() { if !self.depth_maps.contains_key(&entity) { - // TODO: dont pack the textures as they're added let atlas_index = self.create_depth_map( &context.queue, LightType::Directional, @@ -528,7 +509,6 @@ impl Node for ShadowMapsPass { for (entity, pos, _) in world.view_iter::<(Entities, &Transform, Has)>() { if !self.depth_maps.contains_key(&entity) { - // TODO: dont pack the textures as they're added let atlas_index = self.create_depth_map(&context.queue, LightType::Point, entity, *pos, 30.0); index_components_queue.push_back((entity, atlas_index)); @@ -727,7 +707,6 @@ fn light_shadow_pass_impl<'a>( } let buffers = buffers.unwrap(); - //let uniform_index = light_uniforms_buffer.offset_of(light_depth_map.uniform_index[0]) as u32; pass.set_bind_group(0, &uniforms_bind_group, &[]); // Get the bindgroup for job's transform and bind to it using an offset. @@ -805,8 +784,19 @@ impl LightShadowMapAtlas { } } +#[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. @@ -821,8 +811,9 @@ pub struct ShadowSettings { impl Default for ShadowSettings { fn default() -> Self { Self { - pcf_samples_num: 64, - pcss_blocker_search_samples: 36, + filtering_mode: ShadowFilteringMode::default(), + pcf_samples_num: 25, + pcss_blocker_search_samples: 25, } } } @@ -830,19 +821,34 @@ impl Default for ShadowSettings { const PCF_SAMPLES_NUM_MAX: u32 = 128; const PCSS_SAMPLES_NUM_MAX: u32 = 128; -/// Uniform version of [`ShadowSettings`] +/// 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: ShadowSettings) -> Self { + let raw_pcf_samples = value.pcf_samples_num.min(PCF_SAMPLES_NUM_MAX); + let raw_pcss_samples = value.pcss_blocker_search_samples.min(PCSS_SAMPLES_NUM_MAX); + + let (pcf_samples, pcss_samples) = match value.filtering_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: value.pcf_samples_num.min(PCF_SAMPLES_NUM_MAX), - pcss_blocker_search_samples: value.pcss_blocker_search_samples.min(PCSS_SAMPLES_NUM_MAX), + pcf_samples_num: pcf_samples, + pcss_blocker_search_samples: pcss_samples, } } } diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 3d8a627..3c7b17a 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -256,22 +256,30 @@ fn calc_shadow_dir_light(normal: vec3, light_dir: vec3, frag_pos_light // Remap xy to [0.0, 1.0] let xy_remapped = proj_coords.xy * 0.5 + 0.5; - - // get the atlas frame in [0; 1] in the atlas texture - // z is width, w is height - 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)); - region_rect /= f32(atlas_dimensions.x); - let region_coords = vec2( - mix(region_rect.x, region_rect.x + region_rect.z, xy_remapped.x), - mix(region_rect.y, region_rect.y + region_rect.w, xy_remapped.y) - ); // use a bias to avoid shadow acne let bias = 0.005;//max(0.05 * (1.0 - dot(normal, light_dir)), 0.005); let current_depth = proj_coords.z - bias; - //var shadow = pcf_dir_light(region_coords, current_depth, shadow_u, 1.0); - var shadow = pcss_dir_light(xy_remapped, current_depth, shadow_u); + var shadow = 0.0; + if u_shadow_settings.pcf_samples_num > 0u && u_shadow_settings.pcss_blocker_search_samples > 0u { + shadow = pcss_dir_light(xy_remapped, current_depth, shadow_u); + } + // hardware 2x2 PCF via camparison sampler + else if u_shadow_settings.pcf_samples_num == 2u { + let region_coords = to_atlas_frame_coords(shadow_u, xy_remapped); + shadow = textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, region_coords, current_depth); + } else if u_shadow_settings.pcf_samples_num > 0u { + let atlas_dimensions = textureDimensions(t_shadow_maps_atlas); + // TODO: should texel size be using the entire atlas dimensions, or just the frame? + let texel_size = 1.0 / f32(atlas_dimensions.x); // f32(shadow_u.atlas_frame.width) + + shadow = pcf_dir_light(xy_remapped, current_depth, shadow_u, texel_size); + } else { // pcf_samples_num == 0 + let region_coords = to_atlas_frame_coords(shadow_u, xy_remapped); + 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) { From fef709d5f102798a81ea9dfbee18c3d9b8084ba1 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 21 Jul 2024 12:02:35 -0400 Subject: [PATCH 23/28] render: implement PCF for point lights, support per-light shadow settings --- examples/shadows/src/main.rs | 39 ++- lyra-game/src/render/graph/passes/meshes.rs | 23 ++ lyra-game/src/render/graph/passes/shadows.rs | 313 +++++++++++++++---- lyra-game/src/render/shaders/base.wgsl | 160 +++++++--- lyra-game/src/render/shaders/shadows.wgsl | 5 + 5 files changed, 425 insertions(+), 115 deletions(-) diff --git a/examples/shadows/src/main.rs b/examples/shadows/src/main.rs index ec37a89..ebf9987 100644 --- a/examples/shadows/src/main.rs +++ b/examples/shadows/src/main.rs @@ -5,8 +5,11 @@ use lyra_engine::{ Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource, InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput, }, - math::{self, Transform, Vec3}, - render::light::directional::DirectionalLight, + math::{self, Quat, Transform, Vec3}, + render::{ + graph::{ShadowCasterSettings, ShadowFilteringMode}, + light::{directional::DirectionalLight, PointLight}, + }, scene::{ CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform, ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN, @@ -99,7 +102,7 @@ async fn main() { 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(); @@ -189,19 +192,27 @@ fn setup_scene_plugin(game: &mut Game) { light_tran, )); - /* world.spawn(( - //cube_mesh.clone(), + world.spawn(( + cube_mesh.clone(), PointLight { enabled: true, color: Vec3::new(0.133, 0.098, 0.91), intensity: 2.0, - range: 9.0, + range: 10.0, ..Default::default() }, - Transform::from_xyz(5.0, -2.5, -3.3), + 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), + ), )); - world.spawn(( + /* world.spawn(( //cube_mesh.clone(), PointLight { enabled: true, @@ -216,8 +227,14 @@ fn setup_scene_plugin(game: &mut Game) { let mut camera = CameraComponent::new_3d(); //camera.transform.translation += math::Vec3::new(0.0, 2.0, 10.5); - camera.transform.translation = math::Vec3::new(-3.0, -8.0, -3.0); + /* camera.transform.translation = math::Vec3::new(-3.0, -8.0, -3.0); camera.transform.rotate_x(math::Angle::Degrees(-27.0)); - camera.transform.rotate_y(math::Angle::Degrees(-55.0)); + camera.transform.rotate_y(math::Angle::Degrees(-55.0)); */ + + camera.transform.translation = math::Vec3::new(15.0, -8.0, 1.0); + camera.transform.rotate_x(math::Angle::Degrees(-27.0)); + //camera.transform.rotate_y(math::Angle::Degrees(-90.0)); + camera.transform.rotate_y(math::Angle::Degrees(90.0)); + world.spawn((camera, FreeFlyCamera::default())); -} \ No newline at end of file +} diff --git a/lyra-game/src/render/graph/passes/meshes.rs b/lyra-game/src/render/graph/passes/meshes.rs index 73edf51..ba2858a 100644 --- a/lyra-game/src/render/graph/passes/meshes.rs +++ b/lyra-game/src/render/graph/passes/meshes.rs @@ -135,6 +135,11 @@ impl Node for MeshPass { .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") @@ -206,6 +211,16 @@ impl Node for MeshPass { }, 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, + }, ], }); @@ -251,6 +266,14 @@ impl Node for MeshPass { }, 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, diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index 8ab48f4..8ff4ef3 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -1,8 +1,11 @@ use std::{ - collections::VecDeque, mem, rc::Rc, sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard} + collections::VecDeque, + mem, + rc::Rc, + sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}, }; -use fast_poisson::Poisson2D; +use fast_poisson::{Poisson2D, Poisson3D}; use glam::Vec2; use itertools::Itertools; use lyra_ecs::{ @@ -26,7 +29,7 @@ use crate::render::{ use super::{MeshBufferStorage, RenderAssets, RenderMeshes}; -const SHADOW_SIZE: glam::UVec2 = glam::uvec2(4096, 4096); +const SHADOW_SIZE: glam::UVec2 = glam::uvec2(1024, 1024); #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] pub enum ShadowMapsPassSlots { @@ -38,6 +41,7 @@ pub enum ShadowMapsPassSlots { ShadowLightUniformsBuffer, ShadowSettingsUniform, PcfPoissonDiscBuffer, + PcfPoissonDiscBuffer3d, PcssPoissonDiscBuffer, } @@ -101,7 +105,7 @@ impl ShadowMapsPass { device, wgpu::TextureFormat::Depth32Float, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, - SHADOW_SIZE * 2, + SHADOW_SIZE * 8, ); let atlas_size_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { @@ -182,31 +186,57 @@ impl ShadowMapsPass { light_type: LightType, entity: Entity, light_pos: Transform, - far_plane: f32, + are_settings_custom: bool, + shadow_settings: ShadowCasterSettings, ) -> LightDepthMap { - const NEAR_PLANE: f32 = 0.1; - const FAR_PLANE: f32 = 45.0; - 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(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _) + .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, NEAR_PLANE, FAR_PLANE); - + 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(), @@ -218,12 +248,15 @@ impl ShadowMapsPass { let u = LightShadowUniform { space_mat: light_proj, atlas_frame, - near_plane: NEAR_PLANE, - far_plane, + near_plane: shadow_settings.near_plane, + far_plane: shadow_settings.far_plane, light_size_uv, _padding1: 0, light_pos: light_pos.translation, - _padding2: 0, + has_shadow_settings, + pcf_samples_num: u.pcf_samples_num, + pcss_blocker_search_samples: u.pcss_blocker_search_samples, + _padding2: [0; 2], }; let uniform_index = self.light_uniforms_buffer.insert(queue, &u); @@ -237,8 +270,8 @@ impl ShadowMapsPass { let projection = glam::Mat4::perspective_rh( Angle::Degrees(90.0).to_radians(), aspect, - NEAR_PLANE, - far_plane, + shadow_settings.near_plane, + shadow_settings.far_plane, ); let light_trans = light_pos.translation; @@ -307,12 +340,15 @@ impl ShadowMapsPass { &LightShadowUniform { space_mat: views[i], atlas_frame: frames[i], - near_plane: NEAR_PLANE, - far_plane, + near_plane: shadow_settings.near_plane, + far_plane: shadow_settings.far_plane, light_size_uv: 0.0, _padding1: 0, light_pos: light_trans, - _padding2: 0, + has_shadow_settings, + pcf_samples_num: u.pcf_samples_num, + pcss_blocker_search_samples: u.pcss_blocker_search_samples, + _padding2: [0; 2], }, ); indices[i] = uniform_i; @@ -345,34 +381,69 @@ impl ShadowMapsPass { } /// Create the gpu buffer for a poisson disc - fn create_poisson_disc_buffer(&self, device: &wgpu::Device, label: &str, num_samples: u32) -> wgpu::Buffer { + 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 * 2) as u64, + 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, num_samples: u32) { - let num_floats = num_samples * 2; // points are vec2f + 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 { - let poisson = Poisson2D::new() - .with_dimensions([1.0, 1.0], min_dist) - .with_samples(num_samples); + 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(); - + 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())); } } @@ -441,7 +512,25 @@ impl Node for ShadowMapsPass { ShadowMapsPassSlots::PcfPoissonDiscBuffer, SlotAttribute::Output, Some(SlotValue::Buffer(Arc::new( - self.create_poisson_disc_buffer(device, "buffer_poisson_disc_pcf", PCF_SAMPLES_NUM_MAX), + 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, + ), ))), ); @@ -449,7 +538,12 @@ impl Node for ShadowMapsPass { ShadowMapsPassSlots::PcssPoissonDiscBuffer, SlotAttribute::Output, Some(SlotValue::Buffer(Arc::new( - self.create_poisson_disc_buffer(device, "buffer_poisson_disc_pcss", PCSS_SAMPLES_NUM_MAX), + self.create_poisson_disc_buffer( + device, + "buffer_poisson_disc_pcss", + 2, + PCSS_SAMPLES_NUM_MAX, + ), ))), ); @@ -462,28 +556,43 @@ impl Node for ShadowMapsPass { world: &mut lyra_ecs::World, context: &mut crate::render::graph::RenderGraphContext, ) { - world.add_resource_default_if_absent::(); - if world.has_resource_changed::() { + world.add_resource_default_if_absent::(); + if world.has_resource_changed::() { debug!("Detected change in ShadowSettings, recreating poisson disks"); - let settings = world.get_resource::(); + 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, uniform.pcf_samples_num); + 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::PcssPoissonDiscBuffer) - .unwrap().as_buffer().unwrap(); - self.write_poisson_disc(&context.queue, &buffer, uniform.pcss_blocker_search_samples); + let buffer = graph + .slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer3d) + .unwrap() + .as_buffer() + .unwrap(); + self.write_poisson_disc(&context.queue, &buffer, 3, uniform.pcf_samples_num); - context.queue_buffer_write_with( - ShadowMapsPassSlots::ShadowSettingsUniform, - 0, - uniform, + 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::(); self.render_meshes = world.try_get_resource_data::(); self.transform_buffers = world.try_get_resource_data::(); @@ -494,23 +603,48 @@ impl Node for ShadowMapsPass { // 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, _) in world.view_iter::<(Entities, &Transform, Has)>() { + 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, - 45.0, + custom_settings, + shadow_settings, ); index_components_queue.push_back((entity, atlas_index)); } } - for (entity, pos, _) in world.view_iter::<(Entities, &Transform, Has)>() { + for (entity, pos, shadow_settings, _) in world.view_iter::<( + Entities, + &Transform, + Option<&ShadowCasterSettings>, + Has, + )>() { if !self.depth_maps.contains_key(&entity) { - let atlas_index = - self.create_depth_map(&context.queue, LightType::Point, entity, *pos, 30.0); + 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, + custom_settings, + shadow_settings, + ); index_components_queue.push_back((entity, atlas_index)); } } @@ -736,6 +870,38 @@ fn light_shadow_pass_impl<'a>( } } +/// 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, +} + +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, + } + } +} + #[repr(C)] #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] pub struct LightShadowUniform { @@ -747,7 +913,11 @@ pub struct LightShadowUniform { light_size_uv: f32, _padding1: u32, light_pos: glam::Vec3, - _padding2: u32, + /// Boolean casted as integer + has_shadow_settings: u32, + pcf_samples_num: u32, + pcss_blocker_search_samples: u32, + _padding2: [u32; 2], } /// A component that stores the ID of a shadow map in the shadow map atlas for the entities. @@ -794,15 +964,15 @@ pub enum ShadowFilteringMode { Pcss, } -#[derive(Debug, Copy, Clone)] +/* #[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, @@ -816,13 +986,13 @@ impl Default for ShadowSettings { 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. @@ -834,12 +1004,27 @@ struct ShadowSettingsUniform { pcss_blocker_search_samples: u32, } -impl From for ShadowSettingsUniform { - fn from(value: ShadowSettings) -> Self { - let raw_pcf_samples = value.pcf_samples_num.min(PCF_SAMPLES_NUM_MAX); - let raw_pcss_samples = value.pcss_blocker_search_samples.min(PCSS_SAMPLES_NUM_MAX); +impl From for ShadowSettingsUniform { + fn from(value: ShadowCasterSettings) -> Self { + Self::new( + value.filtering_mode, + value.pcf_samples_num, + value.pcss_blocker_search_samples, + ) + } +} - let (pcf_samples, pcss_samples) = match value.filtering_mode { +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), diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 3c7b17a..0ad3344 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -119,6 +119,10 @@ struct LightShadowMapUniform { 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, } struct ShadowSettingsUniform { @@ -144,6 +148,8 @@ 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 @@ -180,7 +186,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let shadow = calc_shadow_dir_light(in.world_normal, light_dir, frag_pos_light_space, atlas_dimensions, shadow_u); 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) { - let shadow = calc_shadow_point(in.world_position, in.world_normal, light_dir, light, atlas_dimensions); + 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); @@ -249,6 +255,16 @@ fn coords_to_cube_atlas(tex_coord: vec3) -> vec3 { 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(normal: vec3, light_dir: vec3, frag_pos_light_space: vec4, atlas_dimensions: vec2, shadow_u: LightShadowMapUniform) -> f32 { var proj_coords = frag_pos_light_space.xyz / frag_pos_light_space.w; // for some reason the y component is flipped after transforming @@ -261,21 +277,28 @@ fn calc_shadow_dir_light(normal: vec3, light_dir: vec3, frag_pos_light let bias = 0.005;//max(0.05 * (1.0 - dot(normal, light_dir)), 0.005); let current_depth = proj_coords.z - bias; + // get settings + let settings = get_shadow_settings(shadow_u); + let pcf_samples_num = settings.x; + let pcss_blocker_search_samples = settings.y; + var shadow = 0.0; - if u_shadow_settings.pcf_samples_num > 0u && u_shadow_settings.pcss_blocker_search_samples > 0u { + // hardware 2x2 PCF via camparison sampler + if pcf_samples_num == 2u { + let region_coords = to_atlas_frame_coords(shadow_u, xy_remapped); + 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, shadow_u); } - // hardware 2x2 PCF via camparison sampler - else if u_shadow_settings.pcf_samples_num == 2u { - let region_coords = to_atlas_frame_coords(shadow_u, xy_remapped); - shadow = textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, region_coords, current_depth); - } else if u_shadow_settings.pcf_samples_num > 0u { - let atlas_dimensions = textureDimensions(t_shadow_maps_atlas); - // TODO: should texel size be using the entire atlas dimensions, or just the frame? - let texel_size = 1.0 / f32(atlas_dimensions.x); // f32(shadow_u.atlas_frame.width) - + // only PCF + else if pcf_samples_num > 0u { + let texel_size = 1.0 / f32(shadow_u.atlas_frame.width); shadow = pcf_dir_light(xy_remapped, current_depth, shadow_u, texel_size); - } else { // pcf_samples_num == 0 + } + // no filtering + else { let region_coords = to_atlas_frame_coords(shadow_u, xy_remapped); 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); @@ -304,14 +327,19 @@ fn to_atlas_frame_coords(shadow_u: LightShadowMapUniform, coords: 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)); + 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); + + // 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 = (1.0 / f32(shadow_u.atlas_frame.x)) * 4.0; // lerp input coords let region_coords = vec2( - mix(region_rect.x, region_rect.x + region_rect.z, coords.x), - mix(region_rect.y, region_rect.y + region_rect.w, coords.y) + 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; @@ -372,45 +400,96 @@ fn pcf_dir_light(tex_coords: vec2, test_depth: f32, shadow_u: LightShadowMa return saturate(shadow); } -fn calc_shadow_point(world_pos: vec3, world_normal: vec3, light_dir: vec3, light: Light, atlas_dimensions: vec2) -> f32 { +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); - - /// if an unknown cube side was returned, something is broken - /*if cube_idx == 0 { - return 0.0; - }*/ 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]] + ); - // get the atlas frame in [0; 1] in the atlas texture - // z is width, w is height - var region_coords = vec4(f32(u.atlas_frame.x), f32(u.atlas_frame.y), f32(u.atlas_frame.width), f32(u.atlas_frame.height)); - region_coords /= f32(atlas_dimensions.x); - - // simulate `ClampToBorder`, not creating shadows past the shadow map regions - /*if (coords_2d.x >= 1.0 || coords_2d.y >= 1.0) { - return 0.0; - }*/ - - // get the coords inside of the region - coords_2d.x = mix(region_coords.x, region_coords.x + region_coords.z, coords_2d.x); - coords_2d.y = mix(region_coords.y, region_coords.y + region_coords.w, coords_2d.y); - - // use a bias to avoid shadow acne - let bias = max(0.05 * (1.0 - dot(world_normal, light_dir)), 0.005); - var current_depth = length(frag_to_light) - bias; + var current_depth = length(frag_to_light); current_depth /= u.far_plane; + current_depth -= 0.005; // TODO: find a better way to calculate bias - var shadow = textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, coords_2d, current_depth); + // 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); + 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); + 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); + 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 pcf_point_light(tex_coords: vec2, test_depth: f32, shadow_u: LightShadowMapUniform, samples_num: u32, uv_radius: f32) -> f32 { + var shadow = 0.0; + for (var i = 0; i < i32(samples_num); i++) { + let offset = tex_coords + u_pcf_poisson_disc[i] * uv_radius; + let new_coords = to_atlas_frame_coords(shadow_u, offset); + + 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)); @@ -485,7 +564,8 @@ fn blinn_phong_point_light(world_pos: vec3, world_norm: vec3, point_li diffuse_color *= attenuation; specular_color *= attenuation; - return (ambient_color + (1.0 - shadow) * (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 { diff --git a/lyra-game/src/render/shaders/shadows.wgsl b/lyra-game/src/render/shaders/shadows.wgsl index b8e17fc..4e9bf16 100644 --- a/lyra-game/src/render/shaders/shadows.wgsl +++ b/lyra-game/src/render/shaders/shadows.wgsl @@ -13,7 +13,12 @@ struct LightShadowMapUniform { 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, } @group(0) @binding(0) From b0a6d30afc770f4502980299a5b7112225e0854f Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 21 Jul 2024 21:09:29 -0400 Subject: [PATCH 24/28] render: fix directional light shadows --- lyra-game/src/render/shaders/base.wgsl | 40 +++++++++++++++----------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 0ad3344..6103938 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -183,7 +183,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { 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_normal, light_dir, frag_pos_light_space, atlas_dimensions, shadow_u); + 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) { let shadow = calc_shadow_point_light(in.world_position, in.world_normal, light_dir, light, atlas_dimensions); @@ -265,7 +265,10 @@ fn get_shadow_settings(shadow_u: LightShadowMapUniform) -> vec2 { } } -fn calc_shadow_dir_light(normal: vec3, light_dir: vec3, frag_pos_light_space: vec4, atlas_dimensions: vec2, shadow_u: LightShadowMapUniform) -> f32 { +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; @@ -278,28 +281,28 @@ fn calc_shadow_dir_light(normal: vec3, light_dir: vec3, frag_pos_light let current_depth = proj_coords.z - bias; // get settings - let settings = get_shadow_settings(shadow_u); + 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(shadow_u, xy_remapped); + 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, shadow_u); + shadow = pcss_dir_light(xy_remapped, current_depth, map_data); } // only PCF else if pcf_samples_num > 0u { - let texel_size = 1.0 / f32(shadow_u.atlas_frame.width); - shadow = pcf_dir_light(xy_remapped, current_depth, shadow_u, texel_size); + 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(shadow_u, xy_remapped); + 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); } @@ -323,7 +326,10 @@ fn search_width(light_near: f32, uv_light_size: f32, receiver_depth: f32) -> f32 } /// Convert texture coords to be texture coords of an atlas frame. -fn to_atlas_frame_coords(shadow_u: LightShadowMapUniform, coords: vec2) -> vec2 { +/// +/// 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 @@ -332,9 +338,9 @@ fn to_atlas_frame_coords(shadow_u: LightShadowMapUniform, coords: vec2) -> // put the frame rect in atlas UV space region_rect /= f32(atlas_dimensions.x); - // 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 = (1.0 / f32(shadow_u.atlas_frame.x)) * 4.0; + // 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( @@ -354,7 +360,7 @@ fn find_blocker_distance_dir_light(tex_coords: vec2, receiver_depth: f32, b 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); + 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) { @@ -390,7 +396,7 @@ fn pcf_dir_light(tex_coords: vec2, test_depth: f32, shadow_u: LightShadowMa 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); + 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); } @@ -431,7 +437,7 @@ fn calc_shadow_point_light(world_pos: vec3, world_normal: vec3, light_ 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); + 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 @@ -446,7 +452,7 @@ fn calc_shadow_point_light(world_pos: vec3, world_normal: vec3, light_ } // no filtering else { - let region_coords = to_atlas_frame_coords(u, coords_2d); + 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); } @@ -467,7 +473,7 @@ fn pcf_point_light(tex_coords: vec3, test_depth: f32, shadow_us: array Date: Sun, 21 Jul 2024 21:53:02 -0400 Subject: [PATCH 25/28] render: make shadow depth bias configurable per light source --- examples/shadows/src/main.rs | 11 ++++------ lyra-game/src/render/graph/passes/shadows.rs | 23 +++++++++++++++++--- lyra-game/src/render/shaders/base.wgsl | 6 ++--- lyra-game/src/render/shaders/shadows.wgsl | 1 + 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/examples/shadows/src/main.rs b/examples/shadows/src/main.rs index ebf9987..ba929aa 100644 --- a/examples/shadows/src/main.rs +++ b/examples/shadows/src/main.rs @@ -124,13 +124,6 @@ fn setup_scene_plugin(game: &mut Game) { cube_gltf.wait_recurse_dependencies_load(); let cube_mesh = &cube_gltf.data_ref().unwrap().scenes[0]; - let platform_gltf = resman - .request::("../assets/wood-platform.glb") - .unwrap(); - - platform_gltf.wait_recurse_dependencies_load(); - let platform_mesh = &platform_gltf.data_ref().unwrap().scenes[0]; - let palm_tree_platform_gltf = resman .request::("../assets/shadows-platform-palmtree.glb") .unwrap(); @@ -189,6 +182,10 @@ fn setup_scene_plugin(game: &mut Game) { color: Vec3::new(1.0, 0.95, 0.9), intensity: 0.9, }, + ShadowCasterSettings { + filtering_mode: ShadowFilteringMode::Pcss, + ..Default::default() + }, light_tran, )); diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index 8ff4ef3..3e207a9 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -256,7 +256,8 @@ impl ShadowMapsPass { has_shadow_settings, pcf_samples_num: u.pcf_samples_num, pcss_blocker_search_samples: u.pcss_blocker_search_samples, - _padding2: [0; 2], + 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); @@ -348,7 +349,8 @@ impl ShadowMapsPass { has_shadow_settings, pcf_samples_num: u.pcf_samples_num, pcss_blocker_search_samples: u.pcss_blocker_search_samples, - _padding2: [0; 2], + constant_depth_bias: DEFAULT_CONSTANT_DEPTH_BIAS * shadow_settings.constant_depth_bias_scale, + _padding2: 0, }, ); indices[i] = uniform_i; @@ -594,6 +596,11 @@ impl Node for ShadowMapsPass { } 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::>(); @@ -888,8 +895,15 @@ pub struct ShadowCasterSettings { 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 { @@ -898,6 +912,8 @@ impl Default for ShadowCasterSettings { pcss_blocker_search_samples: 25, near_plane: 0.1, far_plane: 45.0, + constant_depth_bias_scale: 1.0, + use_back_faces: false, } } } @@ -917,7 +933,8 @@ pub struct LightShadowUniform { has_shadow_settings: u32, pcf_samples_num: u32, pcss_blocker_search_samples: u32, - _padding2: [u32; 2], + constant_depth_bias: f32, + _padding2: u32, } /// A component that stores the ID of a shadow map in the shadow map atlas for the entities. diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 6103938..2bab59e 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -123,6 +123,7 @@ struct LightShadowMapUniform { has_shadow_settings: u32, pcf_samples_num: u32, pcss_blocker_search_samples: u32, + constant_depth_bias: f32, } struct ShadowSettingsUniform { @@ -277,8 +278,7 @@ fn calc_shadow_dir_light(world_pos: vec3, world_normal: vec3, light_di let xy_remapped = proj_coords.xy * 0.5 + 0.5; // use a bias to avoid shadow acne - let bias = 0.005;//max(0.05 * (1.0 - dot(normal, light_dir)), 0.005); - let current_depth = proj_coords.z - bias; + let current_depth = proj_coords.z - map_data.constant_depth_bias; // get settings let settings = get_shadow_settings(map_data); @@ -427,7 +427,7 @@ fn calc_shadow_point_light(world_pos: vec3, world_normal: vec3, light_ var current_depth = length(frag_to_light); current_depth /= u.far_plane; - current_depth -= 0.005; // TODO: find a better way to calculate bias + current_depth -= u.constant_depth_bias; // get settings let settings = get_shadow_settings(u); diff --git a/lyra-game/src/render/shaders/shadows.wgsl b/lyra-game/src/render/shaders/shadows.wgsl index 4e9bf16..58be2c5 100644 --- a/lyra-game/src/render/shaders/shadows.wgsl +++ b/lyra-game/src/render/shaders/shadows.wgsl @@ -19,6 +19,7 @@ struct LightShadowMapUniform { has_shadow_settings: u32, pcf_samples_num: u32, pcss_blocker_search_samples: u32, + constant_depth_bias: f32, } @group(0) @binding(0) From 8c1738334c2ee46317fa279ea9dd8acb61a8c366 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Wed, 24 Jul 2024 20:10:32 -0400 Subject: [PATCH 26/28] render: shadow maps and PCF for spot lights --- examples/shadows/src/main.rs | 42 ++++-- lyra-game/src/render/graph/passes/shadows.rs | 137 +++++++++++++------ lyra-game/src/render/light/spotlight.rs | 2 + lyra-game/src/render/shaders/base.wgsl | 95 +++++++++---- lyra-game/src/render/shaders/shadows.wgsl | 5 - lyra-math/src/angle.rs | 16 ++- 6 files changed, 218 insertions(+), 79 deletions(-) diff --git a/examples/shadows/src/main.rs b/examples/shadows/src/main.rs index ba929aa..30ad34a 100644 --- a/examples/shadows/src/main.rs +++ b/examples/shadows/src/main.rs @@ -5,10 +5,10 @@ use lyra_engine::{ Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource, InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput, }, - math::{self, Quat, Transform, Vec3}, + math::{self, Angle, Quat, Transform, Vec3}, render::{ graph::{ShadowCasterSettings, ShadowFilteringMode}, - light::{directional::DirectionalLight, PointLight}, + light::{directional::DirectionalLight, PointLight, SpotLight}, }, scene::{ CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform, @@ -189,7 +189,7 @@ fn setup_scene_plugin(game: &mut Game) { light_tran, )); - world.spawn(( + /* world.spawn(( cube_mesh.clone(), PointLight { enabled: true, @@ -207,6 +207,33 @@ fn setup_scene_plugin(game: &mut Game) { 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(), )); /* world.spawn(( @@ -224,14 +251,13 @@ fn setup_scene_plugin(game: &mut Game) { let mut camera = CameraComponent::new_3d(); //camera.transform.translation += math::Vec3::new(0.0, 2.0, 10.5); - /* camera.transform.translation = math::Vec3::new(-3.0, -8.0, -3.0); + 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(-55.0)); */ + camera.transform.rotate_y(math::Angle::Degrees(-90.0)); - camera.transform.translation = math::Vec3::new(15.0, -8.0, 1.0); + /* camera.transform.translation = math::Vec3::new(15.0, -8.0, 1.0); camera.transform.rotate_x(math::Angle::Degrees(-27.0)); - //camera.transform.rotate_y(math::Angle::Degrees(-90.0)); - camera.transform.rotate_y(math::Angle::Degrees(90.0)); + camera.transform.rotate_y(math::Angle::Degrees(90.0)); */ world.spawn((camera, FreeFlyCamera::default())); } diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index 3e207a9..5e59f59 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -20,7 +20,7 @@ use wgpu::util::DeviceExt; use crate::render::{ graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, - light::{directional::DirectionalLight, LightType, PointLight}, + light::{directional::DirectionalLight, LightType, PointLight, SpotLight}, resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, transform_buffer_storage::TransformBuffers, vertex::Vertex, @@ -186,6 +186,7 @@ impl ShadowMapsPass { light_type: LightType, entity: Entity, light_pos: Transform, + light_half_outer_angle: Option, are_settings_custom: bool, shadow_settings: ShadowCasterSettings, ) -> LightDepthMap { @@ -200,17 +201,6 @@ impl ShadowMapsPass { 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 => { @@ -265,7 +255,49 @@ impl ShadowMapsPass { indices[0] = uniform_index; (atlas_index, indices) } - LightType::Spotlight => todo!(), + LightType::Spotlight => { + // allocate a single frame in the shadow map atlas + let atlas_index = atlas + .pack(SHADOW_SIZE.x as _, SHADOW_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( + //Angle::Degrees(90.0).to_radians(), + (light_half_outer_angle.unwrap() * 2.0).to_radians(), + aspect, + shadow_settings.near_plane, + shadow_settings.far_plane, + ); + + let light_trans = light_pos.translation; + let forward = light_pos.forward(); + let up = light_pos.up(); + let view = glam::Mat4::look_to_rh(light_trans, forward, up); + + let light_proj = projection * 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( @@ -626,6 +658,31 @@ impl Node for ShadowMapsPass { LightType::Directional, 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, ); @@ -649,6 +706,7 @@ impl Node for ShadowMapsPass { LightType::Point, entity, *pos, + None, custom_settings, shadow_settings, ); @@ -786,7 +844,7 @@ impl Node for ShadowMapsPass { &frame, light_depth_map.uniform_index[0] as _, ); - } + }, LightType::Point => { pass.set_pipeline(&point_light_pipeline); @@ -806,8 +864,25 @@ impl Node for ShadowMapsPass { ui as _, ); } + }, + LightType::Spotlight => { + pass.set_pipeline(&pipeline); + //pass.set_pipeline(&point_light_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::Spotlight => todo!(), } } } @@ -976,35 +1051,17 @@ pub enum ShadowFilteringMode { None, /// Uses hardware features for 2x2 PCF. Pcf2x2, - Pcf, #[default] + Pcf, + /// Percentage-Closer Soft Shadows + /// https://developer.download.nvidia.com/shaderlibrary/docs/shadow_PCSS.pdf + /// + /// PCSS is only implemented for directional lights. Use PCF for point and spot lights instead. + /// PCSS is expensive per-frame, so it has not been implemented for them. If you use this for + /// point and/or spot lights, the renderer will fall back to PCF. 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; diff --git a/lyra-game/src/render/light/spotlight.rs b/lyra-game/src/render/light/spotlight.rs index fa89c5a..4d5d8bf 100644 --- a/lyra-game/src/render/light/spotlight.rs +++ b/lyra-game/src/render/light/spotlight.rs @@ -9,6 +9,8 @@ pub struct SpotLight { pub range: f32, pub intensity: f32, pub smoothness: f32, + /// Cutoff angle that specifies the light radius. + /// This is half of the light's FOV. pub cutoff: math::Angle, pub outer_cutoff: math::Angle, } diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 2bab59e..3d6ecc0 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -182,7 +182,6 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { if (light.light_ty == LIGHT_TY_DIRECTIONAL) { 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); @@ -190,7 +189,8 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { 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); + light_res += blinn_phong_spot_light(in.world_position, in.world_normal, light, u_material, specular_color, shadow); } } @@ -293,12 +293,12 @@ fn calc_shadow_dir_light(world_pos: vec3, world_normal: vec3, light_di } // PCSS else if pcf_samples_num > 0u && pcss_blocker_search_samples > 0u { - shadow = pcss_dir_light(xy_remapped, current_depth, map_data); + shadow = pcss_dir_light(xy_remapped, current_depth, i32(pcss_blocker_search_samples), i32(pcf_samples_num), 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); + shadow = pcf_dir_light(xy_remapped, current_depth, map_data, i32(pcf_samples_num), texel_size); } // no filtering else { @@ -352,13 +352,13 @@ fn to_atlas_frame_coords(shadow_u: LightShadowMapUniform, coords: vec2, saf } /// 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 { +fn find_blocker_distance_dir_light(tex_coords: vec2, search_samples: i32, 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 samples = i32(u_shadow_settings.pcss_blocker_search_samples); + for (var i = 0; i < search_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); @@ -373,8 +373,8 @@ fn find_blocker_distance_dir_light(tex_coords: vec2, receiver_depth: f32, b 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); +fn pcss_dir_light(tex_coords: vec2, receiver_depth: f32, pcss_blocker_samples: i32, pcf_samples_num: i32, shadow_u: LightShadowMapUniform) -> f32 { + let blocker_search = find_blocker_distance_dir_light(tex_coords, pcss_blocker_samples, receiver_depth, 0.0, shadow_u); // If no blockers were found, exit now to save in filtering if blocker_search.y == 0.0 { @@ -387,13 +387,12 @@ fn pcss_dir_light(tex_coords: vec2, receiver_depth: f32, shadow_u: LightSha // 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); + return pcf_dir_light(tex_coords, receiver_depth, shadow_u, pcf_samples_num, 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 { +fn pcf_dir_light(tex_coords: vec2, test_depth: f32, shadow_u: LightShadowMapUniform, samples_num: i32, 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); @@ -440,15 +439,10 @@ fn calc_shadow_point_light(world_pos: vec3, world_normal: vec3, light_ 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 + // only PCF, PCSS is not supported so no need to check for it 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); + shadow = pcf_point_light(frag_to_light, current_depth, uniforms, pcf_samples_num, texel_size); } // no filtering else { @@ -482,11 +476,11 @@ fn pcf_point_light(tex_coords: vec3, test_depth: f32, shadow_us: array, test_depth: f32, shadow_u: LightShadowMapUniform, samples_num: u32, uv_radius: f32) -> f32 { +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 < i32(samples_num); i++) { + 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); + 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); } @@ -494,7 +488,57 @@ fn pcf_point_light(tex_coords: vec3, test_depth: f32, shadow_us: array, 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); + } + // 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; +} fn debug_grid(in: VertexOutput) -> vec4 { let tile_index_float: vec2 = in.clip_position.xy / 16.0; @@ -574,7 +618,7 @@ fn blinn_phong_point_light(world_pos: vec3, world_norm: vec3, point_li 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; @@ -615,7 +659,8 @@ fn blinn_phong_spot_light(world_pos: vec3, world_norm: vec3, spot_ligh //// 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)); } fn calc_attenuation(light: Light, distance: f32) -> f32 { diff --git a/lyra-game/src/render/shaders/shadows.wgsl b/lyra-game/src/render/shaders/shadows.wgsl index 58be2c5..b924227 100644 --- a/lyra-game/src/render/shaders/shadows.wgsl +++ b/lyra-game/src/render/shaders/shadows.wgsl @@ -24,15 +24,10 @@ struct LightShadowMapUniform { @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, 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 From a85178eeea1cd91782f1abfee5cffecb00d2ff5a Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Fri, 9 Aug 2024 21:51:56 -0400 Subject: [PATCH 27/28] Revert "render: shadow maps and PCF for spot lights" This reverts commit 8c1738334c2ee46317fa279ea9dd8acb61a8c366. --- examples/shadows/src/main.rs | 42 ++---- lyra-game/src/render/graph/passes/shadows.rs | 137 ++++++------------- lyra-game/src/render/light/spotlight.rs | 2 - lyra-game/src/render/shaders/base.wgsl | 95 ++++--------- lyra-game/src/render/shaders/shadows.wgsl | 5 + lyra-math/src/angle.rs | 16 +-- 6 files changed, 79 insertions(+), 218 deletions(-) diff --git a/examples/shadows/src/main.rs b/examples/shadows/src/main.rs index 30ad34a..ba929aa 100644 --- a/examples/shadows/src/main.rs +++ b/examples/shadows/src/main.rs @@ -5,10 +5,10 @@ use lyra_engine::{ Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource, InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput, }, - math::{self, Angle, Quat, Transform, Vec3}, + math::{self, Quat, Transform, Vec3}, render::{ graph::{ShadowCasterSettings, ShadowFilteringMode}, - light::{directional::DirectionalLight, PointLight, SpotLight}, + light::{directional::DirectionalLight, PointLight}, }, scene::{ CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform, @@ -189,7 +189,7 @@ fn setup_scene_plugin(game: &mut Game) { light_tran, )); - /* world.spawn(( + world.spawn(( cube_mesh.clone(), PointLight { enabled: true, @@ -207,33 +207,6 @@ fn setup_scene_plugin(game: &mut Game) { 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(), )); /* world.spawn(( @@ -251,13 +224,14 @@ fn setup_scene_plugin(game: &mut Game) { let mut camera = CameraComponent::new_3d(); //camera.transform.translation += math::Vec3::new(0.0, 2.0, 10.5); - camera.transform.translation = math::Vec3::new(-1.0, -10.0, -1.5); + /* camera.transform.translation = math::Vec3::new(-3.0, -8.0, -3.0); camera.transform.rotate_x(math::Angle::Degrees(-27.0)); - camera.transform.rotate_y(math::Angle::Degrees(-90.0)); + camera.transform.rotate_y(math::Angle::Degrees(-55.0)); */ - /* camera.transform.translation = math::Vec3::new(15.0, -8.0, 1.0); + camera.transform.translation = math::Vec3::new(15.0, -8.0, 1.0); camera.transform.rotate_x(math::Angle::Degrees(-27.0)); - camera.transform.rotate_y(math::Angle::Degrees(90.0)); */ + //camera.transform.rotate_y(math::Angle::Degrees(-90.0)); + camera.transform.rotate_y(math::Angle::Degrees(90.0)); world.spawn((camera, FreeFlyCamera::default())); } diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index 5e59f59..3e207a9 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -20,7 +20,7 @@ use wgpu::util::DeviceExt; use crate::render::{ graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, - light::{directional::DirectionalLight, LightType, PointLight, SpotLight}, + light::{directional::DirectionalLight, LightType, PointLight}, resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, transform_buffer_storage::TransformBuffers, vertex::Vertex, @@ -186,7 +186,6 @@ impl ShadowMapsPass { light_type: LightType, entity: Entity, light_pos: Transform, - light_half_outer_angle: Option, are_settings_custom: bool, shadow_settings: ShadowCasterSettings, ) -> LightDepthMap { @@ -201,6 +200,17 @@ impl ShadowMapsPass { 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 => { @@ -255,49 +265,7 @@ impl ShadowMapsPass { indices[0] = uniform_index; (atlas_index, indices) } - LightType::Spotlight => { - // allocate a single frame in the shadow map atlas - let atlas_index = atlas - .pack(SHADOW_SIZE.x as _, SHADOW_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( - //Angle::Degrees(90.0).to_radians(), - (light_half_outer_angle.unwrap() * 2.0).to_radians(), - aspect, - shadow_settings.near_plane, - shadow_settings.far_plane, - ); - - let light_trans = light_pos.translation; - let forward = light_pos.forward(); - let up = light_pos.up(); - let view = glam::Mat4::look_to_rh(light_trans, forward, up); - - let light_proj = projection * 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::Spotlight => todo!(), LightType::Point => { let aspect = SHADOW_SIZE.x as f32 / SHADOW_SIZE.y as f32; let projection = glam::Mat4::perspective_rh( @@ -658,31 +626,6 @@ impl Node for ShadowMapsPass { LightType::Directional, 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, ); @@ -706,7 +649,6 @@ impl Node for ShadowMapsPass { LightType::Point, entity, *pos, - None, custom_settings, shadow_settings, ); @@ -844,7 +786,7 @@ impl Node for ShadowMapsPass { &frame, light_depth_map.uniform_index[0] as _, ); - }, + } LightType::Point => { pass.set_pipeline(&point_light_pipeline); @@ -864,25 +806,8 @@ impl Node for ShadowMapsPass { ui as _, ); } - }, - LightType::Spotlight => { - pass.set_pipeline(&pipeline); - //pass.set_pipeline(&point_light_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::Spotlight => todo!(), } } } @@ -1051,17 +976,35 @@ pub enum ShadowFilteringMode { None, /// Uses hardware features for 2x2 PCF. Pcf2x2, - #[default] Pcf, - /// Percentage-Closer Soft Shadows - /// https://developer.download.nvidia.com/shaderlibrary/docs/shadow_PCSS.pdf - /// - /// PCSS is only implemented for directional lights. Use PCF for point and spot lights instead. - /// PCSS is expensive per-frame, so it has not been implemented for them. If you use this for - /// point and/or spot lights, the renderer will fall back to 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; diff --git a/lyra-game/src/render/light/spotlight.rs b/lyra-game/src/render/light/spotlight.rs index 4d5d8bf..fa89c5a 100644 --- a/lyra-game/src/render/light/spotlight.rs +++ b/lyra-game/src/render/light/spotlight.rs @@ -9,8 +9,6 @@ pub struct SpotLight { pub range: f32, pub intensity: f32, pub smoothness: f32, - /// Cutoff angle that specifies the light radius. - /// This is half of the light's FOV. pub cutoff: math::Angle, pub outer_cutoff: math::Angle, } diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 3d6ecc0..2bab59e 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -182,6 +182,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { if (light.light_ty == LIGHT_TY_DIRECTIONAL) { 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); @@ -189,8 +190,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { 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) { - let shadow = calc_shadow_spot_light(in.world_position, in.world_normal, light_dir, light); - light_res += blinn_phong_spot_light(in.world_position, in.world_normal, light, u_material, specular_color, shadow); + light_res += blinn_phong_spot_light(in.world_position, in.world_normal, light, u_material, specular_color); } } @@ -293,12 +293,12 @@ fn calc_shadow_dir_light(world_pos: vec3, world_normal: vec3, light_di } // PCSS else if pcf_samples_num > 0u && pcss_blocker_search_samples > 0u { - shadow = pcss_dir_light(xy_remapped, current_depth, i32(pcss_blocker_search_samples), i32(pcf_samples_num), map_data); + 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, i32(pcf_samples_num), texel_size); + shadow = pcf_dir_light(xy_remapped, current_depth, map_data, texel_size); } // no filtering else { @@ -352,13 +352,13 @@ fn to_atlas_frame_coords(shadow_u: LightShadowMapUniform, coords: vec2, saf } /// Find the average blocker distance for a directiona llight -fn find_blocker_distance_dir_light(tex_coords: vec2, search_samples: i32, receiver_depth: f32, bias: f32, shadow_u: LightShadowMapUniform) -> vec2 { +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 < search_samples; i++) { + 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); @@ -373,8 +373,8 @@ fn find_blocker_distance_dir_light(tex_coords: vec2, search_samples: i32, r return vec2(avg_dist / b, b); } -fn pcss_dir_light(tex_coords: vec2, receiver_depth: f32, pcss_blocker_samples: i32, pcf_samples_num: i32, shadow_u: LightShadowMapUniform) -> f32 { - let blocker_search = find_blocker_distance_dir_light(tex_coords, pcss_blocker_samples, receiver_depth, 0.0, shadow_u); +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 { @@ -387,12 +387,13 @@ fn pcss_dir_light(tex_coords: vec2, receiver_depth: f32, pcss_blocker_sampl // 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, pcf_samples_num, uv_radius); + 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, samples_num: i32, uv_radius: f32) -> f32 { +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); @@ -439,10 +440,15 @@ fn calc_shadow_point_light(world_pos: vec3, world_normal: vec3, light_ 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); } - // only PCF, PCSS is not supported so no need to check for it + // 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, texel_size); + 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 { @@ -476,11 +482,11 @@ fn pcf_point_light(tex_coords: vec3, test_depth: f32, shadow_us: array, test_depth: f32, shadow_u: LightShadowMapUniform, samples_num: i32, uv_radius: f32) -> f32 { +/*fn pcf_point_light(tex_coords: vec2, test_depth: f32, shadow_u: LightShadowMapUniform, samples_num: u32, uv_radius: f32) -> f32 { var shadow = 0.0; - for (var i = 0; i < samples_num; i++) { + for (var i = 0; i < i32(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); + let new_coords = to_atlas_frame_coords(shadow_u, offset); shadow += textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas_compare, new_coords, test_depth); } @@ -488,57 +494,7 @@ fn pcf_spot_light(tex_coords: vec2, test_depth: f32, shadow_u: LightShadowM // clamp shadow to [0; 1] return saturate(shadow); -} - -fn calc_shadow_spot_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); - } - // 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; -} +}*/ fn debug_grid(in: VertexOutput) -> vec4 { let tile_index_float: vec2 = in.clip_position.xy / 16.0; @@ -618,7 +574,7 @@ fn blinn_phong_point_light(world_pos: vec3, world_norm: vec3, point_li 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, shadow: f32) -> vec3 { +fn blinn_phong_spot_light(world_pos: vec3, world_norm: vec3, spot_light: Light, material: Material, specular_factor: vec3) -> vec3 { let light_color = spot_light.color; let light_pos = spot_light.position; let camera_view_pos = u_camera.position; @@ -659,8 +615,7 @@ fn blinn_phong_spot_light(world_pos: vec3, world_norm: vec3, spot_ligh //// end of spot light attenuation //// - //return /*ambient_color +*/ diffuse_color + specular_color; - return (shadow * (diffuse_color + specular_color)); + return /*ambient_color +*/ diffuse_color + specular_color; } fn calc_attenuation(light: Light, distance: f32) -> f32 { diff --git a/lyra-game/src/render/shaders/shadows.wgsl b/lyra-game/src/render/shaders/shadows.wgsl index b924227..58be2c5 100644 --- a/lyra-game/src/render/shaders/shadows.wgsl +++ b/lyra-game/src/render/shaders/shadows.wgsl @@ -24,10 +24,15 @@ struct LightShadowMapUniform { @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, diff --git a/lyra-math/src/angle.rs b/lyra-math/src/angle.rs index 71ef507..30d541c 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, Copy, Debug)] +#[derive(Clone, Debug)] pub enum Angle { Degrees(f32), Radians(f32), @@ -68,18 +68,4 @@ 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 From 8545e7e27d1b33c2963d6cd9854f74893fda197e Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Fri, 9 Aug 2024 22:01:57 -0400 Subject: [PATCH 28/28] render: rewrite PCF for spot lights to somehow fix PCSS directional lights --- examples/shadows/src/main.rs | 50 ++++++---- lyra-game/src/render/graph/passes/shadows.rs | 100 ++++++++++++++++++- lyra-game/src/render/shaders/base.wgsl | 74 ++++++++++++-- lyra-math/src/angle.rs | 16 ++- 4 files changed, 206 insertions(+), 34 deletions(-) diff --git a/examples/shadows/src/main.rs b/examples/shadows/src/main.rs index ba929aa..1391c6f 100644 --- a/examples/shadows/src/main.rs +++ b/examples/shadows/src/main.rs @@ -8,7 +8,7 @@ use lyra_engine::{ math::{self, Quat, Transform, Vec3}, render::{ graph::{ShadowCasterSettings, ShadowFilteringMode}, - light::{directional::DirectionalLight, PointLight}, + light::{directional::DirectionalLight, PointLight, SpotLight}, }, scene::{ CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform, @@ -184,12 +184,15 @@ fn setup_scene_plugin(game: &mut Game) { }, 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(( + /* world.spawn(( cube_mesh.clone(), PointLight { enabled: true, @@ -207,31 +210,40 @@ fn setup_scene_plugin(game: &mut Game) { Quat::IDENTITY, Vec3::new(0.5, 0.5, 0.5), ), - )); + )); */ - /* world.spawn(( - //cube_mesh.clone(), - PointLight { + 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(0.278, 0.984, 0.0), - intensity: 2.0, - range: 9.0, + color: Vec3::new(1.0, 0.0, 0.0), + intensity: 3.0, + range: 4.5, + //cutoff: math::Angle::Degrees(45.0), ..Default::default() }, - Transform::from_xyz(-0.5, 2.0, -5.0), - )); */ + /* 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(0.0, 2.0, 10.5); - /* camera.transform.translation = math::Vec3::new(-3.0, -8.0, -3.0); + 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(-55.0)); */ - - camera.transform.translation = math::Vec3::new(15.0, -8.0, 1.0); - camera.transform.rotate_x(math::Angle::Degrees(-27.0)); - //camera.transform.rotate_y(math::Angle::Degrees(-90.0)); - camera.transform.rotate_y(math::Angle::Degrees(90.0)); + camera.transform.rotate_y(math::Angle::Degrees(-90.0)); world.spawn((camera, FreeFlyCamera::default())); } diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index 3e207a9..3df7011 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -20,7 +20,7 @@ use wgpu::util::DeviceExt; use crate::render::{ graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, - light::{directional::DirectionalLight, LightType, PointLight}, + light::{directional::DirectionalLight, LightType, PointLight, SpotLight}, resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, transform_buffer_storage::TransformBuffers, vertex::Vertex, @@ -186,6 +186,7 @@ impl ShadowMapsPass { light_type: LightType, entity: Entity, light_pos: Transform, + light_half_outer_angle: Option, are_settings_custom: bool, shadow_settings: ShadowCasterSettings, ) -> LightDepthMap { @@ -265,7 +266,58 @@ impl ShadowMapsPass { indices[0] = uniform_index; (atlas_index, indices) } - LightType::Spotlight => todo!(), + 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( @@ -626,6 +678,7 @@ impl Node for ShadowMapsPass { LightType::Directional, entity, *pos, + None, custom_settings, shadow_settings, ); @@ -649,6 +702,31 @@ impl Node for ShadowMapsPass { 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, ); @@ -807,7 +885,23 @@ impl Node for ShadowMapsPass { ); } } - LightType::Spotlight => todo!(), + 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 _, + ); + }, } } } diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 2bab59e..0b2f233 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -190,7 +190,8 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { 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); } } @@ -482,11 +483,62 @@ fn pcf_point_light(tex_coords: vec3, test_depth: f32, shadow_us: array, test_depth: f32, shadow_u: LightShadowMapUniform, samples_num: u32, uv_radius: f32) -> f32 { +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; - for (var i = 0; i < i32(samples_num); i++) { + // 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); + 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); } @@ -494,7 +546,7 @@ fn pcf_point_light(tex_coords: vec3, test_depth: f32, shadow_us: array vec4 { let tile_index_float: vec2 = in.clip_position.xy / 16.0; @@ -574,7 +626,7 @@ fn blinn_phong_point_light(world_pos: vec3, world_norm: vec3, point_li 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; @@ -609,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-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