Added particle foundation

master
Mark 2024-01-02 19:11:18 -08:00
parent e5a96621a4
commit 0e38fdb21e
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
14 changed files with 296 additions and 37 deletions

View File

@ -50,5 +50,8 @@ pub const OBJECT_SPRITE_INSTANCE_LIMIT: u64 = 500;
/// We can draw at most this many ui sprites on the screen.
pub const UI_SPRITE_INSTANCE_LIMIT: u64 = 100;
/// The size of our circular particle buffer. When we create particles, the oldest ones are replaced.
pub const PARTICLE_SPRITE_INSTANCE_LIMIT: u64 = 1000;
/// Must be small enough to fit in an i32
pub const STARFIELD_SPRITE_INSTANCE_LIMIT: u64 = STARFIELD_COUNT * 24;

View File

@ -56,7 +56,6 @@ impl crate::Build for Texture {
index: ct.textures.len(),
aspect: dim.0 as f32 / dim.1 as f32,
};
ct.texture_index.insert(texture_name.clone(), h);
if texture_name == ct.starfield_texture_name {
if ct.starfield_handle.is_none() {
@ -67,9 +66,10 @@ impl crate::Build for Texture {
}
}
ct.texture_index.insert(texture_name.clone(), h);
ct.textures.push(Self {
name: texture_name,
path: path,
path,
handle: h,
});
}

View File

@ -3,7 +3,7 @@ struct InstanceInput {
@location(3) transform_matrix_1: vec4<f32>,
@location(4) transform_matrix_2: vec4<f32>,
@location(5) transform_matrix_3: vec4<f32>,
@location(6) texture_idx: u32,
@location(6) texture_index: u32,
};
struct VertexInput {
@ -14,7 +14,7 @@ struct VertexInput {
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) texture_coords: vec2<f32>,
@location(1) index: u32,
@location(1) texture_index: u32,
}
@ -29,6 +29,7 @@ struct GlobalUniform {
starfield_texture: vec2<u32>,
starfield_tile_size: vec2<f32>,
starfield_size_limits: vec2<f32>,
current_time: vec2<f32>,
};
@ -56,7 +57,7 @@ fn vertex_main(
var out: VertexOutput;
out.position = transform * vec4<f32>(vertex.position, 1.0);
out.texture_coords = vertex.texture_coords;
out.index = instance.texture_idx;
out.texture_index = instance.texture_index;
return out;
}
@ -64,7 +65,7 @@ fn vertex_main(
@fragment
fn fragment_main(in: VertexOutput) -> @location(0) vec4<f32> {
return textureSampleLevel(
texture_array[in.index],
texture_array[in.texture_index],
sampler_array[0],
in.texture_coords,
0.0

View File

@ -0,0 +1,85 @@
struct InstanceInput {
@location(2) position: vec3<f32>,
@location(3) size: f32,
@location(4) expires: f32,
@location(5) texture_index: u32,
};
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) texture_coords: vec2<f32>,
}
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) texture_coords: vec2<f32>,
@location(1) texture_index: u32,
}
@group(1) @binding(0)
var<uniform> global: GlobalUniform;
struct GlobalUniform {
camera_position: vec2<f32>,
camera_zoom: vec2<f32>,
camera_zoom_limits: vec2<f32>,
window_size: vec2<f32>,
window_aspect: vec2<f32>,
starfield_texture: vec2<u32>,
starfield_tile_size: vec2<f32>,
starfield_size_limits: vec2<f32>,
current_time: vec2<f32>,
};
@group(0) @binding(0)
var texture_array: binding_array<texture_2d<f32>>;
@group(0) @binding(1)
var sampler_array: binding_array<sampler>;
@vertex
fn vertex_main(
vertex: VertexInput,
instance: InstanceInput,
) -> VertexOutput {
var out: VertexOutput;
out.texture_coords = vertex.texture_coords;
out.texture_index = instance.texture_index;
if instance.expires < global.current_time.x {
out.position = vec4<f32>(2.0, 2.0, 0.0, 1.0);
return out;
}
var scale: f32 = instance.size / global.camera_zoom.x;
var pos: vec2<f32> = vec2(vertex.position.x, vertex.position.y);
pos = pos * vec2<f32>(
1.0 * scale / global.window_aspect.x,
scale
);
var ipos: vec2<f32> = vec2(instance.position.x, instance.position.y) - global.camera_position;
pos = pos + vec2<f32>(
ipos.x / (global.camera_zoom.x/2.0) / global.window_aspect.x,
ipos.y / (global.camera_zoom.x/2.0)
);
out.position = vec4<f32>(pos, 0.0, 1.0) * instance.position.z;
return out;
}
@fragment
fn fragment_main(in: VertexOutput) -> @location(0) vec4<f32> {
return textureSampleLevel(
texture_array[in.texture_index],
sampler_array[0],
in.texture_coords,
0.0
).rgba;
}

