render iteration
This commit is contained in:
147
src/camera.rs
147
src/camera.rs
@@ -1,6 +1,11 @@
|
||||
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
|
||||
@@ -26,147 +31,21 @@ impl CameraUniforms
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Camera
|
||||
{
|
||||
pub position: Vec3,
|
||||
pub target: Vec3,
|
||||
pub up: Vec3,
|
||||
pub fov: f32,
|
||||
pub aspect: f32,
|
||||
pub near: f32,
|
||||
pub far: f32,
|
||||
pub yaw: f32,
|
||||
pub pitch: f32,
|
||||
pub is_following: bool,
|
||||
pub follow_offset: Vec3,
|
||||
}
|
||||
pub struct Camera;
|
||||
|
||||
impl Camera
|
||||
{
|
||||
pub fn init(aspect: f32) -> Self
|
||||
pub fn spawn(world: &mut World, position: Vec3) -> EntityHandle
|
||||
{
|
||||
Self {
|
||||
position: Vec3::new(15.0, 15.0, 15.0),
|
||||
target: Vec3::ZERO,
|
||||
up: Vec3::Y,
|
||||
fov: 45.0_f32.to_radians(),
|
||||
aspect,
|
||||
near: 0.1,
|
||||
far: 100.0,
|
||||
yaw: -135.0_f32.to_radians(),
|
||||
pitch: -30.0_f32.to_radians(),
|
||||
is_following: true,
|
||||
follow_offset: Vec3::ZERO,
|
||||
}
|
||||
}
|
||||
let camera_entity = world.spawn();
|
||||
|
||||
pub fn view_matrix(&self) -> Mat4
|
||||
{
|
||||
Mat4::look_at_rh(self.position, self.target, self.up)
|
||||
}
|
||||
let camera_component = CameraComponent::new(render::aspect_ratio());
|
||||
|
||||
pub fn projection_matrix(&self) -> Mat4
|
||||
{
|
||||
Mat4::perspective_rh(self.fov, self.aspect, self.near, self.far)
|
||||
}
|
||||
let transform = Transform::from_position(position);
|
||||
|
||||
pub fn update_rotation(&mut self, mouse_delta: (f32, f32), sensitivity: f32)
|
||||
{
|
||||
self.yaw += mouse_delta.0 * sensitivity;
|
||||
self.pitch -= mouse_delta.1 * sensitivity;
|
||||
world.cameras.insert(camera_entity, camera_component);
|
||||
world.transforms.insert(camera_entity, transform);
|
||||
|
||||
self.pitch = self
|
||||
.pitch
|
||||
.clamp(-89.0_f32.to_radians(), 89.0_f32.to_radians());
|
||||
}
|
||||
|
||||
pub fn get_forward(&self) -> Vec3
|
||||
{
|
||||
Vec3::new(
|
||||
self.yaw.cos() * self.pitch.cos(),
|
||||
self.pitch.sin(),
|
||||
self.yaw.sin() * self.pitch.cos(),
|
||||
)
|
||||
.normalize()
|
||||
}
|
||||
|
||||
pub fn get_right(&self) -> Vec3
|
||||
{
|
||||
self.get_forward().cross(Vec3::Y).normalize()
|
||||
}
|
||||
|
||||
pub fn get_forward_horizontal(&self) -> Vec3
|
||||
{
|
||||
Vec3::new(self.yaw.cos(), 0.0, self.yaw.sin()).normalize()
|
||||
}
|
||||
|
||||
pub fn get_right_horizontal(&self) -> Vec3
|
||||
{
|
||||
self.get_forward_horizontal().cross(Vec3::Y).normalize()
|
||||
}
|
||||
|
||||
pub fn update_noclip(&mut self, input: Vec3, speed: f32)
|
||||
{
|
||||
let forward = self.get_forward();
|
||||
let right = self.get_right();
|
||||
|
||||
self.position += forward * input.z * speed;
|
||||
self.position += right * input.x * speed;
|
||||
self.position += Vec3::Y * input.y * speed;
|
||||
|
||||
self.target = self.position + forward;
|
||||
}
|
||||
|
||||
pub fn start_following(&mut self, target_position: Vec3)
|
||||
{
|
||||
self.is_following = true;
|
||||
self.follow_offset = self.position - target_position;
|
||||
|
||||
let distance = self.follow_offset.length();
|
||||
if distance > 0.0
|
||||
{
|
||||
self.pitch = (self.follow_offset.y / distance).asin();
|
||||
self.yaw = self.follow_offset.z.atan2(self.follow_offset.x) + std::f32::consts::PI;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_following(&mut self)
|
||||
{
|
||||
self.is_following = false;
|
||||
|
||||
let look_direction = (self.target - self.position).normalize();
|
||||
|
||||
self.yaw = look_direction.z.atan2(look_direction.x);
|
||||
self.pitch = look_direction.y.asin();
|
||||
}
|
||||
|
||||
pub fn update_follow(&mut self, target_position: Vec3, mouse_delta: (f32, f32), sensitivity: f32)
|
||||
{
|
||||
if !self.is_following
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if mouse_delta.0.abs() > 0.0 || mouse_delta.1.abs() > 0.0
|
||||
{
|
||||
self.yaw += mouse_delta.0 * sensitivity;
|
||||
self.pitch += mouse_delta.1 * sensitivity;
|
||||
|
||||
self.pitch = self
|
||||
.pitch
|
||||
.clamp(-89.0_f32.to_radians(), 89.0_f32.to_radians());
|
||||
}
|
||||
|
||||
let distance = self.follow_offset.length();
|
||||
|
||||
let orbit_yaw = self.yaw + std::f32::consts::PI;
|
||||
|
||||
let offset_x = distance * orbit_yaw.cos() * self.pitch.cos();
|
||||
let offset_y = distance * self.pitch.sin();
|
||||
let offset_z = distance * orbit_yaw.sin() * self.pitch.cos();
|
||||
|
||||
self.follow_offset = Vec3::new(offset_x, offset_y, offset_z);
|
||||
self.position = target_position + self.follow_offset;
|
||||
self.target = target_position;
|
||||
camera_entity
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::entity::EntityHandle;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct CameraFollowComponent
|
||||
{
|
||||
pub target_entity: EntityHandle,
|
||||
pub offset: Vec3,
|
||||
pub is_following: bool,
|
||||
}
|
||||
|
||||
impl CameraFollowComponent
|
||||
{
|
||||
pub fn new(target_entity: EntityHandle) -> Self
|
||||
{
|
||||
Self {
|
||||
target_entity,
|
||||
offset: Vec3::ZERO,
|
||||
is_following: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_offset(target_entity: EntityHandle, offset: Vec3) -> Self
|
||||
{
|
||||
Self {
|
||||
target_entity,
|
||||
offset,
|
||||
is_following: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/components/dissolve.rs
Normal file
27
src/components/dissolve.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
pub struct DissolveComponent
|
||||
{
|
||||
pub amount: f32,
|
||||
pub target_amount: f32,
|
||||
pub transition_speed: f32,
|
||||
}
|
||||
|
||||
impl DissolveComponent
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
amount: 0.0,
|
||||
target_amount: 0.0,
|
||||
transition_speed: 3.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_speed(transition_speed: f32) -> Self
|
||||
{
|
||||
Self {
|
||||
amount: 0.0,
|
||||
target_amount: 0.0,
|
||||
transition_speed,
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/components/follow.rs
Normal file
10
src/components/follow.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use crate::entity::EntityHandle;
|
||||
use crate::utility::transform::Transform;
|
||||
|
||||
pub struct FollowComponent
|
||||
{
|
||||
pub target: EntityHandle,
|
||||
pub offset: Transform,
|
||||
pub inherit_rotation: bool,
|
||||
pub inherit_scale: bool,
|
||||
}
|
||||
@@ -41,12 +41,7 @@ impl Default for JumpConfig
|
||||
max_air_momentum: 8.0,
|
||||
air_damping_active: 0.4,
|
||||
air_damping_passive: 0.9,
|
||||
jump_curve: CubicBez::new(
|
||||
(0.0, 0.0),
|
||||
(0.4, 0.75),
|
||||
(0.7, 0.9),
|
||||
(1.0, 1.0),
|
||||
),
|
||||
jump_curve: CubicBez::new((0.0, 0.0), (0.4, 0.75), (0.7, 0.9), (1.0, 1.0)),
|
||||
jump_context: JumpContext::default(),
|
||||
}
|
||||
}
|
||||
|
||||
18
src/components/lights/directional.rs
Normal file
18
src/components/lights/directional.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
3
src/components/lights/mod.rs
Normal file
3
src/components/lights/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod directional;
|
||||
pub mod point;
|
||||
pub mod spot;
|
||||
14
src/components/lights/point.rs
Normal file
14
src/components/lights/point.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use glam::Vec3;
|
||||
|
||||
pub struct PointlightComponent
|
||||
{
|
||||
pub offset: Vec3,
|
||||
}
|
||||
|
||||
impl PointlightComponent
|
||||
{
|
||||
pub fn new(offset: Vec3) -> Self
|
||||
{
|
||||
Self { offset }
|
||||
}
|
||||
}
|
||||
25
src/components/lights/spot.rs
Normal file
25
src/components/lights/spot.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use glam::Vec3;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct SpotlightComponent
|
||||
{
|
||||
pub offset: Vec3,
|
||||
pub direction: Vec3,
|
||||
pub range: f32,
|
||||
pub inner_angle: f32,
|
||||
pub outer_angle: f32,
|
||||
}
|
||||
|
||||
impl SpotlightComponent
|
||||
{
|
||||
pub fn new(offset: Vec3, direction: Vec3, range: f32, inner_angle: f32, outer_angle: f32) -> Self
|
||||
{
|
||||
Self {
|
||||
offset,
|
||||
direction: direction.normalize(),
|
||||
range,
|
||||
inner_angle,
|
||||
outer_angle,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,22 @@
|
||||
pub mod camera;
|
||||
pub mod camera_follow;
|
||||
pub mod dissolve;
|
||||
pub mod follow;
|
||||
pub mod input;
|
||||
pub mod jump;
|
||||
pub mod lights;
|
||||
pub mod mesh;
|
||||
pub mod movement;
|
||||
pub mod physics;
|
||||
pub mod player_tag;
|
||||
pub mod rotate;
|
||||
pub mod state_machine;
|
||||
pub mod tree_tag;
|
||||
|
||||
pub use camera::CameraComponent;
|
||||
pub use camera_follow::CameraFollowComponent;
|
||||
pub use dissolve::DissolveComponent;
|
||||
pub use follow::FollowComponent;
|
||||
pub use input::InputComponent;
|
||||
pub use mesh::MeshComponent;
|
||||
pub use movement::MovementComponent;
|
||||
pub use physics::PhysicsComponent;
|
||||
pub use rotate::RotateComponent;
|
||||
|
||||
@@ -43,7 +43,7 @@ impl MovementConfig
|
||||
(1.0, 1.0),
|
||||
),
|
||||
walking_damping: 0.8,
|
||||
max_walking_speed: 6.0,
|
||||
max_walking_speed: 12.0,
|
||||
idle_damping: 0.1,
|
||||
movement_context: MovementContext::new(),
|
||||
}
|
||||
|
||||
15
src/components/rotate.rs
Normal file
15
src/components/rotate.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use glam::Vec3;
|
||||
|
||||
pub struct RotateComponent
|
||||
{
|
||||
pub axis: Vec3,
|
||||
pub speed: f32,
|
||||
}
|
||||
|
||||
impl RotateComponent
|
||||
{
|
||||
pub fn new(axis: Vec3, speed: f32) -> Self
|
||||
{
|
||||
Self { axis, speed }
|
||||
}
|
||||
}
|
||||
1
src/components/tree_tag.rs
Normal file
1
src/components/tree_tag.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub struct TreeTag;
|
||||
@@ -126,6 +126,8 @@ pub fn render_collider_debug() -> Vec<DrawCall>
|
||||
|
||||
let instance_data = InstanceRaw {
|
||||
model: model.to_cols_array_2d(),
|
||||
dissolve_amount: 0.0,
|
||||
_padding: [0.0; 3],
|
||||
};
|
||||
|
||||
let instance_buffer = render::with_device(|device| {
|
||||
@@ -153,6 +155,8 @@ pub fn render_collider_debug() -> Vec<DrawCall>
|
||||
{
|
||||
let instance_data = InstanceRaw {
|
||||
model: Mat4::IDENTITY.to_cols_array_2d(),
|
||||
dissolve_amount: 0.0,
|
||||
_padding: [0.0; 3],
|
||||
};
|
||||
|
||||
let instance_buffer = render::with_device(|device| {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
pub mod collider_debug;
|
||||
pub mod noclip;
|
||||
|
||||
pub use collider_debug::{render_collider_debug, set_debug_heightfield};
|
||||
pub use noclip::{update_follow_camera, update_noclip_camera};
|
||||
pub use collider_debug::render_collider_debug;
|
||||
|
||||
101
src/empty.rs
Normal file
101
src/empty.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use glam::Mat4;
|
||||
use std::path::Path;
|
||||
|
||||
pub struct EmptyNode
|
||||
{
|
||||
pub name: String,
|
||||
pub transform: Mat4,
|
||||
}
|
||||
|
||||
impl EmptyNode
|
||||
{
|
||||
pub fn new(name: String, transform: Mat4) -> Self
|
||||
{
|
||||
Self { name, transform }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Empties
|
||||
{
|
||||
nodes: Vec<EmptyNode>,
|
||||
}
|
||||
|
||||
impl Empties
|
||||
{
|
||||
fn new(nodes: Vec<EmptyNode>) -> Self
|
||||
{
|
||||
Self { nodes }
|
||||
}
|
||||
|
||||
pub fn into_nodes(self) -> Vec<EmptyNode>
|
||||
{
|
||||
self.nodes
|
||||
}
|
||||
|
||||
pub fn load_gltf_empties(path: impl AsRef<Path>)
|
||||
-> Result<Empties, Box<dyn std::error::Error>>
|
||||
{
|
||||
let (gltf, _buffers, _images) = gltf::import(path)?;
|
||||
|
||||
let mut all_empties = Vec::new();
|
||||
|
||||
for scene in gltf.scenes()
|
||||
{
|
||||
for node in scene.nodes()
|
||||
{
|
||||
Self::process_node(&node, Mat4::IDENTITY, &mut all_empties)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Empties::new(all_empties))
|
||||
}
|
||||
|
||||
fn process_node(
|
||||
node: &gltf::Node,
|
||||
parent_transform: Mat4,
|
||||
all_empties: &mut Vec<EmptyNode>,
|
||||
) -> Result<(), Box<dyn std::error::Error>>
|
||||
{
|
||||
let local_transform = Mat4::from_cols_array_2d(&node.transform().matrix());
|
||||
let global_transform = parent_transform * local_transform;
|
||||
|
||||
let is_empty = node.mesh().is_none() && node.light().is_none() && node.camera().is_none();
|
||||
|
||||
if is_empty
|
||||
{
|
||||
let name = node.name().unwrap_or("Unnamed").to_string();
|
||||
all_empties.push(EmptyNode::new(name, global_transform));
|
||||
}
|
||||
|
||||
for child in node.children()
|
||||
{
|
||||
Self::process_node(&child, global_transform, all_empties)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_empties(path: impl AsRef<Path>) -> Result<Empties, Box<dyn std::error::Error>>
|
||||
{
|
||||
Self::load_gltf_empties(path)
|
||||
}
|
||||
|
||||
pub fn get_empty_by_name(
|
||||
gltf_path: &str,
|
||||
name: &str,
|
||||
) -> anyhow::Result<Option<crate::empty::EmptyNode>>
|
||||
{
|
||||
let empties = Self::load_empties(gltf_path)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to load empty nodes: {}", e))?;
|
||||
|
||||
for empty_node in empties.into_nodes()
|
||||
{
|
||||
if empty_node.name == name
|
||||
{
|
||||
return Ok(Some(empty_node));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
153
src/light.rs
Normal file
153
src/light.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
use glam::{Mat4, Vec3};
|
||||
use gltf::json::Extras;
|
||||
use std::{ops::Deref, path::Path};
|
||||
|
||||
use crate::{
|
||||
components::lights::spot::SpotlightComponent,
|
||||
world::{Transform, World},
|
||||
};
|
||||
|
||||
pub struct LightData
|
||||
{
|
||||
pub component: SpotlightComponent,
|
||||
pub transform: Mat4,
|
||||
pub tag: Option<String>,
|
||||
}
|
||||
|
||||
pub struct Lights
|
||||
{
|
||||
spotlights: Vec<LightData>,
|
||||
}
|
||||
|
||||
impl Lights
|
||||
{
|
||||
fn new(spotlights: Vec<LightData>) -> Self
|
||||
{
|
||||
Self { spotlights }
|
||||
}
|
||||
|
||||
pub fn into_spotlights(self) -> Vec<LightData>
|
||||
{
|
||||
self.spotlights
|
||||
}
|
||||
|
||||
pub fn load_gltf_lights(path: impl AsRef<Path>) -> Result<Lights, Box<dyn std::error::Error>>
|
||||
{
|
||||
let (gltf, _buffers, _images) = gltf::import(path)?;
|
||||
|
||||
let mut all_directional = Vec::new();
|
||||
let mut all_point = Vec::new();
|
||||
let mut all_spot = Vec::new();
|
||||
|
||||
for scene in gltf.scenes()
|
||||
{
|
||||
for node in scene.nodes()
|
||||
{
|
||||
Self::process_node(
|
||||
&node,
|
||||
Mat4::IDENTITY,
|
||||
&mut all_directional,
|
||||
&mut all_point,
|
||||
&mut all_spot,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Lights::new(all_spot))
|
||||
}
|
||||
|
||||
fn process_node(
|
||||
node: &gltf::Node,
|
||||
parent_transform: Mat4,
|
||||
all_directional: &mut Vec<LightData>,
|
||||
all_point: &mut Vec<LightData>,
|
||||
all_spot: &mut Vec<LightData>,
|
||||
) -> Result<(), Box<dyn std::error::Error>>
|
||||
{
|
||||
let local_transform = Mat4::from_cols_array_2d(&node.transform().matrix());
|
||||
let global_transform = parent_transform * local_transform;
|
||||
|
||||
if let Some(light) = node.light()
|
||||
{
|
||||
let local_transform = Mat4::from_cols_array_2d(&node.transform().matrix());
|
||||
let global_transform = parent_transform * local_transform;
|
||||
let (_scale, rotation, _translation) = global_transform.to_scale_rotation_translation();
|
||||
|
||||
let tag = serde_json::to_value(light.extras())
|
||||
.ok()
|
||||
.and_then(|extras| {
|
||||
extras
|
||||
.get("light_tag")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from)
|
||||
});
|
||||
|
||||
match light.kind()
|
||||
{
|
||||
gltf::khr_lights_punctual::Kind::Directional => todo!(),
|
||||
gltf::khr_lights_punctual::Kind::Point => todo!(),
|
||||
gltf::khr_lights_punctual::Kind::Spot {
|
||||
inner_cone_angle,
|
||||
outer_cone_angle,
|
||||
} =>
|
||||
{
|
||||
let range = light.range().unwrap_or(100.0);
|
||||
let spotlight = SpotlightComponent::new(
|
||||
Vec3::ZERO,
|
||||
rotation * -Vec3::Z,
|
||||
range,
|
||||
inner_cone_angle,
|
||||
outer_cone_angle,
|
||||
);
|
||||
all_spot.push(LightData {
|
||||
component: spotlight,
|
||||
transform: global_transform,
|
||||
tag,
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
for child in node.children()
|
||||
{
|
||||
Self::process_node(
|
||||
&child,
|
||||
global_transform,
|
||||
all_directional,
|
||||
all_point,
|
||||
all_spot,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_lights(path: impl AsRef<Path>) -> Result<Lights, Box<dyn std::error::Error>>
|
||||
{
|
||||
crate::render::with_device(|_device| Lights::load_gltf_lights(path))
|
||||
}
|
||||
|
||||
pub fn spawn_lights(world: &mut World, spotlights: Vec<LightData>)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
103
src/main.rs
103
src/main.rs
@@ -2,20 +2,24 @@ mod camera;
|
||||
mod components;
|
||||
mod debug;
|
||||
mod draw;
|
||||
mod empty;
|
||||
mod entity;
|
||||
mod event;
|
||||
mod heightmap;
|
||||
mod light;
|
||||
mod mesh;
|
||||
mod physics;
|
||||
mod picking;
|
||||
mod player;
|
||||
mod postprocess;
|
||||
mod render;
|
||||
mod shader;
|
||||
mod snow;
|
||||
mod snow_light;
|
||||
mod space;
|
||||
mod state;
|
||||
mod systems;
|
||||
mod terrain;
|
||||
mod texture_loader;
|
||||
mod texture;
|
||||
mod utility;
|
||||
mod world;
|
||||
|
||||
@@ -26,16 +30,21 @@ use render::Renderer;
|
||||
use utility::input::InputState;
|
||||
use world::{Transform, World};
|
||||
|
||||
use crate::components::{CameraComponent, CameraFollowComponent};
|
||||
use crate::camera::Camera;
|
||||
use crate::components::CameraComponent;
|
||||
use crate::debug::render_collider_debug;
|
||||
use crate::entity::EntityHandle;
|
||||
use crate::light::Lights;
|
||||
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, start_camera_following, state_machine_physics_system,
|
||||
state_machine_system, stop_camera_following,
|
||||
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::utility::time::Time;
|
||||
|
||||
@@ -45,26 +54,43 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
|
||||
let video_subsystem = sdl_context.video()?;
|
||||
|
||||
let window = video_subsystem
|
||||
.window("snow_trail", 800, 600)
|
||||
.window("snow_trail", 1200, 900)
|
||||
.position_centered()
|
||||
.resizable()
|
||||
.vulkan()
|
||||
.build()?;
|
||||
|
||||
let renderer = pollster::block_on(Renderer::new(&window, 1))?;
|
||||
let renderer = pollster::block_on(Renderer::new(&window, 2))?;
|
||||
render::init(renderer);
|
||||
|
||||
let space = Space::load_space("meshes/terrain.gltf")?;
|
||||
let terrain_config = TerrainConfig::default();
|
||||
|
||||
let player_spawn = space.player_spawn;
|
||||
let camera_spawn = space.camera_spawn_position();
|
||||
|
||||
let mut world = World::new();
|
||||
let player_entity = Player::spawn(&mut world);
|
||||
let _terrain_entity = Terrain::spawn(&mut world, &terrain_config)?;
|
||||
|
||||
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);
|
||||
|
||||
render::set_terrain_data();
|
||||
|
||||
let terrain_half_size = terrain_config.size / 2.0;
|
||||
render::init_snow_light_accumulation(
|
||||
glam::Vec2::new(-terrain_half_size.x, -terrain_half_size.y),
|
||||
glam::Vec2::new(terrain_half_size.x, terrain_half_size.y),
|
||||
);
|
||||
|
||||
let snow_config = SnowConfig::default();
|
||||
let snow_layer = SnowLayer::load(&mut world, &snow_config)?;
|
||||
println!("Snow layer loaded successfully");
|
||||
|
||||
render::set_snow_depth(&snow_layer.depth_texture_view);
|
||||
|
||||
let mut noclip_mode = true;
|
||||
|
||||
let camera_entity = spawn_camera(&mut world, player_entity);
|
||||
let camera_entity = Camera::spawn(&mut world, camera_spawn);
|
||||
if noclip_mode == false
|
||||
{
|
||||
start_camera_following(&mut world, camera_entity);
|
||||
@@ -150,6 +176,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
|
||||
|
||||
state_machine_system(&mut world, delta);
|
||||
|
||||
rotate_system(&mut world, delta);
|
||||
|
||||
let spotlights = spotlight_sync_system(&world);
|
||||
render::update_spotlights(spotlights);
|
||||
|
||||
let mut draw_calls = render_system(&world);
|
||||
draw_calls.extend(render_collider_debug());
|
||||
|
||||
@@ -161,12 +192,22 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
|
||||
get_view_matrix(&world, camera_entity, camera_transform, camera_component);
|
||||
let projection = camera_component.projection_matrix();
|
||||
|
||||
render::render_with_matrices(
|
||||
let player_pos = world
|
||||
.player_tags
|
||||
.all()
|
||||
.first()
|
||||
.and_then(|e| world.transforms.get(*e))
|
||||
.map(|t| t.position)
|
||||
.unwrap_or(Vec3::ZERO);
|
||||
|
||||
render::render(
|
||||
&view,
|
||||
&projection,
|
||||
camera_transform.position,
|
||||
player_pos,
|
||||
&draw_calls,
|
||||
time,
|
||||
delta,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -183,27 +224,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn_camera(world: &mut World, target_entity: EntityHandle) -> EntityHandle
|
||||
{
|
||||
let camera_entity = world.spawn();
|
||||
|
||||
let camera_component = CameraComponent::new(render::aspect_ratio());
|
||||
let camera_follow = CameraFollowComponent::new(target_entity);
|
||||
|
||||
let initial_position = Vec3::new(15.0, 15.0, 15.0);
|
||||
let transform = Transform {
|
||||
position: initial_position,
|
||||
rotation: glam::Quat::IDENTITY,
|
||||
scale: Vec3::ONE,
|
||||
};
|
||||
|
||||
world.cameras.insert(camera_entity, camera_component);
|
||||
world.camera_follows.insert(camera_entity, camera_follow);
|
||||
world.transforms.insert(camera_entity, transform);
|
||||
|
||||
camera_entity
|
||||
}
|
||||
|
||||
fn get_view_matrix(
|
||||
world: &World,
|
||||
camera_entity: EntityHandle,
|
||||
@@ -211,18 +231,15 @@ fn get_view_matrix(
|
||||
camera_component: &CameraComponent,
|
||||
) -> glam::Mat4
|
||||
{
|
||||
if let Some(follow) = world.camera_follows.get(camera_entity)
|
||||
if let Some(follow) = world.follows.get(camera_entity)
|
||||
{
|
||||
if follow.is_following
|
||||
if let Some(target_transform) = world.transforms.get(follow.target)
|
||||
{
|
||||
if let Some(target_transform) = world.transforms.get(follow.target_entity)
|
||||
{
|
||||
return glam::Mat4::look_at_rh(
|
||||
camera_transform.position,
|
||||
target_transform.position,
|
||||
Vec3::Y,
|
||||
);
|
||||
}
|
||||
return glam::Mat4::look_at_rh(
|
||||
camera_transform.position,
|
||||
target_transform.position,
|
||||
Vec3::Y,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
168
src/mesh.rs
168
src/mesh.rs
@@ -48,6 +48,7 @@ pub struct InstanceData
|
||||
pub position: Vec3,
|
||||
pub rotation: Quat,
|
||||
pub scale: Vec3,
|
||||
pub dissolve_amount: f32,
|
||||
}
|
||||
|
||||
impl InstanceData
|
||||
@@ -57,6 +58,8 @@ impl InstanceData
|
||||
let model = Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position);
|
||||
InstanceRaw {
|
||||
model: model.to_cols_array_2d(),
|
||||
dissolve_amount: self.dissolve_amount,
|
||||
_padding: [0.0; 3],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,6 +69,8 @@ impl InstanceData
|
||||
pub struct InstanceRaw
|
||||
{
|
||||
pub model: [[f32; 4]; 4],
|
||||
pub dissolve_amount: f32,
|
||||
pub _padding: [f32; 3],
|
||||
}
|
||||
|
||||
impl InstanceRaw
|
||||
@@ -96,6 +101,11 @@ impl InstanceRaw
|
||||
shader_location: 6,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: (std::mem::size_of::<[f32; 4]>() * 4) as wgpu::BufferAddress,
|
||||
shader_location: 7,
|
||||
format: wgpu::VertexFormat::Float32,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -506,88 +516,92 @@ impl Mesh
|
||||
{
|
||||
let extensions = json_node.get("extensions").unwrap();
|
||||
let instancing_ext = extensions.get("EXT_mesh_gpu_instancing").unwrap();
|
||||
let mut mesh_vertices = Vec::new();
|
||||
let mut mesh_indices = Vec::new();
|
||||
let mut mesh_vertices = Vec::new();
|
||||
let mut mesh_indices = Vec::new();
|
||||
|
||||
for primitive in mesh_data.primitives()
|
||||
for primitive in mesh_data.primitives()
|
||||
{
|
||||
let reader = primitive
|
||||
.reader(|buffer| buffers.get(buffer.index()).map(|data| &data[..]));
|
||||
|
||||
let positions = reader
|
||||
.read_positions()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing position data"))?
|
||||
.collect::<Vec<[f32; 3]>>();
|
||||
|
||||
let normals = reader
|
||||
.read_normals()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing normal data"))?
|
||||
.collect::<Vec<[f32; 3]>>();
|
||||
|
||||
let uvs = reader
|
||||
.read_tex_coords(0)
|
||||
.map(|iter| iter.into_f32().collect::<Vec<[f32; 2]>>())
|
||||
.unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);
|
||||
|
||||
let base_index = mesh_vertices.len() as u32;
|
||||
|
||||
for ((pos, normal), uv) in
|
||||
positions.iter().zip(normals.iter()).zip(uvs.iter())
|
||||
{
|
||||
let reader = primitive
|
||||
.reader(|buffer| buffers.get(buffer.index()).map(|data| &data[..]));
|
||||
|
||||
let positions = reader
|
||||
.read_positions()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing position data"))?
|
||||
.collect::<Vec<[f32; 3]>>();
|
||||
|
||||
let normals = reader
|
||||
.read_normals()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing normal data"))?
|
||||
.collect::<Vec<[f32; 3]>>();
|
||||
|
||||
let uvs = reader
|
||||
.read_tex_coords(0)
|
||||
.map(|iter| iter.into_f32().collect::<Vec<[f32; 2]>>())
|
||||
.unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);
|
||||
|
||||
let base_index = mesh_vertices.len() as u32;
|
||||
|
||||
for ((pos, normal), uv) in
|
||||
positions.iter().zip(normals.iter()).zip(uvs.iter())
|
||||
{
|
||||
mesh_vertices.push(Vertex {
|
||||
position: *pos,
|
||||
normal: *normal,
|
||||
uv: *uv,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(indices_reader) = reader.read_indices()
|
||||
{
|
||||
mesh_indices
|
||||
.extend(indices_reader.into_u32().map(|i| i + base_index));
|
||||
}
|
||||
mesh_vertices.push(Vertex {
|
||||
position: *pos,
|
||||
normal: *normal,
|
||||
uv: *uv,
|
||||
});
|
||||
}
|
||||
|
||||
let attributes = instancing_ext
|
||||
.get("attributes")
|
||||
.and_then(|v| v.as_object())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing attributes in EXT_mesh_gpu_instancing"))?;
|
||||
if let Some(indices_reader) = reader.read_indices()
|
||||
{
|
||||
mesh_indices.extend(indices_reader.into_u32().map(|i| i + base_index));
|
||||
}
|
||||
}
|
||||
|
||||
let translation_accessor_index = attributes
|
||||
.get("TRANSLATION")
|
||||
.and_then(|v| v.as_u64())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing TRANSLATION in instancing extension"))? as usize;
|
||||
let attributes = instancing_ext
|
||||
.get("attributes")
|
||||
.and_then(|v| v.as_object())
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("Missing attributes in EXT_mesh_gpu_instancing")
|
||||
})?;
|
||||
|
||||
let rotation_accessor_index = attributes
|
||||
.get("ROTATION")
|
||||
.and_then(|v| v.as_u64())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing ROTATION in instancing extension"))? as usize;
|
||||
let translation_accessor_index = attributes
|
||||
.get("TRANSLATION")
|
||||
.and_then(|v| v.as_u64())
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("Missing TRANSLATION in instancing extension")
|
||||
})? as usize;
|
||||
|
||||
let scale_accessor_index = attributes
|
||||
.get("SCALE")
|
||||
.and_then(|v| v.as_u64())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing SCALE in instancing extension"))? as usize;
|
||||
let rotation_accessor_index = attributes
|
||||
.get("ROTATION")
|
||||
.and_then(|v| v.as_u64())
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("Missing ROTATION in instancing extension")
|
||||
})? as usize;
|
||||
|
||||
let translations = Self::read_vec3_accessor(
|
||||
&document,
|
||||
&buffers,
|
||||
translation_accessor_index,
|
||||
)?;
|
||||
let rotations =
|
||||
Self::read_quat_accessor(&document, &buffers, rotation_accessor_index)?;
|
||||
let scales =
|
||||
Self::read_vec3_accessor(&document, &buffers, scale_accessor_index)?;
|
||||
let scale_accessor_index = attributes
|
||||
.get("SCALE")
|
||||
.and_then(|v| v.as_u64())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing SCALE in instancing extension"))?
|
||||
as usize;
|
||||
|
||||
let instances: Vec<InstanceData> = translations
|
||||
.into_iter()
|
||||
.zip(rotations.into_iter())
|
||||
.zip(scales.into_iter())
|
||||
.map(|((position, rotation), scale)| InstanceData {
|
||||
position,
|
||||
rotation,
|
||||
scale,
|
||||
})
|
||||
.collect();
|
||||
let translations =
|
||||
Self::read_vec3_accessor(&document, &buffers, translation_accessor_index)?;
|
||||
let rotations =
|
||||
Self::read_quat_accessor(&document, &buffers, rotation_accessor_index)?;
|
||||
let scales =
|
||||
Self::read_vec3_accessor(&document, &buffers, scale_accessor_index)?;
|
||||
|
||||
let instances: Vec<InstanceData> = translations
|
||||
.into_iter()
|
||||
.zip(rotations.into_iter())
|
||||
.zip(scales.into_iter())
|
||||
.map(|((position, rotation), scale)| InstanceData {
|
||||
position,
|
||||
rotation,
|
||||
scale,
|
||||
dissolve_amount: 0.0,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mesh = Mesh::new(device, &mesh_vertices, &mesh_indices);
|
||||
result.push((mesh, instances));
|
||||
@@ -655,7 +669,9 @@ impl Mesh
|
||||
.nth(accessor_index)
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid accessor index"))?;
|
||||
|
||||
let buffer_view = accessor.view().ok_or_else(|| anyhow::anyhow!("Missing buffer view"))?;
|
||||
let buffer_view = accessor
|
||||
.view()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing buffer view"))?;
|
||||
let buffer = &buffers[buffer_view.buffer().index()];
|
||||
let start = buffer_view.offset() + accessor.offset();
|
||||
let stride = buffer_view.stride().unwrap_or(12);
|
||||
@@ -699,7 +715,9 @@ impl Mesh
|
||||
.nth(accessor_index)
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid accessor index"))?;
|
||||
|
||||
let buffer_view = accessor.view().ok_or_else(|| anyhow::anyhow!("Missing buffer view"))?;
|
||||
let buffer_view = accessor
|
||||
.view()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing buffer view"))?;
|
||||
let buffer = &buffers[buffer_view.buffer().index()];
|
||||
let start = buffer_view.offset() + accessor.offset();
|
||||
let stride = buffer_view.stride().unwrap_or(16);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::rc::Rc;
|
||||
use std::{f32::consts::PI, rc::Rc};
|
||||
|
||||
use glam::Vec3;
|
||||
use kurbo::ParamCurve;
|
||||
@@ -10,7 +10,8 @@ use rapier3d::{
|
||||
|
||||
use crate::{
|
||||
components::{
|
||||
jump::JumpComponent, InputComponent, MeshComponent, MovementComponent, PhysicsComponent,
|
||||
jump::JumpComponent, lights::spot::SpotlightComponent, InputComponent, MeshComponent,
|
||||
MovementComponent, PhysicsComponent,
|
||||
},
|
||||
entity::EntityHandle,
|
||||
mesh::Mesh,
|
||||
@@ -24,14 +25,14 @@ pub struct Player;
|
||||
|
||||
impl Player
|
||||
{
|
||||
pub fn spawn(world: &mut World) -> EntityHandle
|
||||
pub fn spawn(world: &mut World, position: Vec3) -> EntityHandle
|
||||
{
|
||||
let entity = world.spawn();
|
||||
|
||||
let initial_position = Vec3::new(0.0, 5.0, 0.0);
|
||||
let spawn_transform = Transform::from_position(position);
|
||||
|
||||
let rigidbody = RigidBodyBuilder::kinematic_position_based()
|
||||
.translation(initial_position.into())
|
||||
.translation(spawn_transform.position.into())
|
||||
.build();
|
||||
let collider = ColliderBuilder::capsule_y(0.5, 0.5).build();
|
||||
let _controller = KinematicCharacterController {
|
||||
@@ -163,7 +164,7 @@ impl Player
|
||||
|
||||
world
|
||||
.transforms
|
||||
.insert(entity, Transform::from_position(initial_position));
|
||||
.insert(entity, spawn_transform);
|
||||
world.movements.insert(entity, MovementComponent::new());
|
||||
world.jumps.insert(entity, JumpComponent::new());
|
||||
world.inputs.insert(entity, InputComponent::default());
|
||||
@@ -178,7 +179,7 @@ impl Player
|
||||
entity,
|
||||
MeshComponent {
|
||||
mesh: Rc::new(mesh),
|
||||
pipeline: Pipeline::Render,
|
||||
pipeline: Pipeline::Standard,
|
||||
instance_buffer: None,
|
||||
num_instances: 1,
|
||||
},
|
||||
@@ -186,6 +187,18 @@ impl Player
|
||||
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
|
||||
}
|
||||
|
||||
@@ -233,7 +246,8 @@ impl State for PlayerFallingState
|
||||
.flatten()
|
||||
.unwrap();
|
||||
|
||||
let terrain_height = PhysicsManager::get_terrain_height_at(current_pos.x, current_pos.z);
|
||||
let next_pos = current_pos + velocity;
|
||||
let terrain_height = PhysicsManager::get_terrain_height_at(next_pos.x, next_pos.z);
|
||||
|
||||
let is_grounded = if let Some(height) = terrain_height
|
||||
{
|
||||
@@ -553,7 +567,8 @@ impl State for PlayerJumpingState
|
||||
let current_time = Time::get_time_elapsed();
|
||||
|
||||
world.jumps.with_mut(self.entity, |jump| {
|
||||
jump.jump_config.jump_context.duration = current_time - jump.jump_config.jump_context.execution_time;
|
||||
jump.jump_config.jump_context.duration =
|
||||
current_time - jump.jump_config.jump_context.execution_time;
|
||||
});
|
||||
|
||||
let jump_config = world
|
||||
|
||||
@@ -147,7 +147,7 @@ pub fn create_blit_pipeline(
|
||||
) -> wgpu::RenderPipeline
|
||||
{
|
||||
let shader_source =
|
||||
std::fs::read_to_string("shaders/blit.wgsl").expect("Failed to read blit shader");
|
||||
std::fs::read_to_string("src/shaders/blit.wgsl").expect("Failed to read blit shader");
|
||||
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Blit Shader"),
|
||||
|
||||
968
src/render.rs
968
src/render.rs
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
use crate::mesh::{InstanceRaw, Vertex};
|
||||
use wesl::{include_wesl, Wesl};
|
||||
|
||||
pub fn create_render_pipeline(
|
||||
device: &wgpu::Device,
|
||||
@@ -6,11 +7,12 @@ pub fn create_render_pipeline(
|
||||
bind_group_layout: &wgpu::BindGroupLayout,
|
||||
) -> wgpu::RenderPipeline
|
||||
{
|
||||
let shared_source =
|
||||
std::fs::read_to_string("shaders/shared.wgsl").expect("Failed to read shared shader");
|
||||
let standard_source =
|
||||
std::fs::read_to_string("shaders/standard.wgsl").expect("Failed to read standard shader");
|
||||
let shader_source = format!("{}\n{}", shared_source, standard_source);
|
||||
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"),
|
||||
@@ -67,3 +69,72 @@ pub fn create_render_pipeline(
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
28
src/shaders/blit.wgsl
Normal file
28
src/shaders/blit.wgsl
Normal file
@@ -0,0 +1,28 @@
|
||||
struct VertexInput {
|
||||
@location(0) position: vec2<f32>,
|
||||
@location(1) uv: vec2<f32>,
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
}
|
||||
|
||||
@group(0) @binding(0)
|
||||
var t_texture: texture_2d<f32>;
|
||||
|
||||
@group(0) @binding(1)
|
||||
var t_sampler: sampler;
|
||||
|
||||
@vertex
|
||||
fn vs_main(input: VertexInput) -> VertexOutput {
|
||||
var output: VertexOutput;
|
||||
output.clip_position = vec4<f32>(input.position, 0.0, 1.0);
|
||||
output.uv = input.uv;
|
||||
return output;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return textureSample(t_texture, t_sampler, input.uv);
|
||||
}
|
||||
95
src/shaders/environment.wesl
Normal file
95
src/shaders/environment.wesl
Normal file
@@ -0,0 +1,95 @@
|
||||
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<f32>(
|
||||
input.instance_model_0,
|
||||
input.instance_model_1,
|
||||
input.instance_model_2,
|
||||
input.instance_model_3
|
||||
);
|
||||
|
||||
let world_pos = instance_model * vec4<f32>(input.position, 1.0);
|
||||
output.world_position = world_pos.xyz;
|
||||
output.clip_position = uniforms.projection * uniforms.view * world_pos;
|
||||
|
||||
let normal_matrix = mat3x3<f32>(
|
||||
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<f32>(
|
||||
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<f32>(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<f32> {
|
||||
|
||||
let debug = 0u;
|
||||
|
||||
if debug == 1u {
|
||||
return vec4<f32>(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<f32>(brightness, brightness, brightness, 1.0);
|
||||
}
|
||||
14
src/shaders/shadow.wesl
Normal file
14
src/shaders/shadow.wesl
Normal file
@@ -0,0 +1,14 @@
|
||||
import package::shared::{ VertexInput, uniforms };
|
||||
|
||||
@vertex
|
||||
fn vs_main(input: VertexInput) -> @builtin(position) vec4<f32> {
|
||||
let instance_model = mat4x4<f32>(
|
||||
input.instance_model_0,
|
||||
input.instance_model_1,
|
||||
input.instance_model_2,
|
||||
input.instance_model_3
|
||||
);
|
||||
|
||||
let world_pos = instance_model * vec4<f32>(input.position, 1.0);
|
||||
return uniforms.light_view_projection * world_pos;
|
||||
}
|
||||
383
src/shaders/shared.wesl
Normal file
383
src/shaders/shared.wesl
Normal file
@@ -0,0 +1,383 @@
|
||||
struct VertexInput {
|
||||
@location(0) position: vec3<f32>,
|
||||
@location(1) normal: vec3<f32>,
|
||||
@location(2) uv: vec2<f32>,
|
||||
@location(3) instance_model_0: vec4<f32>,
|
||||
@location(4) instance_model_1: vec4<f32>,
|
||||
@location(5) instance_model_2: vec4<f32>,
|
||||
@location(6) instance_model_3: vec4<f32>,
|
||||
@location(7) instance_dissolve: f32,
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) world_position: vec3<f32>,
|
||||
@location(1) world_normal: vec3<f32>,
|
||||
@location(2) light_space_position: vec4<f32>,
|
||||
@location(3) dissolve_amount: f32,
|
||||
}
|
||||
|
||||
const MAX_SPOTLIGHTS: u32 = 4u;
|
||||
|
||||
struct Spotlight {
|
||||
position: vec3<f32>,
|
||||
inner_angle: f32,
|
||||
direction: vec3<f32>,
|
||||
outer_angle: f32,
|
||||
range: f32,
|
||||
_padding: f32,
|
||||
_padding2: f32,
|
||||
_padding3: f32,
|
||||
_padding4: f32,
|
||||
_padding5: f32,
|
||||
_padding6: f32,
|
||||
_padding7: f32,
|
||||
}
|
||||
|
||||
struct Uniforms {
|
||||
model: mat4x4<f32>,
|
||||
view: mat4x4<f32>,
|
||||
projection: mat4x4<f32>,
|
||||
light_view_projection: mat4x4<f32>,
|
||||
camera_position: vec3<f32>,
|
||||
height_scale: f32,
|
||||
player_position: vec3<f32>,
|
||||
time: f32,
|
||||
shadow_bias: f32,
|
||||
spotlight_count: u32,
|
||||
_padding1: u32,
|
||||
_padding2: u32,
|
||||
spotlights: array<Spotlight, 4>,
|
||||
}
|
||||
|
||||
@group(0) @binding(0)
|
||||
var<uniform> uniforms: Uniforms;
|
||||
|
||||
@group(0) @binding(1)
|
||||
var shadow_map: texture_depth_2d;
|
||||
|
||||
@group(0) @binding(2)
|
||||
var shadow_sampler: sampler_comparison;
|
||||
|
||||
@group(0) @binding(3)
|
||||
var dither_texture_array: texture_2d_array<f32>;
|
||||
|
||||
@group(0) @binding(4)
|
||||
var dither_sampler: sampler;
|
||||
|
||||
@group(0) @binding(5)
|
||||
var flowmap_texture: texture_2d<f32>;
|
||||
|
||||
@group(0) @binding(6)
|
||||
var flowmap_sampler: sampler;
|
||||
|
||||
@group(0) @binding(7)
|
||||
var blue_noise_texture: texture_2d<f32>;
|
||||
|
||||
@group(0) @binding(8)
|
||||
var blue_noise_sampler: sampler;
|
||||
|
||||
const PI: f32 = 3.14159265359;
|
||||
const TERRAIN_BOUNDS: vec2<f32> = vec2<f32>(1000.0, 1000.0);
|
||||
const LINE_THICKNESS: f32 = 0.1;
|
||||
const OCTAVE_STEPS: f32 = 4.0;
|
||||
const STROKE_LENGTH: f32 = 0.8;
|
||||
|
||||
fn hash2(p: vec2<f32>) -> f32 {
|
||||
let p3 = fract(vec3<f32>(p.x, p.y, p.x) * 0.13);
|
||||
let p3_dot = dot(p3, vec3<f32>(p3.y + 3.333, p3.z + 3.333, p3.x + 3.333));
|
||||
return fract((p3.x + p3.y) * p3_dot);
|
||||
}
|
||||
|
||||
fn rand(p: vec2f) -> f32 {
|
||||
return fract(sin(dot(p, vec2f(12.9898, 78.233))) * 43758.5453);
|
||||
}
|
||||
|
||||
struct StrokeData {
|
||||
world_pos_2d: vec2<f32>,
|
||||
tile_center: vec2<f32>,
|
||||
tile_size: f32,
|
||||
direction_to_light: vec2<f32>,
|
||||
distance_to_light: f32,
|
||||
perpendicular_to_light: vec2<f32>,
|
||||
rotation_t: f32,
|
||||
line_direction: vec2<f32>,
|
||||
perpendicular_to_line: vec2<f32>,
|
||||
octave_index: f32,
|
||||
offset: vec2<f32>,
|
||||
local_pos: vec2<f32>,
|
||||
}
|
||||
|
||||
fn compute_perpendicular(dir: vec2<f32>) -> vec2<f32> {
|
||||
return normalize(vec2<f32>(-dir.y, dir.x));
|
||||
}
|
||||
|
||||
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>) -> f32 {
|
||||
let proj_coords = light_space_pos.xyz / light_space_pos.w;
|
||||
let ndc_coords = proj_coords * vec3<f32>(0.5, -0.5, 1.0) + vec3<f32>(0.5, 0.5, 0.0);
|
||||
|
||||
if 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 {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
let depth = ndc_coords.z - uniforms.shadow_bias;
|
||||
let shadow = textureSampleCompare(shadow_map, shadow_sampler, ndc_coords.xy, depth);
|
||||
|
||||
return shadow;
|
||||
}
|
||||
|
||||
fn hatching_lighting(world_pos: vec3<f32>, clip_pos: vec4<f32>, tile_scale: f32, direction: vec2<f32>, distance: f32) -> f32 {
|
||||
let octave_index = round((1.0 - pow(distance, 2.0)) * OCTAVE_STEPS);
|
||||
let octave_normalized = octave_index / OCTAVE_STEPS;
|
||||
|
||||
if octave_index > 3.0 {
|
||||
return 1.0;
|
||||
} else if octave_index < 1.0 {
|
||||
let screen_pos = clip_pos.xy / clip_pos.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;
|
||||
return step(blue_noise, 0.05);
|
||||
}
|
||||
|
||||
|
||||
let world_pos_2d = vec2<f32>(world_pos.x, world_pos.z);
|
||||
let tile_size = 1.0 / tile_scale;
|
||||
let base_tile_center = floor(world_pos_2d / tile_size) * tile_size + tile_size * 0.5;
|
||||
|
||||
var min_lighting = 1.0;
|
||||
|
||||
for (var tile_y: i32 = -1; tile_y <= 1; tile_y++) {
|
||||
for (var tile_x: i32 = -1; tile_x <= 1; tile_x++) {
|
||||
let tile_center = base_tile_center + vec2<f32>(f32(tile_x), f32(tile_y)) * tile_size;
|
||||
|
||||
let perpendicular_to_light = compute_perpendicular(direction);
|
||||
let t = compute_rotation_t(distance);
|
||||
let parallel = mix(perpendicular_to_light, direction, t / 2.0);
|
||||
let perpendicular = compute_perpendicular(parallel);
|
||||
|
||||
let spacing = LINE_THICKNESS * 1.5;
|
||||
|
||||
var max_offset: i32;
|
||||
switch i32(octave_index) {
|
||||
case default {
|
||||
max_offset = 0;
|
||||
}
|
||||
case 3 {
|
||||
max_offset = 3;
|
||||
}
|
||||
case 2 {
|
||||
max_offset = 9;
|
||||
}
|
||||
case 1 {
|
||||
max_offset = 6;
|
||||
}
|
||||
}
|
||||
for (var i: i32 = -max_offset; i <= max_offset; i++) {
|
||||
let random = rand(tile_center + vec2<f32>(f32(i), f32(-i)));
|
||||
var chance: f32;
|
||||
switch i32(octave_index) {
|
||||
case 1, default: {
|
||||
chance = 1.0;
|
||||
}
|
||||
case 2: {
|
||||
chance = 0.5;
|
||||
}
|
||||
case 3: {
|
||||
chance = 0.8;
|
||||
}
|
||||
}
|
||||
if random > chance {
|
||||
continue;
|
||||
}
|
||||
let offset = perpendicular * f32(i) * tile_size / f32(max_offset);
|
||||
let local_pos = world_pos_2d - tile_center - offset;
|
||||
|
||||
let stroke_data = StrokeData(
|
||||
world_pos_2d,
|
||||
tile_center,
|
||||
tile_size,
|
||||
direction,
|
||||
distance,
|
||||
perpendicular_to_light,
|
||||
t,
|
||||
parallel,
|
||||
perpendicular,
|
||||
octave_index,
|
||||
offset,
|
||||
local_pos,
|
||||
);
|
||||
|
||||
let lighting = line_stroke_lighting(stroke_data, clip_pos);
|
||||
min_lighting = min(min_lighting, lighting);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return min_lighting;
|
||||
}
|
||||
|
||||
struct SpotlightData {
|
||||
direction_normalized: vec2<f32>,
|
||||
combined_distance: f32,
|
||||
is_lit: bool,
|
||||
}
|
||||
|
||||
fn calculate_spotlight_data(world_pos: vec3<f32>, normal: vec3<f32>, spotlight: Spotlight, tile_scale: f32, shadow: f32) -> SpotlightData {
|
||||
var data: SpotlightData;
|
||||
data.is_lit = false;
|
||||
data.direction_normalized = vec2<f32>(0.0, 0.0);
|
||||
data.combined_distance = 2.0;
|
||||
|
||||
let to_fragment = normalize(world_pos - spotlight.position);
|
||||
let angle_to_fragment = acos(dot(to_fragment, spotlight.direction));
|
||||
|
||||
if angle_to_fragment > spotlight.outer_angle {
|
||||
return data;
|
||||
}
|
||||
|
||||
let cube_size = 1.0 / tile_scale;
|
||||
let cube_center = vec3<f32>(
|
||||
floor(world_pos.x / cube_size) * cube_size + cube_size * 0.5,
|
||||
floor(world_pos.y / cube_size) * cube_size + cube_size * 0.5,
|
||||
floor(world_pos.z / cube_size) * cube_size + cube_size * 0.5
|
||||
);
|
||||
let tile_center = vec2<f32>(cube_center.x, cube_center.z);
|
||||
|
||||
let angular_falloff = smoothstep(spotlight.inner_angle, spotlight.outer_angle, angle_to_fragment);
|
||||
|
||||
let direction_to_point_3d = normalize(spotlight.position - cube_center);
|
||||
let diffuse_raw = max(0.0, dot(normalize(normal), direction_to_point_3d));
|
||||
let diffuse_res = 4.0;
|
||||
let diffuse = floor(diffuse_raw * diffuse_res) / diffuse_res;
|
||||
|
||||
let t = (cube_center.y - spotlight.position.y) / spotlight.direction.y;
|
||||
let hit_point = spotlight.position + spotlight.direction * t;
|
||||
let hit_point_2d = vec2<f32>(hit_point.x, hit_point.z);
|
||||
let direction_to_hit = hit_point_2d - tile_center;
|
||||
let distance_to_hit = min(length(direction_to_hit) / spotlight.range, 1.0);
|
||||
data.direction_normalized = normalize(direction_to_hit);
|
||||
|
||||
let lighting_intensity = shadow * pow(diffuse, 0.5) * (1.0 - angular_falloff);
|
||||
let darkness = 1.0 - lighting_intensity;
|
||||
data.combined_distance = distance_to_hit + darkness;
|
||||
|
||||
data.is_lit = data.combined_distance <= 1.0;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
fn is_in_spotlight_light_area(world_pos: vec3<f32>, normal: vec3<f32>, 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<f32>, normal: vec3<f32>, 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<f32>, clip_pos: vec4<f32>, normal: vec3<f32>, 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<f32>, clip_pos: vec4<f32>, normal: vec3<f32>, tile_scale: f32, shadow: 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);
|
||||
}
|
||||
|
||||
return max_lighting;
|
||||
}
|
||||
|
||||
fn line_stroke_lighting(data: StrokeData, clip_pos: vec4<f32>) -> f32 {
|
||||
let octave_normalized = data.octave_index / OCTAVE_STEPS;
|
||||
|
||||
let noise = hash2(data.tile_center + data.offset) * 2.0 - 1.0;
|
||||
|
||||
var noise_at_octave = noise;
|
||||
var jitter: f32;
|
||||
|
||||
switch i32(data.octave_index) {
|
||||
case default {
|
||||
noise_at_octave = noise;
|
||||
jitter = 1.0;
|
||||
}
|
||||
case 2 {
|
||||
noise_at_octave = noise / 10.0;
|
||||
jitter = 1.0;
|
||||
}
|
||||
case 3 {
|
||||
noise_at_octave = noise / 20.0;
|
||||
jitter = 0.5;
|
||||
}
|
||||
}
|
||||
let line = mix(data.perpendicular_to_light, data.direction_to_light, data.rotation_t / (2.0 + noise_at_octave));
|
||||
let perpendicular_to_line = compute_perpendicular(line);
|
||||
|
||||
let parallel_coord = dot(data.local_pos, line);
|
||||
let perpendicular_coord = dot(data.local_pos, perpendicular_to_line);
|
||||
|
||||
let line_half_width = LINE_THICKNESS * (1.0 - octave_normalized * 0.5) * data.tile_size;
|
||||
let straight_section_half_length = max(0.0, data.tile_size * 0.4 - line_half_width);
|
||||
|
||||
let parallel_jitter = (rand(data.tile_center + data.offset * 123.456) * 2.0 - 1.0) * data.tile_size * jitter;
|
||||
let jittered_parallel_coord = parallel_coord - parallel_jitter;
|
||||
|
||||
let overhang = max(0.0, abs(jittered_parallel_coord) - straight_section_half_length);
|
||||
let effective_distance = sqrt(overhang * overhang + perpendicular_coord * perpendicular_coord);
|
||||
|
||||
return step(line_half_width, effective_distance);
|
||||
}
|
||||
|
||||
fn flowmap_path_lighting(world_pos: vec3<f32>, clip_pos: vec4<f32>, tile_scale: f32) -> f32 {
|
||||
let world_pos_2d = vec2<f32>(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<f32>(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<f32>, clip_pos: vec4<f32>, normal: vec3<f32>, tile_scale: f32, shadow: f32) -> f32 {
|
||||
let world_pos_2d = vec2<f32>(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<f32>(x, y));
|
||||
let distance_to_path = flowmap_sample.b;
|
||||
|
||||
let light_dir_3d = normalize(vec3<f32>(-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);
|
||||
}
|
||||
72
src/shaders/snow.wesl
Normal file
72
src/shaders/snow.wesl
Normal file
@@ -0,0 +1,72 @@
|
||||
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<f32>;
|
||||
|
||||
@group(1) @binding(1)
|
||||
var persistent_light_sampler: sampler;
|
||||
|
||||
@vertex
|
||||
fn vs_main(input: VertexInput) -> VertexOutput {
|
||||
var output: VertexOutput;
|
||||
|
||||
let instance_model = mat4x4<f32>(
|
||||
input.instance_model_0,
|
||||
input.instance_model_1,
|
||||
input.instance_model_2,
|
||||
input.instance_model_3
|
||||
);
|
||||
|
||||
let world_pos = instance_model * vec4<f32>(input.position, 1.0);
|
||||
output.world_position = world_pos.xyz;
|
||||
output.clip_position = uniforms.projection * uniforms.view * world_pos;
|
||||
|
||||
let normal_matrix = mat3x3<f32>(
|
||||
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<f32> {
|
||||
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<f32>(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<f32>(brightness, brightness, brightness, 1.0);
|
||||
}
|
||||
44
src/shaders/snow_deform.wgsl
Normal file
44
src/shaders/snow_deform.wgsl
Normal file
@@ -0,0 +1,44 @@
|
||||
@group(0) @binding(0)
|
||||
var snow_depth: texture_storage_2d<r32float, read_write>;
|
||||
|
||||
@group(0) @binding(1)
|
||||
var<uniform> params: DeformParams;
|
||||
|
||||
struct DeformParams {
|
||||
position_x: f32,
|
||||
position_z: f32,
|
||||
radius: f32,
|
||||
depth: f32,
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16, 1)
|
||||
fn deform(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
||||
let texture_size = textureDimensions(snow_depth);
|
||||
|
||||
if (global_id.x >= texture_size.x || global_id.y >= texture_size.y) {
|
||||
return;
|
||||
}
|
||||
|
||||
let coords = vec2<i32>(i32(global_id.x), i32(global_id.y));
|
||||
|
||||
let terrain_size = vec2<f32>(1000.0, 1000.0);
|
||||
let half_size = terrain_size / 2.0;
|
||||
|
||||
let uv = vec2<f32>(f32(global_id.x) / f32(texture_size.x), f32(global_id.y) / f32(texture_size.y));
|
||||
let world_pos = uv * terrain_size - half_size;
|
||||
|
||||
let deform_center = vec2<f32>(params.position_x, params.position_z);
|
||||
let distance = length(world_pos - deform_center);
|
||||
|
||||
if (distance < params.radius) {
|
||||
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 new_depth = max(0.0, current_depth - deform_amount);
|
||||
|
||||
textureStore(snow_depth, coords, vec4<f32>(new_depth, 0.0, 0.0, 0.0));
|
||||
}
|
||||
}
|
||||
117
src/shaders/snow_light_accumulation.wesl
Normal file
117
src/shaders/snow_light_accumulation.wesl
Normal file
@@ -0,0 +1,117 @@
|
||||
import package::shared::{Spotlight, MAX_SPOTLIGHTS, calculate_spotlight_data};
|
||||
|
||||
struct AccumulationUniforms {
|
||||
terrain_min_xz: vec2<f32>,
|
||||
terrain_max_xz: vec2<f32>,
|
||||
decay_rate: f32,
|
||||
delta_time: f32,
|
||||
spotlight_count: u32,
|
||||
_padding: u32,
|
||||
light_view_projection: mat4x4<f32>,
|
||||
shadow_bias: f32,
|
||||
terrain_height_scale: f32,
|
||||
_padding3: f32,
|
||||
_padding4: f32,
|
||||
spotlights: array<Spotlight, 4>,
|
||||
}
|
||||
|
||||
@group(0) @binding(0)
|
||||
var previous_light: texture_2d<f32>;
|
||||
|
||||
@group(0) @binding(1)
|
||||
var light_sampler: sampler;
|
||||
|
||||
@group(0) @binding(2)
|
||||
var<uniform> uniforms: AccumulationUniforms;
|
||||
|
||||
@group(0) @binding(3)
|
||||
var heightmap: texture_2d<f32>;
|
||||
|
||||
@group(0) @binding(4)
|
||||
var heightmap_sampler: sampler;
|
||||
|
||||
@group(0) @binding(5)
|
||||
var shadow_map: texture_depth_2d;
|
||||
|
||||
@group(0) @binding(6)
|
||||
var shadow_sampler: sampler_comparison;
|
||||
|
||||
@group(0) @binding(7)
|
||||
var snow_depth: texture_2d<f32>;
|
||||
|
||||
@group(0) @binding(8)
|
||||
var snow_depth_sampler: sampler;
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
|
||||
let x = f32((vertex_index << 1u) & 2u);
|
||||
let y = f32(vertex_index & 2u);
|
||||
out.position = vec4<f32>(x * 2.0 - 1.0, y * 2.0 - 1.0, 0.0, 1.0);
|
||||
out.uv = vec2<f32>(x, 1.0 - y);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
fn sample_shadow_map(light_space_pos: vec4<f32>) -> f32 {
|
||||
let proj_coords = light_space_pos.xyz / light_space_pos.w;
|
||||
let ndc_coords = proj_coords * vec3<f32>(0.5, -0.5, 1.0) + vec3<f32>(0.5, 0.5, 0.0);
|
||||
|
||||
if 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 {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
let depth = ndc_coords.z - uniforms.shadow_bias;
|
||||
let shadow = textureSampleCompare(shadow_map, shadow_sampler, ndc_coords.xy, depth);
|
||||
|
||||
return shadow;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let prev_light = textureSample(previous_light, light_sampler, in.uv).r;
|
||||
|
||||
let world_xz = mix(uniforms.terrain_min_xz, uniforms.terrain_max_xz, in.uv);
|
||||
let terrain_height = textureSampleLevel(heightmap, heightmap_sampler, in.uv, 0.0).r * uniforms.terrain_height_scale;
|
||||
let depth = textureSampleLevel(snow_depth, snow_depth_sampler, in.uv, 0.0).r;
|
||||
let snow_surface_height = terrain_height + depth;
|
||||
let snow_surface_pos = vec3<f32>(world_xz.x, snow_surface_height, world_xz.y);
|
||||
|
||||
let light_space_position = uniforms.light_view_projection * vec4<f32>(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<f32>(0.0, 1.0, 0.0);
|
||||
|
||||
for (var i = 0u; i < uniforms.spotlight_count; i++) {
|
||||
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);
|
||||
current_light = max(current_light, light);
|
||||
}
|
||||
}
|
||||
|
||||
var accumulated: f32;
|
||||
if current_light > 0.01 {
|
||||
accumulated = current_light;
|
||||
} else {
|
||||
let decay_factor = exp(-uniforms.decay_rate * uniforms.delta_time * 60.0);
|
||||
accumulated = prev_light * decay_factor;
|
||||
|
||||
if accumulated < 0.01 {
|
||||
accumulated = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
return vec4<f32>(accumulated, 0.0, 0.0, 1.0);
|
||||
}
|
||||
66
src/shaders/standard.wesl
Normal file
66
src/shaders/standard.wesl
Normal file
@@ -0,0 +1,66 @@
|
||||
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<f32>(
|
||||
input.instance_model_0,
|
||||
input.instance_model_1,
|
||||
input.instance_model_2,
|
||||
input.instance_model_3
|
||||
);
|
||||
|
||||
let world_pos = instance_model * vec4<f32>(input.position, 1.0);
|
||||
output.world_position = world_pos.xyz;
|
||||
output.clip_position = uniforms.projection * uniforms.view * world_pos;
|
||||
|
||||
let normal_matrix = mat3x3<f32>(
|
||||
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<f32> {
|
||||
let shadow = sample_shadow_map(input.light_space_position);
|
||||
|
||||
let debug = 0u;
|
||||
|
||||
if debug == 3u {
|
||||
return vec4<f32>(shadow, shadow, shadow, 1.0);
|
||||
}
|
||||
|
||||
if debug == 2u {
|
||||
let proj_coords = input.light_space_position.xyz / input.light_space_position.w;
|
||||
return vec4<f32>(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<f32>(0.5, -0.5, 1.0) + vec3<f32>(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<f32>(ndc_coords.x, ndc_coords.y, ndc_coords.z, 1.0);
|
||||
} else {
|
||||
return vec4<f32>(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<f32>(brightness, brightness, brightness, 1.0);
|
||||
}
|
||||
89
src/shaders/terrain.wesl
Normal file
89
src/shaders/terrain.wesl
Normal file
@@ -0,0 +1,89 @@
|
||||
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<f32>(
|
||||
input.instance_model_0,
|
||||
input.instance_model_1,
|
||||
input.instance_model_2,
|
||||
input.instance_model_3
|
||||
);
|
||||
|
||||
let world_pos = instance_model * vec4<f32>(input.position, 1.0);
|
||||
output.world_position = world_pos.xyz;
|
||||
output.clip_position = uniforms.projection * uniforms.view * world_pos;
|
||||
|
||||
let normal_matrix = mat3x3<f32>(
|
||||
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<f32> {
|
||||
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<f32>(0.5, -0.5, 1.0) + vec3<f32>(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<f32>(ndc_coords.x, ndc_coords.y, ndc_coords.z, 1.0);
|
||||
} else {
|
||||
return vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
if debug == 3u {
|
||||
let shadow = sample_shadow_map(input.light_space_position);
|
||||
return vec4<f32>(shadow, shadow, shadow, 1.0);
|
||||
}
|
||||
|
||||
if debug == 1u {
|
||||
let flowmap_uv = (vec2<f32>(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<f32>(flowmap_sample, 1.0);
|
||||
}
|
||||
|
||||
if debug == 2u {
|
||||
let world_pos_2d = vec2<f32>(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<f32>(x, y));
|
||||
let perpendicular_to_path = normalize(vec2<f32>(-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<f32>(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<f32>(brightness, brightness, brightness, 1.0);
|
||||
}
|
||||
451
src/snow.rs
Normal file
451
src/snow.rs
Normal file
@@ -0,0 +1,451 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use exr::prelude::{ReadChannels, ReadLayers};
|
||||
use glam::{Vec2, Vec3};
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
use crate::{
|
||||
components::MeshComponent,
|
||||
entity::EntityHandle,
|
||||
mesh::{Mesh, Vertex},
|
||||
render,
|
||||
world::{Transform, World},
|
||||
};
|
||||
|
||||
pub struct SnowConfig
|
||||
{
|
||||
pub depth_map_path: String,
|
||||
pub heightmap_path: String,
|
||||
pub terrain_size: Vec2,
|
||||
pub resolution: (u32, u32),
|
||||
}
|
||||
|
||||
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 {
|
||||
depth_map_path: "textures/snow_depth.exr".to_string(),
|
||||
heightmap_path: "textures/terrain_heightmap.exr".to_string(),
|
||||
terrain_size: Vec2::new(1000.0, 1000.0),
|
||||
resolution: (1000, 1000),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SnowLayer
|
||||
{
|
||||
pub entity: EntityHandle,
|
||||
pub depth_texture: wgpu::Texture,
|
||||
pub depth_texture_view: wgpu::TextureView,
|
||||
pub depth_bind_group: wgpu::BindGroup,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub deform_bind_group: wgpu::BindGroup,
|
||||
pub deform_pipeline: wgpu::ComputePipeline,
|
||||
pub deform_params_buffer: wgpu::Buffer,
|
||||
}
|
||||
|
||||
impl SnowLayer
|
||||
{
|
||||
pub fn load(world: &mut World, config: &SnowConfig) -> anyhow::Result<Self>
|
||||
{
|
||||
println!("\n=== Loading Snow Layer ===");
|
||||
println!("Depth map path: {}", config.depth_map_path);
|
||||
println!("Heightmap path: {}", config.heightmap_path);
|
||||
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);
|
||||
|
||||
Ok(Self {
|
||||
entity,
|
||||
depth_texture,
|
||||
depth_texture_view,
|
||||
depth_bind_group,
|
||||
width,
|
||||
height,
|
||||
deform_bind_group,
|
||||
deform_pipeline,
|
||||
deform_params_buffer,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_depth_map(path: &str) -> anyhow::Result<(Vec<f32>, u32, u32)>
|
||||
{
|
||||
println!("Loading snow depth map from: {}", path);
|
||||
|
||||
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 width = layer.size.width() as u32;
|
||||
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::<Vec<_>>());
|
||||
|
||||
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"))?;
|
||||
|
||||
println!(" Using channel: {:?}", channel.name);
|
||||
|
||||
let depths: Vec<f32> = channel.sample_data.values_as_f32().collect();
|
||||
|
||||
let min_value = depths.iter().cloned().fold(f32::INFINITY, f32::min);
|
||||
let max_value = depths.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
|
||||
let avg_value = depths.iter().sum::<f32>() / depths.len() as f32;
|
||||
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);
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
Ok((depths, width, height))
|
||||
}
|
||||
|
||||
fn create_depth_texture(
|
||||
depth_data: &[f32],
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> (wgpu::Texture, wgpu::TextureView, wgpu::BindGroup)
|
||||
{
|
||||
render::with_device(|device| {
|
||||
let size = wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
};
|
||||
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("Snow Depth Texture"),
|
||||
size,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::R32Float,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::COPY_DST
|
||||
| wgpu::TextureUsages::STORAGE_BINDING,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
let data_bytes: &[u8] = bytemuck::cast_slice(depth_data);
|
||||
|
||||
render::with_queue(|queue| {
|
||||
queue.write_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
data_bytes,
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(width * 4),
|
||||
rows_per_image: Some(height),
|
||||
},
|
||||
size,
|
||||
);
|
||||
});
|
||||
|
||||
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let bind_group_layout =
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Snow Depth Bind Group Layout"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT | wgpu::ShaderStages::COMPUTE,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: false },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Snow Depth Bind Group"),
|
||||
layout: &bind_group_layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&texture_view),
|
||||
}],
|
||||
});
|
||||
|
||||
(texture, texture_view, bind_group)
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
render::with_device(|device| {
|
||||
let shader_source = std::fs::read_to_string("src/shaders/snow_deform.wgsl")
|
||||
.expect("Failed to load snow deform shader");
|
||||
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Snow Deform Shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
||||
});
|
||||
|
||||
let params_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Snow Deform Params"),
|
||||
size: 32,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
let bind_group_layout =
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Snow Deform Bind Group Layout"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::COMPUTE,
|
||||
ty: wgpu::BindingType::StorageTexture {
|
||||
access: wgpu::StorageTextureAccess::ReadWrite,
|
||||
format: wgpu::TextureFormat::R32Float,
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::COMPUTE,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Snow Deform Bind Group"),
|
||||
layout: &bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(depth_texture_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: params_buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Snow Deform Pipeline Layout"),
|
||||
bind_group_layouts: &[&bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
||||
label: Some("Snow Deform Pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
module: &shader,
|
||||
entry_point: Some("deform"),
|
||||
compilation_options: Default::default(),
|
||||
cache: None,
|
||||
});
|
||||
|
||||
(pipeline, bind_group, params_buffer)
|
||||
})
|
||||
}
|
||||
|
||||
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_bytes: &[u8] = bytemuck::cast_slice(¶ms_data);
|
||||
|
||||
queue.write_buffer(&self.deform_params_buffer, 0, params_bytes);
|
||||
});
|
||||
|
||||
render::with_device(|device| {
|
||||
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("Snow Deform Encoder"),
|
||||
});
|
||||
|
||||
{
|
||||
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
||||
label: Some("Snow Deform Pass"),
|
||||
timestamp_writes: None,
|
||||
});
|
||||
|
||||
compute_pass.set_pipeline(&self.deform_pipeline);
|
||||
compute_pass.set_bind_group(0, &self.deform_bind_group, &[]);
|
||||
|
||||
let workgroup_size = 16;
|
||||
let dispatch_x = (self.width + workgroup_size - 1) / workgroup_size;
|
||||
let dispatch_y = (self.height + workgroup_size - 1) / workgroup_size;
|
||||
compute_pass.dispatch_workgroups(dispatch_x, dispatch_y, 1);
|
||||
}
|
||||
|
||||
render::with_queue(|queue| {
|
||||
queue.submit(Some(encoder.finish()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
550
src/snow_light.rs
Normal file
550
src/snow_light.rs
Normal file
@@ -0,0 +1,550 @@
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use glam::{Vec2, Vec3};
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Pod, Zeroable)]
|
||||
struct AccumulationUniforms
|
||||
{
|
||||
terrain_min_xz: [f32; 2],
|
||||
terrain_max_xz: [f32; 2],
|
||||
decay_rate: f32,
|
||||
delta_time: f32,
|
||||
spotlight_count: u32,
|
||||
_padding: u32,
|
||||
light_view_projection: [[f32; 4]; 4],
|
||||
shadow_bias: f32,
|
||||
terrain_height_scale: f32,
|
||||
_padding3: f32,
|
||||
_padding4: f32,
|
||||
spotlights: [crate::render::SpotlightRaw; crate::render::MAX_SPOTLIGHTS],
|
||||
}
|
||||
|
||||
pub struct SnowLightAccumulation
|
||||
{
|
||||
texture_ping: wgpu::Texture,
|
||||
texture_pong: wgpu::Texture,
|
||||
view_ping: wgpu::TextureView,
|
||||
view_pong: wgpu::TextureView,
|
||||
|
||||
bind_group_layout: wgpu::BindGroupLayout,
|
||||
bind_group_ping: Option<wgpu::BindGroup>,
|
||||
bind_group_pong: Option<wgpu::BindGroup>,
|
||||
|
||||
uniform_buffer: wgpu::Buffer,
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
|
||||
quad_vb: wgpu::Buffer,
|
||||
quad_ib: wgpu::Buffer,
|
||||
quad_num_indices: u32,
|
||||
|
||||
current: bool,
|
||||
needs_clear: bool,
|
||||
|
||||
terrain_min: Vec2,
|
||||
terrain_max: Vec2,
|
||||
pub decay_rate: f32,
|
||||
}
|
||||
|
||||
impl SnowLightAccumulation
|
||||
{
|
||||
pub fn new(
|
||||
device: &wgpu::Device,
|
||||
terrain_min: Vec2,
|
||||
terrain_max: Vec2,
|
||||
resolution: u32,
|
||||
) -> Self
|
||||
{
|
||||
let size = wgpu::Extent3d {
|
||||
width: resolution,
|
||||
height: resolution,
|
||||
depth_or_array_layers: 1,
|
||||
};
|
||||
|
||||
let texture_desc = wgpu::TextureDescriptor {
|
||||
label: Some("Snow Light Accumulation"),
|
||||
size,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::R16Float,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
};
|
||||
|
||||
let texture_ping = device.create_texture(&texture_desc);
|
||||
let texture_pong = device.create_texture(&texture_desc);
|
||||
|
||||
let view_ping = texture_ping.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let view_pong = texture_pong.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let 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 uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Snow Light Accumulation Uniforms"),
|
||||
size: std::mem::size_of::<AccumulationUniforms>() as wgpu::BufferAddress,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Snow Light Accumulation 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,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 2,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
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::D2,
|
||||
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::Depth,
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 6,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
|
||||
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 compiler = wesl::Wesl::new("src/shaders");
|
||||
let shader_source = compiler
|
||||
.compile(&"package::snow_light_accumulation".parse().unwrap())
|
||||
.inspect_err(|e| eprintln!("WESL error: {e}"))
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Snow Light Accumulation Shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
||||
});
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Snow Light Accumulation Pipeline Layout"),
|
||||
bind_group_layouts: &[&bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Snow Light Accumulation Pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: wgpu::TextureFormat::R16Float,
|
||||
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: None,
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
unclipped_depth: false,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
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,
|
||||
];
|
||||
|
||||
let quad_vb = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Snow Light Quad VB"),
|
||||
contents: bytemuck::cast_slice(vertices),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
|
||||
let indices: &[u16] = &[0, 1, 2];
|
||||
let quad_ib = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Snow Light Quad IB"),
|
||||
contents: bytemuck::cast_slice(indices),
|
||||
usage: wgpu::BufferUsages::INDEX,
|
||||
});
|
||||
|
||||
Self {
|
||||
texture_ping,
|
||||
texture_pong,
|
||||
view_ping,
|
||||
view_pong,
|
||||
bind_group_layout,
|
||||
bind_group_ping: None,
|
||||
bind_group_pong: None,
|
||||
uniform_buffer,
|
||||
pipeline,
|
||||
quad_vb,
|
||||
quad_ib,
|
||||
quad_num_indices: 3,
|
||||
current: false,
|
||||
needs_clear: true,
|
||||
terrain_min,
|
||||
terrain_max,
|
||||
decay_rate: 0.015,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&self, encoder: &mut wgpu::CommandEncoder)
|
||||
{
|
||||
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Clear Snow Light Ping"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &self.view_ping,
|
||||
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,
|
||||
});
|
||||
|
||||
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Clear Snow Light Pong"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &self.view_pong,
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_heightmap(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
heightmap_view: &wgpu::TextureView,
|
||||
heightmap_sampler: &wgpu::Sampler,
|
||||
shadow_map_view: &wgpu::TextureView,
|
||||
shadow_sampler: &wgpu::Sampler,
|
||||
snow_depth_view: &wgpu::TextureView,
|
||||
snow_depth_sampler: &wgpu::Sampler,
|
||||
)
|
||||
{
|
||||
let 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()
|
||||
});
|
||||
|
||||
self.bind_group_ping = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Snow Light Accumulation Bind Group Ping"),
|
||||
layout: &self.bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&self.view_pong),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(&sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: self.uniform_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: wgpu::BindingResource::TextureView(heightmap_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 4,
|
||||
resource: wgpu::BindingResource::Sampler(heightmap_sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 5,
|
||||
resource: wgpu::BindingResource::TextureView(shadow_map_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 6,
|
||||
resource: wgpu::BindingResource::Sampler(shadow_sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 7,
|
||||
resource: wgpu::BindingResource::TextureView(snow_depth_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 8,
|
||||
resource: wgpu::BindingResource::Sampler(snow_depth_sampler),
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
self.bind_group_pong = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Snow Light Accumulation Bind Group Pong"),
|
||||
layout: &self.bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&self.view_ping),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(&sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: self.uniform_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: wgpu::BindingResource::TextureView(heightmap_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 4,
|
||||
resource: wgpu::BindingResource::Sampler(heightmap_sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 5,
|
||||
resource: wgpu::BindingResource::TextureView(shadow_map_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 6,
|
||||
resource: wgpu::BindingResource::Sampler(shadow_sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 7,
|
||||
resource: wgpu::BindingResource::TextureView(snow_depth_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 8,
|
||||
resource: wgpu::BindingResource::Sampler(snow_depth_sampler),
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&mut self,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
queue: &wgpu::Queue,
|
||||
spotlights: &[crate::render::Spotlight],
|
||||
delta_time: f32,
|
||||
light_view_projection: &glam::Mat4,
|
||||
shadow_bias: f32,
|
||||
terrain_height_scale: f32,
|
||||
)
|
||||
{
|
||||
if self.needs_clear
|
||||
{
|
||||
self.clear(encoder);
|
||||
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()
|
||||
{
|
||||
spotlight_array[i] = spotlight.to_raw();
|
||||
}
|
||||
|
||||
let uniforms = AccumulationUniforms {
|
||||
terrain_min_xz: self.terrain_min.to_array(),
|
||||
terrain_max_xz: self.terrain_max.to_array(),
|
||||
decay_rate: self.decay_rate,
|
||||
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(),
|
||||
shadow_bias,
|
||||
terrain_height_scale,
|
||||
_padding3: 0.0,
|
||||
_padding4: 0.0,
|
||||
spotlights: spotlight_array,
|
||||
};
|
||||
|
||||
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 Some(bind_group) = bind_group else {
|
||||
return;
|
||||
};
|
||||
|
||||
{
|
||||
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Snow Light Accumulation Pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: write_view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
|
||||
render_pass.set_pipeline(&self.pipeline);
|
||||
render_pass.set_bind_group(0, bind_group, &[]);
|
||||
render_pass.set_vertex_buffer(0, self.quad_vb.slice(..));
|
||||
render_pass.set_index_buffer(self.quad_ib.slice(..), wgpu::IndexFormat::Uint16);
|
||||
render_pass.draw_indexed(0..self.quad_num_indices, 0, 0..1);
|
||||
}
|
||||
|
||||
self.current = !self.current;
|
||||
}
|
||||
|
||||
pub fn read_view(&self) -> &wgpu::TextureView
|
||||
{
|
||||
if self.current { &self.view_pong } else { &self.view_ping }
|
||||
}
|
||||
|
||||
pub fn create_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout
|
||||
{
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Snow Persistent Light 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,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_read_bind_group(
|
||||
&self,
|
||||
device: &wgpu::Device,
|
||||
layout: &wgpu::BindGroupLayout,
|
||||
) -> wgpu::BindGroup
|
||||
{
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("Snow Persistent 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()
|
||||
});
|
||||
|
||||
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Snow Persistent Light Bind Group"),
|
||||
layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(self.read_view()),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(&sampler),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
75
src/space.rs
Normal file
75
src/space.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use anyhow::Result;
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::{
|
||||
empty::Empties,
|
||||
light::{LightData, Lights},
|
||||
mesh::{InstanceData, Mesh},
|
||||
render,
|
||||
};
|
||||
|
||||
pub const CAMERA_SPAWN_OFFSET: Vec3 = Vec3::new(15.0, 15.0, 15.0);
|
||||
|
||||
pub struct Space
|
||||
{
|
||||
pub mesh_data: Vec<(Mesh, Vec<InstanceData>)>,
|
||||
pub spotlights: Vec<LightData>,
|
||||
pub player_spawn: Vec3,
|
||||
}
|
||||
|
||||
impl Space
|
||||
{
|
||||
pub fn load_space(gltf_path: &str) -> Result<Space>
|
||||
{
|
||||
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))?;
|
||||
|
||||
let spotlights = lights.into_spotlights();
|
||||
|
||||
let player_spawn = Self::get_player_spawn(gltf_path)?;
|
||||
|
||||
Ok(Space {
|
||||
mesh_data,
|
||||
spotlights,
|
||||
player_spawn,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_player_spawn(gltf_path: &str) -> Result<Vec3>
|
||||
{
|
||||
let empty = Empties::get_empty_by_name(gltf_path, "PlayerSpawn")?;
|
||||
|
||||
if let Some(empty_node) = empty
|
||||
{
|
||||
let (_scale, _rotation, translation) = empty_node.transform.to_scale_rotation_translation();
|
||||
Ok(translation)
|
||||
}
|
||||
else
|
||||
{
|
||||
println!("Warning: PlayerSpawn empty not found, using default position");
|
||||
Ok(Vec3::new(0.0, 5.0, 0.0))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn camera_spawn_position(&self) -> Vec3
|
||||
{
|
||||
self.player_spawn + CAMERA_SPAWN_OFFSET
|
||||
}
|
||||
|
||||
pub fn terrain_mesh(&self) -> Option<&Mesh>
|
||||
{
|
||||
self.mesh_data.first().map(|(mesh, _)| mesh)
|
||||
}
|
||||
|
||||
pub fn tree_instances(&self) -> Option<(&Mesh, &Vec<InstanceData>)>
|
||||
{
|
||||
self.mesh_data
|
||||
.iter()
|
||||
.find(|(_, instances)| !instances.is_empty())
|
||||
.map(|(mesh, instances)| (mesh, instances))
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::components::FollowComponent;
|
||||
use crate::utility::input::InputState;
|
||||
use crate::world::World;
|
||||
use crate::world::{Transform, World};
|
||||
|
||||
pub fn camera_input_system(world: &mut World, input_state: &InputState)
|
||||
{
|
||||
@@ -18,11 +19,7 @@ pub fn camera_input_system(world: &mut World, input_state: &InputState)
|
||||
|
||||
if input_state.mouse_delta.0.abs() > 0.0 || input_state.mouse_delta.1.abs() > 0.0
|
||||
{
|
||||
let is_following = world
|
||||
.camera_follows
|
||||
.get(camera_entity)
|
||||
.map(|f| f.is_following)
|
||||
.unwrap_or(false);
|
||||
let is_following = world.follows.get(camera_entity).is_some();
|
||||
|
||||
camera.yaw += input_state.mouse_delta.0 * 0.0008;
|
||||
|
||||
@@ -45,26 +42,20 @@ pub fn camera_input_system(world: &mut World, input_state: &InputState)
|
||||
|
||||
pub fn camera_follow_system(world: &mut World)
|
||||
{
|
||||
let camera_entities: Vec<_> = world.camera_follows.all();
|
||||
let camera_entities: Vec<_> = world.follows.all();
|
||||
|
||||
for camera_entity in camera_entities
|
||||
{
|
||||
if let Some(follow) = world.camera_follows.get(camera_entity)
|
||||
if let Some(camera) = world.cameras.get(camera_entity)
|
||||
{
|
||||
if !follow.is_following
|
||||
if let Some(follow) = world.follows.get(camera_entity)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let target_entity = follow.target;
|
||||
let offset = follow.offset.position;
|
||||
|
||||
let target_entity = follow.target_entity;
|
||||
let offset = follow.offset;
|
||||
|
||||
if let Some(target_transform) = world.transforms.get(target_entity)
|
||||
{
|
||||
let target_position = target_transform.position;
|
||||
|
||||
if let Some(camera) = world.cameras.get_mut(camera_entity)
|
||||
if let Some(target_transform) = world.transforms.get(target_entity)
|
||||
{
|
||||
let target_position = target_transform.position;
|
||||
let distance = offset.length();
|
||||
|
||||
let orbit_yaw = camera.yaw + std::f32::consts::PI;
|
||||
@@ -75,15 +66,15 @@ pub fn camera_follow_system(world: &mut World)
|
||||
|
||||
let new_offset = Vec3::new(offset_x, offset_y, offset_z);
|
||||
|
||||
if let Some(camera_transform) = world.transforms.get_mut(camera_entity)
|
||||
{
|
||||
camera_transform.position = target_position + new_offset;
|
||||
}
|
||||
world
|
||||
.transforms
|
||||
.with_mut(camera_entity, |camera_transform| {
|
||||
camera_transform.position = target_position + new_offset;
|
||||
});
|
||||
|
||||
if let Some(follow_mut) = world.camera_follows.get_mut(camera_entity)
|
||||
{
|
||||
follow_mut.offset = new_offset;
|
||||
}
|
||||
world.follows.components.get_mut(&camera_entity).map(|f| {
|
||||
f.offset.position = new_offset;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,19 +143,16 @@ pub fn camera_noclip_system(world: &mut World, input_state: &InputState, delta:
|
||||
|
||||
pub fn start_camera_following(world: &mut World, camera_entity: crate::entity::EntityHandle)
|
||||
{
|
||||
if let Some(follow) = world.camera_follows.get_mut(camera_entity)
|
||||
if let Some(camera_transform) = world.transforms.get(camera_entity)
|
||||
{
|
||||
let target_entity = follow.target_entity;
|
||||
|
||||
if let Some(target_transform) = world.transforms.get(target_entity)
|
||||
let player_entities = world.player_tags.all();
|
||||
if let Some(&player_entity) = player_entities.first()
|
||||
{
|
||||
if let Some(camera_transform) = world.transforms.get(camera_entity)
|
||||
if let Some(target_transform) = world.transforms.get(player_entity)
|
||||
{
|
||||
let offset = camera_transform.position - target_transform.position;
|
||||
follow.offset = offset;
|
||||
follow.is_following = true;
|
||||
|
||||
let distance = offset.length();
|
||||
|
||||
if distance > 0.0
|
||||
{
|
||||
if let Some(camera) = world.cameras.get_mut(camera_entity)
|
||||
@@ -173,6 +161,16 @@ pub fn start_camera_following(world: &mut World, camera_entity: crate::entity::E
|
||||
camera.yaw = offset.z.atan2(offset.x) + std::f32::consts::PI;
|
||||
}
|
||||
}
|
||||
|
||||
world.follows.insert(
|
||||
camera_entity,
|
||||
FollowComponent {
|
||||
target: player_entity,
|
||||
offset: Transform::from_position(offset),
|
||||
inherit_rotation: false,
|
||||
inherit_scale: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,13 +178,13 @@ pub fn start_camera_following(world: &mut World, camera_entity: crate::entity::E
|
||||
|
||||
pub fn stop_camera_following(world: &mut World, camera_entity: crate::entity::EntityHandle)
|
||||
{
|
||||
if let Some(follow) = world.camera_follows.get_mut(camera_entity)
|
||||
if let Some(follow) = world.follows.get(camera_entity)
|
||||
{
|
||||
follow.is_following = false;
|
||||
let target_entity = follow.target;
|
||||
|
||||
if let Some(camera_transform) = world.transforms.get(camera_entity)
|
||||
{
|
||||
if let Some(target_transform) = world.transforms.get(follow.target_entity)
|
||||
if let Some(target_transform) = world.transforms.get(target_entity)
|
||||
{
|
||||
let look_direction =
|
||||
(target_transform.position - camera_transform.position).normalize();
|
||||
@@ -198,5 +196,7 @@ pub fn stop_camera_following(world: &mut World, camera_entity: crate::entity::En
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
world.follows.remove(camera_entity);
|
||||
}
|
||||
}
|
||||
|
||||
48
src/systems/follow.rs
Normal file
48
src/systems/follow.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use crate::world::World;
|
||||
|
||||
pub fn follow_system(world: &mut World)
|
||||
{
|
||||
let following_entities: Vec<_> = world.follows.all();
|
||||
|
||||
for entity in following_entities
|
||||
{
|
||||
if let Some(follow) = world.follows.get(entity)
|
||||
{
|
||||
let target = follow.target;
|
||||
if let Some(target_transform) = world.transforms.get(target)
|
||||
{
|
||||
let target_pos = target_transform.position;
|
||||
let target_rot = target_transform.rotation;
|
||||
let target_scale = target_transform.scale;
|
||||
let offset = follow.offset;
|
||||
let inherit_rot = follow.inherit_rotation;
|
||||
let inherit_scale = follow.inherit_scale;
|
||||
|
||||
world.transforms.with_mut(entity, |transform| {
|
||||
transform.position = target_pos;
|
||||
|
||||
if inherit_rot
|
||||
{
|
||||
let rotated_offset = target_rot * offset.position;
|
||||
transform.position += rotated_offset;
|
||||
transform.rotation = target_rot * offset.rotation;
|
||||
}
|
||||
else
|
||||
{
|
||||
transform.position += offset.position;
|
||||
transform.rotation = offset.rotation;
|
||||
}
|
||||
|
||||
if inherit_scale
|
||||
{
|
||||
transform.scale = target_scale * offset.scale;
|
||||
}
|
||||
else
|
||||
{
|
||||
transform.scale = offset.scale;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,22 @@
|
||||
pub mod camera;
|
||||
pub mod follow;
|
||||
pub mod input;
|
||||
pub mod physics_sync;
|
||||
pub mod render;
|
||||
pub mod rotate;
|
||||
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,
|
||||
};
|
||||
pub use follow::follow_system;
|
||||
pub use input::player_input_system;
|
||||
pub use physics_sync::physics_sync_system;
|
||||
pub use render::render_system;
|
||||
pub use rotate::rotate_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};
|
||||
|
||||
@@ -2,8 +2,6 @@ use crate::mesh::InstanceRaw;
|
||||
use crate::render::DrawCall;
|
||||
use crate::world::World;
|
||||
use bytemuck::cast_slice;
|
||||
use glam::Mat4;
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
pub fn render_system(world: &World) -> Vec<DrawCall>
|
||||
{
|
||||
@@ -24,16 +22,27 @@ pub fn render_system(world: &World) -> Vec<DrawCall>
|
||||
}
|
||||
else
|
||||
{
|
||||
let dissolve_amount = world.dissolves.get(entity).map(|d| d.amount).unwrap_or(0.0);
|
||||
|
||||
let instance_data = InstanceRaw {
|
||||
model: model_matrix.to_cols_array_2d(),
|
||||
dissolve_amount,
|
||||
_padding: [0.0; 3],
|
||||
};
|
||||
|
||||
let buffer = crate::render::with_device(|device| {
|
||||
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Instance Buffer"),
|
||||
contents: cast_slice(&[instance_data]),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
})
|
||||
size: std::mem::size_of::<InstanceRaw>() as u64,
|
||||
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
crate::render::with_queue(|queue| {
|
||||
queue.write_buffer(&buffer, 0, cast_slice(&[instance_data]));
|
||||
});
|
||||
|
||||
buffer
|
||||
});
|
||||
|
||||
(Some(buffer), 1)
|
||||
|
||||
20
src/systems/rotate.rs
Normal file
20
src/systems/rotate.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use glam::Quat;
|
||||
|
||||
use crate::world::World;
|
||||
|
||||
pub fn rotate_system(world: &mut World, delta: f32)
|
||||
{
|
||||
let entities = world.rotates.all();
|
||||
|
||||
for entity in entities
|
||||
{
|
||||
if let Some(rotate) = world.rotates.get(entity)
|
||||
{
|
||||
let rotation_delta = Quat::from_axis_angle(rotate.axis, rotate.speed * delta);
|
||||
|
||||
world.transforms.with_mut(entity, |transform| {
|
||||
transform.rotation = rotation_delta * transform.rotation;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/systems/spotlight_sync.rs
Normal file
32
src/systems/spotlight_sync.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use crate::render::Spotlight;
|
||||
use crate::world::World;
|
||||
|
||||
pub fn spotlight_sync_system(world: &World) -> Vec<Spotlight>
|
||||
{
|
||||
let mut entities = world.spotlights.all();
|
||||
entities.sort();
|
||||
|
||||
let mut spotlights = Vec::new();
|
||||
|
||||
for entity in entities
|
||||
{
|
||||
if let Some(spotlight_component) = world.spotlights.get(entity)
|
||||
{
|
||||
if let Some(transform) = world.transforms.get(entity)
|
||||
{
|
||||
let position = transform.position + spotlight_component.offset;
|
||||
let direction = transform.rotation * spotlight_component.direction;
|
||||
|
||||
spotlights.push(Spotlight::new(
|
||||
position,
|
||||
direction,
|
||||
spotlight_component.inner_angle,
|
||||
spotlight_component.outer_angle,
|
||||
spotlight_component.range,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spotlights
|
||||
}
|
||||
112
src/systems/tree_dissolve.rs
Normal file
112
src/systems/tree_dissolve.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use crate::components::DissolveComponent;
|
||||
use crate::world::World;
|
||||
use glam::Vec3;
|
||||
|
||||
pub fn tree_dissolve_update_system(world: &mut World, delta: f32)
|
||||
{
|
||||
for entity in world.dissolves.all()
|
||||
{
|
||||
if let Some(dissolve) = world.dissolves.get_mut(entity)
|
||||
{
|
||||
let diff = dissolve.target_amount - dissolve.amount;
|
||||
dissolve.amount += diff * dissolve.transition_speed * delta;
|
||||
dissolve.amount = dissolve.amount.clamp(0.0, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tree_occlusion_system(world: &mut World)
|
||||
{
|
||||
let player_entity = world.player_tags.all().first().copied();
|
||||
let player_pos = player_entity.and_then(|e| world.transforms.get(e).map(|t| t.position));
|
||||
|
||||
if let Some(player_pos) = player_pos
|
||||
{
|
||||
let camera_entity = world.cameras.get_active().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;
|
||||
let distance_to_player = to_player.length();
|
||||
|
||||
if distance_to_player < 0.01
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let to_player_normalized = to_player.normalize();
|
||||
|
||||
for tree_entity in world.tree_tags.all()
|
||||
{
|
||||
if let Some(tree_transform) = world.transforms.get(tree_entity)
|
||||
{
|
||||
let tree_pos = tree_transform.position;
|
||||
let to_tree = tree_pos - camera_pos;
|
||||
let distance_to_tree = to_tree.length();
|
||||
|
||||
if distance_to_tree < distance_to_player
|
||||
{
|
||||
let projection = to_tree.dot(to_player_normalized);
|
||||
|
||||
if projection > 0.0
|
||||
{
|
||||
let perpendicular_vec = to_tree - to_player_normalized * projection;
|
||||
let perp_distance = perpendicular_vec.length();
|
||||
|
||||
let occlusion_radius = 2.5;
|
||||
|
||||
if perp_distance < occlusion_radius
|
||||
{
|
||||
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;
|
||||
}
|
||||
else
|
||||
{
|
||||
let mut dissolve = DissolveComponent::new();
|
||||
dissolve.target_amount = dissolve_amount;
|
||||
world.dissolves.insert(tree_entity, dissolve);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(dissolve) = world.dissolves.get_mut(tree_entity)
|
||||
{
|
||||
dissolve.target_amount = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
189
src/terrain.rs
189
src/terrain.rs
@@ -7,11 +7,12 @@ use rapier3d::{
|
||||
math::Isometry,
|
||||
prelude::{ColliderBuilder, RigidBodyBuilder},
|
||||
};
|
||||
use wesl::Wesl;
|
||||
|
||||
use crate::{
|
||||
components::{MeshComponent, PhysicsComponent},
|
||||
entity::EntityHandle,
|
||||
mesh::{InstanceRaw, Mesh, Vertex},
|
||||
mesh::{InstanceData, InstanceRaw, Mesh, Vertex},
|
||||
physics::PhysicsManager,
|
||||
render,
|
||||
world::{Transform, World},
|
||||
@@ -39,7 +40,7 @@ impl TerrainConfig
|
||||
{
|
||||
Self {
|
||||
gltf_path: "meshes/terrain.gltf".to_string(),
|
||||
heightmap_path: "textures/terrain.exr".to_string(),
|
||||
heightmap_path: "textures/terrain_heightmap.exr".to_string(),
|
||||
size: Vec2::new(1000.0, 1000.0),
|
||||
}
|
||||
}
|
||||
@@ -49,111 +50,102 @@ pub struct Terrain;
|
||||
|
||||
impl Terrain
|
||||
{
|
||||
pub fn spawn(world: &mut World, config: &TerrainConfig) -> anyhow::Result<EntityHandle>
|
||||
pub fn spawn(
|
||||
world: &mut World,
|
||||
mesh_data: Vec<(Mesh, Vec<InstanceData>)>,
|
||||
config: &TerrainConfig,
|
||||
) -> anyhow::Result<EntityHandle>
|
||||
{
|
||||
let gltf_data = render::with_device(|device| {
|
||||
Mesh::load_gltf_with_instances(device, &config.gltf_path)
|
||||
})?;
|
||||
|
||||
let terrain_entity = world.spawn();
|
||||
let transform = Transform::IDENTITY;
|
||||
let mut first_entity = None;
|
||||
let mut physics_added = false;
|
||||
|
||||
let mut terrain_mesh = None;
|
||||
let mut tree_mesh = None;
|
||||
let mut tree_instances = None;
|
||||
|
||||
for (mesh, instances) in gltf_data
|
||||
for (mesh, instances) in mesh_data
|
||||
{
|
||||
let entity = world.spawn();
|
||||
|
||||
if first_entity.is_none()
|
||||
{
|
||||
first_entity = Some(entity);
|
||||
}
|
||||
|
||||
if instances.is_empty()
|
||||
{
|
||||
if terrain_mesh.is_none()
|
||||
world.transforms.insert(entity, transform);
|
||||
world.meshes.insert(
|
||||
entity,
|
||||
MeshComponent {
|
||||
mesh: Rc::new(mesh),
|
||||
pipeline: render::Pipeline::Terrain,
|
||||
instance_buffer: None,
|
||||
num_instances: 1,
|
||||
},
|
||||
);
|
||||
|
||||
if !physics_added
|
||||
{
|
||||
terrain_mesh = Some(mesh);
|
||||
let heights = Self::load_heightfield_from_exr(&config.heightmap_path)?;
|
||||
|
||||
let height_scale = 1.0;
|
||||
let scale = vector![config.size.x, height_scale, config.size.y];
|
||||
|
||||
let body = RigidBodyBuilder::fixed()
|
||||
.translation(transform.get_position().into())
|
||||
.build();
|
||||
|
||||
let rigidbody_handle = PhysicsManager::add_rigidbody(body);
|
||||
|
||||
let collider = ColliderBuilder::heightfield(heights.clone(), scale).build();
|
||||
|
||||
let collider_handle =
|
||||
PhysicsManager::add_collider(collider, Some(rigidbody_handle));
|
||||
|
||||
PhysicsManager::set_heightfield_data(
|
||||
heights,
|
||||
scale,
|
||||
transform.get_position().into(),
|
||||
);
|
||||
|
||||
world.physics.insert(
|
||||
entity,
|
||||
PhysicsComponent {
|
||||
rigidbody: rigidbody_handle,
|
||||
collider: Some(collider_handle),
|
||||
},
|
||||
);
|
||||
|
||||
physics_added = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
tree_mesh = Some(mesh);
|
||||
tree_instances = Some(instances);
|
||||
let num_instances = instances.len();
|
||||
|
||||
let instance_raw: Vec<InstanceRaw> = instances.iter().map(|i| i.to_raw()).collect();
|
||||
|
||||
let instance_buffer = render::with_device(|device| {
|
||||
use wgpu::util::DeviceExt;
|
||||
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Tree Instance Buffer"),
|
||||
contents: bytemuck::cast_slice(&instance_raw),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
})
|
||||
});
|
||||
|
||||
world.transforms.insert(entity, Transform::IDENTITY);
|
||||
world.meshes.insert(
|
||||
entity,
|
||||
MeshComponent {
|
||||
mesh: Rc::new(mesh),
|
||||
pipeline: render::Pipeline::Environment,
|
||||
instance_buffer: Some(instance_buffer),
|
||||
num_instances: num_instances as u32,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(terrain_mesh) = terrain_mesh
|
||||
{
|
||||
world.transforms.insert(terrain_entity, transform);
|
||||
world.meshes.insert(
|
||||
terrain_entity,
|
||||
MeshComponent {
|
||||
mesh: Rc::new(terrain_mesh),
|
||||
pipeline: render::Pipeline::Terrain,
|
||||
instance_buffer: None,
|
||||
num_instances: 1,
|
||||
},
|
||||
);
|
||||
|
||||
let heights = Self::load_heightfield_from_exr(&config.heightmap_path)?;
|
||||
|
||||
println!(
|
||||
"Loaded terrain: {} rows × {} cols heightfield from EXR",
|
||||
heights.nrows(),
|
||||
heights.ncols()
|
||||
);
|
||||
|
||||
let height_scale = 1.0;
|
||||
let scale = vector![config.size.x, height_scale, config.size.y];
|
||||
|
||||
let body = RigidBodyBuilder::fixed()
|
||||
.translation(transform.get_position().into())
|
||||
.build();
|
||||
|
||||
let rigidbody_handle = PhysicsManager::add_rigidbody(body);
|
||||
|
||||
let collider = ColliderBuilder::heightfield(heights.clone(), scale).build();
|
||||
|
||||
let collider_handle = PhysicsManager::add_collider(collider, Some(rigidbody_handle));
|
||||
|
||||
PhysicsManager::set_heightfield_data(heights, scale, transform.get_position().into());
|
||||
|
||||
world.physics.insert(
|
||||
terrain_entity,
|
||||
PhysicsComponent {
|
||||
rigidbody: rigidbody_handle,
|
||||
collider: Some(collider_handle),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if let (Some(tree_mesh), Some(instances)) = (tree_mesh, tree_instances)
|
||||
{
|
||||
let num_instances = instances.len();
|
||||
println!("Loaded {} tree instances", num_instances);
|
||||
|
||||
let tree_entity = world.spawn();
|
||||
|
||||
let instance_raw: Vec<InstanceRaw> = instances.iter().map(|i| i.to_raw()).collect();
|
||||
|
||||
let instance_buffer = render::with_device(|device| {
|
||||
use wgpu::util::DeviceExt;
|
||||
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Tree Instance Buffer"),
|
||||
contents: bytemuck::cast_slice(&instance_raw),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
})
|
||||
});
|
||||
|
||||
world.transforms.insert(tree_entity, Transform::IDENTITY);
|
||||
world.meshes.insert(
|
||||
tree_entity,
|
||||
MeshComponent {
|
||||
mesh: Rc::new(tree_mesh),
|
||||
pipeline: render::Pipeline::Render,
|
||||
instance_buffer: Some(instance_buffer),
|
||||
num_instances: num_instances as u32,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(terrain_entity)
|
||||
first_entity.ok_or_else(|| anyhow::anyhow!("No meshes found in glTF file"))
|
||||
}
|
||||
|
||||
fn load_heightfield_from_exr(path: &str) -> anyhow::Result<DMatrix<f32>>
|
||||
@@ -184,11 +176,12 @@ pub fn create_terrain_render_pipeline(
|
||||
bind_group_layout: &wgpu::BindGroupLayout,
|
||||
) -> wgpu::RenderPipeline
|
||||
{
|
||||
let shared_source =
|
||||
std::fs::read_to_string("shaders/shared.wgsl").expect("Failed to read shared shader");
|
||||
let terrain_source =
|
||||
std::fs::read_to_string("shaders/terrain.wgsl").expect("Failed to read terrain shader");
|
||||
let shader_source = format!("{}\n{}", shared_source, terrain_source);
|
||||
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"),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use exr::prelude::{ReadChannels, ReadLayers};
|
||||
use half::f16;
|
||||
|
||||
pub struct DitherTextures
|
||||
{
|
||||
@@ -16,6 +15,13 @@ pub struct FlowmapTexture
|
||||
pub sampler: wgpu::Sampler,
|
||||
}
|
||||
|
||||
pub struct HeightmapTexture
|
||||
{
|
||||
pub texture: wgpu::Texture,
|
||||
pub view: wgpu::TextureView,
|
||||
pub sampler: wgpu::Sampler,
|
||||
}
|
||||
|
||||
impl DitherTextures
|
||||
{
|
||||
pub fn load_octaves(device: &wgpu::Device, queue: &wgpu::Queue) -> Result<Self>
|
||||
@@ -186,16 +192,11 @@ impl FlowmapTexture
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba16Float,
|
||||
format: wgpu::TextureFormat::Rgba32Float,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
let rgba_data_f16: Vec<u16> = rgba_data
|
||||
.iter()
|
||||
.map(|&f| f16::from_f32(f).to_bits())
|
||||
.collect();
|
||||
|
||||
queue.write_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &texture,
|
||||
@@ -203,10 +204,10 @@ impl FlowmapTexture
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
bytemuck::cast_slice(&rgba_data_f16),
|
||||
bytemuck::cast_slice(&rgba_data),
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(8 * width as u32),
|
||||
bytes_per_row: Some(16 * width as u32),
|
||||
rows_per_image: Some(height as u32),
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
@@ -223,8 +224,8 @@ impl FlowmapTexture
|
||||
address_mode_u: wgpu::AddressMode::Repeat,
|
||||
address_mode_v: wgpu::AddressMode::Repeat,
|
||||
address_mode_w: wgpu::AddressMode::Repeat,
|
||||
mag_filter: wgpu::FilterMode::Linear,
|
||||
min_filter: wgpu::FilterMode::Linear,
|
||||
mag_filter: wgpu::FilterMode::Nearest,
|
||||
min_filter: wgpu::FilterMode::Nearest,
|
||||
mipmap_filter: wgpu::FilterMode::Nearest,
|
||||
..Default::default()
|
||||
});
|
||||
@@ -236,3 +237,76 @@ impl FlowmapTexture
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HeightmapTexture
|
||||
{
|
||||
pub fn load(device: &wgpu::Device, queue: &wgpu::Queue, path: &str) -> Result<Self>
|
||||
{
|
||||
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 width = layer.size.width();
|
||||
let height = layer.size.height();
|
||||
|
||||
let channel = &layer.channel_data.list[0];
|
||||
let height_data: Vec<f32> = channel.sample_data.values_as_f32().collect();
|
||||
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("Heightmap Texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: width as u32,
|
||||
height: height as u32,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::R32Float,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
queue.write_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
bytemuck::cast_slice(&height_data),
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(4 * width as u32),
|
||||
rows_per_image: Some(height as u32),
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width: width as u32,
|
||||
height: height as u32,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
|
||||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("Heightmap 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()
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
texture,
|
||||
view,
|
||||
sampler,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,16 @@ impl Transform
|
||||
Self::IDENTITY.translated(position)
|
||||
}
|
||||
|
||||
pub fn from_matrix(matrix: Mat4) -> Self
|
||||
{
|
||||
let (scale, rotation, position) = matrix.to_scale_rotation_translation();
|
||||
Self {
|
||||
position,
|
||||
rotation,
|
||||
scale,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_matrix(&self) -> Mat4
|
||||
{
|
||||
Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position)
|
||||
|
||||
178
src/world.rs
178
src/world.rs
@@ -1,9 +1,12 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::components::dissolve::DissolveComponent;
|
||||
use crate::components::follow::FollowComponent;
|
||||
use crate::components::jump::JumpComponent;
|
||||
use crate::components::lights::spot::SpotlightComponent;
|
||||
use crate::components::{
|
||||
CameraComponent, CameraFollowComponent, InputComponent, MeshComponent, MovementComponent,
|
||||
PhysicsComponent,
|
||||
CameraComponent, InputComponent, MeshComponent, MovementComponent, PhysicsComponent,
|
||||
RotateComponent,
|
||||
};
|
||||
use crate::entity::{EntityHandle, EntityManager};
|
||||
use crate::state::StateMachine;
|
||||
@@ -415,12 +418,12 @@ impl CameraStorage
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CameraFollowStorage
|
||||
pub struct SpotlightStorage
|
||||
{
|
||||
pub components: HashMap<EntityHandle, CameraFollowComponent>,
|
||||
pub components: HashMap<EntityHandle, SpotlightComponent>,
|
||||
}
|
||||
|
||||
impl CameraFollowStorage
|
||||
impl SpotlightStorage
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
@@ -429,26 +432,159 @@ impl CameraFollowStorage
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, entity: EntityHandle, component: CameraFollowComponent)
|
||||
pub fn insert(&mut self, entity: EntityHandle, component: SpotlightComponent)
|
||||
{
|
||||
self.components.insert(entity, component);
|
||||
}
|
||||
|
||||
pub fn get(&self, entity: EntityHandle) -> Option<&CameraFollowComponent>
|
||||
pub fn get(&self, entity: EntityHandle) -> Option<&SpotlightComponent>
|
||||
{
|
||||
self.components.get(&entity)
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, entity: EntityHandle) -> Option<&mut CameraFollowComponent>
|
||||
pub fn get_mut(&mut self, entity: EntityHandle) -> Option<&mut SpotlightComponent>
|
||||
{
|
||||
self.components.get_mut(&entity)
|
||||
}
|
||||
|
||||
pub fn with_mut<F, R>(&mut self, entity: EntityHandle, f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&mut CameraFollowComponent) -> R,
|
||||
pub fn remove(&mut self, entity: EntityHandle)
|
||||
{
|
||||
self.components.get_mut(&entity).map(f)
|
||||
self.components.remove(&entity);
|
||||
}
|
||||
|
||||
pub fn all(&self) -> Vec<EntityHandle>
|
||||
{
|
||||
self.components.keys().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TreeTagStorage
|
||||
{
|
||||
pub components: HashMap<EntityHandle, ()>,
|
||||
}
|
||||
|
||||
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<EntityHandle>
|
||||
{
|
||||
self.components.keys().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DissolveStorage
|
||||
{
|
||||
pub components: HashMap<EntityHandle, DissolveComponent>,
|
||||
}
|
||||
|
||||
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<EntityHandle>
|
||||
{
|
||||
self.components.keys().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FollowStorage
|
||||
{
|
||||
pub components: HashMap<EntityHandle, FollowComponent>,
|
||||
}
|
||||
|
||||
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<EntityHandle>
|
||||
{
|
||||
self.components.keys().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RotateStorage
|
||||
{
|
||||
pub components: HashMap<EntityHandle, RotateComponent>,
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -474,7 +610,11 @@ pub struct World
|
||||
pub player_tags: PlayerTagStorage,
|
||||
pub state_machines: StateMachineStorage,
|
||||
pub cameras: CameraStorage,
|
||||
pub camera_follows: CameraFollowStorage,
|
||||
pub spotlights: SpotlightStorage,
|
||||
pub tree_tags: TreeTagStorage,
|
||||
pub dissolves: DissolveStorage,
|
||||
pub follows: FollowStorage,
|
||||
pub rotates: RotateStorage,
|
||||
}
|
||||
|
||||
impl World
|
||||
@@ -492,7 +632,11 @@ impl World
|
||||
player_tags: PlayerTagStorage::new(),
|
||||
state_machines: StateMachineStorage::new(),
|
||||
cameras: CameraStorage::new(),
|
||||
camera_follows: CameraFollowStorage::new(),
|
||||
spotlights: SpotlightStorage::new(),
|
||||
tree_tags: TreeTagStorage::new(),
|
||||
dissolves: DissolveStorage::new(),
|
||||
follows: FollowStorage::new(),
|
||||
rotates: RotateStorage::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,7 +656,11 @@ impl World
|
||||
self.player_tags.remove(entity);
|
||||
self.state_machines.remove(entity);
|
||||
self.cameras.remove(entity);
|
||||
self.camera_follows.remove(entity);
|
||||
self.spotlights.remove(entity);
|
||||
self.tree_tags.remove(entity);
|
||||
self.dissolves.remove(entity);
|
||||
self.follows.remove(entity);
|
||||
self.rotates.remove(entity);
|
||||
self.entities.despawn(entity);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user