Compare commits

...

3 Commits

Author SHA1 Message Date
Mark c7077fc701
Updated TODO 2024-01-05 19:56:37 -08:00
Mark a357b29436
Minor edits 2024-01-05 19:56:32 -08:00
Mark 0e692feb66
Added object state uniform, moved transform logic 2024-01-05 19:56:26 -08:00
17 changed files with 223 additions and 154 deletions

View File

@ -6,6 +6,10 @@
- Sound system
- Ship death debris
- Sprite reels
- Passive engine glow
- Ship death damage and force events
- Fix particle inherit velocity
- Global shader variable prefix
----------------------------------

View File

@ -16,10 +16,10 @@ lifetime = "inherit"
inherit_velocity = "target"
size = 50.0
[effect."blaster expire"]
[effect."blaster impact"]
sprite = "particle::blaster"
lifetime = "inherit"
inherit_velocity = "projectile"
inherit_velocity = "target"
size = 3.0

View File

@ -29,7 +29,7 @@ projectile.force = 0.0
projectile.collider.ball.radius = 2.0
projectile.impact_effect = "small explosion"
projectile.impact_effect = "blaster impact"
projectile.expire_effect.sprite = "particle::blaster"
projectile.expire_effect.lifetime = "inherit"

View File

@ -5,9 +5,9 @@
fn animate(instance: InstanceInput, age: f32) -> u32 {
let idx = instance.texture_index;
let len = sprites.data[idx].frame_count;
let rep = sprites.data[idx].repeatmode;
let fps = sprites.data[idx].fps;
let len = sprites[idx].frame_count;
let rep = sprites[idx].repeatmode;
let fps = sprites[idx].fps;
var frame: u32 = u32(0);
@ -39,5 +39,5 @@ fn animate(instance: InstanceInput, age: f32) -> u32 {
}
return frame + sprites.data[idx].first_frame;
return frame + sprites[idx].first_frame;
}

View File