View File

@ -26,6 +26,7 @@ struct GlobalUniform {
starfield_texture: vec2<u32>,
starfield_tile_size: vec2<f32>,
starfield_size_limits: vec2<f32>,
current_time: vec2<f32>,
};

View File

@ -4,7 +4,7 @@ struct InstanceInput {
@location(4) transform_matrix_2: vec4<f32>,
@location(5) transform_matrix_3: vec4<f32>,
@location(6) color_transform: vec4<f32>,
@location(7) texture_idx: u32,
@location(7) texture_index: u32,
};
struct VertexInput {
@ -15,7 +15,7 @@ struct VertexInput {
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) texture_coords: vec2<f32>,
@location(1) index: u32,
@location(1) texture_index: u32,
@location(2) color_transform: vec4<f32>,
}
@ -31,6 +31,7 @@ struct GlobalUniform {
starfield_texture: vec2<u32>,
starfield_tile_size: vec2<f32>,
starfield_size_limits: vec2<f32>,
current_time: vec2<f32>,
};
@ -58,7 +59,7 @@ fn vertex_main(
var out: VertexOutput;
out.position = transform * vec4<f32>(vertex.position, 1.0);
out.texture_coords = vertex.texture_coords;
out.index = instance.texture_idx;
out.texture_index = instance.texture_index;
out.color_transform = instance.color_transform;
return out;
}
@ -67,7 +68,7 @@ fn vertex_main(
@fragment
fn fragment_main(in: VertexOutput) -> @location(0) vec4<f32> {
return textureSampleLevel(
texture_array[in.index],
texture_array[in.texture_index],
sampler_array[0],
in.texture_coords,
0.0

View File

@ -30,7 +30,7 @@ pub struct GlobalDataContent {
/// Size ratio of window, in physical pixels
pub window_size: [f32; 2],
// Aspect ration of window
/// Aspect ratio of window
/// Second component is ignored.
pub window_aspect: [f32; 2],
@ -42,8 +42,12 @@ pub struct GlobalDataContent {
/// Second component is ignored.
pub starfield_tile_size: [f32; 2],
// Min and max starfield star size, in game units
/// Min and max starfield star size, in game units
pub starfield_size_limits: [f32; 2],
/// Current game time, in seconds.
/// Second component is ignored.
pub current_time: [f32; 2],
}
impl GlobalDataContent {
@ -70,7 +74,7 @@ impl GlobalData {
},
count: None,
}],
label: Some("camera_bind_group_layout"),
label: Some("globaldata bind group layout"),
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
@ -79,7 +83,7 @@ impl GlobalData {
binding: 0,
resource: buffer.as_entire_binding(),
}],
label: Some("camera_bind_group"),
label: Some("globaldata bind group"),
});
return Self {

View File

@ -2,7 +2,7 @@ use anyhow::Result;
use bytemuck;
use cgmath::{Deg, EuclideanSpace, Matrix4, Point2, Vector3};
use galactica_constants;
use std::{iter, rc::Rc};
use std::{iter, rc::Rc, time::Instant};
use wgpu;
use winit::{self, dpi::LogicalSize, window::Window};
@ -10,13 +10,13 @@ use crate::{
content,
globaldata::{GlobalData, GlobalDataContent},
pipeline::PipelineBuilder,
sprite::ObjectSubSprite,
sprite::{ObjectSubSprite, ParticleBuilder},
starfield::Starfield,
texturearray::TextureArray,
vertexbuffer::{
consts::{SPRITE_INDICES, SPRITE_VERTICES},
types::{ObjectInstance, StarfieldInstance, TexturedVertex, UiInstance},
VertexBuffer,
types::{ObjectInstance, ParticleInstance, StarfieldInstance, TexturedVertex, UiInstance},
BufferObject, VertexBuffer,
},
ObjectSprite, UiSprite, OPENGL_TO_WGPU_MATRIX,
};
@ -39,6 +39,7 @@ pub struct GPUState {
object_pipeline: wgpu::RenderPipeline,
starfield_pipeline: wgpu::RenderPipeline,
particle_pipeline: wgpu::RenderPipeline,
ui_pipeline: wgpu::RenderPipeline,
starfield: Starfield,
@ -51,6 +52,12 @@ struct VertexBuffers {
object: Rc<VertexBuffer>,
starfield: Rc<VertexBuffer>,
ui: Rc<VertexBuffer>,
/// The index of the next particle slot we'll write to.
/// This must cycle to 0 whenever it exceeds the size
/// of the particle instance array.
particle_array_head: u64,
particle: Rc<VertexBuffer>,
}
impl GPUState {
@ -120,7 +127,7 @@ impl GPUState {
let vertex_buffers = VertexBuffers {
object: Rc::new(VertexBuffer::new::<TexturedVertex, ObjectInstance>(
"objecte",
"object",
&device,
Some(SPRITE_VERTICES),
Some(SPRITE_INDICES),
@ -142,6 +149,15 @@ impl GPUState {
Some(SPRITE_INDICES),
galactica_constants::UI_SPRITE_INSTANCE_LIMIT,
)),
particle_array_head: 0,
particle: Rc::new(VertexBuffer::new::<TexturedVertex, ParticleInstance>(
"particle",
&device,
Some(SPRITE_VERTICES),
Some(SPRITE_INDICES),
galactica_constants::PARTICLE_SPRITE_INSTANCE_LIMIT,
)),
};
// Load uniforms
@ -191,6 +207,18 @@ impl GPUState {
.set_bind_group_layouts(bind_group_layouts)
.build();
let particle_pipeline = PipelineBuilder::new("particle", &device)
.set_shader(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/shaders/",
"particle.wgsl"
)))
.set_format(config.format)
.set_triangle(true)
.set_vertex_buffer(&vertex_buffers.particle)
.set_bind_group_layouts(bind_group_layouts)
.build();
let mut starfield = Starfield::new();
starfield.regenerate();
@ -207,6 +235,7 @@ impl GPUState {
object_pipeline,
starfield_pipeline,
ui_pipeline,
particle_pipeline,
starfield,
texture_array,
@ -500,8 +529,12 @@ impl GPUState {
&mut self,
camera_pos: Point2<f32>,
camera_zoom: f32,
// TODO: clean this up, pass one struct
object_sprites: &Vec<ObjectSprite>,
ui_sprites: &Vec<UiSprite>,
new_particles: &mut Vec<ParticleBuilder>,
start_instant: Instant,
) -> Result<(), wgpu::SurfaceError> {
let output = self.surface.get_current_texture()?;
let view = output
@ -535,6 +568,9 @@ impl GPUState {
timestamp_writes: None,
});
// TODO: handle overflow
let time_now = start_instant.elapsed().as_secs_f32();
// Update global values
self.queue.write_buffer(
&self.global_data.buffer,
@ -554,9 +590,31 @@ impl GPUState {
galactica_constants::STARFIELD_SIZE_MIN,
galactica_constants::STARFIELD_SIZE_MAX,
],
current_time: [time_now, 0.0],
}]),
);
for i in new_particles.iter() {
let texture = self.texture_array.get_texture(i.texture);
self.queue.write_buffer(
&self.vertex_buffers.particle.instances,
ParticleInstance::SIZE * self.vertex_buffers.particle_array_head,
bytemuck::cast_slice(&[ParticleInstance {
position: [i.pos.x, i.pos.y, 1.0],
size: i.size,
texture_index: texture.index,
expires: time_now + i.lifetime,
}]),
);
self.vertex_buffers.particle_array_head += 1;
if self.vertex_buffers.particle_array_head
== galactica_constants::PARTICLE_SPRITE_INSTANCE_LIMIT
{
self.vertex_buffers.particle_array_head = 0;
}
}
new_particles.clear();
// Create sprite instances
let (n_object, n_ui) =
self.update_sprite_instances(camera_zoom, camera_pos, object_sprites, ui_sprites);
@ -580,6 +638,15 @@ impl GPUState {
render_pass.set_pipeline(&self.object_pipeline);
render_pass.draw_indexed(0..SPRITE_INDICES.len() as u32, 0, 0..n_object as _);
// Particle pipeline
self.vertex_buffers.particle.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&self.particle_pipeline);
render_pass.draw_indexed(
0..SPRITE_INDICES.len() as u32,
0,
0..galactica_constants::PARTICLE_SPRITE_INSTANCE_LIMIT as _,
);
// Ui pipeline
self.vertex_buffers.ui.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&self.ui_pipeline);

View File

@ -17,7 +17,7 @@ mod vertexbuffer;
use galactica_content as content;
pub use gpustate::GPUState;
pub use sprite::{AnchoredUiPosition, ObjectSprite, ObjectSubSprite, UiSprite};
pub use sprite::{AnchoredUiPosition, ObjectSprite, ObjectSubSprite, ParticleBuilder, UiSprite};
use cgmath::Matrix4;

View File

@ -1,6 +1,23 @@
use crate::content;
use cgmath::{Deg, Point2, Point3};
/// Instructions to create a new particle
pub struct ParticleBuilder {
/// The texture to use for this particle
pub texture: content::TextureHandle,
// TODO: rotation, velocity
/// This object's center, in world coordinates.
pub pos: Point3<f32>,
/// This particle's lifetime, in seconds
pub lifetime: f32,
/// The size of this sprite,
/// given as height in world units.
pub size: f32,
}
/// The location of a UI element, in one of a few
/// possible coordinate systems.
///

View File

@ -195,3 +195,60 @@ impl BufferObject for UiInstance {
}
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct ParticleInstance {
/// World position of this particle
pub position: [f32; 3],
// TODO: docs: particle frames must all have the same size
// TODO: is transparency trimmed? That's not ideal for animated particles!
/// The height of this particle, in world units
pub size: f32,
// TODO: rotation, velocity vector
// TODO: animated sprites
// TODO: texture aspect ratio
/// The time, in seconds, at which this particle expires.
/// Time is kept by a variable in the global uniform.
pub expires: f32,
/// What texture to use for this particle
pub texture_index: u32,
}
impl BufferObject for ParticleInstance {
fn layout() -> wgpu::VertexBufferLayout<'static> {
wgpu::VertexBufferLayout {
array_stride: Self::SIZE,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
// Position
wgpu::VertexAttribute {
offset: 0,
shader_location: 2,
format: wgpu::VertexFormat::Float32x3,
},
// Size
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
shader_location: 3,
format: wgpu::VertexFormat::Float32,
},
// Expires
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress,
shader_location: 4,
format: wgpu::VertexFormat::Float32,
},
// Texture
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 5]>() as wgpu::BufferAddress,
shader_location: 5,
format: wgpu::VertexFormat::Uint32,
},
],
}
}
}

