Compare commits
6 Commits
909ae8612a
...
87177ad97d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87177ad97d | ||
|
|
b0022ad17b | ||
|
|
d37a3c87e3 | ||
|
|
16626cc277 | ||
|
|
2846c04765 | ||
|
|
dffd731b87 |
15
.pi/skills/intent-based-architecture/SKILL.md
Normal file
15
.pi/skills/intent-based-architecture/SKILL.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
name: intent-based-architecture
|
||||||
|
description: Intent-based architecture patterns for the snow_trail_sdl game engine. Load when adding cross-system communication, new systems to the main loop, changing game mode logic (editor/dialog/gameplay), or working with camera, input, or mode-switching. Systems communicate through typed intent queues — no direct cross-system function calls.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Intent-Based Architecture Skill
|
||||||
|
|
||||||
|
Read the architecture document at `docs/intent-based-architecture.md` (resolve relative to the project root `/home/jonas/projects/snow_trail_sdl/`) and follow its patterns when:
|
||||||
|
|
||||||
|
- Adding cross-system communication (use intents, not direct function calls)
|
||||||
|
- Adding new systems to the main loop
|
||||||
|
- Changing how game modes work (editor, dialog, gameplay)
|
||||||
|
- Working with camera, input, or mode-switching logic
|
||||||
|
|
||||||
|
Key rule: **systems communicate through typed intent data in shared queues** — producers insert intents, consumers process and remove them. No system calls another system's functions directly.
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# Intent-Based Architecture Skill
|
|
||||||
|
|
||||||
Read the architecture document at `docs/self-gating-systems.md` (resolve relative to the project root `/home/jonas/projects/snow_trail_sdl/`) and follow its patterns when:
|
|
||||||
|
|
||||||
- Adding cross-system communication (use intents, not direct function calls)
|
|
||||||
- Adding new systems to the main loop
|
|
||||||
- Changing how game modes work (editor, dialog, gameplay)
|
|
||||||
- Working with camera, input, or mode-switching logic
|
|
||||||
|
|
||||||
Key rule: **systems communicate through typed intent data in shared queues** — producers insert intents, consumers process and remove them. No system calls another system's functions directly.
|
|
||||||
@@ -24,6 +24,7 @@ serde_json = "1.0"
|
|||||||
bladeink = "1.2"
|
bladeink = "1.2"
|
||||||
wesl = "0.2"
|
wesl = "0.2"
|
||||||
ab_glyph = "0.2"
|
ab_glyph = "0.2"
|
||||||
|
rand = "0.9"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
wesl = "0.2"
|
wesl = "0.2"
|
||||||
|
|||||||
@@ -144,9 +144,9 @@
|
|||||||
{
|
{
|
||||||
"name":"TestCharSpawn",
|
"name":"TestCharSpawn",
|
||||||
"translation":[
|
"translation":[
|
||||||
-381.1509704589844,
|
-376.7967224121094,
|
||||||
106.53739166259766,
|
115.31475830078125,
|
||||||
107.46959686279297
|
155.05471801757812
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,5 @@
|
|||||||
use bladeink::story::Story;
|
use bladeink::story::Story;
|
||||||
|
use glam::Vec3;
|
||||||
|
|
||||||
use crate::entity::EntityHandle;
|
use crate::entity::EntityHandle;
|
||||||
|
|
||||||
@@ -56,6 +57,7 @@ pub struct DialogProjectileComponent
|
|||||||
pub bubble_entity: EntityHandle,
|
pub bubble_entity: EntityHandle,
|
||||||
pub correct_parry: ParryButton,
|
pub correct_parry: ParryButton,
|
||||||
pub parry_window_open: bool,
|
pub parry_window_open: bool,
|
||||||
|
pub velocity: Vec3,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ pub mod lights;
|
|||||||
pub mod mesh;
|
pub mod mesh;
|
||||||
pub mod movement;
|
pub mod movement;
|
||||||
pub mod noclip;
|
pub mod noclip;
|
||||||
|
pub mod particle;
|
||||||
pub mod physics;
|
pub mod physics;
|
||||||
pub mod player_states;
|
pub mod player_states;
|
||||||
pub mod rotate;
|
pub mod rotate;
|
||||||
@@ -26,6 +27,7 @@ pub use input::InputComponent;
|
|||||||
pub use jump::JumpComponent;
|
pub use jump::JumpComponent;
|
||||||
pub use mesh::MeshComponent;
|
pub use mesh::MeshComponent;
|
||||||
pub use movement::MovementComponent;
|
pub use movement::MovementComponent;
|
||||||
|
pub use particle::{ParticleEmitterConfig, SpawnParticleIntent};
|
||||||
pub use physics::PhysicsComponent;
|
pub use physics::PhysicsComponent;
|
||||||
pub use rotate::RotateComponent;
|
pub use rotate::RotateComponent;
|
||||||
pub use tree_instances::TreeInstancesComponent;
|
pub use tree_instances::TreeInstancesComponent;
|
||||||
|
|||||||
18
src/components/particle.rs
Normal file
18
src/components/particle.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
pub struct ParticleEmitterConfig
|
||||||
|
{
|
||||||
|
pub burst_count: u32,
|
||||||
|
pub lifetime: std::ops::Range<f32>,
|
||||||
|
pub speed: std::ops::Range<f32>,
|
||||||
|
pub direction: Option<glam::Vec3>,
|
||||||
|
pub direction_spread: f32,
|
||||||
|
pub gravity: f32,
|
||||||
|
pub size: std::ops::Range<f32>,
|
||||||
|
pub color_start: [f32; 4],
|
||||||
|
pub color_end: [f32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SpawnParticleIntent
|
||||||
|
{
|
||||||
|
pub origin: glam::Vec3,
|
||||||
|
pub config: ParticleEmitterConfig,
|
||||||
|
}
|
||||||
178
src/debug/gizmo.rs
Normal file
178
src/debug/gizmo.rs
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
use bytemuck::cast_slice;
|
||||||
|
use glam::{Mat4, Vec3};
|
||||||
|
use wgpu::util::DeviceExt;
|
||||||
|
|
||||||
|
use crate::entity::EntityHandle;
|
||||||
|
use crate::loaders::mesh::{InstanceRaw, Mesh, Vertex};
|
||||||
|
use crate::render::{self, DrawCall, Pipeline};
|
||||||
|
use crate::utility::transform::Transform;
|
||||||
|
use crate::world::Storage;
|
||||||
|
|
||||||
|
const GIZMO_DISTANCE_SCALE: f32 = 0.1;
|
||||||
|
const ARROW_BASE: f32 = 0.85;
|
||||||
|
const ARROW_SPREAD: f32 = 0.08;
|
||||||
|
|
||||||
|
pub fn create_gizmo_mesh(device: &wgpu::Device) -> Mesh
|
||||||
|
{
|
||||||
|
let vertices = vec![
|
||||||
|
Vertex {
|
||||||
|
position: [0.0, 0.0, 0.0],
|
||||||
|
normal: [1.0, 0.0, 0.0],
|
||||||
|
uv: [0.0, 0.0],
|
||||||
|
},
|
||||||
|
Vertex {
|
||||||
|
position: [1.0, 0.0, 0.0],
|
||||||
|
normal: [1.0, 0.0, 0.0],
|
||||||
|
uv: [0.0, 0.0],
|
||||||
|
},
|
||||||
|
Vertex {
|
||||||
|
position: [0.0, 0.0, 0.0],
|
||||||
|
normal: [0.0, 1.0, 0.0],
|
||||||
|
uv: [0.0, 0.0],
|
||||||
|
},
|
||||||
|
Vertex {
|
||||||
|
position: [0.0, 1.0, 0.0],
|
||||||
|
normal: [0.0, 1.0, 0.0],
|
||||||
|
uv: [0.0, 0.0],
|
||||||
|
},
|
||||||
|
Vertex {
|
||||||
|
position: [0.0, 0.0, 0.0],
|
||||||
|
normal: [0.0, 0.0, 1.0],
|
||||||
|
uv: [0.0, 0.0],
|
||||||
|
},
|
||||||
|
Vertex {
|
||||||
|
position: [0.0, 0.0, 1.0],
|
||||||
|
normal: [0.0, 0.0, 1.0],
|
||||||
|
uv: [0.0, 0.0],
|
||||||
|
},
|
||||||
|
Vertex {
|
||||||
|
position: [ARROW_BASE, ARROW_SPREAD, 0.0],
|
||||||
|
normal: [1.0, 0.0, 0.0],
|
||||||
|
uv: [0.0, 0.0],
|
||||||
|
},
|
||||||
|
Vertex {
|
||||||
|
position: [ARROW_BASE, -ARROW_SPREAD, 0.0],
|
||||||
|
normal: [1.0, 0.0, 0.0],
|
||||||
|
uv: [0.0, 0.0],
|
||||||
|
},
|
||||||
|
Vertex {
|
||||||
|
position: [ARROW_BASE, 0.0, ARROW_SPREAD],
|
||||||
|
normal: [1.0, 0.0, 0.0],
|
||||||
|
uv: [0.0, 0.0],
|
||||||
|
},
|
||||||
|
Vertex {
|
||||||
|
position: [ARROW_BASE, 0.0, -ARROW_SPREAD],
|
||||||
|
normal: [1.0, 0.0, 0.0],
|
||||||
|
uv: [0.0, 0.0],
|
||||||
|
},
|
||||||
|
Vertex {
|
||||||
|
position: [ARROW_SPREAD, ARROW_BASE, 0.0],
|
||||||
|
normal: [0.0, 1.0, 0.0],
|
||||||
|
uv: [0.0, 0.0],
|
||||||
|
},
|
||||||
|
Vertex {
|
||||||
|
position: [-ARROW_SPREAD, ARROW_BASE, 0.0],
|
||||||
|
normal: [0.0, 1.0, 0.0],
|
||||||
|
uv: [0.0, 0.0],
|
||||||
|
},
|
||||||
|
Vertex {
|
||||||
|
position: [0.0, ARROW_BASE, ARROW_SPREAD],
|
||||||
|
normal: [0.0, 1.0, 0.0],
|
||||||
|
uv: [0.0, 0.0],
|
||||||
|
},
|
||||||
|
Vertex {
|
||||||
|
position: [0.0, ARROW_BASE, -ARROW_SPREAD],
|
||||||
|
normal: [0.0, 1.0, 0.0],
|
||||||
|
uv: [0.0, 0.0],
|
||||||
|
},
|
||||||
|
Vertex {
|
||||||
|
position: [ARROW_SPREAD, 0.0, ARROW_BASE],
|
||||||
|
normal: [0.0, 0.0, 1.0],
|
||||||
|
uv: [0.0, 0.0],
|
||||||
|
},
|
||||||
|
Vertex {
|
||||||
|
position: [-ARROW_SPREAD, 0.0, ARROW_BASE],
|
||||||
|
normal: [0.0, 0.0, 1.0],
|
||||||
|
uv: [0.0, 0.0],
|
||||||
|
},
|
||||||
|
Vertex {
|
||||||
|
position: [0.0, ARROW_SPREAD, ARROW_BASE],
|
||||||
|
normal: [0.0, 0.0, 1.0],
|
||||||
|
uv: [0.0, 0.0],
|
||||||
|
},
|
||||||
|
Vertex {
|
||||||
|
position: [0.0, -ARROW_SPREAD, ARROW_BASE],
|
||||||
|
normal: [0.0, 0.0, 1.0],
|
||||||
|
uv: [0.0, 0.0],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let indices = vec![
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 1, 7, 1, 8, 1, 9, 1, 10, 3, 11, 3, 12, 3, 13, 3, 14, 5, 15, 5, 16, 5,
|
||||||
|
17, 5,
|
||||||
|
];
|
||||||
|
|
||||||
|
Mesh::new(device, &vertices, &indices)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_gizmo_instance_buffer(device: &wgpu::Device) -> wgpu::Buffer
|
||||||
|
{
|
||||||
|
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: Some("Gizmo Instance Buffer"),
|
||||||
|
contents: cast_slice(&[InstanceRaw {
|
||||||
|
model: Mat4::IDENTITY.to_cols_array_2d(),
|
||||||
|
dissolve_amount: 0.0,
|
||||||
|
_padding: [0.0; 3],
|
||||||
|
}]),
|
||||||
|
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_transform_gizmo(
|
||||||
|
entity: EntityHandle,
|
||||||
|
transforms: &Storage<Transform>,
|
||||||
|
camera_position: Vec3,
|
||||||
|
gizmo_mesh: &Mesh,
|
||||||
|
instance_buffer: &wgpu::Buffer,
|
||||||
|
) -> Vec<DrawCall>
|
||||||
|
{
|
||||||
|
let transform = match transforms.get(entity)
|
||||||
|
{
|
||||||
|
Some(t) => t,
|
||||||
|
None => return Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let distance = camera_position.distance(transform.position);
|
||||||
|
let scale = (distance * GIZMO_DISTANCE_SCALE).max(0.1);
|
||||||
|
|
||||||
|
let model = Mat4::from_scale_rotation_translation(
|
||||||
|
Vec3::splat(scale),
|
||||||
|
transform.rotation,
|
||||||
|
transform.position,
|
||||||
|
);
|
||||||
|
|
||||||
|
let instance_data = InstanceRaw {
|
||||||
|
model: model.to_cols_array_2d(),
|
||||||
|
dissolve_amount: 0.0,
|
||||||
|
_padding: [0.0; 3],
|
||||||
|
};
|
||||||
|
|
||||||
|
render::with_queue(|queue| {
|
||||||
|
queue.write_buffer(instance_buffer, 0, cast_slice(&[instance_data]));
|
||||||
|
});
|
||||||
|
|
||||||
|
vec![DrawCall {
|
||||||
|
vertex_buffer: gizmo_mesh.vertex_buffer.clone(),
|
||||||
|
index_buffer: gizmo_mesh.index_buffer.clone(),
|
||||||
|
num_indices: gizmo_mesh.num_indices,
|
||||||
|
model,
|
||||||
|
pipeline: Pipeline::GizmoLines,
|
||||||
|
instance_buffer: Some(instance_buffer.clone()),
|
||||||
|
num_instances: 1,
|
||||||
|
tile_scale: 4.0,
|
||||||
|
enable_dissolve: false,
|
||||||
|
enable_snow_light: false,
|
||||||
|
displacement_bind_group: None,
|
||||||
|
entity: None,
|
||||||
|
}]
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod collider_debug;
|
pub mod collider_debug;
|
||||||
|
pub mod gizmo;
|
||||||
pub mod mode;
|
pub mod mode;
|
||||||
|
|
||||||
pub use mode::DebugMode;
|
pub use mode::DebugMode;
|
||||||
|
|||||||
64
src/main.rs
64
src/main.rs
@@ -22,18 +22,20 @@ use crate::bundles::terrain::{TerrainBundle, TerrainConfig};
|
|||||||
use crate::bundles::test_char::TestCharBundle;
|
use crate::bundles::test_char::TestCharBundle;
|
||||||
use crate::bundles::Bundle;
|
use crate::bundles::Bundle;
|
||||||
use crate::components::intent::{FollowPlayerIntent, StopFollowingIntent};
|
use crate::components::intent::{FollowPlayerIntent, StopFollowingIntent};
|
||||||
use crate::debug::{collider_debug, DebugMode};
|
use crate::debug::{collider_debug, gizmo, DebugMode};
|
||||||
use crate::editor::{editor_loop, EditorState, FrameStats};
|
use crate::editor::{editor_loop, EditorState, FrameStats};
|
||||||
use crate::entity::EntityHandle;
|
use crate::entity::EntityHandle;
|
||||||
use crate::loaders::scene::Space;
|
use crate::loaders::scene::Space;
|
||||||
use crate::physics::PhysicsManager;
|
use crate::physics::PhysicsManager;
|
||||||
|
use crate::render::particle_types::ParticleInstanceRaw;
|
||||||
use crate::render::snow::{SnowConfig, SnowLayer};
|
use crate::render::snow::{SnowConfig, SnowLayer};
|
||||||
use crate::systems::{
|
use crate::systems::{
|
||||||
camera_follow_system, camera_ground_clamp_system, camera_input_system, camera_intent_system,
|
camera_follow_system, camera_ground_clamp_system, camera_input_system, camera_intent_system,
|
||||||
camera_noclip_system, camera_transition_system, camera_view_matrix,
|
camera_noclip_system, camera_transition_system, camera_view_matrix, collect_instances,
|
||||||
dialog_bubble_render_system, dialog_camera_system, dialog_camera_transition_system,
|
dialog_bubble_render_system, dialog_camera_system, dialog_camera_transition_system,
|
||||||
dialog_projectile_system, dialog_system, physics_sync_system, player_input_system,
|
dialog_projectile_system, dialog_system, particle_intent_system, particle_update_system,
|
||||||
render_system, rotate_system, snow_system, spotlight_sync_system, state_machine_physics_system,
|
physics_sync_system, player_input_system, render_system, rotate_system, snow_system,
|
||||||
|
spawn_snow_particles, spotlight_sync_system, state_machine_physics_system,
|
||||||
state_machine_system, tree_dissolve_update_system, tree_instance_buffer_update_system,
|
state_machine_system, tree_dissolve_update_system, tree_instance_buffer_update_system,
|
||||||
tree_occlusion_system, trigger_system,
|
tree_occlusion_system, trigger_system,
|
||||||
};
|
};
|
||||||
@@ -279,6 +281,15 @@ fn toggle_editor(game: &mut Game)
|
|||||||
.set_relative_mouse_mode(&game.window, false);
|
.set_relative_mouse_mode(&game.window, false);
|
||||||
game.editor.right_mouse_held = false;
|
game.editor.right_mouse_held = false;
|
||||||
game.input_state.mouse_captured = false;
|
game.input_state.mouse_captured = false;
|
||||||
|
if game.world.gizmo_mesh.is_none()
|
||||||
|
{
|
||||||
|
game.world.gizmo_mesh = Some(render::with_device(|device| {
|
||||||
|
gizmo::create_gizmo_mesh(device)
|
||||||
|
}));
|
||||||
|
game.world.gizmo_instance_buffer = Some(render::with_device(|device| {
|
||||||
|
gizmo::create_gizmo_instance_buffer(device)
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -311,7 +322,13 @@ fn handle_editor_pick(game: &mut Game, x: f32, y: f32)
|
|||||||
render::set_selected_entity(game.editor.selected_entity);
|
render::set_selected_entity(game.editor.selected_entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn submit_frame(game: &mut Game, draw_calls: &[render::DrawCall], time: f32, delta: f32)
|
fn submit_frame(
|
||||||
|
game: &mut Game,
|
||||||
|
draw_calls: &[render::DrawCall],
|
||||||
|
particle_instances: &[ParticleInstanceRaw],
|
||||||
|
time: f32,
|
||||||
|
delta: f32,
|
||||||
|
)
|
||||||
{
|
{
|
||||||
let (camera_entity, camera_component) = match game.world.active_camera()
|
let (camera_entity, camera_component) = match game.world.active_camera()
|
||||||
{
|
{
|
||||||
@@ -344,6 +361,7 @@ fn submit_frame(game: &mut Game, draw_calls: &[render::DrawCall], time: f32, del
|
|||||||
draw_calls,
|
draw_calls,
|
||||||
&billboard_calls,
|
&billboard_calls,
|
||||||
&text_vertices,
|
&text_vertices,
|
||||||
|
particle_instances,
|
||||||
time,
|
time,
|
||||||
delta,
|
delta,
|
||||||
game.world.debug_mode,
|
game.world.debug_mode,
|
||||||
@@ -448,6 +466,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
|
|||||||
|
|
||||||
snow_system(&mut game.world);
|
snow_system(&mut game.world);
|
||||||
|
|
||||||
|
particle_intent_system(&mut game.world);
|
||||||
|
particle_update_system(&mut game.world, delta);
|
||||||
|
let particle_cam_pos = game.world.active_camera_position();
|
||||||
|
if let Some(ref mut buffers) = game.world.particle_buffers
|
||||||
|
{
|
||||||
|
spawn_snow_particles(buffers, particle_cam_pos, delta);
|
||||||
|
}
|
||||||
|
|
||||||
// --- draw call collection ---
|
// --- draw call collection ---
|
||||||
let mut draw_calls = render_system(&game.world);
|
let mut draw_calls = render_system(&game.world);
|
||||||
if let Some(ref snow_layer) = game.world.snow_layer
|
if let Some(ref snow_layer) = game.world.snow_layer
|
||||||
@@ -460,12 +486,38 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
|
|||||||
draw_calls.extend(collider_debug::render_collider_debug());
|
draw_calls.extend(collider_debug::render_collider_debug());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if game.editor.active
|
||||||
|
{
|
||||||
|
if let Some(entity) = game.editor.selected_entity
|
||||||
|
{
|
||||||
|
let cam_pos = game.world.active_camera_position();
|
||||||
|
if let (Some(ref mesh), Some(ref buf)) =
|
||||||
|
(&game.world.gizmo_mesh, &game.world.gizmo_instance_buffer)
|
||||||
|
{
|
||||||
|
draw_calls.extend(gizmo::render_transform_gizmo(
|
||||||
|
entity,
|
||||||
|
&game.world.transforms,
|
||||||
|
cam_pos,
|
||||||
|
mesh,
|
||||||
|
buf,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
game.stats.draw_call_count = draw_calls.len();
|
game.stats.draw_call_count = draw_calls.len();
|
||||||
game.stats.fps = 1.0 / delta;
|
game.stats.fps = 1.0 / delta;
|
||||||
game.stats.frame_ms = delta * 1000.0;
|
game.stats.frame_ms = delta * 1000.0;
|
||||||
|
|
||||||
|
let particle_instances: Vec<ParticleInstanceRaw> = game
|
||||||
|
.world
|
||||||
|
.particle_buffers
|
||||||
|
.as_mut()
|
||||||
|
.map(|b| collect_instances(b).to_vec())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
// --- render ---
|
// --- render ---
|
||||||
submit_frame(&mut game, &draw_calls, time, delta);
|
submit_frame(&mut game, &draw_calls, &particle_instances, time, delta);
|
||||||
|
|
||||||
// --- end frame ---
|
// --- end frame ---
|
||||||
game.input_state.clear_just_pressed();
|
game.input_state.clear_just_pressed();
|
||||||
|
|||||||
@@ -102,6 +102,11 @@ pub mod shaders
|
|||||||
format!("{}/text.wgsl", SHADERS_DIR)
|
format!("{}/text.wgsl", SHADERS_DIR)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn particle() -> String
|
||||||
|
{
|
||||||
|
format!("{}/particle.wgsl", SHADERS_DIR)
|
||||||
|
}
|
||||||
|
|
||||||
pub const SHADOW_PACKAGE: &str = "package::shadow";
|
pub const SHADOW_PACKAGE: &str = "package::shadow";
|
||||||
pub const MAIN_PACKAGE: &str = "package::main";
|
pub const MAIN_PACKAGE: &str = "package::main";
|
||||||
pub const SNOW_LIGHT_ACCUMULATION_PACKAGE: &str = "package::snow_light_accumulation";
|
pub const SNOW_LIGHT_ACCUMULATION_PACKAGE: &str = "package::snow_light_accumulation";
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use std::cell::RefCell;
|
|||||||
use crate::debug::DebugMode;
|
use crate::debug::DebugMode;
|
||||||
use crate::entity::EntityHandle;
|
use crate::entity::EntityHandle;
|
||||||
|
|
||||||
|
use super::particle_types::ParticleInstanceRaw;
|
||||||
use super::text_pipeline::TextVertex;
|
use super::text_pipeline::TextVertex;
|
||||||
use super::{BillboardDrawCall, DrawCall, FontAtlas, Renderer, Spotlight};
|
use super::{BillboardDrawCall, DrawCall, FontAtlas, Renderer, Spotlight};
|
||||||
|
|
||||||
@@ -115,6 +116,7 @@ pub fn render(
|
|||||||
draw_calls: &[DrawCall],
|
draw_calls: &[DrawCall],
|
||||||
billboard_calls: &[BillboardDrawCall],
|
billboard_calls: &[BillboardDrawCall],
|
||||||
text_vertices: &[TextVertex],
|
text_vertices: &[TextVertex],
|
||||||
|
particle_instances: &[ParticleInstanceRaw],
|
||||||
time: f32,
|
time: f32,
|
||||||
delta_time: f32,
|
delta_time: f32,
|
||||||
debug_mode: DebugMode,
|
debug_mode: DebugMode,
|
||||||
@@ -129,6 +131,7 @@ pub fn render(
|
|||||||
draw_calls,
|
draw_calls,
|
||||||
billboard_calls,
|
billboard_calls,
|
||||||
text_vertices,
|
text_vertices,
|
||||||
|
particle_instances,
|
||||||
time,
|
time,
|
||||||
delta_time,
|
delta_time,
|
||||||
debug_mode,
|
debug_mode,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ mod types;
|
|||||||
|
|
||||||
pub mod billboard;
|
pub mod billboard;
|
||||||
pub mod font_atlas;
|
pub mod font_atlas;
|
||||||
|
pub mod particle_pipeline;
|
||||||
|
pub mod particle_types;
|
||||||
pub mod snow;
|
pub mod snow;
|
||||||
pub mod snow_light;
|
pub mod snow_light;
|
||||||
pub mod text_pipeline;
|
pub mod text_pipeline;
|
||||||
@@ -18,6 +20,8 @@ pub use global::{
|
|||||||
set_terrain_data, update_spotlights, with_device, with_font_atlas, with_queue,
|
set_terrain_data, update_spotlights, with_device, with_font_atlas, with_queue,
|
||||||
with_surface_format,
|
with_surface_format,
|
||||||
};
|
};
|
||||||
|
pub use particle_pipeline::ParticlePipeline;
|
||||||
|
pub use particle_types::ParticleInstanceRaw;
|
||||||
pub use text_pipeline::TextVertex;
|
pub use text_pipeline::TextVertex;
|
||||||
pub use types::{DrawCall, Pipeline, Spotlight, SpotlightRaw, Uniforms, MAX_SPOTLIGHTS};
|
pub use types::{DrawCall, Pipeline, Spotlight, SpotlightRaw, Uniforms, MAX_SPOTLIGHTS};
|
||||||
|
|
||||||
@@ -28,8 +32,8 @@ use crate::paths;
|
|||||||
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::{
|
use pipeline::{
|
||||||
create_debug_lines_pipeline, create_main_pipeline, create_snow_clipmap_pipeline,
|
create_debug_lines_pipeline, create_gizmo_lines_pipeline, create_main_pipeline,
|
||||||
create_wireframe_pipeline,
|
create_snow_clipmap_pipeline, create_wireframe_pipeline,
|
||||||
};
|
};
|
||||||
use std::num::NonZeroU64;
|
use std::num::NonZeroU64;
|
||||||
|
|
||||||
@@ -49,8 +53,10 @@ pub struct Renderer
|
|||||||
snow_clipmap_pipeline: wgpu::RenderPipeline,
|
snow_clipmap_pipeline: wgpu::RenderPipeline,
|
||||||
wireframe_pipeline: Option<wgpu::RenderPipeline>,
|
wireframe_pipeline: Option<wgpu::RenderPipeline>,
|
||||||
debug_lines_pipeline: Option<wgpu::RenderPipeline>,
|
debug_lines_pipeline: Option<wgpu::RenderPipeline>,
|
||||||
|
gizmo_lines_pipeline: Option<wgpu::RenderPipeline>,
|
||||||
debug_overlay: Option<debug_overlay::DebugOverlay>,
|
debug_overlay: Option<debug_overlay::DebugOverlay>,
|
||||||
billboard_pipeline: BillboardPipeline,
|
billboard_pipeline: BillboardPipeline,
|
||||||
|
particle_pipeline: ParticlePipeline,
|
||||||
font_atlas: font_atlas::FontAtlas,
|
font_atlas: font_atlas::FontAtlas,
|
||||||
text_pipeline: text_pipeline::TextPipeline,
|
text_pipeline: text_pipeline::TextPipeline,
|
||||||
wireframe_supported: bool,
|
wireframe_supported: bool,
|
||||||
@@ -526,7 +532,14 @@ impl Renderer
|
|||||||
&bind_group_layout,
|
&bind_group_layout,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
let gizmo_lines_pipeline = Some(create_gizmo_lines_pipeline(
|
||||||
|
&device,
|
||||||
|
config.format,
|
||||||
|
&bind_group_layout,
|
||||||
|
));
|
||||||
|
|
||||||
let billboard_pipeline = BillboardPipeline::new(&device, config.format);
|
let billboard_pipeline = BillboardPipeline::new(&device, config.format);
|
||||||
|
let particle_pipeline = ParticlePipeline::new(&device, config.format);
|
||||||
let font_atlas = font_atlas::FontAtlas::load(&device, &queue);
|
let font_atlas = font_atlas::FontAtlas::load(&device, &queue);
|
||||||
let text_pipeline = text_pipeline::TextPipeline::new(&device, config.format, &font_atlas);
|
let text_pipeline = text_pipeline::TextPipeline::new(&device, config.format, &font_atlas);
|
||||||
|
|
||||||
@@ -558,8 +571,10 @@ impl Renderer
|
|||||||
snow_clipmap_pipeline,
|
snow_clipmap_pipeline,
|
||||||
wireframe_pipeline,
|
wireframe_pipeline,
|
||||||
debug_lines_pipeline,
|
debug_lines_pipeline,
|
||||||
|
gizmo_lines_pipeline,
|
||||||
debug_overlay,
|
debug_overlay,
|
||||||
billboard_pipeline,
|
billboard_pipeline,
|
||||||
|
particle_pipeline,
|
||||||
font_atlas,
|
font_atlas,
|
||||||
text_pipeline,
|
text_pipeline,
|
||||||
wireframe_supported,
|
wireframe_supported,
|
||||||
@@ -607,6 +622,7 @@ impl Renderer
|
|||||||
draw_calls: &[DrawCall],
|
draw_calls: &[DrawCall],
|
||||||
billboard_calls: &[BillboardDrawCall],
|
billboard_calls: &[BillboardDrawCall],
|
||||||
text_vertices: &[text_pipeline::TextVertex],
|
text_vertices: &[text_pipeline::TextVertex],
|
||||||
|
particle_instances: &[ParticleInstanceRaw],
|
||||||
time: f32,
|
time: f32,
|
||||||
delta_time: f32,
|
delta_time: f32,
|
||||||
debug_mode: DebugMode,
|
debug_mode: DebugMode,
|
||||||
@@ -732,6 +748,10 @@ impl Renderer
|
|||||||
.debug_lines_pipeline
|
.debug_lines_pipeline
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap_or(&self.standard_pipeline),
|
.unwrap_or(&self.standard_pipeline),
|
||||||
|
Pipeline::GizmoLines => self
|
||||||
|
.gizmo_lines_pipeline
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&self.standard_pipeline),
|
||||||
};
|
};
|
||||||
render_pass.set_pipeline(pipeline);
|
render_pass.set_pipeline(pipeline);
|
||||||
render_pass.set_bind_group(0, &self.bind_group, &[offset as u32]);
|
render_pass.set_bind_group(0, &self.bind_group, &[offset as u32]);
|
||||||
@@ -762,7 +782,7 @@ impl Renderer
|
|||||||
{
|
{
|
||||||
if matches!(
|
if matches!(
|
||||||
draw_call.pipeline,
|
draw_call.pipeline,
|
||||||
Pipeline::SnowClipmap | Pipeline::DebugLines
|
Pipeline::SnowClipmap | Pipeline::DebugLines | Pipeline::GizmoLines
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -856,7 +876,7 @@ impl Renderer
|
|||||||
|
|
||||||
if matches!(
|
if matches!(
|
||||||
draw_call.pipeline,
|
draw_call.pipeline,
|
||||||
Pipeline::SnowClipmap | Pipeline::DebugLines
|
Pipeline::SnowClipmap | Pipeline::DebugLines | Pipeline::GizmoLines
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -1068,16 +1088,29 @@ impl Renderer
|
|||||||
billboard_calls,
|
billboard_calls,
|
||||||
);
|
);
|
||||||
|
|
||||||
let view_proj = (*projection * *view).to_cols_array_2d();
|
let view_proj_mat = *projection * *view;
|
||||||
self.text_pipeline.render(
|
self.text_pipeline.render(
|
||||||
&mut overlay_encoder,
|
&mut overlay_encoder,
|
||||||
&self.queue,
|
&self.queue,
|
||||||
&screen_view,
|
&screen_view,
|
||||||
&self.fullres_depth_view,
|
&self.fullres_depth_view,
|
||||||
text_vertices,
|
text_vertices,
|
||||||
view_proj,
|
view_proj_mat.to_cols_array_2d(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if !particle_instances.is_empty()
|
||||||
|
{
|
||||||
|
self.particle_pipeline.render(
|
||||||
|
&mut overlay_encoder,
|
||||||
|
&self.queue,
|
||||||
|
&screen_view,
|
||||||
|
&self.fullres_depth_view,
|
||||||
|
particle_instances,
|
||||||
|
view_proj_mat,
|
||||||
|
*view,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
self.queue.submit(std::iter::once(overlay_encoder.finish()));
|
self.queue.submit(std::iter::once(overlay_encoder.finish()));
|
||||||
frame
|
frame
|
||||||
}
|
}
|
||||||
|
|||||||
241
src/render/particle_pipeline.rs
Normal file
241
src/render/particle_pipeline.rs
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
use std::mem::size_of;
|
||||||
|
use std::num::NonZeroU64;
|
||||||
|
|
||||||
|
use bytemuck::{Pod, Zeroable};
|
||||||
|
use wgpu::util::DeviceExt;
|
||||||
|
|
||||||
|
use crate::paths;
|
||||||
|
use crate::render::particle_types::{ParticleInstanceRaw, ParticleVertex, MAX_PARTICLES};
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Pod, Zeroable)]
|
||||||
|
struct ParticleUniforms
|
||||||
|
{
|
||||||
|
view_proj: [[f32; 4]; 4],
|
||||||
|
camera_right: [f32; 3],
|
||||||
|
_pad0: f32,
|
||||||
|
camera_up: [f32; 3],
|
||||||
|
_pad1: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ParticlePipeline
|
||||||
|
{
|
||||||
|
pipeline: wgpu::RenderPipeline,
|
||||||
|
vertex_buffer: wgpu::Buffer,
|
||||||
|
index_buffer: wgpu::Buffer,
|
||||||
|
instance_buffer: wgpu::Buffer,
|
||||||
|
uniform_buffer: wgpu::Buffer,
|
||||||
|
bind_group: wgpu::BindGroup,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParticlePipeline
|
||||||
|
{
|
||||||
|
pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self
|
||||||
|
{
|
||||||
|
let shader_source = std::fs::read_to_string(&paths::shaders::particle())
|
||||||
|
.expect("Failed to read particle.wgsl");
|
||||||
|
|
||||||
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: Some("Particle Shader"),
|
||||||
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
||||||
|
});
|
||||||
|
|
||||||
|
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
label: Some("Particle Bind Group Layout"),
|
||||||
|
entries: &[wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 0,
|
||||||
|
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Buffer {
|
||||||
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: NonZeroU64::new(size_of::<ParticleUniforms>() as u64),
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("Particle Uniform Buffer"),
|
||||||
|
size: 256,
|
||||||
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some("Particle Bind Group"),
|
||||||
|
layout: &bind_group_layout,
|
||||||
|
entries: &[wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: uniform_buffer.as_entire_binding(),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
let vertices: [ParticleVertex; 4] = [
|
||||||
|
ParticleVertex {
|
||||||
|
position: [-0.5, 0.5, 0.0],
|
||||||
|
uv: [0.0, 0.0],
|
||||||
|
},
|
||||||
|
ParticleVertex {
|
||||||
|
position: [0.5, 0.5, 0.0],
|
||||||
|
uv: [1.0, 0.0],
|
||||||
|
},
|
||||||
|
ParticleVertex {
|
||||||
|
position: [0.5, -0.5, 0.0],
|
||||||
|
uv: [1.0, 1.0],
|
||||||
|
},
|
||||||
|
ParticleVertex {
|
||||||
|
position: [-0.5, -0.5, 0.0],
|
||||||
|
uv: [0.0, 1.0],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: Some("Particle Vertex Buffer"),
|
||||||
|
contents: bytemuck::cast_slice(&vertices),
|
||||||
|
usage: wgpu::BufferUsages::VERTEX,
|
||||||
|
});
|
||||||
|
|
||||||
|
let indices: [u16; 6] = [0, 1, 2, 2, 3, 0];
|
||||||
|
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: Some("Particle Index Buffer"),
|
||||||
|
contents: bytemuck::cast_slice(&indices),
|
||||||
|
usage: wgpu::BufferUsages::INDEX,
|
||||||
|
});
|
||||||
|
|
||||||
|
let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("Particle Instance Buffer"),
|
||||||
|
size: (MAX_PARTICLES * size_of::<ParticleInstanceRaw>()) as wgpu::BufferAddress,
|
||||||
|
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some("Particle Pipeline Layout"),
|
||||||
|
bind_group_layouts: &[&bind_group_layout],
|
||||||
|
immediate_size: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("Particle Pipeline"),
|
||||||
|
layout: Some(&pipeline_layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("vs_main"),
|
||||||
|
buffers: &[ParticleVertex::desc(), ParticleInstanceRaw::desc()],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("fs_main"),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format,
|
||||||
|
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||||
|
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: Some(wgpu::DepthStencilState {
|
||||||
|
format: wgpu::TextureFormat::Depth32Float,
|
||||||
|
depth_write_enabled: false,
|
||||||
|
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||||
|
stencil: wgpu::StencilState::default(),
|
||||||
|
bias: wgpu::DepthBiasState::default(),
|
||||||
|
}),
|
||||||
|
multisample: wgpu::MultisampleState {
|
||||||
|
count: 1,
|
||||||
|
mask: !0,
|
||||||
|
alpha_to_coverage_enabled: false,
|
||||||
|
},
|
||||||
|
multiview_mask: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
pipeline,
|
||||||
|
vertex_buffer,
|
||||||
|
index_buffer,
|
||||||
|
instance_buffer,
|
||||||
|
uniform_buffer,
|
||||||
|
bind_group,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(
|
||||||
|
&self,
|
||||||
|
encoder: &mut wgpu::CommandEncoder,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
color_view: &wgpu::TextureView,
|
||||||
|
depth_view: &wgpu::TextureView,
|
||||||
|
instances: &[ParticleInstanceRaw],
|
||||||
|
view_proj: glam::Mat4,
|
||||||
|
view: glam::Mat4,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if instances.is_empty()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let num_instances = instances.len().min(MAX_PARTICLES);
|
||||||
|
queue.write_buffer(
|
||||||
|
&self.instance_buffer,
|
||||||
|
0,
|
||||||
|
bytemuck::cast_slice(&instances[..num_instances]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let inv_view = view.inverse();
|
||||||
|
let camera_right = inv_view.x_axis.truncate();
|
||||||
|
let camera_up = inv_view.y_axis.truncate();
|
||||||
|
|
||||||
|
let uniforms = ParticleUniforms {
|
||||||
|
view_proj: view_proj.to_cols_array_2d(),
|
||||||
|
camera_right: camera_right.into(),
|
||||||
|
_pad0: 0.0,
|
||||||
|
camera_up: camera_up.into(),
|
||||||
|
_pad1: 0.0,
|
||||||
|
};
|
||||||
|
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
label: Some("Particle Pass"),
|
||||||
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
|
view: color_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: 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,
|
||||||
|
});
|
||||||
|
|
||||||
|
pass.set_pipeline(&self.pipeline);
|
||||||
|
pass.set_bind_group(0, &self.bind_group, &[]);
|
||||||
|
pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
|
||||||
|
pass.set_vertex_buffer(1, self.instance_buffer.slice(..));
|
||||||
|
pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
|
||||||
|
pass.draw_indexed(0..6, 0, 0..num_instances as u32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
89
src/render/particle_types.rs
Normal file
89
src/render/particle_types.rs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
use bytemuck::{Pod, Zeroable};
|
||||||
|
|
||||||
|
pub const MAX_PARTICLES: usize = 4096;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Pod, Zeroable)]
|
||||||
|
pub struct ParticleVertex
|
||||||
|
{
|
||||||
|
pub position: [f32; 3],
|
||||||
|
pub uv: [f32; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParticleVertex
|
||||||
|
{
|
||||||
|
pub fn desc() -> wgpu::VertexBufferLayout<'static>
|
||||||
|
{
|
||||||
|
wgpu::VertexBufferLayout {
|
||||||
|
array_stride: std::mem::size_of::<ParticleVertex>() as wgpu::BufferAddress,
|
||||||
|
step_mode: wgpu::VertexStepMode::Vertex,
|
||||||
|
attributes: &[
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: 0,
|
||||||
|
shader_location: 0,
|
||||||
|
format: wgpu::VertexFormat::Float32x3,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
|
||||||
|
shader_location: 1,
|
||||||
|
format: wgpu::VertexFormat::Float32x2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Pod, Zeroable)]
|
||||||
|
pub struct ParticleInstanceRaw
|
||||||
|
{
|
||||||
|
pub position: [f32; 3],
|
||||||
|
pub velocity: [f32; 3],
|
||||||
|
pub size: f32,
|
||||||
|
pub color: [f32; 4],
|
||||||
|
pub age: f32,
|
||||||
|
pub _padding: [f32; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
const _: () = assert!(std::mem::size_of::<ParticleInstanceRaw>() == 60);
|
||||||
|
|
||||||
|
impl ParticleInstanceRaw
|
||||||
|
{
|
||||||
|
pub fn desc() -> wgpu::VertexBufferLayout<'static>
|
||||||
|
{
|
||||||
|
wgpu::VertexBufferLayout {
|
||||||
|
array_stride: std::mem::size_of::<ParticleInstanceRaw>() as wgpu::BufferAddress,
|
||||||
|
step_mode: wgpu::VertexStepMode::Instance,
|
||||||
|
attributes: &[
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: 0,
|
||||||
|
shader_location: 2,
|
||||||
|
format: wgpu::VertexFormat::Float32x3,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
|
||||||
|
shader_location: 3,
|
||||||
|
format: wgpu::VertexFormat::Float32x3,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress * 2,
|
||||||
|
shader_location: 4,
|
||||||
|
format: wgpu::VertexFormat::Float32,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress * 2
|
||||||
|
+ std::mem::size_of::<f32>() as wgpu::BufferAddress,
|
||||||
|
shader_location: 5,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress * 2
|
||||||
|
+ std::mem::size_of::<f32>() as wgpu::BufferAddress
|
||||||
|
+ std::mem::size_of::<[f32; 4]>() as wgpu::BufferAddress,
|
||||||
|
shader_location: 6,
|
||||||
|
format: wgpu::VertexFormat::Float32,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,92 @@
|
|||||||
use crate::paths;
|
use crate::paths;
|
||||||
use wesl::Wesl;
|
use wesl::Wesl;
|
||||||
|
|
||||||
|
fn create_lines_pipeline(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
format: wgpu::TextureFormat,
|
||||||
|
bind_group_layout: &wgpu::BindGroupLayout,
|
||||||
|
fragment_entry_point: &str,
|
||||||
|
label_prefix: &str,
|
||||||
|
) -> wgpu::RenderPipeline
|
||||||
|
{
|
||||||
|
let compiler = Wesl::new(&paths::SHADERS_DIR);
|
||||||
|
let shader_source = compiler
|
||||||
|
.compile(&paths::shaders::MAIN_PACKAGE.parse().unwrap())
|
||||||
|
.inspect_err(|e| eprintln!("WESL error: {e}"))
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let shader_label = format!("{label_prefix} Shader");
|
||||||
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: Some(&shader_label),
|
||||||
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
||||||
|
});
|
||||||
|
|
||||||
|
let layout_label = format!("{label_prefix} Pipeline Layout");
|
||||||
|
let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some(&layout_label),
|
||||||
|
bind_group_layouts: &[bind_group_layout],
|
||||||
|
immediate_size: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
let pipeline_label = format!("{label_prefix} Pipeline");
|
||||||
|
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some(&pipeline_label),
|
||||||
|
layout: Some(&render_pipeline_layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("vs_main"),
|
||||||
|
buffers: &[
|
||||||
|
crate::loaders::mesh::Vertex::desc(),
|
||||||
|
crate::loaders::mesh::InstanceRaw::desc(),
|
||||||
|
],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some(fragment_entry_point),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format,
|
||||||
|
blend: Some(wgpu::BlendState::REPLACE),
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology: wgpu::PrimitiveTopology::LineList,
|
||||||
|
strip_index_format: None,
|
||||||
|
front_face: wgpu::FrontFace::Ccw,
|
||||||
|
cull_mode: None,
|
||||||
|
polygon_mode: wgpu::PolygonMode::Fill,
|
||||||
|
unclipped_depth: false,
|
||||||
|
conservative: false,
|
||||||
|
},
|
||||||
|
depth_stencil: Some(wgpu::DepthStencilState {
|
||||||
|
format: wgpu::TextureFormat::Depth32Float,
|
||||||
|
depth_write_enabled: false,
|
||||||
|
depth_compare: wgpu::CompareFunction::Always,
|
||||||
|
stencil: wgpu::StencilState::default(),
|
||||||
|
bias: wgpu::DepthBiasState::default(),
|
||||||
|
}),
|
||||||
|
multisample: wgpu::MultisampleState {
|
||||||
|
count: 1,
|
||||||
|
mask: !0,
|
||||||
|
alpha_to_coverage_enabled: false,
|
||||||
|
},
|
||||||
|
multiview_mask: None,
|
||||||
|
cache: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_gizmo_lines_pipeline(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
format: wgpu::TextureFormat,
|
||||||
|
bind_group_layout: &wgpu::BindGroupLayout,
|
||||||
|
) -> wgpu::RenderPipeline
|
||||||
|
{
|
||||||
|
create_lines_pipeline(device, format, bind_group_layout, "fs_gizmo", "Gizmo Lines")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn create_shadow_pipeline(
|
pub fn create_shadow_pipeline(
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
bind_group_layout: &wgpu::BindGroupLayout,
|
bind_group_layout: &wgpu::BindGroupLayout,
|
||||||
@@ -215,70 +301,7 @@ pub fn create_debug_lines_pipeline(
|
|||||||
bind_group_layout: &wgpu::BindGroupLayout,
|
bind_group_layout: &wgpu::BindGroupLayout,
|
||||||
) -> wgpu::RenderPipeline
|
) -> wgpu::RenderPipeline
|
||||||
{
|
{
|
||||||
let compiler = Wesl::new(&paths::SHADERS_DIR);
|
create_lines_pipeline(device, format, bind_group_layout, "fs_main", "Debug Lines")
|
||||||
let shader_source = compiler
|
|
||||||
.compile(&paths::shaders::MAIN_PACKAGE.parse().unwrap())
|
|
||||||
.inspect_err(|e| eprintln!("WESL error: {e}"))
|
|
||||||
.unwrap()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
||||||
label: Some("Debug Lines Shader"),
|
|
||||||
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
|
||||||
});
|
|
||||||
|
|
||||||
let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
|
||||||
label: Some("Debug Lines Pipeline Layout"),
|
|
||||||
bind_group_layouts: &[bind_group_layout],
|
|
||||||
immediate_size: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
|
||||||
label: Some("Debug Lines Pipeline"),
|
|
||||||
layout: Some(&render_pipeline_layout),
|
|
||||||
vertex: wgpu::VertexState {
|
|
||||||
module: &shader,
|
|
||||||
entry_point: Some("vs_main"),
|
|
||||||
buffers: &[
|
|
||||||
crate::loaders::mesh::Vertex::desc(),
|
|
||||||
crate::loaders::mesh::InstanceRaw::desc(),
|
|
||||||
],
|
|
||||||
compilation_options: Default::default(),
|
|
||||||
},
|
|
||||||
fragment: Some(wgpu::FragmentState {
|
|
||||||
module: &shader,
|
|
||||||
entry_point: Some("fs_main"),
|
|
||||||
targets: &[Some(wgpu::ColorTargetState {
|
|
||||||
format,
|
|
||||||
blend: Some(wgpu::BlendState::REPLACE),
|
|
||||||
write_mask: wgpu::ColorWrites::ALL,
|
|
||||||
})],
|
|
||||||
compilation_options: Default::default(),
|
|
||||||
}),
|
|
||||||
primitive: wgpu::PrimitiveState {
|
|
||||||
topology: wgpu::PrimitiveTopology::LineList,
|
|
||||||
strip_index_format: None,
|
|
||||||
front_face: wgpu::FrontFace::Ccw,
|
|
||||||
cull_mode: None,
|
|
||||||
polygon_mode: wgpu::PolygonMode::Fill,
|
|
||||||
unclipped_depth: false,
|
|
||||||
conservative: false,
|
|
||||||
},
|
|
||||||
depth_stencil: Some(wgpu::DepthStencilState {
|
|
||||||
format: wgpu::TextureFormat::Depth32Float,
|
|
||||||
depth_write_enabled: false,
|
|
||||||
depth_compare: wgpu::CompareFunction::Always,
|
|
||||||
stencil: wgpu::StencilState::default(),
|
|
||||||
bias: wgpu::DepthBiasState::default(),
|
|
||||||
}),
|
|
||||||
multisample: wgpu::MultisampleState {
|
|
||||||
count: 1,
|
|
||||||
mask: !0,
|
|
||||||
alpha_to_coverage_enabled: false,
|
|
||||||
},
|
|
||||||
multiview_mask: None,
|
|
||||||
cache: None,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_snow_clipmap_pipeline(
|
pub fn create_snow_clipmap_pipeline(
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ pub enum Pipeline
|
|||||||
Standard,
|
Standard,
|
||||||
SnowClipmap,
|
SnowClipmap,
|
||||||
DebugLines,
|
DebugLines,
|
||||||
|
GizmoLines,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DrawCall
|
pub struct DrawCall
|
||||||
|
|||||||
@@ -81,6 +81,15 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fragment shader for transform gizmo lines.
|
||||||
|
/// Outputs `world_normal` directly as the RGB color — the vertex data encodes
|
||||||
|
/// axis color (X=red, Y=green, Z=blue) in the normal attribute rather than
|
||||||
|
/// storing a true geometric normal.
|
||||||
|
@fragment
|
||||||
|
fn fs_gizmo(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
return vec4(in.world_normal, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
@group(1) @binding(0)
|
@group(1) @binding(0)
|
||||||
var heightmap_texture: texture_2d<f32>;
|
var heightmap_texture: texture_2d<f32>;
|
||||||
|
|
||||||
|
|||||||
81
src/shaders/particle.wgsl
Normal file
81
src/shaders/particle.wgsl
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
struct ParticleUniforms
|
||||||
|
{
|
||||||
|
view_proj: mat4x4<f32>,
|
||||||
|
camera_right: vec3<f32>,
|
||||||
|
_pad0: f32,
|
||||||
|
camera_up: vec3<f32>,
|
||||||
|
_pad1: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
@group(0) @binding(0)
|
||||||
|
var<uniform> uniforms: ParticleUniforms;
|
||||||
|
|
||||||
|
struct VertexIn
|
||||||
|
{
|
||||||
|
@location(0) position: vec3<f32>,
|
||||||
|
@location(1) uv: vec2<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ParticleInstance
|
||||||
|
{
|
||||||
|
@location(2) position: vec3<f32>,
|
||||||
|
@location(3) velocity: vec3<f32>,
|
||||||
|
@location(4) size: f32,
|
||||||
|
@location(5) color: vec4<f32>,
|
||||||
|
@location(6) age: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VertexOut
|
||||||
|
{
|
||||||
|
@builtin(position) clip_pos: vec4<f32>,
|
||||||
|
@location(0) uv: vec2<f32>,
|
||||||
|
@location(1) color: vec4<f32>,
|
||||||
|
@location(2) age: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(in: VertexIn, inst: ParticleInstance) -> VertexOut
|
||||||
|
{
|
||||||
|
var out: VertexOut;
|
||||||
|
|
||||||
|
let stretch = length(inst.velocity) * 0.05;
|
||||||
|
let vel_dir = normalize(select(inst.velocity, vec3<f32>(0.0, 1.0, 0.0),
|
||||||
|
length(inst.velocity) > 0.001));
|
||||||
|
|
||||||
|
let world_pos = inst.position
|
||||||
|
+ uniforms.camera_right * (in.uv.x - 0.5) * inst.size
|
||||||
|
+ uniforms.camera_up * (in.uv.y - 0.5) * inst.size
|
||||||
|
+ vel_dir * (in.uv.y - 0.5) * stretch;
|
||||||
|
|
||||||
|
out.clip_pos = uniforms.view_proj * vec4<f32>(world_pos, 1.0);
|
||||||
|
out.uv = in.uv;
|
||||||
|
out.color = inst.color;
|
||||||
|
out.age = inst.age;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
var<private> bayer: array<f32, 16> = array<f32, 16>(
|
||||||
|
0.0, 8.0, 2.0, 10.0,
|
||||||
|
12.0, 4.0, 14.0, 6.0,
|
||||||
|
3.0, 11.0, 1.0, 9.0,
|
||||||
|
15.0, 7.0, 13.0, 5.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOut) -> @location(0) vec4<f32>
|
||||||
|
{
|
||||||
|
let centered = in.uv * 2.0 - vec2<f32>(1.0, 1.0);
|
||||||
|
let dist = length(centered);
|
||||||
|
let circle = 1.0 - smoothstep(0.7, 1.0, dist);
|
||||||
|
let age_alpha = (1.0 - in.age) * in.color.a * circle;
|
||||||
|
|
||||||
|
let fx = u32(in.clip_pos.x) % 4u;
|
||||||
|
let fy = u32(in.clip_pos.y) % 4u;
|
||||||
|
let thresh = bayer[fy * 4u + fx] / 16.0;
|
||||||
|
if age_alpha <= thresh
|
||||||
|
{
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
return vec4<f32>(in.color.rgb, 1.0);
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
|
use std::f32::consts::PI;
|
||||||
|
|
||||||
use glam::Vec3;
|
use glam::Vec3;
|
||||||
|
|
||||||
use crate::components::dialog::{DialogOutcome, DialogOutcomeEvent, ParryButton};
|
use crate::components::dialog::{DialogOutcome, DialogOutcomeEvent, ParryButton};
|
||||||
|
use crate::components::particle::{ParticleEmitterConfig, SpawnParticleIntent};
|
||||||
use crate::entity::EntityHandle;
|
use crate::entity::EntityHandle;
|
||||||
|
|
||||||
use crate::utility::input::InputState;
|
use crate::utility::input::InputState;
|
||||||
use crate::world::World;
|
use crate::world::World;
|
||||||
|
|
||||||
@@ -104,6 +106,15 @@ pub fn dialog_projectile_system(world: &mut World, input_state: &InputState)
|
|||||||
world.transforms.with_mut(proj_entity, |t| {
|
world.transforms.with_mut(proj_entity, |t| {
|
||||||
t.position += direction * PROJECTILE_SPEED * (1.0 / 60.0);
|
t.position += direction * PROJECTILE_SPEED * (1.0 / 60.0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let proj_pos = world.transforms.with(proj_entity, |t| t.position);
|
||||||
|
if let Some(pos) = proj_pos
|
||||||
|
{
|
||||||
|
world.spawn_particle_intents.push(SpawnParticleIntent {
|
||||||
|
origin: pos,
|
||||||
|
config: projectile_swarm_config(),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for entity in to_despawn
|
for entity in to_despawn
|
||||||
@@ -175,6 +186,21 @@ fn resolve_parry(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn projectile_swarm_config() -> ParticleEmitterConfig
|
||||||
|
{
|
||||||
|
ParticleEmitterConfig {
|
||||||
|
burst_count: 3,
|
||||||
|
lifetime: 0.3..0.6,
|
||||||
|
speed: 0.5..2.0,
|
||||||
|
direction: None,
|
||||||
|
direction_spread: PI,
|
||||||
|
gravity: 0.0,
|
||||||
|
size: 0.05..0.15,
|
||||||
|
color_start: [1.0, 0.3, 0.1, 1.0],
|
||||||
|
color_end: [0.2, 0.05, 0.0, 0.0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn is_player_evading(world: &World, player_entity: EntityHandle) -> bool
|
fn is_player_evading(world: &World, player_entity: EntityHandle) -> bool
|
||||||
{
|
{
|
||||||
world.leaping_states.get(player_entity).is_some()
|
world.leaping_states.get(player_entity).is_some()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const MIN_BUBBLE_WIDTH: f32 = 0.5;
|
|||||||
const TAIL_HEIGHT: f32 = 0.242;
|
const TAIL_HEIGHT: f32 = 0.242;
|
||||||
const CORNER_R: f32 = 0.18;
|
const CORNER_R: f32 = 0.18;
|
||||||
const BORDER_W: f32 = 0.06;
|
const BORDER_W: f32 = 0.06;
|
||||||
const HEIGHT_OFFSET: f32 = 8.2;
|
const HEIGHT_OFFSET: f32 = 0.0;
|
||||||
|
|
||||||
const FILL_COLOR: [f32; 4] = [0.05, 0.05, 0.05, 1.0];
|
const FILL_COLOR: [f32; 4] = [0.05, 0.05, 0.05, 1.0];
|
||||||
const BORDER_COLOR: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
|
const BORDER_COLOR: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
|
||||||
@@ -29,15 +29,7 @@ pub fn dialog_bubble_render_system(
|
|||||||
|
|
||||||
for bubble_entity in world.bubble_tags.all()
|
for bubble_entity in world.bubble_tags.all()
|
||||||
{
|
{
|
||||||
let character_entity = match world
|
let bubble_pos = match world.transforms.with(bubble_entity, |t| t.position)
|
||||||
.dialog_bubbles
|
|
||||||
.with(bubble_entity, |b| b.character_entity)
|
|
||||||
{
|
|
||||||
Some(e) => e,
|
|
||||||
None => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let character_pos = match world.transforms.with(character_entity, |t| t.position)
|
|
||||||
{
|
{
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => continue,
|
None => continue,
|
||||||
@@ -79,8 +71,7 @@ pub fn dialog_bubble_render_system(
|
|||||||
let body_frac = body_height / bubble_height;
|
let body_frac = body_height / bubble_height;
|
||||||
|
|
||||||
// Billboard orientation
|
// Billboard orientation
|
||||||
let body_half_h = body_height * 0.5;
|
let anchor = bubble_pos;
|
||||||
let anchor = character_pos + Vec3::Y * (HEIGHT_OFFSET + body_half_h);
|
|
||||||
|
|
||||||
let to_camera = camera_pos - anchor;
|
let to_camera = camera_pos - anchor;
|
||||||
let forward = if to_camera.length_squared() > 1e-6
|
let forward = if to_camera.length_squared() > 1e-6
|
||||||
@@ -104,12 +95,11 @@ pub fn dialog_bubble_render_system(
|
|||||||
let up = forward.cross(right).normalize();
|
let up = forward.cross(right).normalize();
|
||||||
|
|
||||||
let half_w = bubble_width * 0.5;
|
let half_w = bubble_width * 0.5;
|
||||||
let total_down = body_half_h + TAIL_HEIGHT;
|
|
||||||
|
|
||||||
let tl = anchor - right * half_w + up * body_half_h;
|
let tl = anchor - right * half_w + up * body_height;
|
||||||
let tr = anchor + right * half_w + up * body_half_h;
|
let tr = anchor + right * half_w + up * body_height;
|
||||||
let br = anchor + right * half_w - up * total_down;
|
let br = anchor + right * half_w - up * TAIL_HEIGHT;
|
||||||
let bl = anchor - right * half_w - up * total_down;
|
let bl = anchor - right * half_w - up * TAIL_HEIGHT;
|
||||||
|
|
||||||
let vertices = [
|
let vertices = [
|
||||||
BillboardVertex {
|
BillboardVertex {
|
||||||
@@ -143,7 +133,7 @@ pub fn dialog_bubble_render_system(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let inner_half_w = bubble_width * 0.5 - border_world - TEXT_PADDING;
|
let inner_half_w = bubble_width * 0.5 - border_world - TEXT_PADDING;
|
||||||
let inner_top_y = body_half_h - border_world - TEXT_PADDING;
|
let inner_top_y = body_height - border_world - TEXT_PADDING;
|
||||||
|
|
||||||
let text_verts = atlas.build_bubble_text(
|
let text_verts = atlas.build_bubble_text(
|
||||||
&text,
|
&text,
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ fn spawn_bubble(world: &mut World, character_entity: EntityHandle)
|
|||||||
let bubble_entity = world.spawn();
|
let bubble_entity = world.spawn();
|
||||||
world.transforms.insert(
|
world.transforms.insert(
|
||||||
bubble_entity,
|
bubble_entity,
|
||||||
Transform::from_position(character_pos + Vec3::new(0.0, 2.5, 0.0)),
|
Transform::from_position(character_pos + Vec3::new(0.0, 8.0, 0.0)),
|
||||||
);
|
);
|
||||||
world
|
world
|
||||||
.names
|
.names
|
||||||
@@ -143,21 +143,6 @@ fn tick_displaying_bubbles(world: &mut World, delta: f32)
|
|||||||
|
|
||||||
for bubble_entity in bubbles
|
for bubble_entity in bubbles
|
||||||
{
|
{
|
||||||
let character_entity = match world.dialog_bubbles.with(bubble_entity, |b| {
|
|
||||||
if matches!(b.phase, DialogPhase::Displaying { .. })
|
|
||||||
{
|
|
||||||
Some(b.character_entity)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
{
|
|
||||||
Some(Some(e)) => e,
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let expired = world
|
let expired = world
|
||||||
.dialog_bubbles
|
.dialog_bubbles
|
||||||
.with_mut(bubble_entity, |b| {
|
.with_mut(bubble_entity, |b| {
|
||||||
@@ -187,16 +172,28 @@ fn tick_displaying_bubbles(world: &mut World, delta: f32)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let character_pos = world
|
let bubble_pos = world
|
||||||
.transforms
|
.transforms
|
||||||
.with(character_entity, |t| t.position)
|
.with(bubble_entity, |t| t.position)
|
||||||
.unwrap_or(Vec3::ZERO);
|
.unwrap_or(Vec3::ZERO);
|
||||||
|
|
||||||
|
let player_entity = world
|
||||||
|
.player_tags
|
||||||
|
.all()
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.expect("no player entity");
|
||||||
|
let player_pos = world
|
||||||
|
.transforms
|
||||||
|
.with(player_entity, |t| t.position)
|
||||||
|
.unwrap_or(Vec3::ZERO);
|
||||||
|
|
||||||
|
let velocity = player_pos - bubble_pos;
|
||||||
|
|
||||||
let projectile_entity = world.spawn();
|
let projectile_entity = world.spawn();
|
||||||
world.transforms.insert(
|
world
|
||||||
projectile_entity,
|
.transforms
|
||||||
Transform::from_position(character_pos + Vec3::new(0.0, 1.5, 0.0)),
|
.insert(projectile_entity, Transform::from_position(bubble_pos));
|
||||||
);
|
|
||||||
world
|
world
|
||||||
.names
|
.names
|
||||||
.insert(projectile_entity, "DialogProjectile".to_string());
|
.insert(projectile_entity, "DialogProjectile".to_string());
|
||||||
@@ -207,6 +204,7 @@ fn tick_displaying_bubbles(world: &mut World, delta: f32)
|
|||||||
bubble_entity,
|
bubble_entity,
|
||||||
correct_parry,
|
correct_parry,
|
||||||
parry_window_open: false,
|
parry_window_open: false,
|
||||||
|
velocity,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ pub mod dialog_render;
|
|||||||
pub mod dialog_system;
|
pub mod dialog_system;
|
||||||
pub mod follow;
|
pub mod follow;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
|
pub mod particle;
|
||||||
pub mod physics_sync;
|
pub mod physics_sync;
|
||||||
pub mod render;
|
pub mod render;
|
||||||
pub mod rotate;
|
pub mod rotate;
|
||||||
@@ -23,6 +24,9 @@ pub use dialog_projectile::dialog_projectile_system;
|
|||||||
pub use dialog_render::dialog_bubble_render_system;
|
pub use dialog_render::dialog_bubble_render_system;
|
||||||
pub use dialog_system::dialog_system;
|
pub use dialog_system::dialog_system;
|
||||||
pub use input::player_input_system;
|
pub use input::player_input_system;
|
||||||
|
pub use particle::{
|
||||||
|
collect_instances, particle_intent_system, particle_update_system, spawn_snow_particles,
|
||||||
|
};
|
||||||
pub use physics_sync::physics_sync_system;
|
pub use physics_sync::physics_sync_system;
|
||||||
pub use render::render_system;
|
pub use render::render_system;
|
||||||
pub use rotate::rotate_system;
|
pub use rotate::rotate_system;
|
||||||
|
|||||||
196
src/systems/particle.rs
Normal file
196
src/systems/particle.rs
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
use crate::components::particle::{ParticleEmitterConfig, SpawnParticleIntent};
|
||||||
|
use crate::render::particle_types::ParticleInstanceRaw;
|
||||||
|
use crate::world::World;
|
||||||
|
|
||||||
|
pub struct Particle
|
||||||
|
{
|
||||||
|
pub position: glam::Vec3,
|
||||||
|
pub velocity: glam::Vec3,
|
||||||
|
pub age: f32,
|
||||||
|
pub lifetime: f32,
|
||||||
|
pub size: f32,
|
||||||
|
pub gravity: f32,
|
||||||
|
pub color_start: [f32; 4],
|
||||||
|
pub color_end: [f32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ParticleBuffers
|
||||||
|
{
|
||||||
|
pub particles: Vec<Particle>,
|
||||||
|
pub instances: Vec<ParticleInstanceRaw>,
|
||||||
|
pub emit_accumulator: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn random_unit_vec(rng: &mut impl Rng) -> glam::Vec3
|
||||||
|
{
|
||||||
|
loop
|
||||||
|
{
|
||||||
|
let v = glam::Vec3::new(
|
||||||
|
rng.random_range(-1.0_f32..1.0),
|
||||||
|
rng.random_range(-1.0_f32..1.0),
|
||||||
|
rng.random_range(-1.0_f32..1.0),
|
||||||
|
);
|
||||||
|
let len_sq = v.length_squared();
|
||||||
|
if len_sq <= 1.0 && len_sq > 0.0001
|
||||||
|
{
|
||||||
|
return v / len_sq.sqrt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn random_velocity(rng: &mut impl Rng, config: &ParticleEmitterConfig) -> glam::Vec3
|
||||||
|
{
|
||||||
|
let speed = rng.random_range(config.speed.start..config.speed.end);
|
||||||
|
let dir = if let Some(d) = config.direction
|
||||||
|
{
|
||||||
|
let perp = if d.x.abs() < 0.9
|
||||||
|
{
|
||||||
|
glam::Vec3::X
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
glam::Vec3::Y
|
||||||
|
};
|
||||||
|
let right = d.cross(perp).normalize();
|
||||||
|
let up = d.cross(right);
|
||||||
|
let angle = rng.random_range(0.0_f32..config.direction_spread);
|
||||||
|
let azimuth = rng.random_range(0.0_f32..(2.0 * std::f32::consts::PI));
|
||||||
|
(d * angle.cos() + right * angle.sin() * azimuth.cos() + up * angle.sin() * azimuth.sin())
|
||||||
|
.normalize()
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
random_unit_vec(rng)
|
||||||
|
};
|
||||||
|
dir * speed
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_from_config(
|
||||||
|
buffers: &mut ParticleBuffers,
|
||||||
|
origin: glam::Vec3,
|
||||||
|
config: &ParticleEmitterConfig,
|
||||||
|
rng: &mut impl Rng,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
for _ in 0..config.burst_count
|
||||||
|
{
|
||||||
|
let lifetime = rng.random_range(config.lifetime.start..config.lifetime.end);
|
||||||
|
let size = rng.random_range(config.size.start..config.size.end);
|
||||||
|
let velocity = random_velocity(rng, config);
|
||||||
|
buffers.particles.push(Particle {
|
||||||
|
position: origin,
|
||||||
|
velocity,
|
||||||
|
age: 0.0,
|
||||||
|
lifetime,
|
||||||
|
size,
|
||||||
|
gravity: config.gravity,
|
||||||
|
color_start: config.color_start,
|
||||||
|
color_end: config.color_end,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn particle_intent_system(world: &mut World)
|
||||||
|
{
|
||||||
|
if world.particle_buffers.is_none()
|
||||||
|
{
|
||||||
|
world.particle_buffers = Some(ParticleBuffers {
|
||||||
|
particles: Vec::new(),
|
||||||
|
instances: Vec::new(),
|
||||||
|
emit_accumulator: 0.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let intents: Vec<SpawnParticleIntent> = world.spawn_particle_intents.drain(..).collect();
|
||||||
|
if intents.is_empty()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let buffers = world.particle_buffers.as_mut().unwrap();
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
for intent in intents
|
||||||
|
{
|
||||||
|
spawn_from_config(buffers, intent.origin, &intent.config, &mut rng);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn particle_update_system(world: &mut World, delta: f32)
|
||||||
|
{
|
||||||
|
let Some(ref mut buffers) = world.particle_buffers
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
for particle in &mut buffers.particles
|
||||||
|
{
|
||||||
|
particle.velocity.y -= particle.gravity * delta;
|
||||||
|
particle.position += particle.velocity * delta;
|
||||||
|
particle.age += delta / particle.lifetime;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffers.particles.retain(|p| p.age < 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn_snow_particles(buffers: &mut ParticleBuffers, camera_pos: glam::Vec3, delta: f32)
|
||||||
|
{
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
|
||||||
|
let rate = 200.0_f32;
|
||||||
|
buffers.emit_accumulator += delta;
|
||||||
|
let to_emit = (buffers.emit_accumulator * rate) as u32;
|
||||||
|
buffers.emit_accumulator -= to_emit as f32 / rate;
|
||||||
|
|
||||||
|
for _ in 0..to_emit
|
||||||
|
{
|
||||||
|
let x = camera_pos.x + rng.random_range(-20.0_f32..20.0_f32);
|
||||||
|
let z = camera_pos.z + rng.random_range(-20.0_f32..20.0_f32);
|
||||||
|
let y = rng.random_range((camera_pos.y + 8.0)..(camera_pos.y + 20.0));
|
||||||
|
let vx = rng.random_range(-0.3_f32..0.3_f32);
|
||||||
|
let vy = rng.random_range(-2.0_f32..-0.5_f32);
|
||||||
|
let vz = rng.random_range(-0.3_f32..0.3_f32);
|
||||||
|
let size = rng.random_range(0.05_f32..0.15_f32);
|
||||||
|
let lifetime = rng.random_range(2.0_f32..5.0_f32);
|
||||||
|
buffers.particles.push(Particle {
|
||||||
|
position: glam::Vec3::new(x, y, z),
|
||||||
|
velocity: glam::Vec3::new(vx, vy, vz),
|
||||||
|
age: 0.0,
|
||||||
|
lifetime,
|
||||||
|
size,
|
||||||
|
gravity: 0.0,
|
||||||
|
color_start: [1.0, 1.0, 1.0, 1.0],
|
||||||
|
color_end: [1.0, 1.0, 1.0, 0.0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buffers
|
||||||
|
.particles
|
||||||
|
.retain(|p| p.position.y >= camera_pos.y - 5.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collect_instances(buffers: &mut ParticleBuffers) -> &[ParticleInstanceRaw]
|
||||||
|
{
|
||||||
|
buffers.instances.clear();
|
||||||
|
for particle in &buffers.particles
|
||||||
|
{
|
||||||
|
let t = particle.age;
|
||||||
|
let color = [
|
||||||
|
particle.color_start[0] + (particle.color_end[0] - particle.color_start[0]) * t,
|
||||||
|
particle.color_start[1] + (particle.color_end[1] - particle.color_start[1]) * t,
|
||||||
|
particle.color_start[2] + (particle.color_end[2] - particle.color_start[2]) * t,
|
||||||
|
particle.color_start[3] + (particle.color_end[3] - particle.color_start[3]) * t,
|
||||||
|
];
|
||||||
|
buffers.instances.push(ParticleInstanceRaw {
|
||||||
|
position: particle.position.into(),
|
||||||
|
velocity: particle.velocity.into(),
|
||||||
|
size: particle.size,
|
||||||
|
color,
|
||||||
|
age: t,
|
||||||
|
_padding: [0.0; 3],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
&buffers.instances
|
||||||
|
}
|
||||||
11
src/world.rs
11
src/world.rs
@@ -7,6 +7,7 @@ use crate::components::dissolve::DissolveComponent;
|
|||||||
use crate::components::follow::FollowComponent;
|
use crate::components::follow::FollowComponent;
|
||||||
use crate::components::intent::{CameraTransitionIntent, FollowPlayerIntent, StopFollowingIntent};
|
use crate::components::intent::{CameraTransitionIntent, FollowPlayerIntent, StopFollowingIntent};
|
||||||
use crate::components::lights::spot::SpotlightComponent;
|
use crate::components::lights::spot::SpotlightComponent;
|
||||||
|
use crate::components::particle::SpawnParticleIntent;
|
||||||
use crate::components::player_states::{
|
use crate::components::player_states::{
|
||||||
FallingState, IdleState, JumpingState, LeapingState, RollingState, WalkingState,
|
FallingState, IdleState, JumpingState, LeapingState, RollingState, WalkingState,
|
||||||
};
|
};
|
||||||
@@ -18,8 +19,10 @@ use crate::components::{
|
|||||||
};
|
};
|
||||||
use crate::debug::DebugMode;
|
use crate::debug::DebugMode;
|
||||||
use crate::entity::{EntityHandle, EntityManager};
|
use crate::entity::{EntityHandle, EntityManager};
|
||||||
|
use crate::loaders::mesh::Mesh;
|
||||||
use crate::render::snow::SnowLayer;
|
use crate::render::snow::SnowLayer;
|
||||||
use crate::states::state::StateMachine;
|
use crate::states::state::StateMachine;
|
||||||
|
use crate::systems::particle::ParticleBuffers;
|
||||||
|
|
||||||
pub use crate::utility::transform::Transform;
|
pub use crate::utility::transform::Transform;
|
||||||
|
|
||||||
@@ -111,6 +114,8 @@ pub struct World
|
|||||||
pub bubble_tags: Storage<()>,
|
pub bubble_tags: Storage<()>,
|
||||||
pub projectile_tags: Storage<()>,
|
pub projectile_tags: Storage<()>,
|
||||||
pub dialog_outcomes: Vec<DialogOutcomeEvent>,
|
pub dialog_outcomes: Vec<DialogOutcomeEvent>,
|
||||||
|
pub spawn_particle_intents: Vec<SpawnParticleIntent>,
|
||||||
|
pub particle_buffers: Option<ParticleBuffers>,
|
||||||
|
|
||||||
// --- intents (one-frame, consumed after processing) ---
|
// --- intents (one-frame, consumed after processing) ---
|
||||||
pub follow_player_intents: Storage<FollowPlayerIntent>,
|
pub follow_player_intents: Storage<FollowPlayerIntent>,
|
||||||
@@ -121,6 +126,8 @@ pub struct World
|
|||||||
pub snow_layer: Option<SnowLayer>,
|
pub snow_layer: Option<SnowLayer>,
|
||||||
pub debug_mode: DebugMode,
|
pub debug_mode: DebugMode,
|
||||||
pub was_dialog_active: bool,
|
pub was_dialog_active: bool,
|
||||||
|
pub gizmo_mesh: Option<Mesh>,
|
||||||
|
pub gizmo_instance_buffer: Option<wgpu::Buffer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl World
|
impl World
|
||||||
@@ -160,12 +167,16 @@ impl World
|
|||||||
bubble_tags: Storage::new(),
|
bubble_tags: Storage::new(),
|
||||||
projectile_tags: Storage::new(),
|
projectile_tags: Storage::new(),
|
||||||
dialog_outcomes: Vec::new(),
|
dialog_outcomes: Vec::new(),
|
||||||
|
spawn_particle_intents: Vec::new(),
|
||||||
|
particle_buffers: None,
|
||||||
follow_player_intents: Storage::new(),
|
follow_player_intents: Storage::new(),
|
||||||
stop_following_intents: Storage::new(),
|
stop_following_intents: Storage::new(),
|
||||||
camera_transition_intents: Storage::new(),
|
camera_transition_intents: Storage::new(),
|
||||||
snow_layer: None,
|
snow_layer: None,
|
||||||
debug_mode: DebugMode::default(),
|
debug_mode: DebugMode::default(),
|
||||||
was_dialog_active: false,
|
was_dialog_active: false,
|
||||||
|
gizmo_mesh: None,
|
||||||
|
gizmo_instance_buffer: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user