Compare commits

..

5 Commits

Author SHA1 Message Date
Jonas H
c37a9fd5dd tree dissolve + instances 2026-03-05 15:06:29 +01:00
Jonas H
350fddc2af trigger system 2026-03-05 15:05:49 +01:00
Jonas H
bab54b6f21 picking + entity names 2026-03-05 15:05:19 +01:00
Jonas H
5e5c6a47e7 dynamic uniform offsets 2026-03-05 15:03:56 +01:00
Jonas H
28f8c65571 flatten component configs 2026-03-05 15:03:55 +01:00
27 changed files with 883 additions and 263 deletions

View File

@@ -20,6 +20,7 @@ impl Bundle for CameraBundle
let transform = Transform::from_position(self.position); let transform = Transform::from_position(self.position);
world.cameras.insert(camera_entity, camera_component); world.cameras.insert(camera_entity, camera_component);
world.transforms.insert(camera_entity, transform); world.transforms.insert(camera_entity, transform);
world.names.insert(camera_entity, "Camera".to_string());
Ok(camera_entity) Ok(camera_entity)
} }
} }

View File

@@ -6,9 +6,10 @@ use rapier3d::control::{CharacterAutostep, KinematicCharacterController};
use rapier3d::prelude::{ColliderBuilder, RigidBodyBuilder}; use rapier3d::prelude::{ColliderBuilder, RigidBodyBuilder};
use crate::bundles::Bundle; use crate::bundles::Bundle;
use crate::components::jump::JumpComponent;
use crate::components::lights::spot::SpotlightComponent; use crate::components::lights::spot::SpotlightComponent;
use crate::components::{InputComponent, MeshComponent, MovementComponent, PhysicsComponent}; use crate::components::{
InputComponent, JumpComponent, MeshComponent, MovementComponent, PhysicsComponent,
};
use crate::entity::EntityHandle; use crate::entity::EntityHandle;
use crate::loaders::mesh::Mesh; use crate::loaders::mesh::Mesh;
use crate::physics::PhysicsManager; use crate::physics::PhysicsManager;
@@ -70,7 +71,7 @@ impl Bundle for PlayerBundle
state_machine.add_transition::<PlayerFallingState, PlayerIdleState>(move |world| { state_machine.add_transition::<PlayerFallingState, PlayerIdleState>(move |world| {
let is_grounded = world let is_grounded = world
.movements .movements
.with(entity_id, |m| m.movement_config.movement_context.is_floored) .with(entity_id, |m| m.movement_context.is_floored)
.unwrap_or(false); .unwrap_or(false);
let has_input = world let has_input = world
.inputs .inputs
@@ -82,7 +83,7 @@ impl Bundle for PlayerBundle
state_machine.add_transition::<PlayerFallingState, PlayerWalkingState>(move |world| { state_machine.add_transition::<PlayerFallingState, PlayerWalkingState>(move |world| {
let is_grounded = world let is_grounded = world
.movements .movements
.with(entity_id, |m| m.movement_config.movement_context.is_floored) .with(entity_id, |m| m.movement_context.is_floored)
.unwrap_or(false); .unwrap_or(false);
let has_input = world let has_input = world
.inputs .inputs
@@ -94,7 +95,7 @@ impl Bundle for PlayerBundle
state_machine.add_transition::<PlayerIdleState, PlayerWalkingState>(move |world| { state_machine.add_transition::<PlayerIdleState, PlayerWalkingState>(move |world| {
let is_grounded = world let is_grounded = world
.movements .movements
.with(entity_id, |m| m.movement_config.movement_context.is_floored) .with(entity_id, |m| m.movement_context.is_floored)
.unwrap_or(false); .unwrap_or(false);
let has_input = world let has_input = world
.inputs .inputs
@@ -106,7 +107,7 @@ impl Bundle for PlayerBundle
state_machine.add_transition::<PlayerWalkingState, PlayerIdleState>(move |world| { state_machine.add_transition::<PlayerWalkingState, PlayerIdleState>(move |world| {
let is_grounded = world let is_grounded = world
.movements .movements
.with(entity_id, |m| m.movement_config.movement_context.is_floored) .with(entity_id, |m| m.movement_context.is_floored)
.unwrap_or(false); .unwrap_or(false);
let has_input = world let has_input = world
.inputs .inputs
@@ -118,7 +119,7 @@ impl Bundle for PlayerBundle
state_machine.add_transition::<PlayerIdleState, PlayerFallingState>(move |world| { state_machine.add_transition::<PlayerIdleState, PlayerFallingState>(move |world| {
let is_grounded = world let is_grounded = world
.movements .movements
.with(entity_id, |m| m.movement_config.movement_context.is_floored) .with(entity_id, |m| m.movement_context.is_floored)
.unwrap_or(false); .unwrap_or(false);
!is_grounded !is_grounded
}); });
@@ -126,7 +127,7 @@ impl Bundle for PlayerBundle
state_machine.add_transition::<PlayerWalkingState, PlayerFallingState>(move |world| { state_machine.add_transition::<PlayerWalkingState, PlayerFallingState>(move |world| {
let is_grounded = world let is_grounded = world
.movements .movements
.with(entity_id, |m| m.movement_config.movement_context.is_floored) .with(entity_id, |m| m.movement_context.is_floored)
.unwrap_or(false); .unwrap_or(false);
!is_grounded !is_grounded
}); });
@@ -134,7 +135,7 @@ impl Bundle for PlayerBundle
state_machine.add_transition::<PlayerIdleState, PlayerJumpingState>(move |world| { state_machine.add_transition::<PlayerIdleState, PlayerJumpingState>(move |world| {
let is_grounded = world let is_grounded = world
.movements .movements
.with(entity_id, |m| m.movement_config.movement_context.is_floored) .with(entity_id, |m| m.movement_context.is_floored)
.unwrap_or(false); .unwrap_or(false);
let jump_pressed = world let jump_pressed = world
.inputs .inputs
@@ -146,7 +147,7 @@ impl Bundle for PlayerBundle
state_machine.add_transition::<PlayerWalkingState, PlayerJumpingState>(move |world| { state_machine.add_transition::<PlayerWalkingState, PlayerJumpingState>(move |world| {
let is_grounded = world let is_grounded = world
.movements .movements
.with(entity_id, |m| m.movement_config.movement_context.is_floored) .with(entity_id, |m| m.movement_context.is_floored)
.unwrap_or(false); .unwrap_or(false);
let jump_pressed = world let jump_pressed = world
.inputs .inputs
@@ -159,14 +160,14 @@ impl Bundle for PlayerBundle
world world
.jumps .jumps
.with(entity_id, |jump| { .with(entity_id, |jump| {
jump.jump_config.jump_context.duration >= jump.jump_config.jump_duration jump.jump_context.duration >= jump.jump_duration
}) })
.unwrap_or(true) .unwrap_or(true)
}); });
world.transforms.insert(entity, spawn_transform); world.transforms.insert(entity, spawn_transform);
world.movements.insert(entity, MovementComponent::new()); world.movements.insert(entity, MovementComponent::new());
world.jumps.insert(entity, JumpComponent::new()); world.jumps.insert(entity, JumpComponent::default());
world.inputs.insert(entity, InputComponent::default()); world.inputs.insert(entity, InputComponent::default());
world.physics.insert( world.physics.insert(
entity, entity,
@@ -189,6 +190,7 @@ impl Bundle for PlayerBundle
); );
world.player_tags.insert(entity, ()); world.player_tags.insert(entity, ());
world.state_machines.insert(entity, state_machine); world.state_machines.insert(entity, state_machine);
world.names.insert(entity, "Player".to_string());
let outer_angle = PI / 2.0 * 0.9; let outer_angle = PI / 2.0 * 0.9;
world.spotlights.insert( world.spotlights.insert(

View File

@@ -19,6 +19,7 @@ impl Bundle for SpotlightBundle
let transform = Transform::from_matrix(self.light_data.transform); let transform = Transform::from_matrix(self.light_data.transform);
world.transforms.insert(entity, transform); world.transforms.insert(entity, transform);
world.spotlights.insert(entity, self.light_data.component); world.spotlights.insert(entity, self.light_data.component);
world.names.insert(entity, "Spotlight".to_string());
if let Some(tag) = self.light_data.tag if let Some(tag) = self.light_data.tag
{ {
if tag == "lighthouse" if tag == "lighthouse"
@@ -34,12 +35,13 @@ impl Bundle for SpotlightBundle
pub fn spawn_spotlights(world: &mut World, spotlights: Vec<LightData>) pub fn spawn_spotlights(world: &mut World, spotlights: Vec<LightData>)
{ {
for light_data in spotlights for (index, light_data) in spotlights.into_iter().enumerate()
{ {
let entity = world.spawn(); let entity = world.spawn();
let transform = Transform::from_matrix(light_data.transform); let transform = Transform::from_matrix(light_data.transform);
world.transforms.insert(entity, transform); world.transforms.insert(entity, transform);
world.spotlights.insert(entity, light_data.component); world.spotlights.insert(entity, light_data.component);
world.names.insert(entity, format!("Spotlight_{}", index));
if let Some(tag) = light_data.tag if let Some(tag) = light_data.tag
{ {
if tag == "lighthouse" if tag == "lighthouse"

View File

@@ -4,7 +4,7 @@ use glam::Vec2;
use nalgebra::vector; use nalgebra::vector;
use rapier3d::prelude::{ColliderBuilder, RigidBodyBuilder}; use rapier3d::prelude::{ColliderBuilder, RigidBodyBuilder};
use crate::components::{MeshComponent, PhysicsComponent}; use crate::components::{MeshComponent, PhysicsComponent, TreeInstancesComponent};
use crate::entity::EntityHandle; use crate::entity::EntityHandle;
use crate::loaders::mesh::{InstanceData, InstanceRaw, Mesh}; use crate::loaders::mesh::{InstanceData, InstanceRaw, Mesh};
use crate::loaders::terrain::load_heightfield_from_exr; use crate::loaders::terrain::load_heightfield_from_exr;
@@ -78,6 +78,7 @@ impl TerrainBundle
enable_snow_light: true, enable_snow_light: true,
}, },
); );
world.names.insert(entity, "Terrain".to_string());
if !physics_added if !physics_added
{ {
@@ -125,7 +126,7 @@ impl TerrainBundle
device.create_buffer_init(&wgpu::util::BufferInitDescriptor { device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Tree Instance Buffer"), label: Some("Tree Instance Buffer"),
contents: bytemuck::cast_slice(&instance_raw), contents: bytemuck::cast_slice(&instance_raw),
usage: wgpu::BufferUsages::VERTEX, usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
}) })
}); });
@@ -135,13 +136,18 @@ impl TerrainBundle
MeshComponent { MeshComponent {
mesh: Rc::new(mesh), mesh: Rc::new(mesh),
pipeline: render::Pipeline::Standard, pipeline: render::Pipeline::Standard,
instance_buffer: Some(instance_buffer), instance_buffer: Some(instance_buffer.clone()),
num_instances: num_instances as u32, num_instances: num_instances as u32,
tile_scale: 4.0, tile_scale: 4.0,
enable_dissolve: true, enable_dissolve: true,
enable_snow_light: false, enable_snow_light: false,
}, },
); );
world.tree_instances.insert(
entity,
TreeInstancesComponent::new(instances, Rc::new(instance_buffer)),
);
world.names.insert(entity, "Trees".to_string());
} }
} }

View File

@@ -3,22 +3,6 @@ use kurbo::CubicBez;
#[derive(Clone)] #[derive(Clone)]
pub struct JumpComponent pub struct JumpComponent
{
pub jump_config: JumpConfig,
}
impl JumpComponent
{
pub fn new() -> Self
{
Self {
jump_config: JumpConfig::default(),
}
}
}
#[derive(Clone, Copy)]
pub struct JumpConfig
{ {
pub jump_height: f32, pub jump_height: f32,
pub jump_duration: f32, pub jump_duration: f32,
@@ -30,7 +14,7 @@ pub struct JumpConfig
pub jump_context: JumpContext, pub jump_context: JumpContext,
} }
impl Default for JumpConfig impl Default for JumpComponent
{ {
fn default() -> Self fn default() -> Self
{ {
@@ -56,22 +40,3 @@ pub struct JumpContext
pub origin_height: f32, pub origin_height: f32,
pub normal: Vec3, pub normal: Vec3,
} }
impl JumpContext
{
fn start(time: f32, current_height: f32, surface_normal: Vec3) -> Self
{
Self {
in_progress: false,
duration: 0.0,
execution_time: time,
origin_height: current_height,
normal: surface_normal,
}
}
pub fn stop(&mut self)
{
self.in_progress = false;
}
}

View File

@@ -9,12 +9,17 @@ pub mod movement;
pub mod noclip; pub mod noclip;
pub mod physics; pub mod physics;
pub mod rotate; pub mod rotate;
pub mod tree_instances;
pub mod trigger;
pub use camera::CameraComponent; pub use camera::CameraComponent;
pub use dissolve::DissolveComponent; pub use dissolve::DissolveComponent;
pub use follow::FollowComponent; pub use follow::FollowComponent;
pub use input::InputComponent; pub use input::InputComponent;
pub use jump::JumpComponent;
pub use mesh::MeshComponent; pub use mesh::MeshComponent;
pub use movement::MovementComponent; pub use movement::MovementComponent;
pub use physics::PhysicsComponent; pub use physics::PhysicsComponent;
pub use rotate::RotateComponent; pub use rotate::RotateComponent;
pub use tree_instances::TreeInstancesComponent;
pub use trigger::{TriggerComponent, TriggerEvent, TriggerEventKind, TriggerFilter, TriggerShape, TriggerState};

View File

@@ -3,22 +3,6 @@ use kurbo::CubicBez;
#[derive(Clone)] #[derive(Clone)]
pub struct MovementComponent pub struct MovementComponent
{
pub movement_config: MovementConfig,
}
impl MovementComponent
{
pub fn new() -> Self
{
Self {
movement_config: MovementConfig::new(),
}
}
}
#[derive(Clone)]
pub struct MovementConfig
{ {
pub walking_acceleration: f32, pub walking_acceleration: f32,
pub walking_acceleration_duration: f32, pub walking_acceleration_duration: f32,
@@ -29,7 +13,7 @@ pub struct MovementConfig
pub movement_context: MovementContext, pub movement_context: MovementContext,
} }
impl MovementConfig impl MovementComponent
{ {
pub fn new() -> Self pub fn new() -> Self
{ {

View File

@@ -0,0 +1,27 @@
use crate::loaders::mesh::InstanceData;
use std::rc::Rc;
use wgpu::Buffer;
pub struct TreeInstancesComponent
{
pub instances: Vec<InstanceData>,
pub dissolve_amounts: Vec<f32>,
pub dissolve_targets: Vec<f32>,
pub transition_speed: f32,
pub instance_buffer: Rc<Buffer>,
}
impl TreeInstancesComponent
{
pub fn new(instances: Vec<InstanceData>, instance_buffer: Rc<Buffer>) -> Self
{
let num_instances = instances.len();
Self {
instances,
dissolve_amounts: vec![0.0; num_instances],
dissolve_targets: vec![0.0; num_instances],
transition_speed: 5.0,
instance_buffer,
}
}
}

37
src/components/trigger.rs Normal file
View File

@@ -0,0 +1,37 @@
use crate::entity::EntityHandle;
pub enum TriggerShape
{
Sphere { radius: f32 },
}
pub enum TriggerFilter
{
Player,
}
pub enum TriggerState
{
Idle,
Inside,
}
pub struct TriggerComponent
{
pub shape: TriggerShape,
pub filter: TriggerFilter,
pub state: TriggerState,
}
pub enum TriggerEventKind
{
Entered,
Exited,
}
pub struct TriggerEvent
{
pub trigger_entity: EntityHandle,
pub activator_entity: EntityHandle,
pub kind: TriggerEventKind,
}

View File

@@ -150,6 +150,7 @@ pub fn render_collider_debug() -> Vec<DrawCall>
enable_dissolve: false, enable_dissolve: false,
enable_snow_light: false, enable_snow_light: false,
displacement_bind_group: None, displacement_bind_group: None,
entity: None,
}); });
} }
}); });
@@ -183,6 +184,7 @@ pub fn render_collider_debug() -> Vec<DrawCall>
enable_dissolve: false, enable_dissolve: false,
enable_snow_light: false, enable_snow_light: false,
displacement_bind_group: None, displacement_bind_group: None,
entity: None,
}); });
} }
}); });

View File

@@ -1,7 +1,11 @@
use dear_imgui_rs::{Condition, Context}; use dear_imgui_rs::{Condition, Context};
use dear_imgui_wgpu::{WgpuInitInfo, WgpuRenderer}; use dear_imgui_wgpu::{WgpuInitInfo, WgpuRenderer};
use glam::EulerRot;
use sdl3_sys::events::SDL_Event; use sdl3_sys::events::SDL_Event;
use crate::entity::EntityHandle;
use crate::world::World;
pub struct FrameStats pub struct FrameStats
{ {
pub fps: f32, pub fps: f32,
@@ -57,7 +61,12 @@ impl Inspector
self.imgui.io().want_capture_mouse() self.imgui.io().want_capture_mouse()
} }
pub fn build_ui(&mut self, stats: &FrameStats) pub fn build_ui(
&mut self,
stats: &FrameStats,
world: &World,
selected_entity: Option<EntityHandle>,
)
{ {
let ui = self.imgui.frame(); let ui = self.imgui.frame();
ui.window("Inspector") ui.window("Inspector")
@@ -68,6 +77,113 @@ impl Inspector
ui.text(format!("Physics: {:.1} ms", stats.physics_budget_ms)); ui.text(format!("Physics: {:.1} ms", stats.physics_budget_ms));
ui.text(format!("Draw calls: {}", stats.draw_call_count)); ui.text(format!("Draw calls: {}", stats.draw_call_count));
}); });
if let Some(entity) = selected_entity
{
ui.window("Entity")
.position([10.0, 120.0], Condition::FirstUseEver)
.build(|| {
let name = world
.names
.get(entity)
.cloned()
.unwrap_or_else(|| format!("Entity #{}", entity));
ui.text(format!("Name: {}", name));
ui.text(format!("ID: {}", entity));
ui.separator();
ui.text("Transform");
if let Some(transform) = world.transforms.get(entity)
{
let p = transform.position;
ui.text(format!(" Pos ({:.2}, {:.2}, {:.2})", p.x, p.y, p.z));
let (ex, ey, ez) = transform.rotation.to_euler(EulerRot::XYZ);
ui.text(format!(
" Rot ({:.1}, {:.1}, {:.1}) deg",
ex.to_degrees(),
ey.to_degrees(),
ez.to_degrees()
));
let s = transform.scale;
ui.text(format!(" Scale ({:.2}, {:.2}, {:.2})", s.x, s.y, s.z));
}
else
{
ui.text(" (no transform)");
}
ui.separator();
ui.text("Components");
if world.meshes.get(entity).is_some()
{
ui.text(" Mesh");
}
if world.physics.get(entity).is_some()
{
ui.text(" Physics");
}
if let Some(m) = world.movements.get(entity)
{
ui.text(format!(
" Movement (max_speed {:.1})",
m.max_walking_speed
));
}
if world.jumps.get(entity).is_some()
{
ui.text(" Jump");
}
if world.inputs.get(entity).is_some()
{
ui.text(" Input");
}
if world.player_tags.get(entity).is_some()
{
ui.text(" [Player]");
}
if world.tree_tags.get(entity).is_some()
{
ui.text(" [Tree]");
}
if let Some(cam) = world.cameras.get(entity)
{
ui.text(format!(
" Camera fov={:.0} near={:.2} far={:.0}",
cam.fov.to_degrees(),
cam.near,
cam.far
));
}
if let Some(spot) = world.spotlights.get(entity)
{
let o = spot.offset;
ui.text(format!(
" Spotlight offset ({:.1}, {:.1}, {:.1})",
o.x, o.y, o.z
));
}
if let Some(ti) = world.tree_instances.get(entity)
{
ui.text(format!(" TreeInstances ({})", ti.instances.len()));
}
if world.follows.get(entity).is_some()
{
ui.text(" Follow");
}
if world.rotates.get(entity).is_some()
{
ui.text(" Rotate");
}
if let Some(d) = world.dissolves.get(entity)
{
ui.text(format!(" Dissolve ({:.2})", d.amount));
}
});
}
} }
pub fn render(&mut self, encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView) pub fn render(&mut self, encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView)