@ -1,11 +1,8 @@
// INCLUDE: global uniform header
struct InstanceInput {
@location(2) transform_matrix_0: vec4<f32>,
@location(3) transform_matrix_1: vec4<f32>,
@location(4) transform_matrix_2: vec4<f32>,
@location(5) transform_matrix_3: vec4<f32>,
@location(6) texture_index: u32,
@location(2) texture_index: u32,
@location(3) object_index: u32,
};
struct VertexInput {
@ -28,23 +25,55 @@ var sampler_array: binding_array<sampler>;
// INCLUDE: animate.wgsl
fn transform_vertex(obj: ObjectLocation, vertex: VertexInput, instance: InstanceInput) -> vec4<f32> {
// Object scale
let scale = obj.size / (global.camera_zoom.x * obj.zpos);
// Apply scale and sprite aspect
// Note that our mesh starts centered at (0, 0). This is important!
var pos: vec2<f32> = vec2(
vertex.position.x * scale * sprites[instance.texture_index].aspect,
vertex.position.y * scale
);
// Apply rotation
pos = mat2x2(
vec2(cos(obj.angle), sin(obj.angle)),
vec2(-sin(obj.angle), cos(obj.angle))
) * pos;
// Correct for screen aspect, preserving height
// This must be done AFTER rotation.
pos = vec2(
pos.x / global.window_aspect.x,
pos.y
);
// Distance-adjusted world position
let trans = (vec2(obj.xpos, obj.ypos) - global.camera_position) / obj.zpos;
// Finally, translate
//
// Note that we divide camera zoom by two.
// The height of the viewport is `zoom` in game units,
// but it's 2 in screen units (since coordinates range from -1 to 1)
pos = pos + vec2(
trans.x / (global.camera_zoom.x / 2.0) / global.window_aspect.x,
trans.y / (global.camera_zoom.x / 2.0)
);
return vec4<f32>(pos, 0.0, 1.0);;
}
@vertex
fn vertex_main(
vertex: VertexInput,
instance: InstanceInput,
) -> VertexOutput {
let transform = mat4x4<f32>(
instance.transform_matrix_0,
instance.transform_matrix_1,
instance.transform_matrix_2,
instance.transform_matrix_3,
);
var out: VertexOutput;
out.position = transform * vec4<f32>(vertex.position, 1.0);
out.position = transform_vertex(objects[instance.object_index], vertex, instance);
let t = atlas.data[animate(instance, global.current_time.x)];
let t = atlas[animate(instance, global.current_time.x)];
out.texture_index = t.atlas_texture;
out.texture_coords = vec2(t.xpos, t.ypos);
if vertex.texture_coords.x == 1.0 {

View File

@ -55,7 +55,7 @@ fn vertex_main(
var scale: f32 = instance.size / global.camera_zoom.x;
var pos: vec2<f32> = vec2(vertex.position.x, vertex.position.y);
pos = pos * vec2<f32>(
sprites.data[instance.texture_index].aspect * scale / global.window_aspect.x,
sprites[instance.texture_index].aspect * scale / global.window_aspect.x,
scale
);
@ -76,7 +76,7 @@ fn vertex_main(
// Compute texture coordinates
let t = atlas.data[animate(instance, age)];
let t = atlas[animate(instance, age)];
out.texture_index = u32(t.atlas_texture);
out.texture_coords = vec2(t.xpos, t.ypos);
if vertex.texture_coords.x == 1.0 {

View File

@ -109,8 +109,8 @@ fn vertex_main(
// Starfield sprites may not be animated
let i = sprites.data[global.starfield_sprite.x].first_frame;
let t = atlas.data[i];
let i = sprites[global.starfield_sprite.x].first_frame;
let t = atlas[i];
out.texture_index = u32(t.atlas_texture);
out.texture_coords = vec2(t.xpos, t.ypos);
if vertex.texture_coords.x == 1.0 {

View File

@ -48,7 +48,7 @@ fn vertex_main(
out.color_transform = instance.color_transform;
// Pick texture frame
let t = atlas.data[animate(instance, global.current_time.x)];
let t = atlas[animate(instance, global.current_time.x)];
out.texture_index = u32(t.atlas_texture);
out.texture_coords = vec2(t.xpos, t.ypos);
if vertex.texture_coords.x == 1.0 {

View File

@ -19,12 +19,12 @@ pub struct ImageLocation {
}
#[derive(Debug, Copy, Clone)]
pub struct ImageLocationArray {
pub struct AtlasArray {
pub data: [ImageLocation; IMAGE_LIMIT as usize],
}
unsafe impl Pod for ImageLocationArray {}
unsafe impl Zeroable for ImageLocationArray {
unsafe impl Pod for AtlasArray {}
unsafe impl Zeroable for AtlasArray {
fn zeroed() -> Self {
Self {
data: [ImageLocation::zeroed(); IMAGE_LIMIT as usize],
@ -32,18 +32,12 @@ unsafe impl Zeroable for ImageLocationArray {
}
}
impl Default for ImageLocationArray {
impl Default for AtlasArray {
fn default() -> Self {
Self::zeroed()
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone, Default, Pod, Zeroable)]
pub struct AtlasContent {
pub data: ImageLocationArray,
}
impl AtlasContent {
impl AtlasArray {
pub const SIZE: u64 = mem::size_of::<Self>() as wgpu::BufferAddress;
}

View File

@ -1,12 +1,13 @@
use galactica_constants::{IMAGE_LIMIT, SPRITE_LIMIT};
use galactica_constants::{IMAGE_LIMIT, OBJECT_SPRITE_INSTANCE_LIMIT, SPRITE_LIMIT};
use wgpu;
use super::{AtlasContent, DataContent, SpriteContent};
use super::{object::ObjectLocationArray, AtlasArray, DataContent, SpriteDataArray};
pub struct GlobalUniform {
pub data_buffer: wgpu::Buffer,
pub atlas_buffer: wgpu::Buffer,
pub sprite_buffer: wgpu::Buffer,
pub object_buffer: wgpu::Buffer,
pub bind_group: wgpu::BindGroup,
pub bind_group_layout: wgpu::BindGroupLayout,
pub content: DataContent,
@ -16,6 +17,7 @@ impl GlobalUniform {
pub fn shader_header(&self, group: u32) -> String {
let mut out = String::new();
// Global game data
out.push_str(&format!("@group({group}) @binding(0)\n"));
out.push_str(
r#"
@ -35,10 +37,16 @@ impl GlobalUniform {
);
out.push_str("\n");
out.push_str(&format!("@group({group}) @binding(1)\n"));
// Atlas image locations
out.push_str(&format!(
r#"
@group({group}) @binding(1)
var<uniform> atlas: array<ImageLocation, {IMAGE_LIMIT}>;
"#
));
out.push_str("\n");
out.push_str(
r#"
var<uniform> atlas: AtlasUniform;
struct ImageLocation {
xpos: f32,
ypos: f32,
@ -53,14 +61,6 @@ impl GlobalUniform {
};
"#,
);
out.push_str(&format!(
r#"
struct AtlasUniform {{
data: array<ImageLocation, {}>,
}};
"#,
IMAGE_LIMIT,
));
out.push_str("\n");
// TODO: document
@ -69,10 +69,16 @@ impl GlobalUniform {
// `Buffer is bound with size 3456 where the shader expects 5184 in group[1] compact index 2`
// More notes are in datacontent
out.push_str(&format!("@group({group}) @binding(2)\n"));
// Sprite data
out.push_str(&format!(
r#"
@group({group}) @binding(2)
var<uniform> sprites: array<SpriteData, {SPRITE_LIMIT}>;
"#
));
out.push_str("\n");
out.push_str(
r#"
var<uniform> sprites: SpriteUniform;
struct SpriteData {
frame_count: u32,
repeatmode: u32,
@ -86,15 +92,32 @@ impl GlobalUniform {
};
"#,
);
out.push_str("\n");
// Object location data
out.push_str(&format!(
r#"
struct SpriteUniform {{
data: array<SpriteData, {}>,
}};
"#,
SPRITE_LIMIT,
@group({group}) @binding(3)
var<uniform> objects: array<ObjectLocation, {OBJECT_SPRITE_INSTANCE_LIMIT}>;
"#
));
out.push_str("\n");
out.push_str(
r#"
struct ObjectLocation {
xpos: f32,
ypos: f32,
zpos: f32,
angle: f32,
size: f32,
padding_a: f32,
padding_b: f32,
padding_c: f32,
};
"#,
);
out.push_str("\n");
return out;
}
@ -109,14 +132,21 @@ impl GlobalUniform {
let atlas_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("global uniform atlas buffer"),
size: AtlasContent::SIZE,
size: AtlasArray::SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let sprite_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("global uniform sprite buffer"),
size: SpriteContent::SIZE,
size: SpriteDataArray::SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let object_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("global uniform object buffer"),
size: ObjectLocationArray::SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
@ -153,6 +183,16 @@ impl GlobalUniform {
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
label: Some("global uniform bind group layout"),
});
@ -172,6 +212,10 @@ impl GlobalUniform {
binding: 2,
resource: sprite_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: object_buffer.as_entire_binding(),
},
],
label: Some("global uniform bind group"),
});
@ -180,6 +224,7 @@ impl GlobalUniform {
data_buffer,
atlas_buffer,
sprite_buffer,
object_buffer,
bind_group,
bind_group_layout,
content: DataContent::default(),

View File

@ -1,9 +1,11 @@
mod atlascontent;
mod datacontent;
mod atlas;
mod data;
mod globaluniform;
mod spritecontent;
mod object;
mod sprite;
pub use atlascontent::{AtlasContent, ImageLocation, ImageLocationArray};
pub use datacontent::DataContent;
pub use atlas::{AtlasArray, ImageLocation};
pub use data::DataContent;
pub use globaluniform::GlobalUniform;
pub use spritecontent::{SpriteContent, SpriteData, SpriteDataArray};
pub use object::ObjectLocation;
pub use sprite::{SpriteData, SpriteDataArray};

View File

@ -0,0 +1,44 @@
use bytemuck::{Pod, Zeroable};
use galactica_constants::OBJECT_SPRITE_INSTANCE_LIMIT;
use std::mem;
use wgpu;
#[repr(C)]
#[derive(Debug, Copy, Clone, Pod, Zeroable, Default)]
pub struct ObjectLocation {
pub xpos: f32,
pub ypos: f32,
pub zpos: f32,
pub angle: f32,
pub size: f32,
pub _padding: [f32; 3],
}
impl ObjectLocation {
pub const SIZE: u64 = mem::size_of::<Self>() as wgpu::BufferAddress;
}
#[derive(Debug, Copy, Clone)]
pub struct ObjectLocationArray {
pub data: [ObjectLocation; OBJECT_SPRITE_INSTANCE_LIMIT as usize],
}
unsafe impl Pod for ObjectLocationArray {}
unsafe impl Zeroable for ObjectLocationArray {
fn zeroed() -> Self {
Self {
data: [ObjectLocation::zeroed(); OBJECT_SPRITE_INSTANCE_LIMIT as usize],
}
}
}
impl Default for ObjectLocationArray {
fn default() -> Self {
Self::zeroed()
}
}
impl ObjectLocationArray {
pub const SIZE: u64 = mem::size_of::<Self>() as wgpu::BufferAddress;
}

View File

@ -38,12 +38,6 @@ impl Default for SpriteDataArray {
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone, Default, Pod, Zeroable)]
pub struct SpriteContent {
pub data: SpriteDataArray,
}
impl SpriteContent {
impl SpriteDataArray {
pub const SIZE: u64 = mem::size_of::<Self>() as wgpu::BufferAddress;
}

View File

@ -1,6 +1,6 @@
use anyhow::Result;
use bytemuck;
use cgmath::{Deg, EuclideanSpace, Matrix2, Matrix4, Point2, Vector3};
use cgmath::{EuclideanSpace, Matrix2, Matrix4, Point2, Rad, Vector3};
use galactica_constants;
use rand::seq::SliceRandom;
use std::{iter, rc::Rc};
@ -9,7 +9,7 @@ use winit::{self, dpi::LogicalSize, window::Window};
use crate::{
content,
globaluniform::{AtlasContent, DataContent, GlobalUniform, SpriteContent},
globaluniform::{DataContent, GlobalUniform, ObjectLocation},
pipeline::PipelineBuilder,
sprite::ObjectSubSprite,
starfield::Starfield,
@ -322,12 +322,11 @@ impl GPUState {
};
// Game dimensions of this sprite post-scale.
// Don't divide by 2, we use this later.
let height = s.size / s.pos.z;
// Width or height, whichever is larger.
// Accounts for sprite rotation.
let m = height * s.sprite.aspect.max(1.0);
// Post-scale width or height, whichever is larger.
// This is in game units.
//
// We take the maximum to account for rotated sprites.
let m = (s.size / s.pos.z) * s.sprite.aspect.max(1.0);
// Don't draw (or compute matrices for)
// sprites that are off the screen
@ -339,51 +338,27 @@ impl GPUState {
return;
}
// TODO: clean up
let scale = height / state.camera_zoom;
// Note that our mesh starts centered at (0, 0).
// This is essential---we do not want scale and rotation
// changing our sprite's position!
// Apply sprite aspect ratio, preserving height.
// This must be done *before* rotation.
//
// We apply the provided scale here as well as a minor optimization
let sprite_aspect_and_scale =
Matrix4::from_nonuniform_scale(s.sprite.aspect * scale, scale, 1.0);
// Apply rotation
let rotate = Matrix4::from_angle_z(s.angle);
// Apply screen aspect ratio, again preserving height.
// This must be done AFTER rotation... think about it!
let screen_aspect = Matrix4::from_nonuniform_scale(1.0 / self.window_aspect, 1.0, 1.0);
// After finishing all ops, translate.
// This must be done last, all other operations
// require us to be at (0, 0).
//
// Note that we divide camera zoom by two.
// THIS IS IMPORTANT!
// The height of the viewport is `zoom` in game units,
// but it's 2 in screen units! (since coordinates range from -1 to 1)
let translate = Matrix4::from_translation(Vector3 {
x: pos.x / (state.camera_zoom / 2.0) / self.window_aspect,
y: pos.y / (state.camera_zoom / 2.0),
z: 0.0,
});
// Order matters!
// The rightmost matrix is applied first.
let t =
OPENGL_TO_WGPU_MATRIX * translate * screen_aspect * rotate * sprite_aspect_and_scale;
// Write this object's location data
self.queue.write_buffer(
&self.global_uniform.object_buffer,
ObjectLocation::SIZE * instances.len() as u64,
bytemuck::cast_slice(&[ObjectLocation {
xpos: s.pos.x,
ypos: s.pos.y,
zpos: s.pos.z,
angle: Rad::from(s.angle).0,
size: s.size,
_padding: Default::default(),
}]),
);
// Push this object's instance
instances.push(ObjectInstance {
transform: t.into(),
sprite_index: s.sprite.get_index(),
object_index: instances.len() as u32,
});
}
/*
// Add children
if let Some(children) = &s.children {
for c in children {
@ -433,6 +408,7 @@ impl GPUState {
sprite_index: s.sprite.get_index(),
});
}
*/
/// Create a ObjectInstance for a ui sprite and add it to `instances`
fn push_ui_sprite(&self, instances: &mut Vec<UiInstance>, s: &UiSprite) {
@ -551,16 +527,12 @@ impl GPUState {
self.queue.write_buffer(
&self.global_uniform.atlas_buffer,
0,
bytemuck::cast_slice(&[AtlasContent {
data: self.texture_array.image_locations,
}]),
bytemuck::cast_slice(&[self.texture_array.image_locations]),
);
self.queue.write_buffer(
&self.global_uniform.sprite_buffer,
0,
bytemuck::cast_slice(&[SpriteContent {
data: self.texture_array.sprite_data,
}]),
bytemuck::cast_slice(&[self.texture_array.sprite_data]),
);
self.update_starfield_buffer();
@ -601,6 +573,7 @@ impl GPUState {
});
let s = state.content.get_starfield_handle();
// Update global values
self.queue.write_buffer(
&self.global_uniform.data_buffer,

View File

@ -1,6 +1,6 @@
use crate::{
content,
globaluniform::{ImageLocation, ImageLocationArray, SpriteData, SpriteDataArray},
globaluniform::{AtlasArray, ImageLocation, SpriteData, SpriteDataArray},
};
use anyhow::Result;
use bytemuck::Zeroable;
@ -86,7 +86,7 @@ pub struct Texture {
pub struct TextureArray {
pub bind_group: wgpu::BindGroup,
pub bind_group_layout: BindGroupLayout,
pub image_locations: ImageLocationArray,
pub image_locations: AtlasArray,
pub sprite_data: SpriteDataArray,
}
@ -109,7 +109,7 @@ impl TextureArray {
)?);
}
let mut image_locations = ImageLocationArray::zeroed();
let mut image_locations = AtlasArray::zeroed();
let mut sprite_data = SpriteDataArray::zeroed();
println!("sending to gpu");

View File

@ -84,12 +84,11 @@ impl BufferObject for StarfieldInstance {
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct ObjectInstance {
/// Extra transformations this sprite
/// (rotation, etc)
pub transform: [[f32; 4]; 4],
/// What texture to use for this sprite
/// What texture to use for this instance
pub sprite_index: u32,
/// Which object this instance is for
pub object_index: u32,
}
impl BufferObject for ObjectInstance {
@ -101,31 +100,16 @@ impl BufferObject for ObjectInstance {
// instance when the shader starts processing a new instance
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
// 4 arrays = 1 4x4 matrix
// Sprite
wgpu::VertexAttribute {
offset: 0,
shader_location: 2,
format: wgpu::VertexFormat::Float32x4,
format: wgpu::VertexFormat::Uint32,
},
// Object
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress,
offset: mem::size_of::<[f32; 1]>() as wgpu::BufferAddress,
shader_location: 3,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 8]>() as wgpu::BufferAddress,
shader_location: 4,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 12]>() as wgpu::BufferAddress,
shader_location: 5,
format: wgpu::VertexFormat::Float32x4,
},
// Sprite
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 16]>() as wgpu::BufferAddress,
shader_location: 6,
format: wgpu::VertexFormat::Uint32,
},
],