View File

@ -1,4 +1,4 @@
use cgmath::{Deg, EuclideanSpace, InnerSpace, Matrix2, Point2, Rad, Vector2};
use cgmath::{Deg, EuclideanSpace, InnerSpace, Matrix2, Point2, Point3, Rad, Vector2};
use crossbeam::channel::Receiver;
use nalgebra::vector;
use rand::Rng;
@ -12,7 +12,7 @@ use std::{collections::HashMap, f32::consts::PI};
use crate::{objects, objects::ProjectileWorldObject, util, wrapper::Wrapper, ShipPhysicsHandle};
use galactica_content as content;
use galactica_gameobject as object;
use galactica_render::ObjectSprite;
use galactica_render::{ObjectSprite, ParticleBuilder};
/// Keeps track of all objects in the world that we can interact with.
/// Also wraps our physics engine
@ -92,7 +92,7 @@ impl<'a> World {
.linvel(vector![vel.x, vel.y])
.build();
let collider = ColliderBuilder::ball(5.0)
let collider = ColliderBuilder::ball(1.0)
.sensor(true)
.active_events(ActiveEvents::COLLISION_EVENTS)
.build();
@ -169,7 +169,7 @@ impl<'a> World {
}
/// Step this physics system by `t` seconds
pub fn step(&mut self, t: f32, ct: &content::Content) {
pub fn step(&mut self, t: f32, ct: &content::Content, particles: &mut Vec<ParticleBuilder>) {
// Run ship updates
// TODO: maybe reorganize projectile creation?
let mut projectiles = Vec::new();
@ -214,6 +214,18 @@ impl<'a> World {
if let Some(s) = self.ships.get_mut(b) {
let hit = s.ship.handle_projectile_collision(ct, &p.projectile);
if hit {
let r = self.get_rigid_body(p.rigid_body);
let pos = util::rigidbody_position(r);
particles.push(ParticleBuilder {
texture: ct.get_texture_handle("particle::blaster"),
pos: Point3 {
x: pos.x,
y: pos.y,
z: 1.0, // TODO:remove z coordinate
},
lifetime: 0.1,
size: 10.0,
});
self.remove_projectile(*a);
}
}

View File

@ -1,4 +1,4 @@
use cgmath::Point2;
use cgmath::{Point2, Point3};
use content::SystemHandle;
use std::time::Instant;
use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode};
@ -7,7 +7,7 @@ use crate::camera::Camera;
use crate::{content, inputstatus::InputStatus};
use galactica_constants;
use galactica_gameobject as object;
use galactica_render::{ObjectSprite, UiSprite};
use galactica_render::{ObjectSprite, ParticleBuilder, UiSprite};
use galactica_shipbehavior::{behavior, ShipBehavior};
use galactica_ui as ui;
use galactica_world::{util, ShipPhysicsHandle, World};
@ -21,10 +21,14 @@ pub struct Game {
paused: bool,
pub time_scale: f32,
physics: World,
world: World,
shipbehaviors: Vec<Box<dyn ShipBehavior>>,
playerbehavior: behavior::Player,
content: content::Content,
pub start_instant: Instant,
// TODO: clean this up
pub new_particles: Vec<ParticleBuilder>,
}
impl Game {
@ -85,6 +89,7 @@ impl Game {
last_update: Instant::now(),
input: InputStatus::new(),
player: h1,
start_instant: Instant::now(),
camera: Camera {
pos: (0.0, 0.0).into(),
@ -95,10 +100,11 @@ impl Game {
paused: false,
time_scale: 1.0,
physics,
world: physics,
shipbehaviors,
content: ct,
playerbehavior: behavior::Player::new(h1),
new_particles: Vec::new(),
}
}
@ -131,19 +137,19 @@ impl Game {
self.playerbehavior.key_right = self.input.key_right;
self.playerbehavior.key_left = self.input.key_left;
self.playerbehavior
.update_controls(&mut self.physics, &self.content);
.update_controls(&mut self.world, &self.content);
self.shipbehaviors.retain_mut(|b| {
// Remove shipbehaviors of destroyed ships
if self.physics.get_ship_mut(&b.get_handle()).is_none() {
if self.world.get_ship_mut(&b.get_handle()).is_none() {
false
} else {
b.update_controls(&mut self.physics, &self.content);
b.update_controls(&mut self.world, &self.content);
true
}
});
self.physics.step(t, &self.content);
self.world.step(t, &self.content, &mut self.new_particles);
if self.input.v_scroll != 0.0 {
self.camera.zoom = (self.camera.zoom + self.input.v_scroll)
@ -153,11 +159,11 @@ impl Game {
// TODO: Camera physics
let r = self
.physics
.world
.get_ship_mut(&self.player)
.unwrap()
.physics_handle;
let r = self.physics.get_rigid_body(r.0); // TODO: r.0 shouldn't be public
let r = self.world.get_rigid_body(r.0); // TODO: r.0 shouldn't be public
let ship_pos = util::rigidbody_position(r);
self.camera.pos = ship_pos;
self.last_update = Instant::now();
@ -167,7 +173,7 @@ impl Game {
let mut sprites: Vec<ObjectSprite> = Vec::new();
sprites.append(&mut self.system.get_sprites());
sprites.extend(self.physics.get_ship_sprites(&self.content));
sprites.extend(self.world.get_ship_sprites(&self.content));
// Make sure sprites are drawn in the correct order
// (note the reversed a, b in the comparator)
@ -177,7 +183,7 @@ impl Game {
sprites.sort_by(|a, b| b.pos.z.total_cmp(&a.pos.z));
// Don't waste time sorting these, they should always be on top.
sprites.extend(self.physics.get_projectile_sprites());
sprites.extend(self.world.get_projectile_sprites());
return sprites;
}
@ -186,7 +192,7 @@ impl Game {
return ui::build_radar(
&self.content,
&self.player,
&self.physics,
&self.world,
&self.system,
self.camera.zoom,
self.camera.aspect,

View File

@ -37,6 +37,11 @@ fn main() -> Result<()> {
game.camera.zoom,
&game.get_object_sprites(),
&game.get_ui_sprites(),
// TODO: clean this up, single game data struct
// Game in another crate?
// Shipbehavior needs game state too...
&mut game.new_particles,
game.start_instant,
) {
Ok(_) => {}
Err(wgpu::SurfaceError::Lost) => gpu.resize(),