View File

@@ -78,5 +78,6 @@ pub fn editor_loop(
{ {
camera_noclip_system(world, input_state, delta); camera_noclip_system(world, input_state, delta);
} }
editor.inspector.build_ui(stats); let selected = editor.selected_entity;
editor.inspector.build_ui(stats, world, selected);
} }

View File

@@ -2,6 +2,26 @@ use bytemuck::{Pod, Zeroable};
use glam::{Mat4, Quat, Vec3}; use glam::{Mat4, Quat, Vec3};
use std::path::Path; use std::path::Path;
fn compute_aabb(vertices: &[Vertex]) -> (Vec3, Vec3)
{
let mut aabb_min = Vec3::splat(f32::MAX);
let mut aabb_max = Vec3::splat(f32::MIN);
for v in vertices
{
let p = Vec3::from(v.position);
aabb_min = aabb_min.min(p);
aabb_max = aabb_max.max(p);
}
if vertices.is_empty()
{
(Vec3::ZERO, Vec3::ZERO)
}
else
{
(aabb_min, aabb_max)
}
}
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)] #[derive(Clone, Copy, Pod, Zeroable)]
pub struct Vertex pub struct Vertex
@@ -113,6 +133,10 @@ pub struct Mesh
pub vertex_buffer: wgpu::Buffer, pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer, pub index_buffer: wgpu::Buffer,
pub num_indices: u32, pub num_indices: u32,
pub aabb_min: Vec3,
pub aabb_max: Vec3,
pub cpu_positions: Vec<[f32; 3]>,
pub cpu_indices: Vec<u32>,
} }
impl Mesh impl Mesh
@@ -121,6 +145,8 @@ impl Mesh
{ {
use wgpu::util::DeviceExt; use wgpu::util::DeviceExt;
let (aabb_min, aabb_max) = compute_aabb(vertices);
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Vertex Buffer"), label: Some("Vertex Buffer"),
contents: bytemuck::cast_slice(vertices), contents: bytemuck::cast_slice(vertices),
@@ -137,6 +163,10 @@ impl Mesh
vertex_buffer, vertex_buffer,
index_buffer, index_buffer,
num_indices: indices.len() as u32, num_indices: indices.len() as u32,
aabb_min,
aabb_max,
cpu_positions: vertices.iter().map(|v| v.position).collect(),
cpu_indices: indices.to_vec(),
} }
} }

