From 08ddaa2c5dcca8c9c838ff9a73b7ff8ffb09103c Mon Sep 17 00:00:00 2001 From: Jonas H Date: Tue, 3 Mar 2026 19:30:41 +0100 Subject: [PATCH] MAJOR rendering overhaul. Snow deformation, persistent light, flowmap out. Also ECS architexture overhaul --- build.rs | 3 +- src/bundles/camera.rs | 25 + src/bundles/mod.rs | 12 + src/bundles/player.rs | 207 +++ src/bundles/spotlight.rs | 53 + src/{ => bundles}/terrain.rs | 130 +- src/camera.rs | 51 - src/components/lights/directional.rs | 18 - src/components/lights/mod.rs | 2 - src/components/lights/point.rs | 14 - src/components/lights/spot.rs | 8 +- src/components/mesh.rs | 5 +- src/components/mod.rs | 4 +- src/components/noclip.rs | 1 + src/components/player_tag.rs | 2 - src/components/state_machine.rs | 1 - src/components/tree_tag.rs | 1 - src/debug/collider_debug.rs | 14 +- src/debug/mod.rs | 2 - src/debug/noclip.rs | 59 - src/draw.rs | 2 +- src/{ => loaders}/empty.rs | 5 +- src/{ => loaders}/heightmap.rs | 0 src/{light.rs => loaders/lights.rs} | 34 +- src/{ => loaders}/mesh.rs | 3 - src/loaders/mod.rs | 6 + src/{space.rs => loaders/scene.rs} | 18 +- src/loaders/terrain.rs | 23 + src/main.rs | 148 +- src/render.rs | 1495 ------------------- src/render/bind_group.rs | 113 ++ src/render/mod.rs | 885 +++++++++++ src/render/pipeline.rs | 252 ++++ src/render/shadow.rs | 139 ++ src/render/types.rs | 157 ++ src/shader.rs | 140 -- src/shaders/environment.wesl | 95 -- src/shaders/main.wesl | 177 +++ src/shaders/shadow.wesl | 4 +- src/shaders/snow.wesl | 72 - src/shaders/snow_deform.wgsl | 10 +- src/shaders/snow_light_accumulation.wesl | 25 +- src/shaders/standard.wesl | 66 - src/shaders/terrain.wesl | 89 -- src/shaders/{shared.wesl => util.wesl} | 105 +- src/snow.rs | 465 ++++-- src/snow_light.rs | 72 +- src/systems/camera.rs | 26 + src/systems/input.rs | 9 +- src/systems/mod.rs | 11 +- src/systems/noclip.rs | 25 + src/{player.rs => systems/player_states.rs} | 240 +-- src/systems/render.rs | 6 +- src/systems/snow.rs | 13 + src/systems/tree_dissolve.rs | 28 +- src/world.rs | 630 +------- 56 files changed, 2737 insertions(+), 3463 deletions(-) create mode 100644 src/bundles/camera.rs create mode 100644 src/bundles/mod.rs create mode 100644 src/bundles/player.rs create mode 100644 src/bundles/spotlight.rs rename src/{ => bundles}/terrain.rs (50%) delete mode 100644 src/camera.rs delete mode 100644 src/components/lights/directional.rs delete mode 100644 src/components/lights/point.rs create mode 100644 src/components/noclip.rs delete mode 100644 src/components/player_tag.rs delete mode 100644 src/components/state_machine.rs delete mode 100644 src/components/tree_tag.rs delete mode 100644 src/debug/noclip.rs rename src/{ => loaders}/empty.rs (94%) rename src/{ => loaders}/heightmap.rs (100%) rename src/{light.rs => loaders/lights.rs} (80%) rename src/{ => loaders}/mesh.rs (99%) create mode 100644 src/loaders/mod.rs rename src/{space.rs => loaders/scene.rs} (79%) create mode 100644 src/loaders/terrain.rs delete mode 100644 src/render.rs create mode 100644 src/render/bind_group.rs create mode 100644 src/render/mod.rs create mode 100644 src/render/pipeline.rs create mode 100644 src/render/shadow.rs create mode 100644 src/render/types.rs delete mode 100644 src/shader.rs delete mode 100644 src/shaders/environment.wesl create mode 100644 src/shaders/main.wesl delete mode 100644 src/shaders/snow.wesl delete mode 100644 src/shaders/standard.wesl delete mode 100644 src/shaders/terrain.wesl rename src/shaders/{shared.wesl => util.wesl} (74%) create mode 100644 src/systems/noclip.rs rename src/{player.rs => systems/player_states.rs} (62%) create mode 100644 src/systems/snow.rs diff --git a/build.rs b/build.rs index 543f09d..98da93e 100644 --- a/build.rs +++ b/build.rs @@ -1,7 +1,6 @@ fn main() { let wesl = wesl::Wesl::new("src/shaders"); - wesl.build_artifact(&"package::standard".parse().unwrap(), "standard"); + wesl.build_artifact(&"package::main".parse().unwrap(), "main"); wesl.build_artifact(&"package::shadow".parse().unwrap(), "shadow"); - wesl.build_artifact(&"package::terrain".parse().unwrap(), "terrain"); } diff --git a/src/bundles/camera.rs b/src/bundles/camera.rs new file mode 100644 index 0000000..3127ae7 --- /dev/null +++ b/src/bundles/camera.rs @@ -0,0 +1,25 @@ +use glam::Vec3; + +use crate::bundles::Bundle; +use crate::components::CameraComponent; +use crate::entity::EntityHandle; +use crate::render; +use crate::world::{Transform, World}; + +pub struct CameraBundle +{ + pub position: Vec3, +} + +impl Bundle for CameraBundle +{ + fn spawn(self, world: &mut World) -> Result + { + let camera_entity = world.spawn(); + let camera_component = CameraComponent::new(render::aspect_ratio()); + let transform = Transform::from_position(self.position); + world.cameras.insert(camera_entity, camera_component); + world.transforms.insert(camera_entity, transform); + Ok(camera_entity) + } +} diff --git a/src/bundles/mod.rs b/src/bundles/mod.rs new file mode 100644 index 0000000..8bd64c6 --- /dev/null +++ b/src/bundles/mod.rs @@ -0,0 +1,12 @@ +pub mod camera; +pub mod player; +pub mod spotlight; +pub mod terrain; + +use crate::entity::EntityHandle; +use crate::world::World; + +pub trait Bundle +{ + fn spawn(self, world: &mut World) -> Result; +} diff --git a/src/bundles/player.rs b/src/bundles/player.rs new file mode 100644 index 0000000..f9a255a --- /dev/null +++ b/src/bundles/player.rs @@ -0,0 +1,207 @@ +use std::f32::consts::PI; +use std::rc::Rc; + +use glam::Vec3; +use rapier3d::control::{CharacterAutostep, KinematicCharacterController}; +use rapier3d::prelude::{ColliderBuilder, RigidBodyBuilder}; + +use crate::bundles::Bundle; +use crate::components::jump::JumpComponent; +use crate::components::lights::spot::SpotlightComponent; +use crate::components::{InputComponent, MeshComponent, MovementComponent, PhysicsComponent}; +use crate::entity::EntityHandle; +use crate::loaders::mesh::Mesh; +use crate::physics::PhysicsManager; +use crate::render::Pipeline; +use crate::state::StateMachine; +use crate::systems::player_states::{ + PlayerFallingState, PlayerIdleState, PlayerJumpingState, PlayerWalkingState, +}; +use crate::world::{Transform, World}; + +pub struct PlayerBundle +{ + pub position: Vec3, +} + +impl Bundle for PlayerBundle +{ + fn spawn(self, world: &mut World) -> Result + { + let entity = world.spawn(); + + let spawn_transform = Transform::from_position(self.position); + + let rigidbody = RigidBodyBuilder::kinematic_position_based() + .translation(spawn_transform.position.into()) + .build(); + let collider = ColliderBuilder::capsule_y(0.5, 0.5).build(); + let _controller = KinematicCharacterController { + slide: true, + autostep: Some(CharacterAutostep::default()), + max_slope_climb_angle: 45.0, + ..Default::default() + }; + + let rigidbody_handle = PhysicsManager::add_rigidbody(rigidbody); + let collider_handle = PhysicsManager::add_collider(collider, Some(rigidbody_handle)); + + let mesh = Mesh::load_mesh("meshes/player_mesh.glb") + .map_err(|e| format!("missing player mesh: {}", e))?; + + let falling_state = PlayerFallingState { entity }; + let idle_state = PlayerIdleState { entity }; + let walking_state = PlayerWalkingState { + entity, + enter_time_stamp: 0.0, + }; + let jumping_state = PlayerJumpingState { + entity, + enter_time_stamp: 0.0, + }; + + let mut state_machine = StateMachine::new(Box::new(falling_state)); + state_machine.add_state(walking_state); + state_machine.add_state(idle_state); + state_machine.add_state(jumping_state); + + let entity_id = entity; + + state_machine.add_transition::(move |world| { + let is_grounded = world + .movements + .with(entity_id, |m| m.movement_config.movement_context.is_floored) + .unwrap_or(false); + let has_input = world + .inputs + .with(entity_id, |i| i.move_direction.length() > 0.01) + .unwrap_or(false); + is_grounded && !has_input + }); + + state_machine.add_transition::(move |world| { + let is_grounded = world + .movements + .with(entity_id, |m| m.movement_config.movement_context.is_floored) + .unwrap_or(false); + let has_input = world + .inputs + .with(entity_id, |i| i.move_direction.length() > 0.01) + .unwrap_or(false); + is_grounded && has_input + }); + + state_machine.add_transition::(move |world| { + let is_grounded = world + .movements + .with(entity_id, |m| m.movement_config.movement_context.is_floored) + .unwrap_or(false); + let has_input = world + .inputs + .with(entity_id, |i| i.move_direction.length() > 0.01) + .unwrap_or(false); + is_grounded && has_input + }); + + state_machine.add_transition::(move |world| { + let is_grounded = world + .movements + .with(entity_id, |m| m.movement_config.movement_context.is_floored) + .unwrap_or(false); + let has_input = world + .inputs + .with(entity_id, |i| i.move_direction.length() > 0.01) + .unwrap_or(false); + is_grounded && !has_input + }); + + state_machine.add_transition::(move |world| { + let is_grounded = world + .movements + .with(entity_id, |m| m.movement_config.movement_context.is_floored) + .unwrap_or(false); + !is_grounded + }); + + state_machine.add_transition::(move |world| { + let is_grounded = world + .movements + .with(entity_id, |m| m.movement_config.movement_context.is_floored) + .unwrap_or(false); + !is_grounded + }); + + state_machine.add_transition::(move |world| { + let is_grounded = world + .movements + .with(entity_id, |m| m.movement_config.movement_context.is_floored) + .unwrap_or(false); + let jump_pressed = world + .inputs + .with(entity_id, |i| i.jump_just_pressed) + .unwrap_or(false); + is_grounded && jump_pressed + }); + + state_machine.add_transition::(move |world| { + let is_grounded = world + .movements + .with(entity_id, |m| m.movement_config.movement_context.is_floored) + .unwrap_or(false); + let jump_pressed = world + .inputs + .with(entity_id, |i| i.jump_just_pressed) + .unwrap_or(false); + is_grounded && jump_pressed + }); + + state_machine.add_transition::(move |world| { + world + .jumps + .with(entity_id, |jump| { + jump.jump_config.jump_context.duration >= jump.jump_config.jump_duration + }) + .unwrap_or(true) + }); + + world.transforms.insert(entity, spawn_transform); + world.movements.insert(entity, MovementComponent::new()); + world.jumps.insert(entity, JumpComponent::new()); + world.inputs.insert(entity, InputComponent::default()); + world.physics.insert( + entity, + PhysicsComponent { + rigidbody: rigidbody_handle, + collider: Some(collider_handle), + }, + ); + world.meshes.insert( + entity, + MeshComponent { + mesh: Rc::new(mesh), + pipeline: Pipeline::Standard, + instance_buffer: None, + num_instances: 1, + tile_scale: 4.0, + enable_dissolve: false, + enable_snow_light: false, + }, + ); + world.player_tags.insert(entity, ()); + world.state_machines.insert(entity, state_machine); + + let outer_angle = PI / 2.0 * 0.9; + world.spotlights.insert( + entity, + SpotlightComponent::new( + Vec3::new(1.0, 2.5, 1.0), + Vec3::new(0.0, -1.0, 0.0), + 100.0, + outer_angle * 0.5, + outer_angle, + ), + ); + + Ok(entity) + } +} diff --git a/src/bundles/spotlight.rs b/src/bundles/spotlight.rs new file mode 100644 index 0000000..2dba3a1 --- /dev/null +++ b/src/bundles/spotlight.rs @@ -0,0 +1,53 @@ +use glam::Vec3; + +use crate::bundles::Bundle; +use crate::components::RotateComponent; +use crate::entity::EntityHandle; +use crate::loaders::lights::LightData; +use crate::world::{Transform, World}; + +pub struct SpotlightBundle +{ + pub light_data: LightData, +} + +impl Bundle for SpotlightBundle +{ + fn spawn(self, world: &mut World) -> Result + { + let entity = world.spawn(); + let transform = Transform::from_matrix(self.light_data.transform); + world.transforms.insert(entity, transform); + world.spotlights.insert(entity, self.light_data.component); + if let Some(tag) = self.light_data.tag + { + if tag == "lighthouse" + { + world + .rotates + .insert(entity, RotateComponent::new(Vec3::Y, 1.0)); + } + } + Ok(entity) + } +} + +pub fn spawn_spotlights(world: &mut World, spotlights: Vec) +{ + for light_data in spotlights + { + let entity = world.spawn(); + let transform = Transform::from_matrix(light_data.transform); + world.transforms.insert(entity, transform); + world.spotlights.insert(entity, light_data.component); + if let Some(tag) = light_data.tag + { + if tag == "lighthouse" + { + world + .rotates + .insert(entity, RotateComponent::new(Vec3::Y, 1.0)); + } + } + } +} diff --git a/src/terrain.rs b/src/bundles/terrain.rs similarity index 50% rename from src/terrain.rs rename to src/bundles/terrain.rs index c63ac5c..87a0c36 100644 --- a/src/terrain.rs +++ b/src/bundles/terrain.rs @@ -1,22 +1,16 @@ use std::rc::Rc; -use exr::prelude::{ReadChannels, ReadLayers}; use glam::Vec2; -use nalgebra::{vector, DMatrix}; -use rapier3d::{ - math::Isometry, - prelude::{ColliderBuilder, RigidBodyBuilder}, -}; -use wesl::Wesl; +use nalgebra::vector; +use rapier3d::prelude::{ColliderBuilder, RigidBodyBuilder}; -use crate::{ - components::{MeshComponent, PhysicsComponent}, - entity::EntityHandle, - mesh::{InstanceData, InstanceRaw, Mesh, Vertex}, - physics::PhysicsManager, - render, - world::{Transform, World}, -}; +use crate::components::{MeshComponent, PhysicsComponent}; +use crate::entity::EntityHandle; +use crate::loaders::mesh::{InstanceData, InstanceRaw, Mesh}; +use crate::loaders::terrain::load_heightfield_from_exr; +use crate::physics::PhysicsManager; +use crate::render; +use crate::world::{Transform, World}; pub struct TerrainConfig { @@ -46,9 +40,9 @@ impl TerrainConfig } } -pub struct Terrain; +pub struct TerrainBundle; -impl Terrain +impl TerrainBundle { pub fn spawn( world: &mut World, @@ -76,15 +70,18 @@ impl Terrain entity, MeshComponent { mesh: Rc::new(mesh), - pipeline: render::Pipeline::Terrain, + pipeline: render::Pipeline::Standard, instance_buffer: None, num_instances: 1, + tile_scale: 2.0, + enable_dissolve: false, + enable_snow_light: true, }, ); if !physics_added { - let heights = Self::load_heightfield_from_exr(&config.heightmap_path)?; + let heights = load_heightfield_from_exr(&config.heightmap_path)?; let height_scale = 1.0; let scale = vector![config.size.x, height_scale, config.size.y]; @@ -137,9 +134,12 @@ impl Terrain entity, MeshComponent { mesh: Rc::new(mesh), - pipeline: render::Pipeline::Environment, + pipeline: render::Pipeline::Standard, instance_buffer: Some(instance_buffer), num_instances: num_instances as u32, + tile_scale: 4.0, + enable_dissolve: true, + enable_snow_light: false, }, ); } @@ -147,94 +147,4 @@ impl Terrain first_entity.ok_or_else(|| anyhow::anyhow!("No meshes found in glTF file")) } - - fn load_heightfield_from_exr(path: &str) -> anyhow::Result> - { - let image = exr::prelude::read() - .no_deep_data() - .largest_resolution_level() - .all_channels() - .all_layers() - .all_attributes() - .from_file(path)?; - - let layer = &image.layer_data[0]; - let channel = &layer.channel_data.list[0]; - - let width = layer.size.width(); - let height = layer.size.height(); - - let heights: Vec = channel.sample_data.values_as_f32().collect(); - - Ok(DMatrix::from_row_slice(height, width, &heights)) - } -} - -pub fn create_terrain_render_pipeline( - device: &wgpu::Device, - config: &wgpu::SurfaceConfiguration, - bind_group_layout: &wgpu::BindGroupLayout, -) -> wgpu::RenderPipeline -{ - let compiler = Wesl::new("src/shaders"); - let shader_source = compiler - .compile(&"package::terrain".parse().unwrap()) - .inspect_err(|e| eprintln!("WESL error: {e}")) - .unwrap() - .to_string(); - - let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Terrain Shader"), - source: wgpu::ShaderSource::Wgsl(shader_source.into()), - }); - - let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Terrain Render Pipeline Layout"), - bind_group_layouts: &[bind_group_layout], - push_constant_ranges: &[], - }); - - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Terrain Render Pipeline"), - layout: Some(&render_pipeline_layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: Some("vs_main"), - buffers: &[Vertex::desc(), InstanceRaw::desc()], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: Some("fs_main"), - targets: &[Some(wgpu::ColorTargetState { - format: config.format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: Some(wgpu::Face::Back), - polygon_mode: wgpu::PolygonMode::Fill, - unclipped_depth: false, - conservative: false, - }, - 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(), - }), - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview: None, - cache: None, - }) } diff --git a/src/camera.rs b/src/camera.rs deleted file mode 100644 index 23e1208..0000000 --- a/src/camera.rs +++ /dev/null @@ -1,51 +0,0 @@ -use bytemuck::{Pod, Zeroable}; -use glam::{Mat4, Vec3}; - -use crate::components::CameraComponent; -use crate::entity::EntityHandle; -use crate::render; -use crate::world::{Transform, World}; - -#[repr(C)] -#[derive(Clone, Copy, Pod, Zeroable)] -pub struct CameraUniforms -{ - pub model: [[f32; 4]; 4], - pub view: [[f32; 4]; 4], - pub projection: [[f32; 4]; 4], - pub light_direction: [f32; 3], - pub _padding: f32, -} - -impl CameraUniforms -{ - pub fn new(model: Mat4, view: Mat4, projection: Mat4, light_direction: Vec3) -> Self - { - Self { - model: model.to_cols_array_2d(), - view: view.to_cols_array_2d(), - projection: projection.to_cols_array_2d(), - light_direction: light_direction.to_array(), - _padding: 0.0, - } - } -} - -pub struct Camera; - -impl Camera -{ - pub fn spawn(world: &mut World, position: Vec3) -> EntityHandle - { - let camera_entity = world.spawn(); - - let camera_component = CameraComponent::new(render::aspect_ratio()); - - let transform = Transform::from_position(position); - - world.cameras.insert(camera_entity, camera_component); - world.transforms.insert(camera_entity, transform); - - camera_entity - } -} diff --git a/src/components/lights/directional.rs b/src/components/lights/directional.rs deleted file mode 100644 index 27c391e..0000000 --- a/src/components/lights/directional.rs +++ /dev/null @@ -1,18 +0,0 @@ -use glam::Vec3; - -pub struct DirectionallightComponent -{ - pub offset: Vec3, - pub direction: Vec3, -} - -impl DirectionallightComponent -{ - pub fn new(offset: Vec3, direction: Vec3) -> Self - { - Self { - offset, - direction: direction.normalize(), - } - } -} diff --git a/src/components/lights/mod.rs b/src/components/lights/mod.rs index dad9654..334444a 100644 --- a/src/components/lights/mod.rs +++ b/src/components/lights/mod.rs @@ -1,3 +1 @@ -pub mod directional; -pub mod point; pub mod spot; diff --git a/src/components/lights/point.rs b/src/components/lights/point.rs deleted file mode 100644 index 95fa91c..0000000 --- a/src/components/lights/point.rs +++ /dev/null @@ -1,14 +0,0 @@ -use glam::Vec3; - -pub struct PointlightComponent -{ - pub offset: Vec3, -} - -impl PointlightComponent -{ - pub fn new(offset: Vec3) -> Self - { - Self { offset } - } -} diff --git a/src/components/lights/spot.rs b/src/components/lights/spot.rs index 3960f7c..962925e 100644 --- a/src/components/lights/spot.rs +++ b/src/components/lights/spot.rs @@ -12,7 +12,13 @@ pub struct SpotlightComponent impl SpotlightComponent { - pub fn new(offset: Vec3, direction: Vec3, range: f32, inner_angle: f32, outer_angle: f32) -> Self + pub fn new( + offset: Vec3, + direction: Vec3, + range: f32, + inner_angle: f32, + outer_angle: f32, + ) -> Self { Self { offset, diff --git a/src/components/mesh.rs b/src/components/mesh.rs index 2a85a9e..ff6bc40 100644 --- a/src/components/mesh.rs +++ b/src/components/mesh.rs @@ -1,6 +1,6 @@ use std::rc::Rc; -use crate::mesh::Mesh; +use crate::loaders::mesh::Mesh; use crate::render::Pipeline; #[derive(Clone)] @@ -10,4 +10,7 @@ pub struct MeshComponent pub pipeline: Pipeline, pub instance_buffer: Option, pub num_instances: u32, + pub tile_scale: f32, + pub enable_dissolve: bool, + pub enable_snow_light: bool, } diff --git a/src/components/mod.rs b/src/components/mod.rs index d263049..ce140f6 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -6,11 +6,9 @@ pub mod jump; pub mod lights; pub mod mesh; pub mod movement; +pub mod noclip; pub mod physics; -pub mod player_tag; pub mod rotate; -pub mod state_machine; -pub mod tree_tag; pub use camera::CameraComponent; pub use dissolve::DissolveComponent; diff --git a/src/components/noclip.rs b/src/components/noclip.rs new file mode 100644 index 0000000..62e7ed7 --- /dev/null +++ b/src/components/noclip.rs @@ -0,0 +1 @@ +pub struct NoclipTag; diff --git a/src/components/player_tag.rs b/src/components/player_tag.rs deleted file mode 100644 index ae1e27b..0000000 --- a/src/components/player_tag.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[derive(Copy, Clone, Debug)] -pub struct PlayerTag; diff --git a/src/components/state_machine.rs b/src/components/state_machine.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/components/state_machine.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/components/tree_tag.rs b/src/components/tree_tag.rs deleted file mode 100644 index c25a7b9..0000000 --- a/src/components/tree_tag.rs +++ /dev/null @@ -1 +0,0 @@ -pub struct TreeTag; diff --git a/src/debug/collider_debug.rs b/src/debug/collider_debug.rs index 8273dab..c7132e4 100644 --- a/src/debug/collider_debug.rs +++ b/src/debug/collider_debug.rs @@ -6,7 +6,7 @@ use nalgebra::DMatrix; use rapier3d::parry::shape::HeightField; use crate::{ - mesh::{InstanceRaw, Mesh, Vertex}, + loaders::mesh::{InstanceRaw, Mesh, Vertex}, physics::PhysicsManager, render::{self, DrawCall, Pipeline}, }; @@ -143,9 +143,13 @@ pub fn render_collider_debug() -> Vec index_buffer: wireframe_box.index_buffer.clone(), num_indices: wireframe_box.num_indices, model, - pipeline: Pipeline::Wireframe, + pipeline: Pipeline::Standard, instance_buffer: Some(instance_buffer), num_instances: 1, + tile_scale: 4.0, + enable_dissolve: false, + enable_snow_light: false, + displacement_bind_group: None, }); } }); @@ -172,9 +176,13 @@ pub fn render_collider_debug() -> Vec index_buffer: heightfield_mesh.index_buffer.clone(), num_indices: heightfield_mesh.num_indices, model: Mat4::IDENTITY, - pipeline: Pipeline::Wireframe, + pipeline: Pipeline::Standard, instance_buffer: Some(instance_buffer), num_instances: 1, + tile_scale: 4.0, + enable_dissolve: false, + enable_snow_light: false, + displacement_bind_group: None, }); } }); diff --git a/src/debug/mod.rs b/src/debug/mod.rs index 1ca8372..db69d25 100644 --- a/src/debug/mod.rs +++ b/src/debug/mod.rs @@ -1,3 +1 @@ pub mod collider_debug; - -pub use collider_debug::render_collider_debug; diff --git a/src/debug/noclip.rs b/src/debug/noclip.rs deleted file mode 100644 index d2f6073..0000000 --- a/src/debug/noclip.rs +++ /dev/null @@ -1,59 +0,0 @@ -use glam::Vec3; - -use crate::camera::Camera; -use crate::utility::input::InputState; -use crate::world::World; - -pub fn update_noclip_camera(camera: &mut Camera, input_state: &InputState, delta: f32) -{ - camera.update_rotation(input_state.mouse_delta, 0.0008); - - let mut input_vec = Vec3::ZERO; - - if input_state.w - { - input_vec.z += 1.0; - } - if input_state.s - { - input_vec.z -= 1.0; - } - if input_state.d - { - input_vec.x += 1.0; - } - if input_state.a - { - input_vec.x -= 1.0; - } - if input_state.space - { - input_vec.y += 1.0; - } - - if input_vec.length_squared() > 0.0 - { - input_vec = input_vec.normalize(); - } - - let mut speed = 10.0 * delta; - if input_state.shift - { - speed *= 2.0; - } - - camera.update_noclip(input_vec, speed); -} - -pub fn update_follow_camera(camera: &mut Camera, world: &World, input_state: &InputState) -{ - let player_entities = world.player_tags.all(); - - if let Some(&player_entity) = player_entities.first() - { - if let Some(player_transform) = world.transforms.get(player_entity) - { - camera.update_follow(player_transform.position, input_state.mouse_delta, 0.0008); - } - } -} diff --git a/src/draw.rs b/src/draw.rs index 0761904..01d21b5 100644 --- a/src/draw.rs +++ b/src/draw.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::rc::Rc; use crate::entity::EntityHandle; -use crate::mesh::Mesh; +use crate::loaders::mesh::Mesh; use crate::render::{DrawCall, Pipeline}; pub type DrawHandle = usize; diff --git a/src/empty.rs b/src/loaders/empty.rs similarity index 94% rename from src/empty.rs rename to src/loaders/empty.rs index 34eb0fc..6d7e947 100644 --- a/src/empty.rs +++ b/src/loaders/empty.rs @@ -80,10 +80,7 @@ impl Empties Self::load_gltf_empties(path) } - pub fn get_empty_by_name( - gltf_path: &str, - name: &str, - ) -> anyhow::Result> + pub fn get_empty_by_name(gltf_path: &str, name: &str) -> anyhow::Result> { let empties = Self::load_empties(gltf_path) .map_err(|e| anyhow::anyhow!("Failed to load empty nodes: {}", e))?; diff --git a/src/heightmap.rs b/src/loaders/heightmap.rs similarity index 100% rename from src/heightmap.rs rename to src/loaders/heightmap.rs diff --git a/src/light.rs b/src/loaders/lights.rs similarity index 80% rename from src/light.rs rename to src/loaders/lights.rs index 4c9399f..c62e8d2 100644 --- a/src/light.rs +++ b/src/loaders/lights.rs @@ -1,11 +1,7 @@ use glam::{Mat4, Vec3}; -use gltf::json::Extras; -use std::{ops::Deref, path::Path}; +use std::path::Path; -use crate::{ - components::lights::spot::SpotlightComponent, - world::{Transform, World}, -}; +use crate::components::lights::spot::SpotlightComponent; pub struct LightData { @@ -104,7 +100,7 @@ impl Lights transform: global_transform, tag, }); - }, + } } } @@ -126,28 +122,4 @@ impl Lights { crate::render::with_device(|_device| Lights::load_gltf_lights(path)) } - - pub fn spawn_lights(world: &mut World, spotlights: Vec) - { - use crate::components::RotateComponent; - - for light_data in spotlights - { - let entity = world.spawn(); - let transform = Transform::from_matrix(light_data.transform); - - world.transforms.insert(entity, transform); - world.spotlights.insert(entity, light_data.component); - - if let Some(tag) = light_data.tag - { - if tag == "lighthouse" - { - world - .rotates - .insert(entity, RotateComponent::new(Vec3::Y, 1.0)); - } - } - } - } } diff --git a/src/mesh.rs b/src/loaders/mesh.rs similarity index 99% rename from src/mesh.rs rename to src/loaders/mesh.rs index 26fb8ac..8850fc8 100644 --- a/src/mesh.rs +++ b/src/loaders/mesh.rs @@ -1,9 +1,6 @@ use bytemuck::{Pod, Zeroable}; use glam::{Mat4, Quat, Vec3}; use std::path::Path; -use std::rc::Rc; - -use crate::utility::transform::Transform; #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] diff --git a/src/loaders/mod.rs b/src/loaders/mod.rs new file mode 100644 index 0000000..e6f4dae --- /dev/null +++ b/src/loaders/mod.rs @@ -0,0 +1,6 @@ +pub mod empty; +pub mod heightmap; +pub mod lights; +pub mod mesh; +pub mod scene; +pub mod terrain; diff --git a/src/space.rs b/src/loaders/scene.rs similarity index 79% rename from src/space.rs rename to src/loaders/scene.rs index 4e68b61..d0e917b 100644 --- a/src/space.rs +++ b/src/loaders/scene.rs @@ -1,12 +1,10 @@ use anyhow::Result; use glam::Vec3; -use crate::{ - empty::Empties, - light::{LightData, Lights}, - mesh::{InstanceData, Mesh}, - render, -}; +use crate::loaders::empty::Empties; +use crate::loaders::lights::{LightData, Lights}; +use crate::loaders::mesh::{InstanceData, Mesh}; +use crate::render; pub const CAMERA_SPAWN_OFFSET: Vec3 = Vec3::new(15.0, 15.0, 15.0); @@ -21,9 +19,8 @@ impl Space { pub fn load_space(gltf_path: &str) -> Result { - let mesh_data = render::with_device(|device| { - Mesh::load_gltf_with_instances(device, gltf_path) - })?; + let mesh_data = + render::with_device(|device| Mesh::load_gltf_with_instances(device, gltf_path))?; let lights = Lights::load_lights(gltf_path) .map_err(|e| anyhow::anyhow!("Failed to load lights: {}", e))?; @@ -45,7 +42,8 @@ impl Space if let Some(empty_node) = empty { - let (_scale, _rotation, translation) = empty_node.transform.to_scale_rotation_translation(); + let (_scale, _rotation, translation) = + empty_node.transform.to_scale_rotation_translation(); Ok(translation) } else diff --git a/src/loaders/terrain.rs b/src/loaders/terrain.rs new file mode 100644 index 0000000..994ccc1 --- /dev/null +++ b/src/loaders/terrain.rs @@ -0,0 +1,23 @@ +use exr::prelude::{ReadChannels, ReadLayers}; +use nalgebra::DMatrix; + +pub fn load_heightfield_from_exr(path: &str) -> anyhow::Result> +{ + let image = exr::prelude::read() + .no_deep_data() + .largest_resolution_level() + .all_channels() + .all_layers() + .all_attributes() + .from_file(path)?; + + let layer = &image.layer_data[0]; + let channel = &layer.channel_data.list[0]; + + let width = layer.size.width(); + let height = layer.size.height(); + + let heights: Vec = channel.sample_data.values_as_f32().collect(); + + Ok(DMatrix::from_row_slice(height, width, &heights)) +} diff --git a/src/main.rs b/src/main.rs index 77b4251..f504f07 100755 --- a/src/main.rs +++ b/src/main.rs @@ -1,24 +1,17 @@ -mod camera; +mod bundles; mod components; mod debug; mod draw; -mod empty; mod entity; mod event; -mod heightmap; -mod light; -mod mesh; +mod loaders; mod physics; -mod player; mod postprocess; mod render; -mod shader; mod snow; mod snow_light; -mod space; mod state; mod systems; -mod terrain; mod texture; mod utility; mod world; @@ -28,24 +21,22 @@ use std::time::{Duration, Instant}; use glam::Vec3; use render::Renderer; use utility::input::InputState; -use world::{Transform, World}; +use world::World; -use crate::camera::Camera; -use crate::components::CameraComponent; -use crate::debug::render_collider_debug; -use crate::entity::EntityHandle; -use crate::light::Lights; +use crate::bundles::camera::CameraBundle; +use crate::bundles::player::PlayerBundle; +use crate::bundles::spotlight::spawn_spotlights; +use crate::bundles::terrain::{TerrainBundle, TerrainConfig}; +use crate::bundles::Bundle; +use crate::loaders::scene::Space; use crate::physics::PhysicsManager; -use crate::player::Player; -use crate::space::Space; -use crate::systems::{ - camera_follow_system, camera_input_system, camera_noclip_system, physics_sync_system, - player_input_system, render_system, rotate_system, spotlight_sync_system, - start_camera_following, state_machine_physics_system, state_machine_system, - stop_camera_following, -}; use crate::snow::{SnowConfig, SnowLayer}; -use crate::terrain::{Terrain, TerrainConfig}; +use crate::systems::{ + camera_follow_system, camera_input_system, camera_noclip_system, camera_view_matrix, + noclip_toggle_system, physics_sync_system, player_input_system, render_system, rotate_system, + snow_system, spotlight_sync_system, start_camera_following, state_machine_physics_system, + state_machine_system, +}; use crate::utility::time::Time; fn main() -> Result<(), Box> @@ -68,11 +59,21 @@ fn main() -> Result<(), Box> let player_spawn = space.player_spawn; let camera_spawn = space.camera_spawn_position(); + let tree_positions: Vec = space + .mesh_data + .iter() + .flat_map(|(_, instances)| instances.iter().map(|inst| inst.position)) + .collect(); + let mut world = World::new(); - let _player_entity = Player::spawn(&mut world, player_spawn); - let _terrain_entity = Terrain::spawn(&mut world, space.mesh_data, &terrain_config)?; - Lights::spawn_lights(&mut world, space.spotlights); + let _player_entity = PlayerBundle { + position: player_spawn, + } + .spawn(&mut world) + .unwrap(); + let _terrain_entity = TerrainBundle::spawn(&mut world, space.mesh_data, &terrain_config)?; + spawn_spotlights(&mut world, space.spotlights); render::set_terrain_data(); @@ -83,15 +84,23 @@ fn main() -> Result<(), Box> ); let snow_config = SnowConfig::default(); - let snow_layer = SnowLayer::load(&mut world, &snow_config)?; + let mut snow_layer = SnowLayer::load(&snow_config)?; + for pos in &tree_positions + { + snow_layer.deform_at_position(*pos, 5.0, 50.0); + } println!("Snow layer loaded successfully"); render::set_snow_depth(&snow_layer.depth_texture_view); let mut noclip_mode = true; - let camera_entity = Camera::spawn(&mut world, camera_spawn); - if noclip_mode == false + let camera_entity = CameraBundle { + position: camera_spawn, + } + .spawn(&mut world) + .unwrap(); + if !noclip_mode { start_camera_following(&mut world, camera_entity); } @@ -135,19 +144,7 @@ fn main() -> Result<(), Box> input_state.process_post_events(); - if input_state.noclip_just_pressed - { - noclip_mode = !noclip_mode; - - if noclip_mode - { - stop_camera_following(&mut world, camera_entity); - } - else - { - start_camera_following(&mut world, camera_entity); - } - } + noclip_toggle_system(&mut world, &input_state, camera_entity, &mut noclip_mode); camera_input_system(&mut world, &input_state); @@ -181,34 +178,31 @@ fn main() -> Result<(), Box> let spotlights = spotlight_sync_system(&world); render::update_spotlights(spotlights); - let mut draw_calls = render_system(&world); - draw_calls.extend(render_collider_debug()); + snow_system(&world, &mut snow_layer, noclip_mode); - if let Some((camera_entity, camera_component)) = world.cameras.get_active() + let mut draw_calls = render_system(&world); + draw_calls.extend(snow_layer.get_draw_calls()); + + if let Some((camera_entity, camera_component)) = world.active_camera() { if let Some(camera_transform) = world.transforms.get(camera_entity) { - let view = - get_view_matrix(&world, camera_entity, camera_transform, camera_component); - let projection = camera_component.projection_matrix(); + let player_pos = world.player_position(); - let player_pos = world - .player_tags - .all() - .first() - .and_then(|e| world.transforms.get(*e)) - .map(|t| t.position) - .unwrap_or(Vec3::ZERO); + if let Some(view) = camera_view_matrix(&world) + { + let projection = camera_component.projection_matrix(); - render::render( - &view, - &projection, - camera_transform.position, - player_pos, - &draw_calls, - time, - delta, - ); + render::render( + &view, + &projection, + camera_transform.position, + player_pos, + &draw_calls, + time, + delta, + ); + } } } @@ -223,27 +217,3 @@ fn main() -> Result<(), Box> Ok(()) } - -fn get_view_matrix( - world: &World, - camera_entity: EntityHandle, - camera_transform: &Transform, - camera_component: &CameraComponent, -) -> glam::Mat4 -{ - if let Some(follow) = world.follows.get(camera_entity) - { - if let Some(target_transform) = world.transforms.get(follow.target) - { - return glam::Mat4::look_at_rh( - camera_transform.position, - target_transform.position, - Vec3::Y, - ); - } - } - - let forward = camera_component.get_forward(); - let target = camera_transform.position + forward; - glam::Mat4::look_at_rh(camera_transform.position, target, Vec3::Y) -} diff --git a/src/render.rs b/src/render.rs deleted file mode 100644 index dfd26ea..0000000 --- a/src/render.rs +++ /dev/null @@ -1,1495 +0,0 @@ -use crate::camera::CameraUniforms; -use crate::postprocess::{create_blit_pipeline, create_fullscreen_quad, LowResFramebuffer}; -use crate::shader::{create_environment_pipeline, create_render_pipeline}; -use crate::terrain::create_terrain_render_pipeline; -use crate::texture::{DitherTextures, FlowmapTexture}; -use bytemuck::{Pod, Zeroable}; -use glam::Mat4; -use std::cell::RefCell; -use wesl::Wesl; - -pub const MAX_SPOTLIGHTS: usize = 4; - -#[repr(C)] -#[derive(Clone, Copy, Pod, Zeroable, Default)] -pub struct SpotlightRaw -{ - pub position: [f32; 3], - pub inner_angle: f32, - pub direction: [f32; 3], - pub outer_angle: f32, - pub range: f32, - pub _padding: [f32; 7], -} - -pub struct Spotlight -{ - pub position: glam::Vec3, - pub direction: glam::Vec3, - pub inner_angle: f32, - pub outer_angle: f32, - pub range: f32, -} - -impl Spotlight -{ - pub fn new( - position: glam::Vec3, - direction: glam::Vec3, - inner_angle: f32, - outer_angle: f32, - range: f32, - ) -> Self - { - Self { - position, - direction: direction.normalize(), - inner_angle, - outer_angle, - range, - } - } - - pub fn to_raw(&self) -> SpotlightRaw - { - SpotlightRaw { - position: self.position.to_array(), - inner_angle: self.inner_angle, - direction: self.direction.to_array(), - outer_angle: self.outer_angle, - range: self.range, - _padding: [0.0; 7], - } - } -} - -#[repr(C)] -#[derive(Clone, Copy, Pod, Zeroable)] -struct TerrainUniforms -{ - model: [[f32; 4]; 4], - view: [[f32; 4]; 4], - projection: [[f32; 4]; 4], - light_view_projection: [[f32; 4]; 4], - camera_position: [f32; 3], - height_scale: f32, - player_position: [f32; 3], - time: f32, - shadow_bias: f32, - spotlight_count: u32, - _padding1: u32, - _padding2: u32, - spotlights: [SpotlightRaw; MAX_SPOTLIGHTS], -} - -impl TerrainUniforms -{ - fn new( - model: Mat4, - view: Mat4, - projection: Mat4, - light_view_projection: Mat4, - camera_position: glam::Vec3, - player_position: glam::Vec3, - height_scale: f32, - time: f32, - shadow_bias: f32, - spotlights: &[Spotlight], - ) -> Self - { - let mut spotlight_array = [SpotlightRaw::default(); MAX_SPOTLIGHTS]; - for (i, spotlight) in spotlights.iter().take(MAX_SPOTLIGHTS).enumerate() - { - spotlight_array[i] = spotlight.to_raw(); - } - - Self { - model: model.to_cols_array_2d(), - view: view.to_cols_array_2d(), - projection: projection.to_cols_array_2d(), - light_view_projection: light_view_projection.to_cols_array_2d(), - camera_position: camera_position.to_array(), - height_scale, - player_position: player_position.to_array(), - time, - shadow_bias, - spotlight_count: spotlights.len().min(MAX_SPOTLIGHTS) as u32, - _padding1: 0, - _padding2: 0, - spotlights: spotlight_array, - } - } -} - -#[derive(Clone, Copy)] -pub enum Pipeline -{ - Standard, - Terrain, - Environment, - Wireframe, - Snow, -} - -pub struct DrawCall -{ - pub vertex_buffer: wgpu::Buffer, - pub index_buffer: wgpu::Buffer, - pub num_indices: u32, - pub model: Mat4, - pub pipeline: Pipeline, - pub instance_buffer: Option, - pub num_instances: u32, -} - -pub struct Renderer -{ - pub device: wgpu::Device, - pub queue: wgpu::Queue, - pub surface: wgpu::Surface<'static>, - pub config: wgpu::SurfaceConfiguration, - - framebuffer: LowResFramebuffer, - render_pipeline: wgpu::RenderPipeline, - environment_pipeline: wgpu::RenderPipeline, - - uniform_buffer: wgpu::Buffer, - bind_group: wgpu::BindGroup, - - quad_vb: wgpu::Buffer, - quad_ib: wgpu::Buffer, - quad_num_indices: u32, - blit_pipeline: wgpu::RenderPipeline, - blit_bind_group: wgpu::BindGroup, - - terrain_pipeline: Option, - terrain_bind_group_layout: wgpu::BindGroupLayout, - terrain_uniform_buffer: wgpu::Buffer, - terrain_bind_group: Option, - terrain_height_scale: f32, - - shadow_pipeline: Option, - shadow_bind_group_layout: wgpu::BindGroupLayout, - shadow_bind_group: Option, - - wireframe_pipeline: wgpu::RenderPipeline, - snow_pipeline: wgpu::RenderPipeline, - snow_bind_group_layout: wgpu::BindGroupLayout, - snow_persistent_light_layout: wgpu::BindGroupLayout, - snow_bind_group: wgpu::BindGroup, - snow_persistent_light_bind_group: Option, - - pub spotlights: Vec, - pub shadow_bias: f32, - - shadow_map_texture: wgpu::Texture, - shadow_map_view: wgpu::TextureView, - shadow_map_sampler: wgpu::Sampler, - shadow_map_size: u32, - - dither_textures: Option, - flowmap_texture: Option, - blue_noise_view: wgpu::TextureView, - blue_noise_sampler: wgpu::Sampler, - - snow_light_accumulation: Option, -} - -impl Renderer -{ - pub async fn new( - window: &sdl3::video::Window, - render_scale: u32, - ) -> Result> - { - let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { - backends: wgpu::Backends::VULKAN, - ..Default::default() - }); - - let surface = unsafe { - let target = wgpu::SurfaceTargetUnsafe::from_window(window)?; - instance.create_surface_unsafe(target)? - }; - - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::HighPerformance, - compatible_surface: Some(&surface), - force_fallback_adapter: false, - }) - .await - .map_err(|_| "Failed to find adapter")?; - - let (device, queue) = adapter - .request_device(&wgpu::DeviceDescriptor::default()) - .await?; - - let size = window.size(); - let config = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: surface.get_capabilities(&adapter).formats[0], - width: size.0, - height: size.1, - present_mode: wgpu::PresentMode::Fifo, - alpha_mode: wgpu::CompositeAlphaMode::Auto, - view_formats: vec![], - desired_maximum_frame_latency: 2, - }; - - surface.configure(&device, &config); - - let low_res_width = config.width / render_scale; - let low_res_height = config.height / render_scale; - - let framebuffer = - LowResFramebuffer::new(&device, low_res_width, low_res_height, config.format); - - let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Uniform Buffer"), - size: std::mem::size_of::() as wgpu::BufferAddress, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - let dither_textures = match DitherTextures::load_octaves(&device, &queue) - { - Ok(textures) => - { - println!("Loaded dither textures successfully"); - Some(textures) - } - Err(e) => - { - eprintln!( - "Warning: Could not load dither textures: {}. Rendering may look incorrect.", - e - ); - None - } - }; - - let flowmap_texture = - match FlowmapTexture::load(&device, &queue, "textures/terrain_flowmap.exr") - { - Ok(texture) => - { - println!("Loaded terrain flowmap successfully"); - Some(texture) - } - Err(e) => - { - eprintln!( - "Warning: Could not load terrain flowmap: {}. Path lighting will not work.", - e - ); - None - } - }; - - let blue_noise_data = image::open("textures/blue_noise.png") - .expect("Failed to load blue noise texture") - .to_luma8(); - let blue_noise_size = blue_noise_data.dimensions(); - - let blue_noise_texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("Blue Noise"), - size: wgpu::Extent3d { - width: blue_noise_size.0, - height: blue_noise_size.1, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::R8Unorm, - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, - view_formats: &[], - }); - - queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture: &blue_noise_texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - &blue_noise_data, - wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(blue_noise_size.0), - rows_per_image: Some(blue_noise_size.1), - }, - wgpu::Extent3d { - width: blue_noise_size.0, - height: blue_noise_size.1, - depth_or_array_layers: 1, - }, - ); - - let blue_noise_view = - blue_noise_texture.create_view(&wgpu::TextureViewDescriptor::default()); - let blue_noise_sampler = device.create_sampler(&wgpu::SamplerDescriptor { - label: Some("Blue Noise Sampler"), - address_mode_u: wgpu::AddressMode::Repeat, - address_mode_v: wgpu::AddressMode::Repeat, - mag_filter: wgpu::FilterMode::Nearest, - min_filter: wgpu::FilterMode::Nearest, - ..Default::default() - }); - - let shadow_map_size = 4096; - let shadow_map_texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("Shadow Map"), - size: wgpu::Extent3d { - width: shadow_map_size, - height: shadow_map_size, - 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 shadow_map_view = - shadow_map_texture.create_view(&wgpu::TextureViewDescriptor::default()); - - let shadow_map_sampler = device.create_sampler(&wgpu::SamplerDescriptor { - label: Some("Shadow Map Sampler"), - 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::Nearest, - compare: Some(wgpu::CompareFunction::LessEqual), - ..Default::default() - }); - - let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Bind Group Layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - 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: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 3, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: false }, - view_dimension: wgpu::TextureViewDimension::D2Array, - multisampled: false, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 4, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 5, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: false }, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 6, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 7, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: false }, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 8, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), - count: None, - }, - ], - }); - - let bind_group = if let (Some(ref dither_tex), Some(ref flowmap)) = - (&dither_textures, &flowmap_texture) - { - device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Bind Group"), - layout: &bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: uniform_buffer.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(&shadow_map_view), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Sampler(&shadow_map_sampler), - }, - wgpu::BindGroupEntry { - binding: 3, - resource: wgpu::BindingResource::TextureView(&dither_tex.view), - }, - wgpu::BindGroupEntry { - binding: 4, - resource: wgpu::BindingResource::Sampler(&dither_tex.sampler), - }, - wgpu::BindGroupEntry { - binding: 5, - resource: wgpu::BindingResource::TextureView(&flowmap.view), - }, - wgpu::BindGroupEntry { - binding: 6, - resource: wgpu::BindingResource::Sampler(&flowmap.sampler), - }, - wgpu::BindGroupEntry { - binding: 7, - resource: wgpu::BindingResource::TextureView(&blue_noise_view), - }, - wgpu::BindGroupEntry { - binding: 8, - resource: wgpu::BindingResource::Sampler(&blue_noise_sampler), - }, - ], - }) - } - else - { - panic!("Cannot create renderer without dither textures and flowmap"); - }; - - let render_pipeline = create_render_pipeline(&device, &config, &bind_group_layout); - let environment_pipeline = - create_environment_pipeline(&device, &config, &bind_group_layout); - - let (quad_vb, quad_ib, quad_num_indices) = create_fullscreen_quad(&device); - - let blit_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Blit Bind Group Layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - }); - - let blit_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Blit Bind Group"), - layout: &blit_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&framebuffer.view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&framebuffer.sampler), - }, - ], - }); - - let blit_pipeline = create_blit_pipeline(&device, config.format, &blit_bind_group_layout); - - let shadow_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Shadow Bind Group Layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - }); - - let wireframe_pipeline = - create_wireframe_pipeline(&device, config.format, &bind_group_layout); - - let snow_persistent_light_layout = - crate::snow_light::SnowLightAccumulation::create_bind_group_layout(&device); - - let snow_pipeline = create_snow_pipeline( - &device, - config.format, - &bind_group_layout, - &snow_persistent_light_layout, - ); - - let snow_bind_group_layout = bind_group_layout.clone(); - - Ok(Self { - device, - queue, - surface, - config, - framebuffer, - render_pipeline, - environment_pipeline, - uniform_buffer: uniform_buffer.clone(), - bind_group: bind_group.clone(), - quad_vb, - quad_ib, - quad_num_indices, - blit_pipeline, - blit_bind_group, - terrain_pipeline: None, - terrain_bind_group_layout: bind_group_layout.clone(), - terrain_uniform_buffer: uniform_buffer, - terrain_bind_group: Some(bind_group.clone()), - terrain_height_scale: 10.0, - shadow_pipeline: None, - shadow_bind_group_layout, - shadow_bind_group: None, - wireframe_pipeline, - snow_pipeline, - snow_bind_group_layout, - snow_persistent_light_layout, - snow_bind_group: bind_group, - snow_persistent_light_bind_group: None, - spotlights: vec![Spotlight::new( - glam::Vec3::new(0.0, 50.0, 0.0), - glam::Vec3::new(-0.5, -1.0, 1.0).normalize(), - 0.4, - 1.0, - 100.0, - )], - shadow_bias: 0.005, - shadow_map_texture, - shadow_map_view, - shadow_map_sampler, - shadow_map_size, - dither_textures, - flowmap_texture, - blue_noise_view, - blue_noise_sampler, - snow_light_accumulation: None, - }) - } - - pub fn render( - &mut self, - view: &glam::Mat4, - projection: &glam::Mat4, - camera_position: glam::Vec3, - player_position: glam::Vec3, - draw_calls: &[DrawCall], - time: f32, - delta_time: f32, - ) - { - let light_view_projection = self.calculate_light_view_projection(); - - if let Some(ref mut snow_light_accum) = self.snow_light_accumulation - { - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Snow Light Accumulation Encoder"), - }); - - snow_light_accum.render( - &mut encoder, - &self.queue, - &self.spotlights, - delta_time, - &light_view_projection, - self.shadow_bias, - self.terrain_height_scale, - ); - - self.queue.submit(std::iter::once(encoder.finish())); - - if self.snow_persistent_light_bind_group.is_none() - { - let bind_group = snow_light_accum - .create_read_bind_group(&self.device, &self.snow_persistent_light_layout); - self.snow_persistent_light_bind_group = Some(bind_group); - } - } - - self.render_shadow_pass(draw_calls, light_view_projection, player_position, time); - - for (i, draw_call) in draw_calls.iter().enumerate() - { - match draw_call.pipeline - { - Pipeline::Standard | Pipeline::Wireframe => - { - let light_dir = if self.spotlights.is_empty() - { - glam::Vec3::new(0.0, -1.0, 0.0) - } - else - { - self.spotlights[0].direction - }; - let uniforms = - CameraUniforms::new(draw_call.model, *view, *projection, light_dir); - self.queue.write_buffer( - &self.uniform_buffer, - 0, - bytemuck::cast_slice(&[uniforms]), - ); - } - Pipeline::Snow | Pipeline::Terrain | Pipeline::Environment => - { - let uniforms = TerrainUniforms::new( - draw_call.model, - *view, - *projection, - light_view_projection, - camera_position, - player_position, - self.terrain_height_scale, - time, - self.shadow_bias, - &self.spotlights, - ); - self.queue.write_buffer( - &self.terrain_uniform_buffer, - 0, - bytemuck::cast_slice(&[uniforms]), - ); - } - } - - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Render Encoder"), - }); - - { - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("3D Render Pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &self.framebuffer.view, - resolve_target: None, - ops: wgpu::Operations { - load: if i == 0 - { - wgpu::LoadOp::Clear(wgpu::Color { - r: 0.2, - g: 0.2, - b: 0.2, - a: 1.0, - }) - } - else - { - wgpu::LoadOp::Load - }, - store: wgpu::StoreOp::Store, - }, - depth_slice: None, - })], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &self.framebuffer.depth_view, - depth_ops: Some(wgpu::Operations { - load: if i == 0 - { - wgpu::LoadOp::Clear(1.0) - } - else - { - wgpu::LoadOp::Load - }, - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - timestamp_writes: None, - occlusion_query_set: None, - }); - - let pipeline = match draw_call.pipeline - { - Pipeline::Standard => &self.render_pipeline, - Pipeline::Terrain => &self - .terrain_pipeline - .as_ref() - .expect("terrain_data_missing"), - Pipeline::Environment => &self.environment_pipeline, - Pipeline::Wireframe => &self.wireframe_pipeline, - Pipeline::Snow => &self.snow_pipeline, - }; - render_pass.set_pipeline(pipeline); - - match draw_call.pipeline - { - Pipeline::Standard | Pipeline::Wireframe => - { - render_pass.set_bind_group(0, &self.bind_group, &[]); - } - Pipeline::Terrain | Pipeline::Environment => - { - let bind_group = self - .terrain_bind_group - .as_ref() - .expect("terrain_data_missing"); - render_pass.set_bind_group(0, bind_group, &[]); - } - Pipeline::Snow => - { - render_pass.set_bind_group(0, &self.snow_bind_group, &[]); - if let Some(ref persistent_light_bind_group) = - self.snow_persistent_light_bind_group - { - render_pass.set_bind_group(1, persistent_light_bind_group, &[]); - } - } - } - - render_pass.set_vertex_buffer(0, draw_call.vertex_buffer.slice(..)); - - if let Some(ref instance_buffer) = draw_call.instance_buffer - { - render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); - } - - render_pass - .set_index_buffer(draw_call.index_buffer.slice(..), wgpu::IndexFormat::Uint32); - render_pass.draw_indexed(0..draw_call.num_indices, 0, 0..draw_call.num_instances); - } - - self.queue.submit(std::iter::once(encoder.finish())); - } - - let frame = match self.surface.get_current_texture() - { - Ok(frame) => frame, - Err(_) => - { - self.surface.configure(&self.device, &self.config); - self.surface - .get_current_texture() - .expect("Failed to acquire next surface texture") - } - }; - - let screen_view = frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - - let mut blit_encoder = - self.device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Blit Encoder"), - }); - - { - let mut blit_pass = blit_encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Blit Pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &screen_view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), - store: wgpu::StoreOp::Store, - }, - depth_slice: None, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - blit_pass.set_pipeline(&self.blit_pipeline); - blit_pass.set_bind_group(0, &self.blit_bind_group, &[]); - blit_pass.set_vertex_buffer(0, self.quad_vb.slice(..)); - blit_pass.set_index_buffer(self.quad_ib.slice(..), wgpu::IndexFormat::Uint16); - blit_pass.draw_indexed(0..self.quad_num_indices, 0, 0..1); - } - - self.queue.submit(std::iter::once(blit_encoder.finish())); - frame.present(); - } - - pub fn render_scale(&self) -> (u32, u32) - { - ( - self.config.width / self.framebuffer.width, - self.config.height / self.framebuffer.height, - ) - } - - pub fn set_terrain_data(&mut self) - { - let dither_textures = self - .dither_textures - .as_ref() - .expect("Dither textures should be loaded during initialization"); - - let flowmap_texture = self - .flowmap_texture - .as_ref() - .expect("Flowmap texture should be loaded during initialization"); - - let terrain_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Terrain Bind Group"), - layout: &self.terrain_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: self.terrain_uniform_buffer.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(&self.shadow_map_view), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Sampler(&self.shadow_map_sampler), - }, - wgpu::BindGroupEntry { - binding: 3, - resource: wgpu::BindingResource::TextureView(&dither_textures.view), - }, - wgpu::BindGroupEntry { - binding: 4, - resource: wgpu::BindingResource::Sampler(&dither_textures.sampler), - }, - wgpu::BindGroupEntry { - binding: 5, - resource: wgpu::BindingResource::TextureView(&flowmap_texture.view), - }, - wgpu::BindGroupEntry { - binding: 6, - resource: wgpu::BindingResource::Sampler(&flowmap_texture.sampler), - }, - wgpu::BindGroupEntry { - binding: 7, - resource: wgpu::BindingResource::TextureView(&self.blue_noise_view), - }, - wgpu::BindGroupEntry { - binding: 8, - resource: wgpu::BindingResource::Sampler(&self.blue_noise_sampler), - }, - ], - }); - - let shadow_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Shadow Bind Group"), - layout: &self.shadow_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: self.terrain_uniform_buffer.as_entire_binding(), - }], - }); - - let terrain_pipeline = create_terrain_render_pipeline( - &self.device, - &self.config, - &self.terrain_bind_group_layout, - ); - - let shadow_pipeline = create_shadow_pipeline(&self.device, &self.shadow_bind_group_layout); - - self.terrain_bind_group = Some(terrain_bind_group); - self.terrain_pipeline = Some(terrain_pipeline); - self.shadow_bind_group = Some(shadow_bind_group); - self.shadow_pipeline = Some(shadow_pipeline); - self.terrain_height_scale = 1.0; - } - - pub fn init_snow_light_accumulation(&mut self, terrain_min: glam::Vec2, terrain_max: glam::Vec2) - { - let snow_light_accumulation = crate::snow_light::SnowLightAccumulation::new( - &self.device, - terrain_min, - terrain_max, - 512, - ); - - self.snow_light_accumulation = Some(snow_light_accumulation); - } - - pub fn set_snow_depth(&mut self, snow_depth_view: &wgpu::TextureView) - { - println!("set_snow_depth() called"); - if let Some(ref mut snow_light_accum) = self.snow_light_accumulation - { - println!("Snow light accumulation exists, setting up bind groups"); - let snow_depth_sampler = self.device.create_sampler(&wgpu::SamplerDescriptor { - label: Some("Snow Depth Sampler"), - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Nearest, - min_filter: wgpu::FilterMode::Nearest, - ..Default::default() - }); - - match crate::texture::HeightmapTexture::load( - &self.device, - &self.queue, - "textures/terrain_heightmap.exr", - ) - { - Ok(heightmap) => - { - snow_light_accum.set_heightmap( - &self.device, - &heightmap.view, - &heightmap.sampler, - &self.shadow_map_view, - &self.shadow_map_sampler, - snow_depth_view, - &snow_depth_sampler, - ); - - let bind_group = snow_light_accum - .create_read_bind_group(&self.device, &self.snow_persistent_light_layout); - self.snow_persistent_light_bind_group = Some(bind_group); - } - Err(e) => - { - eprintln!("Failed to load heightmap for snow depth setup: {}", e); - } - } - } - } - - pub fn get_device(&self) -> &wgpu::Device - { - &self.device - } - - pub fn aspect_ratio(&self) -> f32 - { - self.config.width as f32 / self.config.height as f32 - } - - fn render_shadow_pass( - &mut self, - draw_calls: &[DrawCall], - light_view_projection: Mat4, - player_position: glam::Vec3, - time: f32, - ) - { - let uniforms = TerrainUniforms::new( - Mat4::IDENTITY, - Mat4::IDENTITY, - light_view_projection, - light_view_projection, - glam::Vec3::ZERO, - player_position, - self.terrain_height_scale, - time, - self.shadow_bias, - &self.spotlights, - ); - self.queue.write_buffer( - &self.terrain_uniform_buffer, - 0, - bytemuck::cast_slice(&[uniforms]), - ); - - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Shadow Pass Encoder"), - }); - - { - let mut shadow_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Shadow Pass"), - color_attachments: &[], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &self.shadow_map_view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Clear(1.0), - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - timestamp_writes: None, - occlusion_query_set: None, - }); - - shadow_pass.set_pipeline( - self.shadow_pipeline - .as_ref() - .expect("shadow pipeline missing"), - ); - shadow_pass.set_bind_group( - 0, - self.shadow_bind_group - .as_ref() - .expect("shadow bind group missing"), - &[], - ); - - for draw_call in draw_calls.iter() - { - if !matches!( - draw_call.pipeline, - Pipeline::Terrain | Pipeline::Standard | Pipeline::Environment | Pipeline::Snow - ) - { - continue; - } - - shadow_pass.set_vertex_buffer(0, draw_call.vertex_buffer.slice(..)); - - if let Some(ref instance_buffer) = draw_call.instance_buffer - { - shadow_pass.set_vertex_buffer(1, instance_buffer.slice(..)); - } - - shadow_pass - .set_index_buffer(draw_call.index_buffer.slice(..), wgpu::IndexFormat::Uint32); - shadow_pass.draw_indexed(0..draw_call.num_indices, 0, 0..draw_call.num_instances); - } - } - - self.queue.submit(std::iter::once(encoder.finish())); - } - - fn calculate_light_view_projection(&self) -> Mat4 - { - if self.spotlights.is_empty() - { - return Mat4::IDENTITY; - } - - let spotlight = &self.spotlights[0]; - let light_position = spotlight.position; - let light_target = spotlight.position + spotlight.direction; - - let up_vector = if spotlight.direction.y.abs() > 0.99 - { - glam::Vec3::Z - } - else - { - glam::Vec3::Y - }; - - let light_view = Mat4::look_at_rh(light_position, light_target, up_vector); - - let fov = spotlight.outer_angle * 2.0; - let near = 0.1; - let far = 150.0; - let light_projection = Mat4::perspective_rh(fov, 1.0, near, far); - - light_projection * light_view - } - - fn calculate_light_view_projection_ortho(&self) -> Mat4 - { - let light_dir = self.spotlights[0].direction; - let shadow_focus_point = glam::Vec3::ZERO; - let shadow_ortho_size = 600.0; - let shadow_distance = 1000.0; - let light_position = shadow_focus_point - light_dir * shadow_distance; - - let up_vector = if light_dir.y.abs() > 0.99 - { - glam::Vec3::Z - } - else - { - glam::Vec3::Y - }; - - let light_view = Mat4::look_at_rh(light_position, shadow_focus_point, up_vector); - - let far_plane = shadow_distance * 2.0 + 50.0; - let light_projection = Mat4::orthographic_rh( - -shadow_ortho_size, - shadow_ortho_size, - -shadow_ortho_size, - shadow_ortho_size, - 0.1, - far_plane, - ); - - println!("Shadow Frustum - Size: {:.1}×{:.1}, Coverage: {:.1}×{:.1}, Depth: 0.1-{:.1}, Focus: {:?}, Light: {:?}", - shadow_ortho_size * 2.0, shadow_ortho_size * 2.0, - shadow_ortho_size, shadow_ortho_size, - far_plane, shadow_focus_point, light_position); - - light_projection * light_view - } -} - -thread_local! { - static GLOBAL_RENDERER: RefCell> = RefCell::new(None); -} - -pub fn init(renderer: Renderer) -{ - GLOBAL_RENDERER.with(|r| *r.borrow_mut() = Some(renderer)); -} - -pub fn with_device(f: F) -> R -where - F: FnOnce(&wgpu::Device) -> R, -{ - GLOBAL_RENDERER.with(|r| { - let renderer = r.borrow(); - let renderer = renderer.as_ref().expect("Renderer not set"); - f(&renderer.device) - }) -} - -pub fn with_queue(f: F) -> R -where - F: FnOnce(&wgpu::Queue) -> R, -{ - GLOBAL_RENDERER.with(|r| { - let renderer = r.borrow(); - let renderer = renderer.as_ref().expect("Renderer not set"); - f(&renderer.queue) - }) -} - -pub fn set_terrain_data() -{ - GLOBAL_RENDERER.with(|r| { - let mut renderer = r.borrow_mut(); - let renderer = renderer.as_mut().expect("Renderer not set"); - renderer.set_terrain_data(); - }); -} - -pub fn init_snow_light_accumulation(terrain_min: glam::Vec2, terrain_max: glam::Vec2) -{ - GLOBAL_RENDERER.with(|r| { - let mut renderer = r.borrow_mut(); - let renderer = renderer.as_mut().expect("Renderer not set"); - renderer.init_snow_light_accumulation(terrain_min, terrain_max); - }); -} - -pub fn set_snow_depth(snow_depth_view: &wgpu::TextureView) -{ - GLOBAL_RENDERER.with(|r| { - let mut renderer = r.borrow_mut(); - let renderer = renderer.as_mut().expect("Renderer not set"); - renderer.set_snow_depth(snow_depth_view); - }); -} - -pub fn aspect_ratio() -> f32 -{ - GLOBAL_RENDERER.with(|r| { - let renderer = r.borrow(); - let renderer = renderer.as_ref().expect("Renderer not set"); - renderer.aspect_ratio() - }) -} - -pub fn render( - view: &glam::Mat4, - projection: &glam::Mat4, - camera_position: glam::Vec3, - player_position: glam::Vec3, - draw_calls: &[DrawCall], - time: f32, - delta_time: f32, -) -{ - GLOBAL_RENDERER.with(|r| { - let mut renderer = r.borrow_mut(); - let renderer = renderer.as_mut().expect("Renderer not set"); - renderer.render( - view, - projection, - camera_position, - player_position, - draw_calls, - time, - delta_time, - ); - }); -} - -pub fn set_shadow_bias(bias: f32) -{ - GLOBAL_RENDERER.with(|r| { - let mut renderer = r.borrow_mut(); - let renderer = renderer.as_mut().expect("Renderer not set"); - renderer.shadow_bias = bias; - }); -} - -pub fn update_spotlights(spotlights: Vec) -{ - GLOBAL_RENDERER.with(|r| { - let mut renderer = r.borrow_mut(); - let renderer = renderer.as_mut().expect("Renderer not set"); - renderer.spotlights = spotlights; - }); -} - -fn create_shadow_pipeline( - device: &wgpu::Device, - bind_group_layout: &wgpu::BindGroupLayout, -) -> wgpu::RenderPipeline -{ - let compiler = Wesl::new("src/shaders"); - let shader_source = compiler - .compile(&"package::shadow".parse().unwrap()) - .inspect_err(|e| eprintln!("WESL error: {e}")) - .unwrap() - .to_string(); - - let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Shadow Shader"), - source: wgpu::ShaderSource::Wgsl(shader_source.into()), - }); - - let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Shadow Pipeline Layout"), - bind_group_layouts: &[bind_group_layout], - push_constant_ranges: &[], - }); - - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Shadow Pipeline"), - layout: Some(&render_pipeline_layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: Some("vs_main"), - buffers: &[ - crate::mesh::Vertex::desc(), - crate::mesh::InstanceRaw::desc(), - ], - compilation_options: Default::default(), - }, - fragment: None, - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: Some(wgpu::Face::Back), - polygon_mode: wgpu::PolygonMode::Fill, - unclipped_depth: false, - conservative: false, - }, - 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(), - }), - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview: None, - cache: None, - }) -} - -fn create_wireframe_pipeline( - device: &wgpu::Device, - format: wgpu::TextureFormat, - bind_group_layout: &wgpu::BindGroupLayout, -) -> wgpu::RenderPipeline -{ - let compiler = Wesl::new("src/shaders"); - let shader_source = compiler - .compile(&"package::standard".parse().unwrap()) - .inspect_err(|e| eprintln!("WESL error: {e}")) - .unwrap() - .to_string(); - - let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Wireframe Shader"), - source: wgpu::ShaderSource::Wgsl(shader_source.into()), - }); - - let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Wireframe Pipeline Layout"), - bind_group_layouts: &[bind_group_layout], - push_constant_ranges: &[], - }); - - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Wireframe Pipeline"), - layout: Some(&render_pipeline_layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: Some("vs_main"), - buffers: &[ - crate::mesh::Vertex::desc(), - crate::mesh::InstanceRaw::desc(), - ], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: Some("fs_main"), - targets: &[Some(wgpu::ColorTargetState { - format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::LineList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: None, - polygon_mode: wgpu::PolygonMode::Fill, - unclipped_depth: false, - conservative: false, - }, - 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(), - }), - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview: None, - cache: None, - }) -} - -fn create_snow_pipeline( - device: &wgpu::Device, - format: wgpu::TextureFormat, - bind_group_layout_0: &wgpu::BindGroupLayout, - bind_group_layout_1: &wgpu::BindGroupLayout, -) -> wgpu::RenderPipeline -{ - let compiler = Wesl::new("src/shaders"); - let shader_source = compiler - .compile(&"package::snow".parse().unwrap()) - .inspect_err(|e| eprintln!("WESL error: {e}")) - .unwrap() - .to_string(); - - let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Snow Shader"), - source: wgpu::ShaderSource::Wgsl(shader_source.into()), - }); - - let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Snow Pipeline Layout"), - bind_group_layouts: &[bind_group_layout_0, bind_group_layout_1], - push_constant_ranges: &[], - }); - - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Snow Pipeline"), - layout: Some(&render_pipeline_layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: Some("vs_main"), - buffers: &[ - crate::mesh::Vertex::desc(), - crate::mesh::InstanceRaw::desc(), - ], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: Some("fs_main"), - targets: &[Some(wgpu::ColorTargetState { - format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: Some(wgpu::Face::Back), - polygon_mode: wgpu::PolygonMode::Fill, - unclipped_depth: false, - conservative: false, - }, - 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 { - constant: -1, - slope_scale: -1.0, - clamp: 0.0, - }, - }), - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview: None, - cache: None, - }) -} diff --git a/src/render/bind_group.rs b/src/render/bind_group.rs new file mode 100644 index 0000000..7744e97 --- /dev/null +++ b/src/render/bind_group.rs @@ -0,0 +1,113 @@ +use crate::texture::{DitherTextures, FlowmapTexture}; + +use super::Renderer; + +impl Renderer +{ + pub(super) fn create_bind_group_from_parts( + device: &wgpu::Device, + layout: &wgpu::BindGroupLayout, + uniform_buffer: &wgpu::Buffer, + shadow_map_view: &wgpu::TextureView, + shadow_map_sampler: &wgpu::Sampler, + dither_textures: &DitherTextures, + flowmap_texture: &FlowmapTexture, + blue_noise_view: &wgpu::TextureView, + blue_noise_sampler: &wgpu::Sampler, + snow_light_view: &wgpu::TextureView, + snow_light_sampler: &wgpu::Sampler, + ) -> wgpu::BindGroup + { + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Bind Group"), + layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: uniform_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(shadow_map_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(shadow_map_sampler), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::TextureView(&dither_textures.view), + }, + wgpu::BindGroupEntry { + binding: 4, + resource: wgpu::BindingResource::Sampler(&dither_textures.sampler), + }, + wgpu::BindGroupEntry { + binding: 5, + resource: wgpu::BindingResource::TextureView(&flowmap_texture.view), + }, + wgpu::BindGroupEntry { + binding: 6, + resource: wgpu::BindingResource::Sampler(&flowmap_texture.sampler), + }, + wgpu::BindGroupEntry { + binding: 7, + resource: wgpu::BindingResource::TextureView(blue_noise_view), + }, + wgpu::BindGroupEntry { + binding: 8, + resource: wgpu::BindingResource::Sampler(blue_noise_sampler), + }, + wgpu::BindGroupEntry { + binding: 9, + resource: wgpu::BindingResource::TextureView(snow_light_view), + }, + wgpu::BindGroupEntry { + binding: 10, + resource: wgpu::BindingResource::Sampler(snow_light_sampler), + }, + ], + }) + } + + pub(super) fn rebuild_bind_group_with_snow_light(&mut self) + { + let dither_textures = self + .dither_textures + .as_ref() + .expect("Dither textures required"); + let flowmap_texture = self + .flowmap_texture + .as_ref() + .expect("Flowmap texture required"); + + let snow_light_sampler = self.device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("Snow Light Sampler"), + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); + + let snow_light_view = self + .snow_light_accumulation + .as_ref() + .map(|s| s.read_view()) + .unwrap_or(&self.dummy_snow_light_view); + + self.bind_group = Self::create_bind_group_from_parts( + &self.device, + &self.bind_group_layout, + &self.uniform_buffer, + &self.shadow_map_view, + &self.shadow_map_sampler, + dither_textures, + flowmap_texture, + &self.blue_noise_view, + &self.blue_noise_sampler, + snow_light_view, + &snow_light_sampler, + ); + } +} diff --git a/src/render/mod.rs b/src/render/mod.rs new file mode 100644 index 0000000..a36b7e8 --- /dev/null +++ b/src/render/mod.rs @@ -0,0 +1,885 @@ +mod bind_group; +mod pipeline; +mod shadow; +mod types; + +pub use types::{DrawCall, Pipeline, Spotlight, SpotlightRaw, Uniforms, MAX_SPOTLIGHTS}; + +use crate::postprocess::{create_blit_pipeline, create_fullscreen_quad, LowResFramebuffer}; +use crate::texture::{DitherTextures, FlowmapTexture}; +use std::cell::RefCell; + +use pipeline::{create_main_pipeline, create_snow_clipmap_pipeline}; + +pub struct Renderer +{ + pub device: wgpu::Device, + pub queue: wgpu::Queue, + pub surface: wgpu::Surface<'static>, + pub config: wgpu::SurfaceConfiguration, + + framebuffer: LowResFramebuffer, + + standard_pipeline: wgpu::RenderPipeline, + snow_clipmap_pipeline: wgpu::RenderPipeline, + + uniform_buffer: wgpu::Buffer, + bind_group_layout: wgpu::BindGroupLayout, + bind_group: wgpu::BindGroup, + + quad_vb: wgpu::Buffer, + quad_ib: wgpu::Buffer, + quad_num_indices: u32, + blit_pipeline: wgpu::RenderPipeline, + blit_bind_group: wgpu::BindGroup, + + shadow_pipeline: Option, + shadow_bind_group_layout: wgpu::BindGroupLayout, + shadow_bind_group: Option, + + terrain_height_scale: f32, + + pub spotlights: Vec, + pub shadow_bias: f32, + + shadow_map_texture: wgpu::Texture, + shadow_map_view: wgpu::TextureView, + shadow_map_sampler: wgpu::Sampler, + + dither_textures: Option, + flowmap_texture: Option, + blue_noise_view: wgpu::TextureView, + blue_noise_sampler: wgpu::Sampler, + + dummy_snow_light_view: wgpu::TextureView, + dummy_snow_light_sampler: wgpu::Sampler, + + snow_light_accumulation: Option, + snow_light_bound: bool, +} + +impl Renderer +{ + pub async fn new( + window: &sdl3::video::Window, + render_scale: u32, + ) -> Result> + { + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + backends: wgpu::Backends::VULKAN, + ..Default::default() + }); + + let surface = unsafe { + let target = wgpu::SurfaceTargetUnsafe::from_window(window)?; + instance.create_surface_unsafe(target)? + }; + + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: Some(&surface), + force_fallback_adapter: false, + }) + .await + .map_err(|_| "Failed to find adapter")?; + + let (device, queue) = adapter + .request_device(&wgpu::DeviceDescriptor::default()) + .await?; + + let size = window.size(); + let config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: surface.get_capabilities(&adapter).formats[0], + width: size.0, + height: size.1, + present_mode: wgpu::PresentMode::Fifo, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + + surface.configure(&device, &config); + + let low_res_width = config.width / render_scale; + let low_res_height = config.height / render_scale; + + let framebuffer = + LowResFramebuffer::new(&device, low_res_width, low_res_height, config.format); + + let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Uniform Buffer"), + size: std::mem::size_of::() as wgpu::BufferAddress, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let dither_textures = match DitherTextures::load_octaves(&device, &queue) + { + Ok(textures) => + { + println!("Loaded dither textures successfully"); + Some(textures) + } + Err(e) => + { + eprintln!( + "Warning: Could not load dither textures: {}. Rendering may look incorrect.", + e + ); + None + } + }; + + let flowmap_texture = + match FlowmapTexture::load(&device, &queue, "textures/terrain_flowmap.exr") + { + Ok(texture) => + { + println!("Loaded terrain flowmap successfully"); + Some(texture) + } + Err(e) => + { + eprintln!( + "Warning: Could not load terrain flowmap: {}. Path lighting will not work.", + e + ); + None + } + }; + + let blue_noise_data = image::open("textures/blue_noise.png") + .expect("Failed to load blue noise texture") + .to_luma8(); + let blue_noise_size = blue_noise_data.dimensions(); + + let blue_noise_texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("Blue Noise"), + size: wgpu::Extent3d { + width: blue_noise_size.0, + height: blue_noise_size.1, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::R8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }); + + queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &blue_noise_texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &blue_noise_data, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(blue_noise_size.0), + rows_per_image: Some(blue_noise_size.1), + }, + wgpu::Extent3d { + width: blue_noise_size.0, + height: blue_noise_size.1, + depth_or_array_layers: 1, + }, + ); + + let blue_noise_view = + blue_noise_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let blue_noise_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("Blue Noise Sampler"), + address_mode_u: wgpu::AddressMode::Repeat, + address_mode_v: wgpu::AddressMode::Repeat, + mag_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + let shadow_map_size = 4096; + let shadow_map_texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("Shadow Map"), + size: wgpu::Extent3d { + width: shadow_map_size, + height: shadow_map_size, + depth_or_array_layers: MAX_SPOTLIGHTS as u32, + }, + 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 shadow_map_view = shadow_map_texture.create_view(&wgpu::TextureViewDescriptor { + dimension: Some(wgpu::TextureViewDimension::D2Array), + ..Default::default() + }); + + let shadow_map_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("Shadow Map Sampler"), + 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::Nearest, + compare: Some(wgpu::CompareFunction::LessEqual), + ..Default::default() + }); + + let dummy_texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("Dummy Snow Light"), + size: wgpu::Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::R16Float, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }); + + queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &dummy_texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &[0u8; 2], + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(2), + rows_per_image: Some(1), + }, + wgpu::Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }, + ); + let dummy_snow_light_view = + dummy_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let dummy_snow_light_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("Snow Light Sampler"), + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); + + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Bind Group Layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Depth, + view_dimension: wgpu::TextureViewDimension::D2Array, + multisampled: false, + }, + 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::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: false }, + view_dimension: wgpu::TextureViewDimension::D2Array, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 4, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 5, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: false }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 6, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 7, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: false }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 8, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 9, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 10, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + + let bind_group = Self::create_bind_group_from_parts( + &device, + &bind_group_layout, + &uniform_buffer, + &shadow_map_view, + &shadow_map_sampler, + dither_textures.as_ref().expect("Dither textures required"), + flowmap_texture.as_ref().expect("Flowmap texture required"), + &blue_noise_view, + &blue_noise_sampler, + &dummy_snow_light_view, + &dummy_snow_light_sampler, + ); + + let standard_pipeline = create_main_pipeline( + &device, + config.format, + &bind_group_layout, + wgpu::DepthBiasState::default(), + "Standard Pipeline", + ); + + let snow_clipmap_pipeline = + create_snow_clipmap_pipeline(&device, config.format, &bind_group_layout); + + let (quad_vb, quad_ib, quad_num_indices) = create_fullscreen_quad(&device); + + let blit_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Blit Bind Group Layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + + let blit_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Blit Bind Group"), + layout: &blit_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&framebuffer.view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&framebuffer.sampler), + }, + ], + }); + + let blit_pipeline = create_blit_pipeline(&device, config.format, &blit_bind_group_layout); + + let shadow_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Shadow Bind Group Layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + + Ok(Self { + device, + queue, + surface, + config, + framebuffer, + standard_pipeline, + snow_clipmap_pipeline, + uniform_buffer, + bind_group_layout, + bind_group, + quad_vb, + quad_ib, + quad_num_indices, + blit_pipeline, + blit_bind_group, + shadow_pipeline: None, + shadow_bind_group_layout, + shadow_bind_group: None, + terrain_height_scale: 10.0, + spotlights: vec![Spotlight::new( + glam::Vec3::new(0.0, 50.0, 0.0), + glam::Vec3::new(-0.5, -1.0, 1.0).normalize(), + 0.4, + 1.0, + 100.0, + )], + shadow_bias: 0.005, + shadow_map_texture, + shadow_map_view, + shadow_map_sampler, + dither_textures, + flowmap_texture, + blue_noise_view, + blue_noise_sampler, + dummy_snow_light_view, + dummy_snow_light_sampler, + snow_light_accumulation: None, + snow_light_bound: false, + }) + } + + pub fn render( + &mut self, + view: &glam::Mat4, + projection: &glam::Mat4, + camera_position: glam::Vec3, + player_position: glam::Vec3, + draw_calls: &[DrawCall], + time: f32, + delta_time: f32, + ) + { + let light_view_projections = self.calculate_light_view_projections(); + + if let Some(ref mut snow_light_accum) = self.snow_light_accumulation + { + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Snow Light Accumulation Encoder"), + }); + + snow_light_accum.render( + &mut encoder, + &self.queue, + &self.spotlights, + delta_time, + &light_view_projections, + self.shadow_bias, + self.terrain_height_scale, + ); + + self.queue.submit(std::iter::once(encoder.finish())); + } + + if self.snow_light_accumulation.is_some() && !self.snow_light_bound + { + self.rebuild_bind_group_with_snow_light(); + self.snow_light_bound = true; + } + + self.render_shadow_pass(draw_calls, &light_view_projections, player_position, time); + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Render Encoder"), + }); + + for (i, draw_call) in draw_calls.iter().enumerate() + { + let uniforms = Uniforms::new( + draw_call.model, + *view, + *projection, + &light_view_projections, + camera_position, + player_position, + self.terrain_height_scale, + time, + self.shadow_bias, + draw_call.tile_scale, + draw_call.enable_dissolve, + draw_call.enable_snow_light, + &self.spotlights, + ); + self.queue + .write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms])); + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("3D Render Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &self.framebuffer.view, + resolve_target: None, + ops: wgpu::Operations { + load: if i == 0 + { + wgpu::LoadOp::Clear(wgpu::Color { + r: 0.2, + g: 0.2, + b: 0.2, + a: 1.0, + }) + } + else + { + wgpu::LoadOp::Load + }, + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &self.framebuffer.depth_view, + depth_ops: Some(wgpu::Operations { + load: if i == 0 + { + wgpu::LoadOp::Clear(1.0) + } + else + { + wgpu::LoadOp::Load + }, + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + + let pipeline = match draw_call.pipeline + { + Pipeline::Standard => &self.standard_pipeline, + Pipeline::SnowClipmap => &self.snow_clipmap_pipeline, + }; + render_pass.set_pipeline(pipeline); + render_pass.set_bind_group(0, &self.bind_group, &[]); + + if let Some(ref displacement_bind_group) = draw_call.displacement_bind_group + { + render_pass.set_bind_group(1, displacement_bind_group, &[]); + } + + render_pass.set_vertex_buffer(0, draw_call.vertex_buffer.slice(..)); + + if let Some(ref instance_buffer) = draw_call.instance_buffer + { + render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); + } + + render_pass + .set_index_buffer(draw_call.index_buffer.slice(..), wgpu::IndexFormat::Uint32); + render_pass.draw_indexed(0..draw_call.num_indices, 0, 0..draw_call.num_instances); + } + } + + self.queue.submit(std::iter::once(encoder.finish())); + + let frame = match self.surface.get_current_texture() + { + Ok(frame) => frame, + Err(_) => + { + self.surface.configure(&self.device, &self.config); + self.surface + .get_current_texture() + .expect("Failed to acquire next surface texture") + } + }; + + let screen_view = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let mut blit_encoder = + self.device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Blit Encoder"), + }); + + { + let mut blit_pass = blit_encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Blit Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &screen_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + blit_pass.set_pipeline(&self.blit_pipeline); + blit_pass.set_bind_group(0, &self.blit_bind_group, &[]); + blit_pass.set_vertex_buffer(0, self.quad_vb.slice(..)); + blit_pass.set_index_buffer(self.quad_ib.slice(..), wgpu::IndexFormat::Uint16); + blit_pass.draw_indexed(0..self.quad_num_indices, 0, 0..1); + } + + self.queue.submit(std::iter::once(blit_encoder.finish())); + frame.present(); + } + + pub fn render_scale(&self) -> (u32, u32) + { + ( + self.config.width / self.framebuffer.width, + self.config.height / self.framebuffer.height, + ) + } + + pub fn set_terrain_data(&mut self) + { + let shadow_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Shadow Bind Group"), + layout: &self.shadow_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: self.uniform_buffer.as_entire_binding(), + }], + }); + + let shadow_pipeline = + pipeline::create_shadow_pipeline(&self.device, &self.shadow_bind_group_layout); + + self.shadow_bind_group = Some(shadow_bind_group); + self.shadow_pipeline = Some(shadow_pipeline); + self.terrain_height_scale = 1.0; + } + + pub fn init_snow_light_accumulation(&mut self, terrain_min: glam::Vec2, terrain_max: glam::Vec2) + { + let snow_light_accumulation = crate::snow_light::SnowLightAccumulation::new( + &self.device, + terrain_min, + terrain_max, + 512, + ); + + self.snow_light_accumulation = Some(snow_light_accumulation); + } + + pub fn set_snow_depth(&mut self, snow_depth_view: &wgpu::TextureView) + { + println!("set_snow_depth() called"); + if let Some(ref mut snow_light_accum) = self.snow_light_accumulation + { + println!("Snow light accumulation exists, setting up bind groups"); + let snow_depth_sampler = self.device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("Snow Depth Sampler"), + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + match crate::texture::HeightmapTexture::load( + &self.device, + &self.queue, + "textures/terrain_heightmap.exr", + ) + { + Ok(heightmap) => + { + snow_light_accum.set_heightmap( + &self.device, + &heightmap.view, + &heightmap.sampler, + &self.shadow_map_view, + &self.shadow_map_sampler, + snow_depth_view, + &snow_depth_sampler, + ); + } + Err(e) => + { + eprintln!("Failed to load heightmap for snow depth setup: {}", e); + } + } + } + } + + pub fn aspect_ratio(&self) -> f32 + { + self.config.width as f32 / self.config.height as f32 + } +} + +thread_local! { + static GLOBAL_RENDERER: RefCell> = RefCell::new(None); +} + +pub fn init(renderer: Renderer) +{ + GLOBAL_RENDERER.with(|r| *r.borrow_mut() = Some(renderer)); +} + +pub fn with_device(f: F) -> R +where + F: FnOnce(&wgpu::Device) -> R, +{ + GLOBAL_RENDERER.with(|r| { + let renderer = r.borrow(); + let renderer = renderer.as_ref().expect("Renderer not set"); + f(&renderer.device) + }) +} + +pub fn with_queue(f: F) -> R +where + F: FnOnce(&wgpu::Queue) -> R, +{ + GLOBAL_RENDERER.with(|r| { + let renderer = r.borrow(); + let renderer = renderer.as_ref().expect("Renderer not set"); + f(&renderer.queue) + }) +} + +pub fn set_terrain_data() +{ + GLOBAL_RENDERER.with(|r| { + let mut renderer = r.borrow_mut(); + let renderer = renderer.as_mut().expect("Renderer not set"); + renderer.set_terrain_data(); + }); +} + +pub fn init_snow_light_accumulation(terrain_min: glam::Vec2, terrain_max: glam::Vec2) +{ + GLOBAL_RENDERER.with(|r| { + let mut renderer = r.borrow_mut(); + let renderer = renderer.as_mut().expect("Renderer not set"); + renderer.init_snow_light_accumulation(terrain_min, terrain_max); + }); +} + +pub fn set_snow_depth(snow_depth_view: &wgpu::TextureView) +{ + GLOBAL_RENDERER.with(|r| { + let mut renderer = r.borrow_mut(); + let renderer = renderer.as_mut().expect("Renderer not set"); + renderer.set_snow_depth(snow_depth_view); + }); +} + +pub fn aspect_ratio() -> f32 +{ + GLOBAL_RENDERER.with(|r| { + let renderer = r.borrow(); + let renderer = renderer.as_ref().expect("Renderer not set"); + renderer.aspect_ratio() + }) +} + +pub fn render( + view: &glam::Mat4, + projection: &glam::Mat4, + camera_position: glam::Vec3, + player_position: glam::Vec3, + draw_calls: &[DrawCall], + time: f32, + delta_time: f32, +) +{ + GLOBAL_RENDERER.with(|r| { + let mut renderer = r.borrow_mut(); + let renderer = renderer.as_mut().expect("Renderer not set"); + renderer.render( + view, + projection, + camera_position, + player_position, + draw_calls, + time, + delta_time, + ); + }); +} + +pub fn set_shadow_bias(bias: f32) +{ + GLOBAL_RENDERER.with(|r| { + let mut renderer = r.borrow_mut(); + let renderer = renderer.as_mut().expect("Renderer not set"); + renderer.shadow_bias = bias; + }); +} + +pub fn update_spotlights(spotlights: Vec) +{ + GLOBAL_RENDERER.with(|r| { + let mut renderer = r.borrow_mut(); + let renderer = renderer.as_mut().expect("Renderer not set"); + renderer.spotlights = spotlights; + }); +} diff --git a/src/render/pipeline.rs b/src/render/pipeline.rs new file mode 100644 index 0000000..901934d --- /dev/null +++ b/src/render/pipeline.rs @@ -0,0 +1,252 @@ +use wesl::Wesl; + +pub fn create_shadow_pipeline( + device: &wgpu::Device, + bind_group_layout: &wgpu::BindGroupLayout, +) -> wgpu::RenderPipeline +{ + let compiler = Wesl::new("src/shaders"); + let shader_source = compiler + .compile(&"package::shadow".parse().unwrap()) + .inspect_err(|e| eprintln!("WESL error: {e}")) + .unwrap() + .to_string(); + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Shadow Shader"), + source: wgpu::ShaderSource::Wgsl(shader_source.into()), + }); + + let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Shadow Pipeline Layout"), + bind_group_layouts: &[bind_group_layout], + push_constant_ranges: &[], + }); + + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Shadow Pipeline"), + layout: Some(&render_pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[ + crate::loaders::mesh::Vertex::desc(), + crate::loaders::mesh::InstanceRaw::desc(), + ], + compilation_options: Default::default(), + }, + fragment: None, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: Some(wgpu::Face::Back), + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + 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(), + }), + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + cache: None, + }) +} + +pub fn create_main_pipeline( + device: &wgpu::Device, + format: wgpu::TextureFormat, + bind_group_layout: &wgpu::BindGroupLayout, + depth_bias: wgpu::DepthBiasState, + label: &str, +) -> wgpu::RenderPipeline +{ + let compiler = Wesl::new("src/shaders"); + let shader_source = compiler + .compile(&"package::main".parse().unwrap()) + .inspect_err(|e| eprintln!("WESL error: {e}")) + .unwrap() + .to_string(); + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some(label), + source: wgpu::ShaderSource::Wgsl(shader_source.into()), + }); + + let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Main Pipeline Layout"), + bind_group_layouts: &[bind_group_layout], + push_constant_ranges: &[], + }); + + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some(label), + layout: Some(&render_pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[ + crate::loaders::mesh::Vertex::desc(), + crate::loaders::mesh::InstanceRaw::desc(), + ], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: Some(wgpu::Face::Back), + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: depth_bias, + }), + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + cache: None, + }) +} + +pub fn create_snow_clipmap_pipeline( + device: &wgpu::Device, + format: wgpu::TextureFormat, + main_bind_group_layout: &wgpu::BindGroupLayout, +) -> wgpu::RenderPipeline +{ + let compiler = Wesl::new("src/shaders"); + let shader_source = compiler + .compile(&"package::main".parse().unwrap()) + .inspect_err(|e| eprintln!("WESL error: {e}")) + .unwrap() + .to_string(); + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Snow Clipmap Shader"), + source: wgpu::ShaderSource::Wgsl(shader_source.into()), + }); + + let displacement_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Snow Displacement Bind Group Layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: false }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: false }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), + count: None, + }, + ], + }); + + let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Snow Clipmap Pipeline Layout"), + bind_group_layouts: &[main_bind_group_layout, &displacement_bind_group_layout], + push_constant_ranges: &[], + }); + + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Snow Clipmap Pipeline"), + layout: Some(&render_pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_snow_main"), + buffers: &[ + crate::loaders::mesh::Vertex::desc(), + crate::loaders::mesh::InstanceRaw::desc(), + ], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: Some(wgpu::Face::Back), + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + 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 { + constant: -1, + slope_scale: -1.0, + clamp: 0.0, + }, + }), + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + cache: None, + }) +} diff --git a/src/render/shadow.rs b/src/render/shadow.rs new file mode 100644 index 0000000..c8ab344 --- /dev/null +++ b/src/render/shadow.rs @@ -0,0 +1,139 @@ +use glam::Mat4; + +use super::types::{DrawCall, Uniforms, MAX_SPOTLIGHTS}; +use super::Renderer; + +impl Renderer +{ + pub(super) fn render_shadow_pass( + &mut self, + draw_calls: &[DrawCall], + light_view_projections: &[Mat4], + player_position: glam::Vec3, + time: f32, + ) + { + let spotlight_count = self.spotlights.len().min(MAX_SPOTLIGHTS); + + for layer in 0..spotlight_count + { + let shadow_uniforms = Uniforms::new( + Mat4::IDENTITY, + Mat4::IDENTITY, + light_view_projections[layer], + light_view_projections, + glam::Vec3::ZERO, + player_position, + self.terrain_height_scale, + time, + self.shadow_bias, + 2.0, + false, + false, + &self.spotlights, + ); + self.queue.write_buffer( + &self.uniform_buffer, + 0, + bytemuck::cast_slice(&[shadow_uniforms]), + ); + + let layer_view = self + .shadow_map_texture + .create_view(&wgpu::TextureViewDescriptor { + dimension: Some(wgpu::TextureViewDimension::D2), + base_array_layer: layer as u32, + array_layer_count: Some(1), + ..Default::default() + }); + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Shadow Pass Encoder"), + }); + + { + let mut shadow_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Shadow Pass"), + color_attachments: &[], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &layer_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + + shadow_pass.set_pipeline( + self.shadow_pipeline + .as_ref() + .expect("shadow pipeline missing"), + ); + shadow_pass.set_bind_group( + 0, + self.shadow_bind_group + .as_ref() + .expect("shadow bind group missing"), + &[], + ); + + for draw_call in draw_calls.iter() + { + shadow_pass.set_vertex_buffer(0, draw_call.vertex_buffer.slice(..)); + + if let Some(ref instance_buffer) = draw_call.instance_buffer + { + shadow_pass.set_vertex_buffer(1, instance_buffer.slice(..)); + } + + shadow_pass.set_index_buffer( + draw_call.index_buffer.slice(..), + wgpu::IndexFormat::Uint32, + ); + shadow_pass.draw_indexed( + 0..draw_call.num_indices, + 0, + 0..draw_call.num_instances, + ); + } + } + + self.queue.submit(std::iter::once(encoder.finish())); + } + } + + pub(super) fn calculate_light_view_projections(&self) -> Vec + { + self.spotlights + .iter() + .take(MAX_SPOTLIGHTS) + .map(|spotlight| { + let light_position = spotlight.position; + let light_target = spotlight.position + spotlight.direction; + + let up_vector = if spotlight.direction.y.abs() > 0.99 + { + glam::Vec3::Z + } + else + { + glam::Vec3::Y + }; + + let light_view = Mat4::look_at_rh(light_position, light_target, up_vector); + + let fov = spotlight.outer_angle * 2.0; + let near = 0.1; + let far = 150.0; + let light_projection = Mat4::perspective_rh(fov, 1.0, near, far); + + light_projection * light_view + }) + .collect() + } +} diff --git a/src/render/types.rs b/src/render/types.rs new file mode 100644 index 0000000..5110bf5 --- /dev/null +++ b/src/render/types.rs @@ -0,0 +1,157 @@ +use bytemuck::{Pod, Zeroable}; +use glam::Mat4; + +pub const MAX_SPOTLIGHTS: usize = 4; + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable, Default)] +pub struct SpotlightRaw +{ + pub position: [f32; 3], + pub inner_angle: f32, + pub direction: [f32; 3], + pub outer_angle: f32, + pub range: f32, + pub _padding: [f32; 7], +} + +pub struct Spotlight +{ + pub position: glam::Vec3, + pub direction: glam::Vec3, + pub inner_angle: f32, + pub outer_angle: f32, + pub range: f32, +} + +impl Spotlight +{ + pub fn new( + position: glam::Vec3, + direction: glam::Vec3, + inner_angle: f32, + outer_angle: f32, + range: f32, + ) -> Self + { + Self { + position, + direction: direction.normalize(), + inner_angle, + outer_angle, + range, + } + } + + pub fn to_raw(&self) -> SpotlightRaw + { + SpotlightRaw { + position: self.position.to_array(), + inner_angle: self.inner_angle, + direction: self.direction.to_array(), + outer_angle: self.outer_angle, + range: self.range, + _padding: [0.0; 7], + } + } +} + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +pub struct Uniforms +{ + pub model: [[f32; 4]; 4], + pub view: [[f32; 4]; 4], + pub projection: [[f32; 4]; 4], + pub light_view_projections: [[[f32; 4]; 4]; MAX_SPOTLIGHTS], + pub camera_position: [f32; 3], + pub height_scale: f32, + pub player_position: [f32; 3], + pub time: f32, + pub shadow_bias: f32, + pub tile_scale: f32, + pub enable_dissolve: u32, + pub enable_snow_light: u32, + pub spotlight_count: u32, + pub _padding1: u32, + pub _padding2: u32, + pub _padding3: u32, + pub spotlights: [SpotlightRaw; MAX_SPOTLIGHTS], +} + +impl Uniforms +{ + pub fn new( + model: Mat4, + view: Mat4, + projection: Mat4, + light_view_projections: &[Mat4], + camera_position: glam::Vec3, + player_position: glam::Vec3, + height_scale: f32, + time: f32, + shadow_bias: f32, + tile_scale: f32, + enable_dissolve: bool, + enable_snow_light: bool, + spotlights: &[Spotlight], + ) -> Self + { + let mut spotlight_array = [SpotlightRaw::default(); MAX_SPOTLIGHTS]; + for (i, spotlight) in spotlights.iter().take(MAX_SPOTLIGHTS).enumerate() + { + spotlight_array[i] = spotlight.to_raw(); + } + + let mut lvp_array = [[[0.0f32; 4]; 4]; MAX_SPOTLIGHTS]; + for (i, mat) in light_view_projections + .iter() + .take(MAX_SPOTLIGHTS) + .enumerate() + { + lvp_array[i] = mat.to_cols_array_2d(); + } + + Self { + model: model.to_cols_array_2d(), + view: view.to_cols_array_2d(), + projection: projection.to_cols_array_2d(), + light_view_projections: lvp_array, + camera_position: camera_position.to_array(), + height_scale, + player_position: player_position.to_array(), + time, + shadow_bias, + tile_scale, + enable_dissolve: if enable_dissolve { 1 } else { 0 }, + enable_snow_light: if enable_snow_light { 1 } else { 0 }, + spotlight_count: spotlights.len().min(MAX_SPOTLIGHTS) as u32, + _padding1: 0, + _padding2: 0, + _padding3: 0, + spotlights: spotlight_array, + } + } +} + +#[derive(Clone, Copy)] +pub enum Pipeline +{ + Standard, + SnowClipmap, +} + +pub struct DrawCall +{ + pub vertex_buffer: wgpu::Buffer, + pub index_buffer: wgpu::Buffer, + pub num_indices: u32, + pub model: Mat4, + pub pipeline: Pipeline, + pub instance_buffer: Option, + pub num_instances: u32, + pub tile_scale: f32, + pub enable_dissolve: bool, + pub enable_snow_light: bool, + pub displacement_bind_group: Option, +} diff --git a/src/shader.rs b/src/shader.rs deleted file mode 100644 index 8fa535a..0000000 --- a/src/shader.rs +++ /dev/null @@ -1,140 +0,0 @@ -use crate::mesh::{InstanceRaw, Vertex}; -use wesl::{include_wesl, Wesl}; - -pub fn create_render_pipeline( - device: &wgpu::Device, - config: &wgpu::SurfaceConfiguration, - bind_group_layout: &wgpu::BindGroupLayout, -) -> wgpu::RenderPipeline -{ - let compiler = Wesl::new("src/shaders"); - let shader_source = compiler - .compile(&"package::standard".parse().unwrap()) - .inspect_err(|e| eprintln!("WESL error: {e}")) - .unwrap() - .to_string(); - - let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Shader"), - source: wgpu::ShaderSource::Wgsl(shader_source.into()), - }); - - let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Render Pipeline Layout"), - bind_group_layouts: &[bind_group_layout], - push_constant_ranges: &[], - }); - - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Render Pipeline"), - layout: Some(&render_pipeline_layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: Some("vs_main"), - buffers: &[Vertex::desc(), InstanceRaw::desc()], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: Some("fs_main"), - targets: &[Some(wgpu::ColorTargetState { - format: config.format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: Some(wgpu::Face::Back), - polygon_mode: wgpu::PolygonMode::Fill, - unclipped_depth: false, - conservative: false, - }, - 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(), - }), - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview: None, - cache: None, - }) -} - -pub fn create_environment_pipeline( - device: &wgpu::Device, - config: &wgpu::SurfaceConfiguration, - bind_group_layout: &wgpu::BindGroupLayout, -) -> wgpu::RenderPipeline -{ - let compiler = Wesl::new("src/shaders"); - let shader_source = compiler - .compile(&"package::environment".parse().unwrap()) - .inspect_err(|e| eprintln!("WESL error: {e}")) - .unwrap() - .to_string(); - - let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Environment Shader"), - source: wgpu::ShaderSource::Wgsl(shader_source.into()), - }); - - let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Environment Pipeline Layout"), - bind_group_layouts: &[bind_group_layout], - push_constant_ranges: &[], - }); - - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Environment Pipeline"), - layout: Some(&render_pipeline_layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: Some("vs_main"), - buffers: &[Vertex::desc(), InstanceRaw::desc()], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: Some("fs_main"), - targets: &[Some(wgpu::ColorTargetState { - format: config.format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: Some(wgpu::Face::Back), - polygon_mode: wgpu::PolygonMode::Fill, - unclipped_depth: false, - conservative: false, - }, - 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(), - }), - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview: None, - cache: None, - }) -} diff --git a/src/shaders/environment.wesl b/src/shaders/environment.wesl deleted file mode 100644 index 4a24f12..0000000 --- a/src/shaders/environment.wesl +++ /dev/null @@ -1,95 +0,0 @@ -import package::shared::{VertexInput, VertexOutput, sample_shadow_map, flowmap_path_lighting_with_shadow, all_spotlights_lighting, uniforms, blue_noise_texture, blue_noise_sampler}; - -@vertex -fn vs_main(input: VertexInput) -> VertexOutput { - var output: VertexOutput; - - let instance_model = mat4x4( - input.instance_model_0, - input.instance_model_1, - input.instance_model_2, - input.instance_model_3 - ); - - let world_pos = instance_model * vec4(input.position, 1.0); - output.world_position = world_pos.xyz; - output.clip_position = uniforms.projection * uniforms.view * world_pos; - - let normal_matrix = mat3x3( - instance_model[0].xyz, - instance_model[1].xyz, - instance_model[2].xyz - ); - output.world_normal = normalize(normal_matrix * input.normal); - - output.light_space_position = uniforms.light_view_projection * world_pos; - - let instance_position = vec3( - instance_model[3][0], - instance_model[3][1], - instance_model[3][2] - ); - - let to_player = uniforms.player_position - uniforms.camera_position; - let distance_to_player = length(to_player); - - var dissolve_amount = 0.0; - - if distance_to_player > 0.01 { - let ray_dir = to_player / distance_to_player; - let ray_origin = uniforms.camera_position; - - let tree_height = 16.0; - let occlusion_radius = 6.5; - - let w = instance_position - ray_origin; - let projection_t = dot(w, ray_dir); - - if projection_t > 0.0 && projection_t < distance_to_player { - let closest_on_ray = ray_origin + ray_dir * projection_t; - - let diff = closest_on_ray - instance_position; - let height_on_trunk = clamp(diff.y, 0.0, tree_height); - let closest_on_trunk = instance_position + vec3(0.0, height_on_trunk, 0.0); - - let perp_distance = length(closest_on_trunk - closest_on_ray); - - if perp_distance < occlusion_radius { - let dissolve_t = pow(perp_distance / occlusion_radius, 0.5); - dissolve_amount = 1.0 - clamp(dissolve_t, 0.0, 1.0); - } - } - } - - output.dissolve_amount = dissolve_amount; - - return output; -} - -@fragment -fn fs_main(input: VertexOutput) -> @location(0) vec4 { - - let debug = 0u; - - if debug == 1u { - return vec4(input.dissolve_amount); - } - - if input.dissolve_amount > 0.0 { - let screen_pos = input.clip_position.xy; - let noise_uv = fract(screen_pos / 128.0); - let noise_value = textureSampleLevel(blue_noise_texture, blue_noise_sampler, noise_uv, 0.0).r; - - if noise_value < input.dissolve_amount { - discard; - } - } - - let shadow = sample_shadow_map(input.light_space_position); - - let tile_scale = 4.0; - let spotlight_strokes = all_spotlights_lighting(input.world_position, input.clip_position, input.world_normal, tile_scale, shadow); - let brightness = spotlight_strokes; - - return vec4(brightness, brightness, brightness, 1.0); -} diff --git a/src/shaders/main.wesl b/src/shaders/main.wesl new file mode 100644 index 0000000..f451461 --- /dev/null +++ b/src/shaders/main.wesl @@ -0,0 +1,177 @@ +import package::util::{ + VertexInput, + VertexOutput, + uniforms, + all_spotlights_lighting, + blue_noise_texture, + blue_noise_sampler, + snow_light_texture, + snow_light_sampler, + TERRAIN_BOUNDS +}; + +@vertex +fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + + let instance_model = mat4x4( + input.instance_model_0, + input.instance_model_1, + input.instance_model_2, + input.instance_model_3 + ); + + let world_pos = instance_model * vec4(input.position, 1.0); + output.world_position = world_pos.xyz; + output.clip_position = uniforms.projection * uniforms.view * world_pos; + + let normal_matrix = mat3x3( + instance_model[0].xyz, + instance_model[1].xyz, + instance_model[2].xyz + ); + output.world_normal = normalize(normal_matrix * input.normal); + + var dissolve_amount = 0.0; + + if uniforms.enable_dissolve == 1u { + let instance_position = vec3( + instance_model[3][0], + instance_model[3][1], + instance_model[3][2] + ); + + let to_player = uniforms.player_position - uniforms.camera_position; + let distance_to_player = length(to_player); + + if distance_to_player > 0.01 { + let ray_dir = to_player / distance_to_player; + let ray_origin = uniforms.camera_position; + + let tree_height = 16.0; + let occlusion_radius = 6.5; + + let w = instance_position - ray_origin; + let projection_t = dot(w, ray_dir); + + if projection_t > 0.0 && projection_t < distance_to_player { + let closest_on_ray = ray_origin + ray_dir * projection_t; + + let diff = closest_on_ray - instance_position; + let height_on_trunk = clamp(diff.y, 0.0, tree_height); + let closest_on_trunk = instance_position + vec3(0.0, height_on_trunk, 0.0); + + let perp_distance = length(closest_on_trunk - closest_on_ray); + + if perp_distance < occlusion_radius { + let dissolve_t = pow(perp_distance / occlusion_radius, 0.5); + dissolve_amount = 1.0 - clamp(dissolve_t, 0.0, 1.0); + } + } + } + } + + output.dissolve_amount = dissolve_amount; + + return output; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + if uniforms.enable_dissolve == 1u && in.dissolve_amount > 0.0 { + let screen_pos = in.clip_position.xy; + let noise_uv = fract(screen_pos / 128.0); + let noise_value = textureSampleLevel(blue_noise_texture, blue_noise_sampler, noise_uv, 0.0).r; + + if noise_value < in.dissolve_amount { + discard; + } + } + + let tile_scale = uniforms.tile_scale; + let spotlight_strokes = all_spotlights_lighting(in.world_position, in.clip_position, in.world_normal, tile_scale); + + var brightness = spotlight_strokes; + + if uniforms.enable_snow_light == 1u { + let terrain_uv = (vec2(in.world_position.x, in.world_position.z) + TERRAIN_BOUNDS * 0.5) / TERRAIN_BOUNDS; + let snow_light = textureSample(snow_light_texture, snow_light_sampler, terrain_uv).r; + + if snow_light > 0.05 { + let screen_pos = in.clip_position.xy / in.clip_position.w; + let blue_noise_uv = screen_pos * 0.5 + 0.5; + let blue_noise = textureSample(blue_noise_texture, blue_noise_sampler, blue_noise_uv * 10.0).r; + let blue_step = step(blue_noise, snow_light / 30.0); + + brightness = max(brightness, blue_step); + } + } + + return vec4(brightness, brightness, brightness, 1.0); +} + +@group(1) @binding(0) +var heightmap_texture: texture_2d; + +@group(1) @binding(1) +var heightmap_sampler: sampler; + +@group(1) @binding(2) +var snow_depth_texture: texture_2d; + +@group(1) @binding(3) +var snow_depth_sampler: sampler; + +fn sample_snow_depth_bilinear(uv: vec2) -> f32 { + let tex_size = vec2(textureDimensions(snow_depth_texture, 0)); + let texel_coord = uv * tex_size - 0.5; + let base = vec2(floor(texel_coord)); + let f = fract(texel_coord); + + let s00 = textureLoad(snow_depth_texture, clamp(base, vec2(0), vec2(tex_size) - 1), 0).r; + let s10 = textureLoad(snow_depth_texture, clamp(base + vec2(1, 0), vec2(0), vec2(tex_size) - 1), 0).r; + let s01 = textureLoad(snow_depth_texture, clamp(base + vec2(0, 1), vec2(0), vec2(tex_size) - 1), 0).r; + let s11 = textureLoad(snow_depth_texture, clamp(base + vec2(1, 1), vec2(0), vec2(tex_size) - 1), 0).r; + + return mix(mix(s00, s10, f.x), mix(s01, s11, f.x), f.y); +} + +@vertex +fn vs_snow_main(in: VertexInput) -> VertexOutput { + let instance_model = mat4x4( + in.instance_model_0, + in.instance_model_1, + in.instance_model_2, + in.instance_model_3, + ); + + let instance_world = instance_model * vec4(in.position, 1.0); + let world_xz = instance_world.xz; + let level_bias = instance_world.y; + + let uv = (world_xz + TERRAIN_BOUNDS * 0.5) / TERRAIN_BOUNDS; + + let terrain_height = textureSampleLevel(heightmap_texture, heightmap_sampler, uv, 0.0).r; + let snow_depth = sample_snow_depth_bilinear(uv) * 5.0; + + let world_y = terrain_height + snow_depth + 0.2 + level_bias; + let world_position = vec3(world_xz.x, world_y, world_xz.y); + + let texel_size = 1.0 / vec2(textureDimensions(heightmap_texture, 0)); + let h_right = textureSampleLevel(heightmap_texture, heightmap_sampler, uv + vec2(texel_size.x, 0.0), 0.0).r + sample_snow_depth_bilinear(uv + vec2(texel_size.x, 0.0)); + let h_left = textureSampleLevel(heightmap_texture, heightmap_sampler, uv - vec2(texel_size.x, 0.0), 0.0).r + sample_snow_depth_bilinear(uv - vec2(texel_size.x, 0.0)); + let h_up = textureSampleLevel(heightmap_texture, heightmap_sampler, uv + vec2(0.0, texel_size.y), 0.0).r + sample_snow_depth_bilinear(uv + vec2(0.0, texel_size.y)); + let h_down = textureSampleLevel(heightmap_texture, heightmap_sampler, uv - vec2(0.0, texel_size.y), 0.0).r + sample_snow_depth_bilinear(uv - vec2(0.0, texel_size.y)); + + let dx = h_right - h_left; + let dz = h_up - h_down; + let normal = normalize(vec3(-dx, 2.0 * texel_size.x * TERRAIN_BOUNDS.x, -dz)); + + var out: VertexOutput; + out.world_position = world_position; + out.world_normal = normal; + out.clip_position = uniforms.projection * uniforms.view * vec4(world_position, 1.0); + out.dissolve_amount = in.instance_dissolve; + + return out; +} diff --git a/src/shaders/shadow.wesl b/src/shaders/shadow.wesl index c23dbec..6b6982a 100644 --- a/src/shaders/shadow.wesl +++ b/src/shaders/shadow.wesl @@ -1,4 +1,4 @@ -import package::shared::{ VertexInput, uniforms }; +import package::util::{ VertexInput, uniforms }; @vertex fn vs_main(input: VertexInput) -> @builtin(position) vec4 { @@ -10,5 +10,5 @@ fn vs_main(input: VertexInput) -> @builtin(position) vec4 { ); let world_pos = instance_model * vec4(input.position, 1.0); - return uniforms.light_view_projection * world_pos; + return uniforms.projection * world_pos; } diff --git a/src/shaders/snow.wesl b/src/shaders/snow.wesl deleted file mode 100644 index 8c51da3..0000000 --- a/src/shaders/snow.wesl +++ /dev/null @@ -1,72 +0,0 @@ -import package::shared::{ - VertexInput, - VertexOutput, - uniforms, - sample_shadow_map, - all_spotlights_lighting, - is_in_any_spotlight_light_area, - blue_noise_texture, - blue_noise_sampler, - TERRAIN_BOUNDS, - Spotlight -}; - -@group(1) @binding(0) -var persistent_light_texture: texture_2d; - -@group(1) @binding(1) -var persistent_light_sampler: sampler; - -@vertex -fn vs_main(input: VertexInput) -> VertexOutput { - var output: VertexOutput; - - let instance_model = mat4x4( - input.instance_model_0, - input.instance_model_1, - input.instance_model_2, - input.instance_model_3 - ); - - let world_pos = instance_model * vec4(input.position, 1.0); - output.world_position = world_pos.xyz; - output.clip_position = uniforms.projection * uniforms.view * world_pos; - - let normal_matrix = mat3x3( - instance_model[0].xyz, - instance_model[1].xyz, - instance_model[2].xyz - ); - output.world_normal = normalize(normal_matrix * input.normal); - - output.light_space_position = uniforms.light_view_projection * world_pos; - - return output; -} - -@fragment -fn fs_main(in: VertexOutput) -> @location(0) vec4 { - let shadow = sample_shadow_map(in.light_space_position); - - let tile_scale = 2.0; - let in_spotlight_light_area = is_in_any_spotlight_light_area(in.world_position, in.world_normal, tile_scale, shadow); - let spotlight_strokes = all_spotlights_lighting(in.world_position, in.clip_position, in.world_normal, tile_scale, shadow); - - var brightness = spotlight_strokes; - - if !in_spotlight_light_area { - let terrain_uv = (vec2(in.world_position.x, in.world_position.z) + TERRAIN_BOUNDS * 0.5) / TERRAIN_BOUNDS; - let persistent_light = textureSample(persistent_light_texture, persistent_light_sampler, terrain_uv).r; - - if persistent_light > 0.05 { - let screen_pos = in.clip_position.xy / in.clip_position.w; - let blue_noise_uv = screen_pos * 0.5 + 0.5; - let blue_noise = textureSample(blue_noise_texture, blue_noise_sampler, blue_noise_uv * 10.0).r; - let blue_step = step(blue_noise, persistent_light / 30.0); - - brightness = max(brightness, blue_step); - } - } - - return vec4(brightness, brightness, brightness, 1.0); -} diff --git a/src/shaders/snow_deform.wgsl b/src/shaders/snow_deform.wgsl index 13736a2..e87a948 100644 --- a/src/shaders/snow_deform.wgsl +++ b/src/shaders/snow_deform.wgsl @@ -9,6 +9,10 @@ struct DeformParams { position_z: f32, radius: f32, depth: f32, + terrain_width: f32, + terrain_height: f32, + _padding1: f32, + _padding2: f32, } @compute @workgroup_size(16, 16, 1) @@ -21,7 +25,7 @@ fn deform(@builtin(global_invocation_id) global_id: vec3) { let coords = vec2(i32(global_id.x), i32(global_id.y)); - let terrain_size = vec2(1000.0, 1000.0); + let terrain_size = vec2(params.terrain_width, params.terrain_height); let half_size = terrain_size / 2.0; let uv = vec2(f32(global_id.x) / f32(texture_size.x), f32(global_id.y) / f32(texture_size.y)); @@ -34,9 +38,7 @@ fn deform(@builtin(global_invocation_id) global_id: vec3) { let current_depth = textureLoad(snow_depth, coords).r; let falloff = 1.0 - (distance / params.radius); - let falloff_smooth = falloff * falloff; - - let deform_amount = params.depth * falloff_smooth; + let deform_amount = params.depth * falloff * falloff; let new_depth = max(0.0, current_depth - deform_amount); textureStore(snow_depth, coords, vec4(new_depth, 0.0, 0.0, 0.0)); diff --git a/src/shaders/snow_light_accumulation.wesl b/src/shaders/snow_light_accumulation.wesl index ea91cad..fdacdee 100644 --- a/src/shaders/snow_light_accumulation.wesl +++ b/src/shaders/snow_light_accumulation.wesl @@ -1,4 +1,4 @@ -import package::shared::{Spotlight, MAX_SPOTLIGHTS, calculate_spotlight_data}; +import package::util::{Spotlight, MAX_SPOTLIGHTS, calculate_spotlight_data}; struct AccumulationUniforms { terrain_min_xz: vec2, @@ -7,12 +7,12 @@ struct AccumulationUniforms { delta_time: f32, spotlight_count: u32, _padding: u32, - light_view_projection: mat4x4, + light_view_projections: array, MAX_SPOTLIGHTS>, shadow_bias: f32, terrain_height_scale: f32, _padding3: f32, _padding4: f32, - spotlights: array, + spotlights: array, } @group(0) @binding(0) @@ -31,7 +31,7 @@ var heightmap: texture_2d; var heightmap_sampler: sampler; @group(0) @binding(5) -var shadow_map: texture_depth_2d; +var shadow_map: texture_depth_2d_array; @group(0) @binding(6) var shadow_sampler: sampler_comparison; @@ -59,7 +59,8 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { return out; } -fn sample_shadow_map(light_space_pos: vec4) -> f32 { +fn sample_shadow_map_local(world_pos: vec3, spotlight_index: u32) -> f32 { + let light_space_pos = uniforms.light_view_projections[spotlight_index] * vec4(world_pos, 1.0); let proj_coords = light_space_pos.xyz / light_space_pos.w; let ndc_coords = proj_coords * vec3(0.5, -0.5, 1.0) + vec3(0.5, 0.5, 0.0); @@ -70,7 +71,7 @@ fn sample_shadow_map(light_space_pos: vec4) -> f32 { } let depth = ndc_coords.z - uniforms.shadow_bias; - let shadow = textureSampleCompare(shadow_map, shadow_sampler, ndc_coords.xy, depth); + let shadow = textureSampleCompare(shadow_map, shadow_sampler, ndc_coords.xy, spotlight_index, depth); return shadow; } @@ -85,15 +86,13 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let snow_surface_height = terrain_height + depth; let snow_surface_pos = vec3(world_xz.x, snow_surface_height, world_xz.y); - let light_space_position = uniforms.light_view_projection * vec4(snow_surface_pos, 1.0); - let shadow = sample_shadow_map(light_space_position); - var current_light = 0.0; - if shadow > 0.0 { - let tile_scale = 2.0; - let surface_normal = vec3(0.0, 1.0, 0.0); + let tile_scale = 2.0; + let surface_normal = vec3(0.0, 1.0, 0.0); - for (var i = 0u; i < uniforms.spotlight_count; i++) { + for (var i = 0u; i < uniforms.spotlight_count; i++) { + let shadow = sample_shadow_map_local(snow_surface_pos, i); + if shadow > 0.0 { let spotlight = uniforms.spotlights[i]; let data = calculate_spotlight_data(snow_surface_pos, surface_normal, spotlight, tile_scale, shadow); let light = f32(data.is_lit); diff --git a/src/shaders/standard.wesl b/src/shaders/standard.wesl deleted file mode 100644 index 147b448..0000000 --- a/src/shaders/standard.wesl +++ /dev/null @@ -1,66 +0,0 @@ -import package::shared::{VertexInput, VertexOutput, sample_shadow_map, flowmap_path_lighting_with_shadow, all_spotlights_lighting, uniforms, blue_noise_texture, blue_noise_sampler}; - -@vertex -fn vs_main(input: VertexInput) -> VertexOutput { - var output: VertexOutput; - - let instance_model = mat4x4( - input.instance_model_0, - input.instance_model_1, - input.instance_model_2, - input.instance_model_3 - ); - - let world_pos = instance_model * vec4(input.position, 1.0); - output.world_position = world_pos.xyz; - output.clip_position = uniforms.projection * uniforms.view * world_pos; - - let normal_matrix = mat3x3( - instance_model[0].xyz, - instance_model[1].xyz, - instance_model[2].xyz - ); - output.world_normal = normalize(normal_matrix * input.normal); - - output.light_space_position = uniforms.light_view_projection * world_pos; - output.dissolve_amount = 0.0; - - return output; -} - -@fragment -fn fs_main(input: VertexOutput) -> @location(0) vec4 { - let shadow = sample_shadow_map(input.light_space_position); - - let debug = 0u; - - if debug == 3u { - return vec4(shadow, shadow, shadow, 1.0); - } - - if debug == 2u { - let proj_coords = input.light_space_position.xyz / input.light_space_position.w; - return vec4(proj_coords.x, proj_coords.y, proj_coords.z, 1.0); - } - - if debug == 1u { - let proj_coords = input.light_space_position.xyz / input.light_space_position.w; - let ndc_coords = proj_coords * vec3(0.5, -0.5, 1.0) + vec3(0.5, 0.5, 0.0); - let in_bounds = ndc_coords.x >= 0.0 && ndc_coords.x <= 1.0 && - ndc_coords.y >= 0.0 && ndc_coords.y <= 1.0 && - ndc_coords.z >= 0.0 && ndc_coords.z <= 1.0; - if in_bounds { - return vec4(ndc_coords.x, ndc_coords.y, ndc_coords.z, 1.0); - } else { - return vec4(0.0, 0.0, 0.0, 1.0); - } - } - - let tile_scale = 4.0; - let flowmap_strokes = flowmap_path_lighting_with_shadow(input.world_position, input.clip_position, input.world_normal, tile_scale, shadow); - let spotlight_strokes = all_spotlights_lighting(input.world_position, input.clip_position, input.world_normal, tile_scale, shadow); - // let brightness = max(flowmap_strokes, spotlight_strokes); - let brightness = 0.0; - - return vec4(brightness, brightness, brightness, 1.0); -} diff --git a/src/shaders/terrain.wesl b/src/shaders/terrain.wesl deleted file mode 100644 index bfa0448..0000000 --- a/src/shaders/terrain.wesl +++ /dev/null @@ -1,89 +0,0 @@ -import package::shared::{VertexInput, VertexOutput, sample_shadow_map, flowmap_path_lighting_with_shadow, all_spotlights_lighting, uniforms, TERRAIN_BOUNDS, flowmap_texture, flowmap_sampler}; - -@vertex -fn vs_main(input: VertexInput) -> VertexOutput { - var output: VertexOutput; - - let instance_model = mat4x4( - input.instance_model_0, - input.instance_model_1, - input.instance_model_2, - input.instance_model_3 - ); - - let world_pos = instance_model * vec4(input.position, 1.0); - output.world_position = world_pos.xyz; - output.clip_position = uniforms.projection * uniforms.view * world_pos; - - let normal_matrix = mat3x3( - instance_model[0].xyz, - instance_model[1].xyz, - instance_model[2].xyz - ); - output.world_normal = normalize(normal_matrix * input.normal); - - output.light_space_position = uniforms.light_view_projection * world_pos; - - return output; -} - -@fragment -fn fs_main(input: VertexOutput) -> @location(0) vec4 { - let debug = 0u; - - if debug == 4u { - let proj_coords = input.light_space_position.xyz / input.light_space_position.w; - let ndc_coords = proj_coords * vec3(0.5, -0.5, 1.0) + vec3(0.5, 0.5, 0.0); - let in_bounds = ndc_coords.x >= 0.0 && ndc_coords.x <= 1.0 && - ndc_coords.y >= 0.0 && ndc_coords.y <= 1.0 && - ndc_coords.z >= 0.0 && ndc_coords.z <= 1.0; - if in_bounds { - return vec4(ndc_coords.x, ndc_coords.y, ndc_coords.z, 1.0); - } else { - return vec4(0.0, 0.0, 0.0, 1.0); - } - } - - if debug == 3u { - let shadow = sample_shadow_map(input.light_space_position); - return vec4(shadow, shadow, shadow, 1.0); - } - - if debug == 1u { - let flowmap_uv = (vec2(input.world_position.x, input.world_position.z) + TERRAIN_BOUNDS * 0.5) / TERRAIN_BOUNDS; - let flowmap_sample = textureSampleLevel(flowmap_texture, flowmap_sampler, flowmap_uv, 0.0).rgb; - return vec4(flowmap_sample, 1.0); - } - - if debug == 2u { - let world_pos_2d = vec2(input.world_position.x, input.world_position.z); - let tile_size = 10.0; - let tile_center = floor(world_pos_2d / tile_size) * tile_size + tile_size * 0.5; - let flowmap_uv = (tile_center + TERRAIN_BOUNDS * 0.5) / TERRAIN_BOUNDS; - - let flowmap_sample = textureSampleLevel(flowmap_texture, flowmap_sampler, flowmap_uv, 0.0).rgb; - let x = (flowmap_sample.r) * 2.0 - 1.0; - let y = flowmap_sample.g * 2.0 - 1.0; - let direction_to_path = normalize(vec2(x, y)); - let perpendicular_to_path = normalize(vec2(-direction_to_path.y, direction_to_path.x)); - - let local_pos = world_pos_2d - tile_center; - - let arrow_scale = 0.05; - let parallel_coord = dot(local_pos, direction_to_path) * arrow_scale; - let perpendicular_coord = dot(local_pos, perpendicular_to_path) * arrow_scale; - - let to_path = step(0.95, fract(parallel_coord)); - let to_perp = step(0.95, fract(perpendicular_coord)); - - return vec4(to_perp, to_perp, to_perp, 1.0); - } - - let shadow = sample_shadow_map(input.light_space_position); - - let tile_scale = 2.0; - let spotlight_strokes = all_spotlights_lighting(input.world_position, input.clip_position, input.world_normal, tile_scale, shadow); - let brightness = spotlight_strokes; - - return vec4(brightness, brightness, brightness, 1.0); -} diff --git a/src/shaders/shared.wesl b/src/shaders/util.wesl similarity index 74% rename from src/shaders/shared.wesl rename to src/shaders/util.wesl index 1c38b5f..ce2036b 100644 --- a/src/shaders/shared.wesl +++ b/src/shaders/util.wesl @@ -13,8 +13,7 @@ struct VertexOutput { @builtin(position) clip_position: vec4, @location(0) world_position: vec3, @location(1) world_normal: vec3, - @location(2) light_space_position: vec4, - @location(3) dissolve_amount: f32, + @location(2) dissolve_amount: f32, } const MAX_SPOTLIGHTS: u32 = 4u; @@ -38,15 +37,19 @@ struct Uniforms { model: mat4x4, view: mat4x4, projection: mat4x4, - light_view_projection: mat4x4, + light_view_projections: array, 4>, camera_position: vec3, height_scale: f32, player_position: vec3, time: f32, shadow_bias: f32, + tile_scale: f32, + enable_dissolve: u32, + enable_snow_light: u32, spotlight_count: u32, _padding1: u32, _padding2: u32, + _padding3: u32, spotlights: array, } @@ -54,29 +57,23 @@ struct Uniforms { var uniforms: Uniforms; @group(0) @binding(1) -var shadow_map: texture_depth_2d; +var shadow_map: texture_depth_2d_array; @group(0) @binding(2) var shadow_sampler: sampler_comparison; -@group(0) @binding(3) -var dither_texture_array: texture_2d_array; - -@group(0) @binding(4) -var dither_sampler: sampler; - -@group(0) @binding(5) -var flowmap_texture: texture_2d; - -@group(0) @binding(6) -var flowmap_sampler: sampler; - @group(0) @binding(7) var blue_noise_texture: texture_2d; @group(0) @binding(8) var blue_noise_sampler: sampler; +@group(0) @binding(9) +var snow_light_texture: texture_2d; + +@group(0) @binding(10) +var snow_light_sampler: sampler; + const PI: f32 = 3.14159265359; const TERRAIN_BOUNDS: vec2 = vec2(1000.0, 1000.0); const LINE_THICKNESS: f32 = 0.1; @@ -116,7 +113,8 @@ fn compute_rotation_t(distance_to_light: f32) -> f32 { return pow(min(max(distance_to_light - 1.0 / OCTAVE_STEPS, 0.0) * OCTAVE_STEPS, 1.0), 2.0); } -fn sample_shadow_map(light_space_pos: vec4) -> f32 { +fn sample_shadow_map(world_pos: vec3, spotlight_index: u32) -> f32 { + let light_space_pos = uniforms.light_view_projections[spotlight_index] * vec4(world_pos, 1.0); let proj_coords = light_space_pos.xyz / light_space_pos.w; let ndc_coords = proj_coords * vec3(0.5, -0.5, 1.0) + vec3(0.5, 0.5, 0.0); @@ -125,7 +123,7 @@ fn sample_shadow_map(light_space_pos: vec4) -> f32 { } let depth = ndc_coords.z - uniforms.shadow_bias; - let shadow = textureSampleCompare(shadow_map, shadow_sampler, ndc_coords.xy, depth); + let shadow = textureSampleCompare(shadow_map, shadow_sampler, ndc_coords.xy, spotlight_index, depth); return shadow; } @@ -270,36 +268,18 @@ fn calculate_spotlight_data(world_pos: vec3, normal: vec3, spotlight: return data; } -fn is_in_spotlight_light_area(world_pos: vec3, normal: vec3, spotlight: Spotlight, tile_scale: f32, shadow: f32) -> bool { - return calculate_spotlight_data(world_pos, normal, spotlight, tile_scale, shadow).is_lit; -} - -fn is_in_any_spotlight_light_area(world_pos: vec3, normal: vec3, tile_scale: f32, shadow: f32) -> bool { - for (var i = 0u; i < uniforms.spotlight_count; i++) { - if is_in_spotlight_light_area(world_pos, normal, uniforms.spotlights[i], tile_scale, shadow) { - return true; - } - } - return false; -} - -fn spotlight_lighting(world_pos: vec3, clip_pos: vec4, normal: vec3, spotlight: Spotlight, tile_scale: f32, shadow: f32) -> f32 { - let data = calculate_spotlight_data(world_pos, normal, spotlight, tile_scale, shadow); - - if !data.is_lit { - return 0.0; - } - - return hatching_lighting(world_pos, clip_pos, tile_scale, data.direction_normalized, data.combined_distance); -} - -fn all_spotlights_lighting(world_pos: vec3, clip_pos: vec4, normal: vec3, tile_scale: f32, shadow: f32) -> f32 { +fn all_spotlights_lighting(world_pos: vec3, clip_pos: vec4, normal: vec3, tile_scale: f32) -> f32 { var max_lighting = 0.0; for (var i = 0u; i < uniforms.spotlight_count; i++) { let spotlight = uniforms.spotlights[i]; - let lighting = spotlight_lighting(world_pos, clip_pos, normal, spotlight, tile_scale, shadow); - max_lighting = max(max_lighting, lighting); + let shadow = sample_shadow_map(world_pos, i); + let data = calculate_spotlight_data(world_pos, normal, spotlight, tile_scale, shadow); + + if data.is_lit { + let lighting = hatching_lighting(world_pos, clip_pos, tile_scale, data.direction_normalized, data.combined_distance); + max_lighting = max(max_lighting, lighting); + } } return max_lighting; @@ -344,40 +324,3 @@ fn line_stroke_lighting(data: StrokeData, clip_pos: vec4) -> f32 { return step(line_half_width, effective_distance); } - -fn flowmap_path_lighting(world_pos: vec3, clip_pos: vec4, tile_scale: f32) -> f32 { - let world_pos_2d = vec2(world_pos.x, world_pos.z); - let tile_size = 1.0 / tile_scale; - let tile_center = floor(world_pos_2d / tile_size) * tile_size + tile_size * 0.5; - - let flowmap_uv = (tile_center + TERRAIN_BOUNDS * 0.5) / TERRAIN_BOUNDS; - let flowmap_sample = textureSampleLevel(flowmap_texture, flowmap_sampler, flowmap_uv, 0.0); - let x = flowmap_sample.r * 2.0 - 1.0; - let y = flowmap_sample.g * 2.0 - 1.0; - let direction_to_path = normalize(vec2(x, y)); - let distance_to_path = flowmap_sample.b; - - return hatching_lighting(world_pos, clip_pos, tile_scale, direction_to_path, distance_to_path); -} - -fn flowmap_path_lighting_with_shadow(world_pos: vec3, clip_pos: vec4, normal: vec3, tile_scale: f32, shadow: f32) -> f32 { - let world_pos_2d = vec2(world_pos.x, world_pos.z); - let tile_size = 1.0 / tile_scale; - let tile_center = floor(world_pos_2d / tile_size) * tile_size + tile_size * 0.5; - - let flowmap_uv = (tile_center + TERRAIN_BOUNDS * 0.5) / TERRAIN_BOUNDS; - let flowmap_sample = textureSampleLevel(flowmap_texture, flowmap_sampler, flowmap_uv, 0.0); - let x = flowmap_sample.r * 2.0 - 1.0; - let y = flowmap_sample.g * 2.0 - 1.0; - let direction_to_path = normalize(vec2(x, y)); - let distance_to_path = flowmap_sample.b; - - let light_dir_3d = normalize(vec3(-x, 100.0, -y)); - let diffuse = max(0.0, dot(normalize(normal), light_dir_3d)); - - let lighting_intensity = diffuse * shadow; - let darkness = 1.0 - lighting_intensity; - let combined_distance = min(distance_to_path + darkness * 0.5, 1.0); - - return hatching_lighting(world_pos, clip_pos, tile_scale, direction_to_path, combined_distance); -} diff --git a/src/snow.rs b/src/snow.rs index aa75688..f8b6081 100644 --- a/src/snow.rs +++ b/src/snow.rs @@ -5,11 +5,9 @@ use glam::{Vec2, Vec3}; use wgpu::util::DeviceExt; use crate::{ - components::MeshComponent, - entity::EntityHandle, - mesh::{Mesh, Vertex}, - render, - world::{Transform, World}, + loaders::mesh::{InstanceRaw, Mesh, Vertex}, + render::{self, DrawCall, Pipeline}, + texture::HeightmapTexture, }; pub struct SnowConfig @@ -22,16 +20,6 @@ pub struct SnowConfig impl SnowConfig { - pub fn new(depth_map_path: &str, heightmap_path: &str, terrain_size: Vec2, resolution: (u32, u32)) -> Self - { - Self { - depth_map_path: depth_map_path.to_string(), - heightmap_path: heightmap_path.to_string(), - terrain_size, - resolution, - } - } - pub fn default() -> Self { Self { @@ -43,9 +31,33 @@ impl SnowConfig } } +pub struct ClipmapConfig +{ + pub num_levels: u32, + pub grid_size: u32, + pub base_cell_size: f32, +} + +impl Default for ClipmapConfig +{ + fn default() -> Self + { + Self { + num_levels: 3, + grid_size: 192, + base_cell_size: 0.25, + } + } +} + +struct ClipmapLevel +{ + mesh: Rc, + instance_buffer: wgpu::Buffer, +} + pub struct SnowLayer { - pub entity: EntityHandle, pub depth_texture: wgpu::Texture, pub depth_texture_view: wgpu::TextureView, pub depth_bind_group: wgpu::BindGroup, @@ -54,11 +66,30 @@ pub struct SnowLayer pub deform_bind_group: wgpu::BindGroup, pub deform_pipeline: wgpu::ComputePipeline, pub deform_params_buffer: wgpu::Buffer, + pub terrain_size: Vec2, + + clipmap_config: ClipmapConfig, + levels: Vec, + displacement_bind_group: wgpu::BindGroup, + heightmap_texture: HeightmapTexture, + depth_sampler: wgpu::Sampler, +} + +fn create_instance_buffer() -> wgpu::Buffer +{ + render::with_device(|device| { + device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Snow Clipmap Instance Buffer"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }) + }) } impl SnowLayer { - pub fn load(world: &mut World, config: &SnowConfig) -> anyhow::Result + pub fn load(config: &SnowConfig) -> anyhow::Result { println!("\n=== Loading Snow Layer ==="); println!("Depth map path: {}", config.depth_map_path); @@ -66,44 +97,42 @@ impl SnowLayer println!("Terrain size: {:?}", config.terrain_size); let (depth_data, width, height) = Self::load_depth_map(&config.depth_map_path)?; - let (heightmap_data, hm_width, hm_height) = Self::load_depth_map(&config.heightmap_path)?; - - if width != hm_width || height != hm_height { - anyhow::bail!("Snow depth map ({}×{}) and heightmap ({}×{}) dimensions don't match!", - width, height, hm_width, hm_height); - } println!("Using EXR dimensions: {}×{}", width, height); let (depth_texture, depth_texture_view, depth_bind_group) = Self::create_depth_texture(&depth_data, width, height); - let mesh = Self::generate_snow_mesh(&depth_data, &heightmap_data, width, height, config.terrain_size); - let num_indices = mesh.num_indices; - - let entity = world.spawn(); - world.transforms.insert(entity, Transform::IDENTITY); - - if num_indices > 0 { - world.meshes.insert( - entity, - MeshComponent { - mesh: Rc::new(mesh), - pipeline: render::Pipeline::Snow, - instance_buffer: None, - num_instances: 1, - }, - ); - println!("Snow mesh created with {} indices", num_indices); - } else { - println!("⚠️ No snow mesh created - all depth values are zero"); - } - let (deform_pipeline, deform_bind_group, deform_params_buffer) = Self::create_deform_pipeline(&depth_texture_view); + let heightmap_texture = render::with_device(|device| { + render::with_queue(|queue| { + HeightmapTexture::load(device, queue, &config.heightmap_path) + }) + })?; + + let depth_sampler = render::with_device(|device| { + device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("Snow Depth Sampler"), + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }) + }); + + let displacement_bind_group = Self::create_displacement_bind_group( + &heightmap_texture, + &depth_texture_view, + &depth_sampler, + ); + + let clipmap_config = ClipmapConfig::default(); + let levels = Vec::new(); + Ok(Self { - entity, depth_texture, depth_texture_view, depth_bind_group, @@ -112,6 +141,12 @@ impl SnowLayer deform_bind_group, deform_pipeline, deform_params_buffer, + terrain_size: config.terrain_size, + clipmap_config, + levels, + displacement_bind_group, + heightmap_texture, + depth_sampler, }) } @@ -132,9 +167,20 @@ impl SnowLayer let height = layer.size.height() as u32; println!(" Layer size: {}×{}", width, height); - println!(" Available channels: {:?}", layer.channel_data.list.iter().map(|c| &c.name).collect::>()); + println!( + " Available channels: {:?}", + layer + .channel_data + .list + .iter() + .map(|c| &c.name) + .collect::>() + ); - let channel = layer.channel_data.list.iter() + let channel = layer + .channel_data + .list + .iter() .find(|c| format!("{:?}", c.name).contains("\"R\"")) .or_else(|| layer.channel_data.list.first()) .ok_or_else(|| anyhow::anyhow!("No channels found in EXR"))?; @@ -149,12 +195,24 @@ impl SnowLayer let non_zero_count = depths.iter().filter(|&&v| v > 0.0001).count(); println!(" Total values: {}", depths.len()); - println!(" Min: {:.6}, Max: {:.6}, Avg: {:.6}", min_value, max_value, avg_value); - println!(" Non-zero values: {} ({:.1}%)", non_zero_count, (non_zero_count as f32 / depths.len() as f32) * 100.0); + println!( + " Min: {:.6}, Max: {:.6}, Avg: {:.6}", + min_value, max_value, avg_value + ); + println!( + " Non-zero values: {} ({:.1}%)", + non_zero_count, + (non_zero_count as f32 / depths.len() as f32) * 100.0 + ); - if max_value < 0.0001 { - println!(" ⚠️ WARNING: All values are effectively zero! Snow depth map may be invalid."); - } else { + if max_value < 0.0001 + { + println!( + " ⚠️ WARNING: All values are effectively zero! Snow depth map may be invalid." + ); + } + else + { println!(" ✓ Snow depth data loaded successfully"); } @@ -237,96 +295,6 @@ impl SnowLayer }) } - fn generate_snow_mesh( - depth_data: &[f32], - heightmap_data: &[f32], - width: u32, - height: u32, - terrain_size: Vec2, - ) -> Mesh - { - let mut vertices = Vec::new(); - let mut indices = Vec::new(); - - let cell_size_x = terrain_size.x / (width - 1) as f32; - let cell_size_z = terrain_size.y / (height - 1) as f32; - let half_width = terrain_size.x / 2.0; - let half_height = terrain_size.y / 2.0; - - for z in 0..height - { - for x in 0..width - { - let index = (z * width + x) as usize; - let snow_depth = depth_data.get(index).copied().unwrap_or(0.0); - let terrain_height = heightmap_data.get(index).copied().unwrap_or(0.0); - - let world_x = x as f32 * cell_size_x - half_width; - let world_z = z as f32 * cell_size_z - half_height; - let world_y = terrain_height + snow_depth; - - vertices.push(Vertex { - position: [world_x, world_y, world_z], - normal: [0.0, 1.0, 0.0], - uv: [x as f32 / width as f32, z as f32 / height as f32], - }); - } - } - - for z in 0..(height - 1) - { - for x in 0..(width - 1) - { - let index = (z * width + x) as usize; - let depth_tl = depth_data.get(index).copied().unwrap_or(0.0); - let depth_tr = depth_data.get(index + 1).copied().unwrap_or(0.0); - let depth_bl = depth_data.get(index + width as usize).copied().unwrap_or(0.0); - let depth_br = depth_data - .get(index + width as usize + 1) - .copied() - .unwrap_or(0.0); - - if depth_tl > 0.001 - || depth_tr > 0.001 - || depth_bl > 0.001 - || depth_br > 0.001 - { - let vertex_index = (z * width + x) as u32; - - indices.push(vertex_index); - indices.push(vertex_index + width); - indices.push(vertex_index + 1); - - indices.push(vertex_index + 1); - indices.push(vertex_index + width); - indices.push(vertex_index + width + 1); - } - } - } - - let vertex_buffer = render::with_device(|device| { - device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Snow Vertex Buffer"), - contents: bytemuck::cast_slice(&vertices), - usage: wgpu::BufferUsages::VERTEX, - }) - }); - - let index_buffer = render::with_device(|device| { - device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Snow Index Buffer"), - contents: bytemuck::cast_slice(&indices), - usage: wgpu::BufferUsages::INDEX, - }) - }); - - Mesh { - vertex_buffer, - index_buffer, - num_indices: indices.len() as u32, - } - } - fn create_deform_pipeline( depth_texture_view: &wgpu::TextureView, ) -> (wgpu::ComputePipeline, wgpu::BindGroup, wgpu::Buffer) @@ -408,10 +376,215 @@ impl SnowLayer }) } + fn create_displacement_bind_group( + heightmap: &HeightmapTexture, + depth_view: &wgpu::TextureView, + depth_sampler: &wgpu::Sampler, + ) -> wgpu::BindGroup + { + render::with_device(|device| { + let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Snow Displacement Bind Group Layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: false }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: false }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), + count: None, + }, + ], + }); + + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Snow Displacement Bind Group"), + layout: &layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&heightmap.view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&heightmap.sampler), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::TextureView(depth_view), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::Sampler(depth_sampler), + }, + ], + }) + }) + } + + fn generate_clipmap_grid(&self, level: u32) -> Mesh + { + let cell_size = self.clipmap_config.base_cell_size * (1 << level) as f32; + let grid_size = self.clipmap_config.grid_size; + let half_extent = (grid_size as f32 * cell_size) / 2.0; + + let mut vertices = Vec::new(); + let mut indices = Vec::new(); + + for z in 0..=grid_size + { + for x in 0..=grid_size + { + vertices.push(Vertex { + position: [ + -half_extent + x as f32 * cell_size, + 0.0, + -half_extent + z as f32 * cell_size, + ], + normal: [0.0, 1.0, 0.0], + uv: [0.0, 0.0], + }); + } + } + + let row = grid_size + 1; + for z in 0..grid_size + { + for x in 0..grid_size + { + let i0 = z * row + x; + let i1 = i0 + 1; + let i2 = i0 + row; + let i3 = i2 + 1; + indices.push(i0); + indices.push(i2); + indices.push(i1); + indices.push(i1); + indices.push(i2); + indices.push(i3); + } + } + + let vertex_buffer = render::with_device(|device| { + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("Snow Clipmap Level {} Vertex Buffer", level)), + contents: bytemuck::cast_slice(&vertices), + usage: wgpu::BufferUsages::VERTEX, + }) + }); + + let index_buffer = render::with_device(|device| { + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("Snow Clipmap Level {} Index Buffer", level)), + contents: bytemuck::cast_slice(&indices), + usage: wgpu::BufferUsages::INDEX, + }) + }); + + Mesh { + vertex_buffer, + index_buffer, + num_indices: indices.len() as u32, + } + } + + pub fn update(&mut self, center: Vec3) + { + if self.levels.is_empty() + { + for level in 0..self.clipmap_config.num_levels + { + let mesh = self.generate_clipmap_grid(level); + let instance_buffer = create_instance_buffer(); + + self.levels.push(ClipmapLevel { + mesh: Rc::new(mesh), + instance_buffer, + }); + } + } + + render::with_queue(|queue| { + for (level, clipmap_level) in self.levels.iter().enumerate() + { + let cell_size = self.clipmap_config.base_cell_size * (1u32 << level) as f32; + let snapped_x = (center.x / cell_size).round() * cell_size; + let snapped_z = (center.z / cell_size).round() * cell_size; + let y_bias = -(level as f32) * 0.005; + let model = + glam::Mat4::from_translation(glam::Vec3::new(snapped_x, y_bias, snapped_z)); + let instance_data = InstanceRaw { + model: model.to_cols_array_2d(), + dissolve_amount: 0.0, + _padding: [0.0; 3], + }; + queue.write_buffer( + &clipmap_level.instance_buffer, + 0, + bytemuck::cast_slice(&[instance_data]), + ); + } + }); + } + + pub fn get_draw_calls(&self) -> Vec + { + self.levels + .iter() + .rev() + .map(|level| DrawCall { + vertex_buffer: level.mesh.vertex_buffer.clone(), + index_buffer: level.mesh.index_buffer.clone(), + num_indices: level.mesh.num_indices, + model: glam::Mat4::IDENTITY, + pipeline: Pipeline::SnowClipmap, + instance_buffer: Some(level.instance_buffer.clone()), + num_instances: 1, + tile_scale: 2.0, + enable_dissolve: false, + enable_snow_light: true, + displacement_bind_group: Some(self.displacement_bind_group.clone()), + }) + .collect() + } + pub fn deform_at_position(&self, position: Vec3, radius: f32, depth: f32) { render::with_queue(|queue| { - let params_data = [position.x, position.z, radius, depth]; + let params_data = [ + position.x, + position.z, + radius, + depth, + self.terrain_size.x, + self.terrain_size.y, + 0.0, + 0.0, + ]; let params_bytes: &[u8] = bytemuck::cast_slice(¶ms_data); queue.write_buffer(&self.deform_params_buffer, 0, params_bytes); @@ -442,10 +615,4 @@ impl SnowLayer }); }); } - - #[allow(dead_code)] - pub fn regenerate_mesh(&mut self, _world: &mut World, _config: &SnowConfig) - { - todo!("Implement regenerate_mesh with correct wgpu types for texture-to-buffer copy"); - } } diff --git a/src/snow_light.rs b/src/snow_light.rs index 7797536..979405d 100644 --- a/src/snow_light.rs +++ b/src/snow_light.rs @@ -12,7 +12,7 @@ struct AccumulationUniforms delta_time: f32, spotlight_count: u32, _padding: u32, - light_view_projection: [[f32; 4]; 4], + light_view_projections: [[[f32; 4]; 4]; crate::render::MAX_SPOTLIGHTS], shadow_bias: f32, terrain_height_scale: f32, _padding3: f32, @@ -48,12 +48,8 @@ pub struct SnowLightAccumulation impl SnowLightAccumulation { - pub fn new( - device: &wgpu::Device, - terrain_min: Vec2, - terrain_max: Vec2, - resolution: u32, - ) -> Self + pub fn new(device: &wgpu::Device, terrain_min: Vec2, terrain_max: Vec2, resolution: u32) + -> Self { let size = wgpu::Extent3d { width: resolution, @@ -68,8 +64,7 @@ impl SnowLightAccumulation sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::R16Float, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::TEXTURE_BINDING, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, view_formats: &[], }; @@ -145,7 +140,7 @@ impl SnowLightAccumulation visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Depth, - view_dimension: wgpu::TextureViewDimension::D2, + view_dimension: wgpu::TextureViewDimension::D2Array, multisampled: false, }, count: None, @@ -232,9 +227,7 @@ impl SnowLightAccumulation }); let vertices: &[f32] = &[ - -1.0, -1.0, 0.0, 1.0, - 3.0, -1.0, 2.0, 1.0, - -1.0, 3.0, 0.0, -1.0, + -1.0, -1.0, 0.0, 1.0, 3.0, -1.0, 2.0, 1.0, -1.0, 3.0, 0.0, -1.0, ]; let quad_vb = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { @@ -419,7 +412,7 @@ impl SnowLightAccumulation queue: &wgpu::Queue, spotlights: &[crate::render::Spotlight], delta_time: f32, - light_view_projection: &glam::Mat4, + light_view_projections: &[glam::Mat4], shadow_bias: f32, terrain_height_scale: f32, ) @@ -430,8 +423,12 @@ impl SnowLightAccumulation self.needs_clear = false; } - let mut spotlight_array = [crate::render::SpotlightRaw::default(); crate::render::MAX_SPOTLIGHTS]; - for (i, spotlight) in spotlights.iter().take(crate::render::MAX_SPOTLIGHTS).enumerate() + let mut spotlight_array = + [crate::render::SpotlightRaw::default(); crate::render::MAX_SPOTLIGHTS]; + for (i, spotlight) in spotlights + .iter() + .take(crate::render::MAX_SPOTLIGHTS) + .enumerate() { spotlight_array[i] = spotlight.to_raw(); } @@ -443,7 +440,17 @@ impl SnowLightAccumulation delta_time, spotlight_count: spotlights.len().min(crate::render::MAX_SPOTLIGHTS) as u32, _padding: 0, - light_view_projection: light_view_projection.to_cols_array_2d(), + light_view_projections: { + let mut arr = [[[0.0f32; 4]; 4]; crate::render::MAX_SPOTLIGHTS]; + for (i, mat) in light_view_projections + .iter() + .take(crate::render::MAX_SPOTLIGHTS) + .enumerate() + { + arr[i] = mat.to_cols_array_2d(); + } + arr + }, shadow_bias, terrain_height_scale, _padding3: 0.0, @@ -453,10 +460,26 @@ impl SnowLightAccumulation queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms])); - let write_view = if self.current { &self.view_ping } else { &self.view_pong }; - let bind_group = if self.current { self.bind_group_ping.as_ref() } else { self.bind_group_pong.as_ref() }; + let write_view = if self.current + { + &self.view_ping + } + else + { + &self.view_pong + }; + let bind_group = if self.current + { + self.bind_group_ping.as_ref() + } + else + { + self.bind_group_pong.as_ref() + }; - let Some(bind_group) = bind_group else { + let Some(bind_group) = bind_group + else + { return; }; @@ -489,7 +512,14 @@ impl SnowLightAccumulation pub fn read_view(&self) -> &wgpu::TextureView { - if self.current { &self.view_pong } else { &self.view_ping } + if self.current + { + &self.view_pong + } + else + { + &self.view_ping + } } pub fn create_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout diff --git a/src/systems/camera.rs b/src/systems/camera.rs index af41fd1..02458d8 100644 --- a/src/systems/camera.rs +++ b/src/systems/camera.rs @@ -4,6 +4,32 @@ use crate::components::FollowComponent; use crate::utility::input::InputState; use crate::world::{Transform, World}; +pub fn camera_view_matrix(world: &World) -> Option +{ + let (camera_entity, camera_component) = world.active_camera()?; + let camera_transform = world.transforms.get(camera_entity)?; + + if let Some(follow) = world.follows.get(camera_entity) + { + if let Some(target_transform) = world.transforms.get(follow.target) + { + return Some(glam::Mat4::look_at_rh( + camera_transform.position, + target_transform.position, + glam::Vec3::Y, + )); + } + } + + let forward = camera_component.get_forward(); + let target = camera_transform.position + forward; + Some(glam::Mat4::look_at_rh( + camera_transform.position, + target, + glam::Vec3::Y, + )) +} + pub fn camera_input_system(world: &mut World, input_state: &InputState) { let cameras: Vec<_> = world.cameras.all(); diff --git a/src/systems/input.rs b/src/systems/input.rs index f21e1eb..4caa50f 100644 --- a/src/systems/input.rs +++ b/src/systems/input.rs @@ -5,14 +5,11 @@ use crate::world::World; pub fn player_input_system(world: &mut World, input_state: &InputState) { - let active_camera = world.cameras.get_active(); - - if active_camera.is_none() + let Some((_, camera)) = world.active_camera() + else { return; - } - - let (_, camera) = active_camera.unwrap(); + }; let forward = camera.get_forward_horizontal(); let right = camera.get_right_horizontal(); diff --git a/src/systems/mod.rs b/src/systems/mod.rs index c1ab09f..d96a15c 100644 --- a/src/systems/mod.rs +++ b/src/systems/mod.rs @@ -1,22 +1,25 @@ pub mod camera; pub mod follow; pub mod input; +pub mod noclip; pub mod physics_sync; +pub mod player_states; pub mod render; pub mod rotate; +pub mod snow; pub mod spotlight_sync; pub mod state_machine; pub mod tree_dissolve; pub use camera::{ - camera_follow_system, camera_input_system, camera_noclip_system, start_camera_following, - stop_camera_following, + camera_follow_system, camera_input_system, camera_noclip_system, camera_view_matrix, + start_camera_following, }; -pub use follow::follow_system; pub use input::player_input_system; +pub use noclip::noclip_toggle_system; pub use physics_sync::physics_sync_system; pub use render::render_system; pub use rotate::rotate_system; +pub use snow::snow_system; pub use spotlight_sync::spotlight_sync_system; pub use state_machine::{state_machine_physics_system, state_machine_system}; -pub use tree_dissolve::{tree_dissolve_update_system, tree_occlusion_system}; diff --git a/src/systems/noclip.rs b/src/systems/noclip.rs new file mode 100644 index 0000000..b983730 --- /dev/null +++ b/src/systems/noclip.rs @@ -0,0 +1,25 @@ +use crate::entity::EntityHandle; +use crate::systems::camera::{start_camera_following, stop_camera_following}; +use crate::utility::input::InputState; +use crate::world::World; + +pub fn noclip_toggle_system( + world: &mut World, + input_state: &InputState, + camera_entity: EntityHandle, + noclip_mode: &mut bool, +) +{ + if input_state.noclip_just_pressed + { + *noclip_mode = !*noclip_mode; + if *noclip_mode + { + stop_camera_following(world, camera_entity); + } + else + { + start_camera_following(world, camera_entity); + } + } +} diff --git a/src/player.rs b/src/systems/player_states.rs similarity index 62% rename from src/player.rs rename to src/systems/player_states.rs index a70753a..4e56d36 100644 --- a/src/player.rs +++ b/src/systems/player_states.rs @@ -1,216 +1,16 @@ -use std::{f32::consts::PI, rc::Rc}; - use glam::Vec3; use kurbo::ParamCurve; -use rapier3d::{ - control::{CharacterAutostep, KinematicCharacterController}, - math::Vector, - prelude::{ColliderBuilder, RigidBodyBuilder}, -}; +use rapier3d::math::Vector; -use crate::{ - components::{ - jump::JumpComponent, lights::spot::SpotlightComponent, InputComponent, MeshComponent, - MovementComponent, PhysicsComponent, - }, - entity::EntityHandle, - mesh::Mesh, - physics::PhysicsManager, - render::Pipeline, - state::{State, StateMachine}, - world::{Transform, World}, -}; - -pub struct Player; - -impl Player -{ - pub fn spawn(world: &mut World, position: Vec3) -> EntityHandle - { - let entity = world.spawn(); - - let spawn_transform = Transform::from_position(position); - - let rigidbody = RigidBodyBuilder::kinematic_position_based() - .translation(spawn_transform.position.into()) - .build(); - let collider = ColliderBuilder::capsule_y(0.5, 0.5).build(); - let _controller = KinematicCharacterController { - slide: true, - autostep: Some(CharacterAutostep::default()), - max_slope_climb_angle: 45.0, - ..Default::default() - }; - - let rigidbody_handle = PhysicsManager::add_rigidbody(rigidbody); - let collider_handle = PhysicsManager::add_collider(collider, Some(rigidbody_handle)); - - let mesh = Mesh::load_mesh("meshes/player_mesh.glb").expect("missing player mesh"); - - let falling_state = PlayerFallingState { entity }; - let idle_state = PlayerIdleState { entity }; - let walking_state = PlayerWalkingState { - entity, - enter_time_stamp: 0.0, - }; - let jumping_state = PlayerJumpingState { - entity, - enter_time_stamp: 0.0, - }; - - let mut state_machine = StateMachine::new(Box::new(falling_state)); - state_machine.add_state(walking_state); - state_machine.add_state(idle_state); - state_machine.add_state(jumping_state); - - let entity_id = entity; - - state_machine.add_transition::(move |world| { - let is_grounded = world - .movements - .with(entity_id, |m| m.movement_config.movement_context.is_floored) - .unwrap_or(false); - let has_input = world - .inputs - .with(entity_id, |i| i.move_direction.length() > 0.01) - .unwrap_or(false); - is_grounded && !has_input - }); - - state_machine.add_transition::(move |world| { - let is_grounded = world - .movements - .with(entity_id, |m| m.movement_config.movement_context.is_floored) - .unwrap_or(false); - let has_input = world - .inputs - .with(entity_id, |i| i.move_direction.length() > 0.01) - .unwrap_or(false); - is_grounded && has_input - }); - - state_machine.add_transition::(move |world| { - let is_grounded = world - .movements - .with(entity_id, |m| m.movement_config.movement_context.is_floored) - .unwrap_or(false); - let has_input = world - .inputs - .with(entity_id, |i| i.move_direction.length() > 0.01) - .unwrap_or(false); - is_grounded && has_input - }); - - state_machine.add_transition::(move |world| { - let is_grounded = world - .movements - .with(entity_id, |m| m.movement_config.movement_context.is_floored) - .unwrap_or(false); - let has_input = world - .inputs - .with(entity_id, |i| i.move_direction.length() > 0.01) - .unwrap_or(false); - is_grounded && !has_input - }); - - state_machine.add_transition::(move |world| { - let is_grounded = world - .movements - .with(entity_id, |m| m.movement_config.movement_context.is_floored) - .unwrap_or(false); - !is_grounded - }); - - state_machine.add_transition::(move |world| { - let is_grounded = world - .movements - .with(entity_id, |m| m.movement_config.movement_context.is_floored) - .unwrap_or(false); - !is_grounded - }); - - state_machine.add_transition::(move |world| { - let is_grounded = world - .movements - .with(entity_id, |m| m.movement_config.movement_context.is_floored) - .unwrap_or(false); - let jump_pressed = world - .inputs - .with(entity_id, |i| i.jump_just_pressed) - .unwrap_or(false); - is_grounded && jump_pressed - }); - - state_machine.add_transition::(move |world| { - let is_grounded = world - .movements - .with(entity_id, |m| m.movement_config.movement_context.is_floored) - .unwrap_or(false); - let jump_pressed = world - .inputs - .with(entity_id, |i| i.jump_just_pressed) - .unwrap_or(false); - is_grounded && jump_pressed - }); - - state_machine.add_transition::(move |world| { - world - .jumps - .with(entity_id, |jump| { - jump.jump_config.jump_context.duration >= jump.jump_config.jump_duration - }) - .unwrap_or(true) - }); - - world - .transforms - .insert(entity, spawn_transform); - world.movements.insert(entity, MovementComponent::new()); - world.jumps.insert(entity, JumpComponent::new()); - world.inputs.insert(entity, InputComponent::default()); - world.physics.insert( - entity, - PhysicsComponent { - rigidbody: rigidbody_handle, - collider: Some(collider_handle), - }, - ); - world.meshes.insert( - entity, - MeshComponent { - mesh: Rc::new(mesh), - pipeline: Pipeline::Standard, - instance_buffer: None, - num_instances: 1, - }, - ); - world.player_tags.insert(entity); - world.state_machines.insert(entity, state_machine); - - let outer_angle = PI / 2.0 * 0.9; - world.spotlights.insert( - entity, - SpotlightComponent::new( - Vec3::new(1.0, 2.0, 1.0), - Vec3::new(0.0, -1.0, 0.0), - 100.0, - outer_angle * 0.5, - outer_angle, - ), - ); - - entity - } - - pub fn despawn(world: &mut World, entity: EntityHandle) - { - world.despawn(entity); - } -} +use crate::entity::EntityHandle; +use crate::physics::PhysicsManager; +use crate::state::State; +use crate::utility::time::Time; +use crate::world::World; pub struct PlayerFallingState { - entity: EntityHandle, + pub entity: EntityHandle, } impl State for PlayerFallingState @@ -220,14 +20,14 @@ impl State for PlayerFallingState "PlayerFallingState" } - fn on_state_enter(&mut self, world: &mut World) + fn on_state_enter(&mut self, _world: &mut World) { println!("entered falling"); } - fn on_state_exit(&mut self, world: &mut World) {} + fn on_state_exit(&mut self, _world: &mut World) {} - fn on_state_update(&mut self, world: &mut World, delta: f32) {} + fn on_state_update(&mut self, _world: &mut World, _delta: f32) {} fn on_state_physics_update(&mut self, world: &mut World, delta: f32) { @@ -297,7 +97,7 @@ impl State for PlayerFallingState pub struct PlayerIdleState { - entity: EntityHandle, + pub entity: EntityHandle, } impl State for PlayerIdleState @@ -338,7 +138,7 @@ impl State for PlayerIdleState fn on_state_update(&mut self, _world: &mut World, _delta: f32) {} - fn on_state_physics_update(&mut self, world: &mut World, delta: f32) + fn on_state_physics_update(&mut self, world: &mut World, _delta: f32) { const GROUND_CHECK_DISTANCE: f32 = 0.6; @@ -380,8 +180,8 @@ impl State for PlayerIdleState pub struct PlayerWalkingState { - entity: EntityHandle, - enter_time_stamp: f32, + pub entity: EntityHandle, + pub enter_time_stamp: f32, } impl State for PlayerWalkingState @@ -393,19 +193,16 @@ impl State for PlayerWalkingState fn on_state_enter(&mut self, _world: &mut World) { - use crate::utility::time::Time; self.enter_time_stamp = Time::get_time_elapsed(); println!("entered walking"); } fn on_state_exit(&mut self, _world: &mut World) {} - fn on_state_update(&mut self, world: &mut World, delta: f32) {} + fn on_state_update(&mut self, _world: &mut World, _delta: f32) {} fn on_state_physics_update(&mut self, world: &mut World, delta: f32) { - use crate::utility::time::Time; - let (movement_input, walking_config) = world .movements .with(self.entity, |movement| { @@ -519,8 +316,8 @@ impl State for PlayerWalkingState pub struct PlayerJumpingState { - entity: EntityHandle, - enter_time_stamp: f32, + pub entity: EntityHandle, + pub enter_time_stamp: f32, } impl State for PlayerJumpingState @@ -532,7 +329,6 @@ impl State for PlayerJumpingState fn on_state_enter(&mut self, world: &mut World) { - use crate::utility::time::Time; self.enter_time_stamp = Time::get_time_elapsed(); let current_position = world.transforms.get(self.entity).unwrap().position; @@ -562,8 +358,6 @@ impl State for PlayerJumpingState fn on_state_physics_update(&mut self, world: &mut World, delta: f32) { - use crate::utility::time::Time; - let current_time = Time::get_time_elapsed(); world.jumps.with_mut(self.entity, |jump| { diff --git a/src/systems/render.rs b/src/systems/render.rs index 9c3a04c..ce22765 100644 --- a/src/systems/render.rs +++ b/src/systems/render.rs @@ -1,4 +1,4 @@ -use crate::mesh::InstanceRaw; +use crate::loaders::mesh::InstanceRaw; use crate::render::DrawCall; use crate::world::World; use bytemuck::cast_slice; @@ -56,6 +56,10 @@ pub fn render_system(world: &World) -> Vec pipeline: mesh_component.pipeline, instance_buffer, num_instances, + tile_scale: mesh_component.tile_scale, + enable_dissolve: mesh_component.enable_dissolve, + enable_snow_light: mesh_component.enable_snow_light, + displacement_bind_group: None, }) }) .collect() diff --git a/src/systems/snow.rs b/src/systems/snow.rs new file mode 100644 index 0000000..52ffb88 --- /dev/null +++ b/src/systems/snow.rs @@ -0,0 +1,13 @@ +use crate::snow::SnowLayer; +use crate::world::World; + +pub fn snow_system(world: &World, snow_layer: &mut SnowLayer, noclip: bool) +{ + let camera_pos = world.active_camera_position(); + let player_pos = world.player_position(); + if !noclip + { + snow_layer.deform_at_position(player_pos, 1.5, 10.0); + } + snow_layer.update(camera_pos); +} diff --git a/src/systems/tree_dissolve.rs b/src/systems/tree_dissolve.rs index 90d9df0..7f379e3 100644 --- a/src/systems/tree_dissolve.rs +++ b/src/systems/tree_dissolve.rs @@ -1,6 +1,5 @@ use crate::components::DissolveComponent; use crate::world::World; -use glam::Vec3; pub fn tree_dissolve_update_system(world: &mut World, delta: f32) { @@ -22,22 +21,9 @@ pub fn tree_occlusion_system(world: &mut World) if let Some(player_pos) = player_pos { - let camera_entity = world.cameras.get_active().map(|(e, _)| e); + let camera_entity = world.active_camera().map(|(e, _)| e); let camera_pos = camera_entity.and_then(|e| world.transforms.get(e).map(|t| t.position)); - let tree_count = world.tree_tags.all().len(); - if tree_count > 0 - { - static mut FRAME_COUNT: u32 = 0; - unsafe { - FRAME_COUNT += 1; - if FRAME_COUNT % 60 == 0 - { - println!("Tree occlusion system: {} trees detected", tree_count); - } - } - } - if let Some(camera_pos) = camera_pos { let to_player = player_pos - camera_pos; @@ -74,18 +60,6 @@ pub fn tree_occlusion_system(world: &mut World) let dissolve_amount = 1.0 - (perp_distance / occlusion_radius).clamp(0.0, 1.0); - static mut DEBUG_FRAME: u32 = 0; - unsafe { - DEBUG_FRAME += 1; - if DEBUG_FRAME % 60 == 0 - { - println!( - "Tree occluding! perp_dist: {:.2}, dissolve: {:.2}", - perp_distance, dissolve_amount - ); - } - } - if let Some(dissolve) = world.dissolves.get_mut(tree_entity) { dissolve.target_amount = dissolve_amount; diff --git a/src/world.rs b/src/world.rs index 0e1cb47..a29e72f 100644 --- a/src/world.rs +++ b/src/world.rs @@ -13,12 +13,12 @@ use crate::state::StateMachine; pub use crate::utility::transform::Transform; -pub struct TransformStorage +pub struct Storage { - pub components: HashMap, + pub components: HashMap, } -impl TransformStorage +impl Storage { pub fn new() -> Self { @@ -27,31 +27,31 @@ impl TransformStorage } } - pub fn insert(&mut self, entity: EntityHandle, component: Transform) + pub fn insert(&mut self, entity: EntityHandle, component: T) { self.components.insert(entity, component); } - pub fn get(&self, entity: EntityHandle) -> Option<&Transform> + pub fn get(&self, entity: EntityHandle) -> Option<&T> { self.components.get(&entity) } - pub fn get_mut(&mut self, entity: EntityHandle) -> Option<&mut Transform> + pub fn get_mut(&mut self, entity: EntityHandle) -> Option<&mut T> { self.components.get_mut(&entity) } pub fn with(&self, entity: EntityHandle, f: F) -> Option where - F: FnOnce(&Transform) -> R, + F: FnOnce(&T) -> R, { self.components.get(&entity).map(f) } pub fn with_mut(&mut self, entity: EntityHandle, f: F) -> Option where - F: FnOnce(&mut Transform) -> R, + F: FnOnce(&mut T) -> R, { self.components.get_mut(&entity).map(f) } @@ -67,554 +67,23 @@ impl TransformStorage } } -pub struct MeshStorage -{ - pub components: HashMap, -} - -impl MeshStorage -{ - pub fn new() -> Self - { - Self { - components: HashMap::new(), - } - } - - pub fn insert(&mut self, entity: EntityHandle, component: MeshComponent) - { - self.components.insert(entity, component); - } - - pub fn get(&self, entity: EntityHandle) -> Option<&MeshComponent> - { - self.components.get(&entity) - } - - pub fn remove(&mut self, entity: EntityHandle) - { - self.components.remove(&entity); - } - - pub fn all(&self) -> Vec - { - self.components.keys().copied().collect() - } -} - -pub struct PhysicsStorage -{ - pub components: HashMap, -} - -impl PhysicsStorage -{ - pub fn new() -> Self - { - Self { - components: HashMap::new(), - } - } - - pub fn insert(&mut self, entity: EntityHandle, component: PhysicsComponent) - { - self.components.insert(entity, component); - } - - pub fn get(&self, entity: EntityHandle) -> Option - { - self.components.get(&entity).copied() - } - - pub fn with(&self, entity: EntityHandle, f: F) -> Option - where - F: FnOnce(&PhysicsComponent) -> R, - { - self.components.get(&entity).map(f) - } - - pub fn remove(&mut self, entity: EntityHandle) - { - self.components.remove(&entity); - } - - pub fn all(&self) -> Vec - { - self.components.keys().copied().collect() - } -} - -pub struct MovementStorage -{ - pub components: HashMap, -} - -impl MovementStorage -{ - pub fn new() -> Self - { - Self { - components: HashMap::new(), - } - } - - pub fn insert(&mut self, entity: EntityHandle, component: MovementComponent) - { - self.components.insert(entity, component); - } - - pub fn get(&self, entity: EntityHandle) -> Option<&MovementComponent> - { - self.components.get(&entity) - } - - pub fn with(&self, entity: EntityHandle, f: F) -> Option - where - F: FnOnce(&MovementComponent) -> R, - { - self.components.get(&entity).map(f) - } - - pub fn with_mut(&mut self, entity: EntityHandle, f: F) -> Option - where - F: FnOnce(&mut MovementComponent) -> R, - { - self.components.get_mut(&entity).map(f) - } - - pub fn remove(&mut self, entity: EntityHandle) - { - self.components.remove(&entity); - } - - pub fn all(&self) -> Vec - { - self.components.keys().copied().collect() - } -} - -pub struct JumpStorage -{ - pub components: HashMap, -} - -impl JumpStorage -{ - pub fn new() -> Self - { - Self { - components: HashMap::new(), - } - } - - pub fn insert(&mut self, entity: EntityHandle, component: JumpComponent) - { - self.components.insert(entity, component); - } - - pub fn get(&self, entity: EntityHandle) -> Option<&JumpComponent> - { - self.components.get(&entity) - } - - pub fn get_mut(&mut self, entity: EntityHandle) -> Option<&mut JumpComponent> - { - self.components.get_mut(&entity) - } - - pub fn with(&self, entity: EntityHandle, f: F) -> Option - where - F: FnOnce(&JumpComponent) -> R, - { - self.components.get(&entity).map(f) - } - - pub fn with_mut(&mut self, entity: EntityHandle, f: F) -> Option - where - F: FnOnce(&mut JumpComponent) -> R, - { - self.components.get_mut(&entity).map(f) - } - - pub fn remove(&mut self, entity: EntityHandle) - { - self.components.remove(&entity); - } - - pub fn all(&self) -> Vec - { - self.components.keys().copied().collect() - } -} - -pub struct InputStorage -{ - pub components: HashMap, -} - -impl InputStorage -{ - pub fn new() -> Self - { - Self { - components: HashMap::new(), - } - } - - pub fn insert(&mut self, entity: EntityHandle, component: InputComponent) - { - self.components.insert(entity, component); - } - - pub fn get(&self, entity: EntityHandle) -> Option<&InputComponent> - { - self.components.get(&entity) - } - - pub fn with(&self, entity: EntityHandle, f: F) -> Option - where - F: FnOnce(&InputComponent) -> R, - { - self.components.get(&entity).map(f) - } - - pub fn with_mut(&mut self, entity: EntityHandle, f: F) -> Option - where - F: FnOnce(&mut InputComponent) -> R, - { - self.components.get_mut(&entity).map(f) - } - - pub fn remove(&mut self, entity: EntityHandle) - { - self.components.remove(&entity); - } - - pub fn all(&self) -> Vec - { - self.components.keys().copied().collect() - } -} - -pub struct PlayerTagStorage -{ - pub components: HashMap, -} - -impl PlayerTagStorage -{ - pub fn new() -> Self - { - Self { - components: HashMap::new(), - } - } - - pub fn insert(&mut self, entity: EntityHandle) - { - self.components.insert(entity, ()); - } - - pub fn remove(&mut self, entity: EntityHandle) - { - self.components.remove(&entity); - } - - pub fn all(&self) -> Vec - { - self.components.keys().copied().collect() - } -} - -pub struct StateMachineStorage -{ - pub components: HashMap, -} - -impl StateMachineStorage -{ - pub fn new() -> Self - { - Self { - components: HashMap::new(), - } - } - - pub fn insert(&mut self, entity: EntityHandle, component: StateMachine) - { - self.components.insert(entity, component); - } - - pub fn with_mut(&mut self, entity: EntityHandle, f: F) -> Option - where - F: FnOnce(&mut StateMachine) -> R, - { - self.components.get_mut(&entity).map(f) - } - - pub fn remove(&mut self, entity: EntityHandle) - { - self.components.remove(&entity); - } - - pub fn all(&self) -> Vec - { - self.components.keys().copied().collect() - } -} - -pub struct CameraStorage -{ - pub components: HashMap, -} - -impl CameraStorage -{ - pub fn new() -> Self - { - Self { - components: HashMap::new(), - } - } - - pub fn insert(&mut self, entity: EntityHandle, component: CameraComponent) - { - self.components.insert(entity, component); - } - - pub fn get(&self, entity: EntityHandle) -> Option<&CameraComponent> - { - self.components.get(&entity) - } - - pub fn get_mut(&mut self, entity: EntityHandle) -> Option<&mut CameraComponent> - { - self.components.get_mut(&entity) - } - - pub fn with_mut(&mut self, entity: EntityHandle, f: F) -> Option - where - F: FnOnce(&mut CameraComponent) -> R, - { - self.components.get_mut(&entity).map(f) - } - - pub fn remove(&mut self, entity: EntityHandle) - { - self.components.remove(&entity); - } - - pub fn all(&self) -> Vec - { - self.components.keys().copied().collect() - } - - pub fn get_active(&self) -> Option<(EntityHandle, &CameraComponent)> - { - self.components - .iter() - .find(|(_, cam)| cam.is_active) - .map(|(e, c)| (*e, c)) - } -} - -pub struct SpotlightStorage -{ - pub components: HashMap, -} - -impl SpotlightStorage -{ - pub fn new() -> Self - { - Self { - components: HashMap::new(), - } - } - - pub fn insert(&mut self, entity: EntityHandle, component: SpotlightComponent) - { - self.components.insert(entity, component); - } - - pub fn get(&self, entity: EntityHandle) -> Option<&SpotlightComponent> - { - self.components.get(&entity) - } - - pub fn get_mut(&mut self, entity: EntityHandle) -> Option<&mut SpotlightComponent> - { - self.components.get_mut(&entity) - } - - pub fn remove(&mut self, entity: EntityHandle) - { - self.components.remove(&entity); - } - - pub fn all(&self) -> Vec - { - self.components.keys().copied().collect() - } -} - -pub struct TreeTagStorage -{ - pub components: HashMap, -} - -impl TreeTagStorage -{ - pub fn new() -> Self - { - Self { - components: HashMap::new(), - } - } - - pub fn insert(&mut self, entity: EntityHandle) - { - self.components.insert(entity, ()); - } - - pub fn remove(&mut self, entity: EntityHandle) - { - self.components.remove(&entity); - } - - pub fn all(&self) -> Vec - { - self.components.keys().copied().collect() - } -} - -pub struct DissolveStorage -{ - pub components: HashMap, -} - -impl DissolveStorage -{ - pub fn new() -> Self - { - Self { - components: HashMap::new(), - } - } - - pub fn insert(&mut self, entity: EntityHandle, component: DissolveComponent) - { - self.components.insert(entity, component); - } - - pub fn get(&self, entity: EntityHandle) -> Option<&DissolveComponent> - { - self.components.get(&entity) - } - - pub fn get_mut(&mut self, entity: EntityHandle) -> Option<&mut DissolveComponent> - { - self.components.get_mut(&entity) - } - - pub fn remove(&mut self, entity: EntityHandle) - { - self.components.remove(&entity); - } - - pub fn all(&self) -> Vec - { - self.components.keys().copied().collect() - } -} - -pub struct FollowStorage -{ - pub components: HashMap, -} - -impl FollowStorage -{ - pub fn new() -> Self - { - Self { - components: HashMap::new(), - } - } - - pub fn insert(&mut self, entity: EntityHandle, component: FollowComponent) - { - self.components.insert(entity, component); - } - - pub fn get(&self, entity: EntityHandle) -> Option<&FollowComponent> - { - self.components.get(&entity) - } - - pub fn remove(&mut self, entity: EntityHandle) - { - self.components.remove(&entity); - } - - pub fn all(&self) -> Vec - { - self.components.keys().copied().collect() - } -} - -pub struct RotateStorage -{ - pub components: HashMap, -} - -impl RotateStorage -{ - pub fn new() -> Self - { - Self { - components: HashMap::new(), - } - } - - pub fn insert(&mut self, entity: EntityHandle, component: RotateComponent) - { - self.components.insert(entity, component); - } - - pub fn get(&self, entity: EntityHandle) -> Option<&RotateComponent> - { - self.components.get(&entity) - } - - pub fn remove(&mut self, entity: EntityHandle) - { - self.components.remove(&entity); - } - - pub fn all(&self) -> Vec - { - self.components.keys().copied().collect() - } -} - pub struct World { pub entities: EntityManager, - pub transforms: TransformStorage, - pub meshes: MeshStorage, - pub physics: PhysicsStorage, - pub movements: MovementStorage, - pub jumps: JumpStorage, - pub inputs: InputStorage, - pub player_tags: PlayerTagStorage, - pub state_machines: StateMachineStorage, - pub cameras: CameraStorage, - pub spotlights: SpotlightStorage, - pub tree_tags: TreeTagStorage, - pub dissolves: DissolveStorage, - pub follows: FollowStorage, - pub rotates: RotateStorage, + pub transforms: Storage, + pub meshes: Storage, + pub physics: Storage, + pub movements: Storage, + pub jumps: Storage, + pub inputs: Storage, + pub player_tags: Storage<()>, + pub state_machines: Storage, + pub cameras: Storage, + pub spotlights: Storage, + pub tree_tags: Storage<()>, + pub dissolves: Storage, + pub follows: Storage, + pub rotates: Storage, } impl World @@ -623,20 +92,20 @@ impl World { Self { entities: EntityManager::new(), - transforms: TransformStorage::new(), - meshes: MeshStorage::new(), - physics: PhysicsStorage::new(), - movements: MovementStorage::new(), - jumps: JumpStorage::new(), - inputs: InputStorage::new(), - player_tags: PlayerTagStorage::new(), - state_machines: StateMachineStorage::new(), - cameras: CameraStorage::new(), - spotlights: SpotlightStorage::new(), - tree_tags: TreeTagStorage::new(), - dissolves: DissolveStorage::new(), - follows: FollowStorage::new(), - rotates: RotateStorage::new(), + transforms: Storage::new(), + meshes: Storage::new(), + physics: Storage::new(), + movements: Storage::new(), + jumps: Storage::new(), + inputs: Storage::new(), + player_tags: Storage::new(), + state_machines: Storage::new(), + cameras: Storage::new(), + spotlights: Storage::new(), + tree_tags: Storage::new(), + dissolves: Storage::new(), + follows: Storage::new(), + rotates: Storage::new(), } } @@ -663,4 +132,31 @@ impl World self.rotates.remove(entity); self.entities.despawn(entity); } + + pub fn active_camera(&self) -> Option<(EntityHandle, &CameraComponent)> + { + self.cameras + .components + .iter() + .find(|(_, cam)| cam.is_active) + .map(|(e, c)| (*e, c)) + } + + pub fn active_camera_position(&self) -> glam::Vec3 + { + self.active_camera() + .and_then(|(entity, _)| self.transforms.get(entity)) + .map(|t| t.position) + .unwrap_or(glam::Vec3::ZERO) + } + + pub fn player_position(&self) -> glam::Vec3 + { + self.player_tags + .all() + .first() + .and_then(|e| self.transforms.get(*e)) + .map(|t| t.position) + .unwrap_or(glam::Vec3::ZERO) + } }