View File

@@ -5,6 +5,7 @@ mod editor;
mod entity; mod entity;
mod loaders; mod loaders;
mod physics; mod physics;
mod picking;
mod postprocess; mod postprocess;
mod render; mod render;
mod snow; mod snow;
@@ -40,6 +41,8 @@ use crate::systems::{
camera_follow_system, camera_input_system, camera_view_matrix, physics_sync_system, camera_follow_system, camera_input_system, camera_view_matrix, physics_sync_system,
player_input_system, render_system, rotate_system, snow_system, spotlight_sync_system, player_input_system, render_system, rotate_system, snow_system, spotlight_sync_system,
start_camera_following, state_machine_physics_system, state_machine_system, start_camera_following, state_machine_physics_system, state_machine_system,
tree_dissolve_update_system, tree_instance_buffer_update_system, tree_occlusion_system,
trigger_system,
}; };
use crate::systems::camera::stop_camera_following; use crate::systems::camera::stop_camera_following;
use crate::utility::time::Time; use crate::utility::time::Time;
@@ -165,12 +168,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
editor.active = !editor.active; editor.active = !editor.active;
if editor.active if editor.active
{ {
stop_camera_following(&mut world, camera_entity);
sdl_context.mouse().set_relative_mouse_mode(&window, false); sdl_context.mouse().set_relative_mouse_mode(&window, false);
editor.right_mouse_held = false; editor.right_mouse_held = false;
input_state.mouse_captured = false; input_state.mouse_captured = false;
} }
else else
{ {
start_camera_following(&mut world, camera_entity);
input_state.mouse_captured = true; input_state.mouse_captured = true;
sdl_context.mouse().set_relative_mouse_mode(&window, true); sdl_context.mouse().set_relative_mouse_mode(&window, true);
} }
@@ -196,11 +201,38 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
{ {
editor.right_mouse_held = false; editor.right_mouse_held = false;
input_state.mouse_captured = false; input_state.mouse_captured = false;
start_camera_following(&mut world, camera_entity);
sdl_context.mouse().set_relative_mouse_mode(&window, false); sdl_context.mouse().set_relative_mouse_mode(&window, false);
continue; continue;
} }
Event::MouseButtonDown {
mouse_btn: MouseButton::Left,
x,
y,
..
} if editor.active && !editor.wants_mouse() =>
{
if let Some(view) = crate::systems::camera_view_matrix(&world)
{
if let Some((_, cam)) = world.active_camera()
{
let projection = cam.projection_matrix();
let (win_w, win_h) = window.size();
let ray = crate::picking::Ray::from_screen_position(
*x,
*y,
win_w,
win_h,
&view,
&projection,
);
editor.selected_entity = crate::picking::pick_entity(&ray, &world);
render::set_selected_entity(editor.selected_entity);
}
}
continue;
}
_ => _ =>
{} {}
} }
@@ -253,6 +285,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
PhysicsManager::physics_step(); PhysicsManager::physics_step();
physics_sync_system(&mut world); physics_sync_system(&mut world);
trigger_system(&mut world);
physics_accumulator -= FIXED_TIMESTEP; physics_accumulator -= FIXED_TIMESTEP;
} }
@@ -263,6 +296,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
rotate_system(&mut world, delta); rotate_system(&mut world, delta);
tree_occlusion_system(&mut world);
tree_dissolve_update_system(&mut world, delta);
tree_instance_buffer_update_system(&mut world);
let spotlights = spotlight_sync_system(&world); let spotlights = spotlight_sync_system(&world);
render::update_spotlights(spotlights); render::update_spotlights(spotlights);
@@ -303,8 +340,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
if editor.active if editor.active
{ {
let screen_view = let screen_view = frame
frame.texture.create_view(&wgpu::TextureViewDescriptor::default()); .texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = render::with_device(|d| { let mut encoder = render::with_device(|d| {
d.create_command_encoder(&wgpu::CommandEncoderDescriptor { d.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("ImGui Encoder"), label: Some("ImGui Encoder"),

View File

@@ -1,7 +1,10 @@
use crate::camera::Camera;
use crate::mesh::Mesh;
use glam::{Mat4, Vec3, Vec4}; use glam::{Mat4, Vec3, Vec4};
use crate::components::TreeInstancesComponent;
use crate::entity::EntityHandle;
use crate::loaders::mesh::Mesh;
use crate::world::World;
pub struct Ray pub struct Ray
{ {
pub origin: Vec3, pub origin: Vec3,
@@ -15,7 +18,8 @@ impl Ray
screen_y: f32, screen_y: f32,
screen_width: u32, screen_width: u32,
screen_height: u32, screen_height: u32,
camera: &Camera, view: &Mat4,
projection: &Mat4,
) -> Self ) -> Self
{ {
let ndc_x = (2.0 * screen_x) / screen_width as f32 - 1.0; let ndc_x = (2.0 * screen_x) / screen_width as f32 - 1.0;
@@ -23,10 +27,8 @@ impl Ray
let clip_coords = Vec4::new(ndc_x, ndc_y, -1.0, 1.0); let clip_coords = Vec4::new(ndc_x, ndc_y, -1.0, 1.0);
let view_matrix = camera.view_matrix(); let inv_projection = projection.inverse();
let projection_matrix = camera.projection_matrix(); let inv_view = view.inverse();
let inv_projection = projection_matrix.inverse();
let inv_view = view_matrix.inverse();
let eye_coords = inv_projection * clip_coords; let eye_coords = inv_projection * clip_coords;
let eye_coords = Vec4::new(eye_coords.x, eye_coords.y, -1.0, 0.0); let eye_coords = Vec4::new(eye_coords.x, eye_coords.y, -1.0, 0.0);
@@ -34,31 +36,127 @@ impl Ray
let world_coords = inv_view * eye_coords; let world_coords = inv_view * eye_coords;
let direction = Vec3::new(world_coords.x, world_coords.y, world_coords.z).normalize(); let direction = Vec3::new(world_coords.x, world_coords.y, world_coords.z).normalize();
Ray { let origin = inv_view.col(3).truncate();
origin: camera.position,
direction, Ray { origin, direction }
}
} }
pub fn intersects_mesh(&self, mesh: &Mesh, transform: &Mat4) -> Option<f32> pub fn intersects_aabb(&self, min: Vec3, max: Vec3) -> Option<f32>
{ {
let inv_transform = transform.inverse(); let inv_dir = Vec3::new(
let local_origin = inv_transform.transform_point3(self.origin); 1.0 / self.direction.x,
let local_direction = inv_transform.transform_vector3(self.direction).normalize(); 1.0 / self.direction.y,
1.0 / self.direction.z,
);
let mut closest_distance = f32::MAX; let t1 = (min - self.origin) * inv_dir;
let t2 = (max - self.origin) * inv_dir;
let tmin = t1.min(t2);
let tmax = t1.max(t2);
let tenter = tmin.x.max(tmin.y).max(tmin.z);
let texit = tmax.x.min(tmax.y).min(tmax.z);
if tenter <= texit && texit >= 0.0
{
Some(tenter.max(0.0))
}
else
{
None
}
}
}
fn transform_aabb(aabb_min: Vec3, aabb_max: Vec3, model: &Mat4) -> (Vec3, Vec3)
{
let corners = [
Vec3::new(aabb_min.x, aabb_min.y, aabb_min.z),
Vec3::new(aabb_max.x, aabb_min.y, aabb_min.z),
Vec3::new(aabb_min.x, aabb_max.y, aabb_min.z),
Vec3::new(aabb_max.x, aabb_max.y, aabb_min.z),
Vec3::new(aabb_min.x, aabb_min.y, aabb_max.z),
Vec3::new(aabb_max.x, aabb_min.y, aabb_max.z),
Vec3::new(aabb_min.x, aabb_max.y, aabb_max.z),
Vec3::new(aabb_max.x, aabb_max.y, aabb_max.z),
];
let mut world_min = Vec3::splat(f32::MAX);
let mut world_max = Vec3::splat(f32::MIN);
for corner in &corners
{
let world_corner = model.transform_point3(*corner);
world_min = world_min.min(world_corner);
world_max = world_max.max(world_corner);
}
(world_min, world_max)
}
fn intersect_triangle(origin: Vec3, dir: Vec3, v0: Vec3, v1: Vec3, v2: Vec3) -> Option<f32>
{
let edge1 = v1 - v0;
let edge2 = v2 - v0;
let h = dir.cross(edge2);
let a = edge1.dot(h);
if a.abs() < 1e-7
{
return None;
}
let f = 1.0 / a;
let s = origin - v0;
let u = f * s.dot(h);
if !(0.0..=1.0).contains(&u)
{
return None;
}
let q = s.cross(edge1);
let v = f * dir.dot(q);
if v < 0.0 || u + v > 1.0
{
return None;
}
let t = f * edge2.dot(q);
if t > 1e-7
{
Some(t)
}
else
{
None
}
}
/// Raymesh triangle intersection in local space, returning world-space distance.
///
/// Transforms the ray into local mesh space, tests every triangle with
/// MöllerTrumbore, then maps the closest local hit back to a world-space
/// distance so results can be compared across entities.
fn intersect_mesh_triangles(ray: &Ray, mesh: &Mesh, model: &Mat4) -> Option<f32>
{
if mesh.cpu_indices.is_empty()
{
return None;
}
let inv_model = model.inverse();
let local_origin = inv_model.transform_point3(ray.origin);
let local_dir = inv_model.transform_vector3(ray.direction);
let mut closest_t = f32::MAX;
let mut hit = false; let mut hit = false;
for triangle_idx in (0..mesh.num_indices).step_by(3) for tri in mesh.cpu_indices.chunks_exact(3)
{ {
let distance = let v0 = Vec3::from(mesh.cpu_positions[tri[0] as usize]);
self.intersects_triangle_local(local_origin, local_direction, mesh, triangle_idx); let v1 = Vec3::from(mesh.cpu_positions[tri[1] as usize]);
let v2 = Vec3::from(mesh.cpu_positions[tri[2] as usize]);
if let Some(d) = distance if let Some(t) = intersect_triangle(local_origin, local_dir, v0, v1, v2)
{ {
if d < closest_distance if t < closest_t
{ {
closest_distance = d; closest_t = t;
hit = true; hit = true;
} }
} }
@@ -66,22 +164,87 @@ impl Ray
if hit if hit
{ {
Some(closest_distance) let local_hit = local_origin + local_dir * closest_t;
let world_hit = model.transform_point3(local_hit);
Some((world_hit - ray.origin).length())
} }
else else
{ {
None None
} }
}
fn intersect_instances(
ray: &Ray,
mesh: &Mesh,
tree_instances: &TreeInstancesComponent,
) -> Option<f32>
{
let mut closest: Option<f32> = None;
for instance in &tree_instances.instances
{
let instance_model = Mat4::from_scale_rotation_translation(
instance.scale,
instance.rotation,
instance.position,
);
let (world_min, world_max) = transform_aabb(mesh.aabb_min, mesh.aabb_max, &instance_model);
if let Some(d) = ray.intersects_aabb(world_min, world_max)
{
if closest.map_or(true, |c| d < c)
{
closest = Some(d);
}
}
} }
fn intersects_triangle_local( closest
&self, }
local_origin: Vec3,
local_direction: Vec3, pub fn pick_entity(ray: &Ray, world: &World) -> Option<EntityHandle>
_mesh: &Mesh, {
_triangle_idx: u32, let mut closest_entity = None;
) -> Option<f32> let mut closest_distance = f32::MAX;
{
None for (&entity, mesh_component) in &world.meshes.components
} {
let transform = match world.transforms.get(entity)
{
Some(t) => t,
None => continue,
};
let model = transform.to_matrix();
let distance = if let Some(tree_instances) = world.tree_instances.get(entity)
{
intersect_instances(ray, &mesh_component.mesh, tree_instances)
}
else
{
let (world_min, world_max) = transform_aabb(
mesh_component.mesh.aabb_min,
mesh_component.mesh.aabb_max,
&model,
);
if ray.intersects_aabb(world_min, world_max).is_none()
{
continue;
}
intersect_mesh_triangles(ray, &mesh_component.mesh, &model)
};
if let Some(d) = distance
{
if d < closest_distance
{
closest_distance = d;
closest_entity = Some(entity);
}
}
}
closest_entity
} }

View File

@@ -1,5 +1,7 @@
use crate::texture::{DitherTextures, FlowmapTexture}; use crate::texture::{DitherTextures, FlowmapTexture};
use std::num::NonZeroU64;
use super::types::Uniforms;
use super::Renderer; use super::Renderer;
impl Renderer impl Renderer
@@ -24,7 +26,11 @@ impl Renderer
entries: &[ entries: &[
wgpu::BindGroupEntry { wgpu::BindGroupEntry {
binding: 0, binding: 0,
resource: uniform_buffer.as_entire_binding(), resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: uniform_buffer,
offset: 0,
size: NonZeroU64::new(std::mem::size_of::<Uniforms>() as u64),
}),
}, },
wgpu::BindGroupEntry { wgpu::BindGroupEntry {
binding: 1, binding: 1,

View File

@@ -102,8 +102,7 @@ impl DebugOverlay
cache: None, cache: None,
}); });
let pipeline_snow_light = let pipeline_snow_light = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Snow Light Debug Pipeline"), label: Some("Snow Light Debug Pipeline"),
layout: Some(&snow_light_layout), layout: Some(&snow_light_layout),
vertex: wgpu::VertexState { vertex: wgpu::VertexState {

View File

@@ -6,12 +6,19 @@ mod types;
pub use types::{DrawCall, Pipeline, Spotlight, SpotlightRaw, Uniforms, MAX_SPOTLIGHTS}; pub use types::{DrawCall, Pipeline, Spotlight, SpotlightRaw, Uniforms, MAX_SPOTLIGHTS};
use crate::entity::EntityHandle;
use crate::debug::DebugMode; use crate::debug::DebugMode;
use crate::postprocess::{create_blit_pipeline, create_fullscreen_quad, LowResFramebuffer}; use crate::postprocess::{create_blit_pipeline, create_fullscreen_quad, LowResFramebuffer};
use crate::texture::{DitherTextures, FlowmapTexture}; use crate::texture::{DitherTextures, FlowmapTexture};
use pipeline::{create_debug_lines_pipeline, create_main_pipeline, create_snow_clipmap_pipeline, use pipeline::{
create_wireframe_pipeline}; create_debug_lines_pipeline, create_main_pipeline, create_snow_clipmap_pipeline,
create_wireframe_pipeline,
};
use std::cell::RefCell; use std::cell::RefCell;
use std::num::NonZeroU64;
const MAX_DRAW_CALLS: usize = 64;
pub struct Renderer pub struct Renderer
{ {
@@ -62,6 +69,8 @@ pub struct Renderer
snow_light_accumulation: Option<crate::snow_light::SnowLightAccumulation>, snow_light_accumulation: Option<crate::snow_light::SnowLightAccumulation>,
snow_light_bound: bool, snow_light_bound: bool,
pub selected_entity: Option<EntityHandle>,
} }
impl Renderer impl Renderer
@@ -90,8 +99,9 @@ impl Renderer
.await .await
.map_err(|_| "Failed to find adapter")?; .map_err(|_| "Failed to find adapter")?;
let wireframe_supported = let wireframe_supported = adapter
adapter.features().contains(wgpu::Features::POLYGON_MODE_LINE); .features()
.contains(wgpu::Features::POLYGON_MODE_LINE);
let required_features = if wireframe_supported let required_features = if wireframe_supported
{ {
wgpu::Features::POLYGON_MODE_LINE wgpu::Features::POLYGON_MODE_LINE
@@ -129,7 +139,7 @@ impl Renderer
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Uniform Buffer"), label: Some("Uniform Buffer"),
size: std::mem::size_of::<Uniforms>() as wgpu::BufferAddress, size: (std::mem::size_of::<Uniforms>() * MAX_DRAW_CALLS) as wgpu::BufferAddress,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false, mapped_at_creation: false,
}); });
@@ -306,8 +316,8 @@ impl Renderer
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer { ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform, ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false, has_dynamic_offset: true,
min_binding_size: None, min_binding_size: NonZeroU64::new(std::mem::size_of::<Uniforms>() as u64),
}, },
count: None, count: None,
}, },
@@ -463,15 +473,22 @@ impl Renderer
let wireframe_pipeline = if wireframe_supported let wireframe_pipeline = if wireframe_supported
{ {
Some(create_wireframe_pipeline(&device, config.format, &bind_group_layout)) Some(create_wireframe_pipeline(
&device,
config.format,
&bind_group_layout,
))
} }
else else
{ {
None None
}; };
let debug_lines_pipeline = let debug_lines_pipeline = Some(create_debug_lines_pipeline(
Some(create_debug_lines_pipeline(&device, config.format, &bind_group_layout)); &device,
config.format,
&bind_group_layout,
));
let debug_overlay = Some(debug_overlay::DebugOverlay::new(&device, config.format)); let debug_overlay = Some(debug_overlay::DebugOverlay::new(&device, config.format));
@@ -533,6 +550,7 @@ impl Renderer
dummy_snow_light_sampler, dummy_snow_light_sampler,
snow_light_accumulation: None, snow_light_accumulation: None,
snow_light_bound: false, snow_light_bound: false,
selected_entity: None,
}) })
} }
@@ -585,6 +603,9 @@ impl Renderer
label: Some("Render Encoder"), label: Some("Render Encoder"),
}); });
let uniform_size = std::mem::size_of::<Uniforms>() as u64;
let mut uniform_slot: u64 = 0;
for (i, draw_call) in draw_calls.iter().enumerate() for (i, draw_call) in draw_calls.iter().enumerate()
{ {
let uniforms = Uniforms::new( let uniforms = Uniforms::new(
@@ -603,8 +624,15 @@ impl Renderer
&self.spotlights, &self.spotlights,
debug_mode.as_u32(), debug_mode.as_u32(),
); );
self.queue
.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms])); let offset = uniform_slot * uniform_size;
uniform_slot += 1;
self.queue.write_buffer(
&self.uniform_buffer,
offset,
bytemuck::cast_slice(&[uniforms]),
);
{ {
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
@@ -660,7 +688,7 @@ impl Renderer
.unwrap_or(&self.standard_pipeline), .unwrap_or(&self.standard_pipeline),
}; };
render_pass.set_pipeline(pipeline); render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, &self.bind_group, &[]); render_pass.set_bind_group(0, &self.bind_group, &[offset as u32]);
if let Some(ref displacement_bind_group) = draw_call.displacement_bind_group if let Some(ref displacement_bind_group) = draw_call.displacement_bind_group
{ {
@@ -686,7 +714,10 @@ impl Renderer
{ {
for draw_call in draw_calls.iter() for draw_call in draw_calls.iter()
{ {
if matches!(draw_call.pipeline, Pipeline::SnowClipmap | Pipeline::DebugLines) if matches!(
draw_call.pipeline,
Pipeline::SnowClipmap | Pipeline::DebugLines
)
{ {
continue; continue;
} }
@@ -707,9 +738,11 @@ impl Renderer
&self.spotlights, &self.spotlights,
4, 4,
); );
let wire_offset = uniform_slot * uniform_size;
uniform_slot += 1;
self.queue.write_buffer( self.queue.write_buffer(
&self.uniform_buffer, &self.uniform_buffer,
0, wire_offset,
bytemuck::cast_slice(&[uniforms]), bytemuck::cast_slice(&[uniforms]),
); );
@@ -742,7 +775,7 @@ impl Renderer
}); });
wire_pass.set_pipeline(wireframe_pipeline); wire_pass.set_pipeline(wireframe_pipeline);
wire_pass.set_bind_group(0, &self.bind_group, &[]); wire_pass.set_bind_group(0, &self.bind_group, &[wire_offset as u32]);
wire_pass.set_vertex_buffer(0, draw_call.vertex_buffer.slice(..)); wire_pass.set_vertex_buffer(0, draw_call.vertex_buffer.slice(..));
if let Some(ref instance_buffer) = draw_call.instance_buffer if let Some(ref instance_buffer) = draw_call.instance_buffer
@@ -764,6 +797,99 @@ impl Renderer
} }
} }
if let Some(selected) = self.selected_entity
{
if let Some(ref wireframe_pipeline) = self.wireframe_pipeline
{
for draw_call in draw_calls.iter()
{
if draw_call.entity != Some(selected)
{
continue;
}
if matches!(
draw_call.pipeline,
Pipeline::SnowClipmap | Pipeline::DebugLines
)
{
continue;
}
let uniforms = Uniforms::new(
draw_call.model,
*view,
*projection,
&light_view_projections,
camera_position,
player_position,
self.terrain_height_scale,
time,
self.shadow_bias,
draw_call.tile_scale,
draw_call.enable_dissolve,
draw_call.enable_snow_light,
&self.spotlights,
4,
);
let sel_offset = uniform_slot * uniform_size;
uniform_slot += 1;
self.queue.write_buffer(
&self.uniform_buffer,
sel_offset,
bytemuck::cast_slice(&[uniforms]),
);
{
let mut sel_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Selection Wireframe Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &self.framebuffer.view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: Some(
wgpu::RenderPassDepthStencilAttachment {
view: &self.framebuffer.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
},
),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
sel_pass.set_pipeline(wireframe_pipeline);
sel_pass.set_bind_group(0, &self.bind_group, &[sel_offset as u32]);
sel_pass.set_vertex_buffer(0, draw_call.vertex_buffer.slice(..));
if let Some(ref instance_buffer) = draw_call.instance_buffer
{
sel_pass.set_vertex_buffer(1, instance_buffer.slice(..));
}
sel_pass.set_index_buffer(
draw_call.index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
sel_pass.draw_indexed(
0..draw_call.num_indices,
0,
0..draw_call.num_instances,
);
}
}
}
}
if debug_mode == DebugMode::ShadowMap if debug_mode == DebugMode::ShadowMap
{ {
if let Some(ref overlay) = self.debug_overlay if let Some(ref overlay) = self.debug_overlay
@@ -1074,3 +1200,12 @@ pub fn update_spotlights(spotlights: Vec<Spotlight>)
renderer.spotlights = spotlights; renderer.spotlights = spotlights;
}); });
} }
pub fn set_selected_entity(entity: Option<EntityHandle>)
{
GLOBAL_RENDERER.with(|r| {
let mut renderer = r.borrow_mut();
let renderer = renderer.as_mut().expect("Renderer not set");
renderer.selected_entity = entity;
});
}

View File

@@ -1,6 +1,8 @@
use bytemuck::{Pod, Zeroable}; use bytemuck::{Pod, Zeroable};
use glam::Mat4; use glam::Mat4;
use crate::entity::EntityHandle;
pub const MAX_SPOTLIGHTS: usize = 4; pub const MAX_SPOTLIGHTS: usize = 4;
#[repr(C)] #[repr(C)]
@@ -156,4 +158,5 @@ pub struct DrawCall
pub enable_dissolve: bool, pub enable_dissolve: bool,
pub enable_snow_light: bool, pub enable_snow_light: bool,
pub displacement_bind_group: Option<wgpu::BindGroup>, pub displacement_bind_group: Option<wgpu::BindGroup>,
pub entity: Option<EntityHandle>,
} }

View File

@@ -32,46 +32,7 @@ fn vs_main(input: VertexInput) -> VertexOutput {
); );
output.world_normal = normalize(normal_matrix * input.normal); output.world_normal = normalize(normal_matrix * input.normal);
var dissolve_amount = 0.0; output.dissolve_amount = input.instance_dissolve;
if uniforms.enable_dissolve == 1u {
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);
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;
output.uv = input.uv; output.uv = input.uv;
return output; return output;
@@ -80,11 +41,10 @@ fn vs_main(input: VertexInput) -> VertexOutput {
@fragment @fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
if uniforms.enable_dissolve == 1u && in.dissolve_amount > 0.0 { if uniforms.enable_dissolve == 1u && in.dissolve_amount > 0.0 {
let screen_pos = in.clip_position.xy; let screen_pos = in.clip_position.xy / in.clip_position.w;
let noise_uv = fract(screen_pos / 128.0); let noise_uv = screen_pos * 0.5 + 0.5;
let noise_value = textureSampleLevel(blue_noise_texture, blue_noise_sampler, noise_uv, 0.0).r; let noise = textureSample(blue_noise_texture, blue_noise_sampler, noise_uv * 10.0).r;
if in.dissolve_amount > noise {
if noise_value < in.dissolve_amount {
discard; discard;
} }
} }

View File

@@ -505,10 +505,23 @@ impl SnowLayer
}) })
}); });
let mut aabb_min = Vec3::splat(f32::MAX);
let mut aabb_max = Vec3::splat(f32::MIN);
for v in &vertices
{
let p = Vec3::from(v.position);
aabb_min = aabb_min.min(p);
aabb_max = aabb_max.max(p);
}
Mesh { Mesh {
vertex_buffer, vertex_buffer,
index_buffer, index_buffer,
num_indices: indices.len() as u32, num_indices: indices.len() as u32,
aabb_min,
aabb_max,
cpu_positions: Vec::new(),
cpu_indices: Vec::new(),
} }
} }
@@ -568,6 +581,7 @@ impl SnowLayer
enable_dissolve: false, enable_dissolve: false,
enable_snow_light: true, enable_snow_light: true,
displacement_bind_group: Some(self.displacement_bind_group.clone()), displacement_bind_group: Some(self.displacement_bind_group.clone()),
entity: None,
}) })
.collect() .collect()
} }

View File

@@ -9,6 +9,7 @@ pub mod snow;
pub mod spotlight_sync; pub mod spotlight_sync;
pub mod state_machine; pub mod state_machine;
pub mod tree_dissolve; pub mod tree_dissolve;
pub mod trigger;
pub use camera::{ pub use camera::{
camera_follow_system, camera_input_system, camera_noclip_system, camera_view_matrix, camera_follow_system, camera_input_system, camera_noclip_system, camera_view_matrix,
@@ -21,3 +22,7 @@ pub use rotate::rotate_system;
pub use snow::snow_system; pub use snow::snow_system;
pub use spotlight_sync::spotlight_sync_system; pub use spotlight_sync::spotlight_sync_system;
pub use state_machine::{state_machine_physics_system, state_machine_system}; pub use state_machine::{state_machine_physics_system, state_machine_system};
pub use tree_dissolve::{
tree_dissolve_update_system, tree_instance_buffer_update_system, tree_occlusion_system,
};
pub use trigger::trigger_system;

View File

@@ -90,7 +90,7 @@ impl State for PlayerFallingState
}; };
world.movements.with_mut(self.entity, |movement| { world.movements.with_mut(self.entity, |movement| {
movement.movement_config.movement_context.is_floored = is_grounded; movement.movement_context.is_floored = is_grounded;
}); });
} }
} }
@@ -116,7 +116,7 @@ impl State for PlayerIdleState
let current_velocity = *rigidbody.linvel(); let current_velocity = *rigidbody.linvel();
let idle_damping = world let idle_damping = world
.movements .movements
.with(self.entity, |m| m.movement_config.idle_damping) .with(self.entity, |m| m.idle_damping)
.unwrap_or(0.1); .unwrap_or(0.1);
let horizontal_velocity = Vec3::new(current_velocity.x, 0.0, current_velocity.z); let horizontal_velocity = Vec3::new(current_velocity.x, 0.0, current_velocity.z);
@@ -171,7 +171,7 @@ impl State for PlayerIdleState
}); });
world.movements.with_mut(self.entity, |movement| { world.movements.with_mut(self.entity, |movement| {
movement.movement_config.movement_context.is_floored = true; movement.movement_context.is_floored = true;
}); });
} }
} }
@@ -210,7 +210,7 @@ impl State for PlayerWalkingState
.inputs .inputs
.with(self.entity, |input| input.move_direction) .with(self.entity, |input| input.move_direction)
.unwrap_or(Vec3::ZERO); .unwrap_or(Vec3::ZERO);
(input, movement.movement_config.clone()) (input, movement.clone())
}) })
.unwrap(); .unwrap();
@@ -301,7 +301,7 @@ impl State for PlayerWalkingState
}); });
world.movements.with_mut(self.entity, |movement| { world.movements.with_mut(self.entity, |movement| {
movement.movement_config.movement_context.is_floored = terrain_height.is_some(); movement.movement_context.is_floored = terrain_height.is_some();
}); });
if movement_input.length_squared() > 0.1 if movement_input.length_squared() > 0.1
@@ -334,11 +334,11 @@ impl State for PlayerJumpingState
let current_position = world.transforms.get(self.entity).unwrap().position; let current_position = world.transforms.get(self.entity).unwrap().position;
world.jumps.with_mut(self.entity, |jump| { world.jumps.with_mut(self.entity, |jump| {
jump.jump_config.jump_context.in_progress = true; jump.jump_context.in_progress = true;
jump.jump_config.jump_context.execution_time = self.enter_time_stamp; jump.jump_context.execution_time = self.enter_time_stamp;
jump.jump_config.jump_context.origin_height = current_position.y; jump.jump_context.origin_height = current_position.y;
jump.jump_config.jump_context.duration = 0.0; jump.jump_context.duration = 0.0;
jump.jump_config.jump_context.normal = Vec3::Y; jump.jump_context.normal = Vec3::Y;
}); });
println!("entered jumping"); println!("entered jumping");
@@ -347,8 +347,8 @@ impl State for PlayerJumpingState
fn on_state_exit(&mut self, world: &mut World) fn on_state_exit(&mut self, world: &mut World)
{ {
world.jumps.with_mut(self.entity, |jump| { world.jumps.with_mut(self.entity, |jump| {
jump.jump_config.jump_context.in_progress = false; jump.jump_context.in_progress = false;
jump.jump_config.jump_context.duration = 0.0; jump.jump_context.duration = 0.0;
}); });
println!("exited jumping"); println!("exited jumping");
@@ -361,21 +361,21 @@ impl State for PlayerJumpingState
let current_time = Time::get_time_elapsed(); let current_time = Time::get_time_elapsed();
world.jumps.with_mut(self.entity, |jump| { world.jumps.with_mut(self.entity, |jump| {
jump.jump_config.jump_context.duration = jump.jump_context.duration =
current_time - jump.jump_config.jump_context.execution_time; current_time - jump.jump_context.execution_time;
}); });
let jump_config = world let jump = world
.jumps .jumps
.with_mut(self.entity, |jump| jump.jump_config.clone()) .with(self.entity, |jump| jump.clone())
.unwrap(); .unwrap();
let elapsed_time = jump_config.jump_context.duration; let elapsed_time = jump.jump_context.duration;
let normalized_time = (elapsed_time / jump_config.jump_duration).min(1.0); let normalized_time = (elapsed_time / jump.jump_duration).min(1.0);
let height_progress = jump_config.jump_curve.eval(normalized_time as f64).y as f32; let height_progress = jump.jump_curve.eval(normalized_time as f64).y as f32;
let origin_height = jump_config.jump_context.origin_height; let origin_height = jump.jump_context.origin_height;
let target_height = origin_height + height_progress * jump_config.jump_height; let target_height = origin_height + height_progress * jump.jump_height;
let current_translation = world let current_translation = world
.physics .physics
@@ -409,7 +409,7 @@ impl State for PlayerJumpingState
}); });
world.movements.with_mut(self.entity, |movement| { world.movements.with_mut(self.entity, |movement| {
movement.movement_config.movement_context.is_floored = false; movement.movement_context.is_floored = false;
}); });
} }
} }

View File

@@ -60,6 +60,7 @@ pub fn render_system(world: &World) -> Vec<DrawCall>
enable_dissolve: mesh_component.enable_dissolve, enable_dissolve: mesh_component.enable_dissolve,
enable_snow_light: mesh_component.enable_snow_light, enable_snow_light: mesh_component.enable_snow_light,
displacement_bind_group: None, displacement_bind_group: None,
entity: Some(entity),
}) })
}) })
.collect() .collect()

View File

@@ -1,15 +1,21 @@
use crate::components::DissolveComponent; use crate::loaders::mesh::InstanceRaw;
use crate::world::World; use crate::world::World;
use bytemuck::cast_slice;
pub fn tree_dissolve_update_system(world: &mut World, delta: f32) pub fn tree_dissolve_update_system(world: &mut World, delta: f32)
{ {
for entity in world.dissolves.all() for entity in world.tree_instances.all()
{ {
if let Some(dissolve) = world.dissolves.get_mut(entity) if let Some(tree_instances) = world.tree_instances.get_mut(entity)
{ {
let diff = dissolve.target_amount - dissolve.amount; for i in 0..tree_instances.dissolve_amounts.len()
dissolve.amount += diff * dissolve.transition_speed * delta; {
dissolve.amount = dissolve.amount.clamp(0.0, 1.0); let diff = tree_instances.dissolve_targets[i] - tree_instances.dissolve_amounts[i];
tree_instances.dissolve_amounts[i] +=
diff * tree_instances.transition_speed * delta;
tree_instances.dissolve_amounts[i] =
tree_instances.dissolve_amounts[i].clamp(0.0, 1.0);
}
} }
} }
} }
@@ -35,52 +41,70 @@ pub fn tree_occlusion_system(world: &mut World)
} }
let to_player_normalized = to_player.normalize(); let to_player_normalized = to_player.normalize();
let occlusion_radius = 10.0;
for tree_entity in world.tree_tags.all() for tree_entity in world.tree_instances.all()
{ {
if let Some(tree_transform) = world.transforms.get(tree_entity) if let Some(tree_instances) = world.tree_instances.get_mut(tree_entity)
{ {
let tree_pos = tree_transform.position; for (idx, instance) in tree_instances.instances.iter().enumerate()
let to_tree = tree_pos - camera_pos; {
let distance_to_tree = to_tree.length(); let instance_pos = instance.position;
let to_instance = instance_pos - camera_pos;
let distance_to_instance = to_instance.length();
if distance_to_tree < distance_to_player if distance_to_instance < distance_to_player
{ {
let projection = to_tree.dot(to_player_normalized); let projection = to_instance.dot(to_player_normalized);
if projection > 0.0 if projection > 0.0 && projection <= distance_to_player
{ {
let perpendicular_vec = to_tree - to_player_normalized * projection; let perpendicular_vec =
to_instance - to_player_normalized * projection;
let perp_distance = perpendicular_vec.length(); let perp_distance = perpendicular_vec.length();
let occlusion_radius = 2.5; if perp_distance <= occlusion_radius
if perp_distance < occlusion_radius
{ {
let dissolve_amount = let dissolve_amount =
1.0 - (perp_distance / occlusion_radius).clamp(0.0, 1.0); 1.0 - (perp_distance / occlusion_radius).clamp(0.0, 1.0);
tree_instances.dissolve_targets[idx] = 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; continue;
} }
} }
} }
if let Some(dissolve) = world.dissolves.get_mut(tree_entity) tree_instances.dissolve_targets[idx] = 0.0;
{
dissolve.target_amount = 0.0;
} }
} }
} }
} }
} }
} }
pub fn tree_instance_buffer_update_system(world: &mut World)
{
for entity in world.tree_instances.all()
{
if let Some(tree_instances) = world.tree_instances.get(entity)
{
let instance_data_vec: Vec<InstanceRaw> = tree_instances
.instances
.iter()
.enumerate()
.map(|(idx, instance)| InstanceRaw {
model: instance.to_raw().model,
dissolve_amount: tree_instances.dissolve_amounts[idx],
_padding: [0.0; 3],
})
.collect();
crate::render::with_queue(|queue| {
queue.write_buffer(
&tree_instances.instance_buffer,
0,
cast_slice(&instance_data_vec),
);
});
}
}
}

82
src/systems/trigger.rs Normal file
View File

@@ -0,0 +1,82 @@
use glam::Vec3;
use crate::components::trigger::{TriggerEvent, TriggerEventKind, TriggerFilter, TriggerShape, TriggerState};
use crate::entity::EntityHandle;
use crate::world::World;
pub fn trigger_system(world: &mut World)
{
world.trigger_events.clear();
let trigger_entities: Vec<EntityHandle> = world.triggers.all();
let mut pending_events: Vec<TriggerEvent> = Vec::new();
for trigger_entity in trigger_entities
{
let trigger_pos = match world.transforms.get(trigger_entity)
{
Some(t) => t.position,
None => continue,
};
let candidate_entities: Vec<EntityHandle> = match world.triggers.get(trigger_entity)
{
Some(trigger) => match &trigger.filter
{
TriggerFilter::Player => world.player_tags.all(),
},
None => continue,
};
let activator_positions: Vec<(EntityHandle, Vec3)> = candidate_entities
.into_iter()
.filter_map(|e| world.transforms.get(e).map(|t| (e, t.position)))
.collect();
let overlapping = match world.triggers.get(trigger_entity)
{
Some(trigger) => activator_positions.iter().any(|(_, pos)| match &trigger.shape
{
TriggerShape::Sphere { radius } => (trigger_pos - *pos).length() < *radius,
}),
None => continue,
};
let first_activator = activator_positions.first().map(|(e, _)| *e);
if let Some(trigger) = world.triggers.get_mut(trigger_entity)
{
match (&trigger.state, overlapping)
{
(TriggerState::Idle, true) =>
{
trigger.state = TriggerState::Inside;
if let Some(activator_entity) = first_activator
{
pending_events.push(TriggerEvent {
trigger_entity,
activator_entity,
kind: TriggerEventKind::Entered,
});
}
}
(TriggerState::Inside, false) =>
{
trigger.state = TriggerState::Idle;
if let Some(activator_entity) = first_activator
{
pending_events.push(TriggerEvent {
trigger_entity,
activator_entity,
kind: TriggerEventKind::Exited,
});
}
}
_ =>
{}
}
}
}
world.trigger_events.extend(pending_events);
}

View File

@@ -2,11 +2,12 @@ use std::collections::HashMap;
use crate::components::dissolve::DissolveComponent; use crate::components::dissolve::DissolveComponent;
use crate::components::follow::FollowComponent; use crate::components::follow::FollowComponent;
use crate::components::jump::JumpComponent;
use crate::components::lights::spot::SpotlightComponent; use crate::components::lights::spot::SpotlightComponent;
use crate::components::tree_instances::TreeInstancesComponent;
use crate::components::trigger::{TriggerComponent, TriggerEvent};
use crate::components::{ use crate::components::{
CameraComponent, InputComponent, MeshComponent, MovementComponent, PhysicsComponent, CameraComponent, InputComponent, JumpComponent, MeshComponent, MovementComponent,
RotateComponent, PhysicsComponent, RotateComponent,
}; };
use crate::entity::{EntityHandle, EntityManager}; use crate::entity::{EntityHandle, EntityManager};
use crate::state::StateMachine; use crate::state::StateMachine;
@@ -84,6 +85,10 @@ pub struct World
pub dissolves: Storage<DissolveComponent>, pub dissolves: Storage<DissolveComponent>,
pub follows: Storage<FollowComponent>, pub follows: Storage<FollowComponent>,
pub rotates: Storage<RotateComponent>, pub rotates: Storage<RotateComponent>,
pub tree_instances: Storage<TreeInstancesComponent>,
pub names: Storage<String>,
pub triggers: Storage<TriggerComponent>,
pub trigger_events: Vec<TriggerEvent>,
} }
impl World impl World
@@ -106,6 +111,10 @@ impl World
dissolves: Storage::new(), dissolves: Storage::new(),
follows: Storage::new(), follows: Storage::new(),
rotates: Storage::new(), rotates: Storage::new(),
tree_instances: Storage::new(),
names: Storage::new(),
triggers: Storage::new(),
trigger_events: Vec::new(),
} }
} }
@@ -130,6 +139,9 @@ impl World
self.dissolves.remove(entity); self.dissolves.remove(entity);
self.follows.remove(entity); self.follows.remove(entity);
self.rotates.remove(entity); self.rotates.remove(entity);
self.tree_instances.remove(entity);
self.names.remove(entity);
self.triggers.remove(entity);
self.entities.despawn(entity); self.entities.despawn(entity);
} }