diff --git a/src/render/globaldata.rs b/src/render/globaldata.rs new file mode 100644 index 0000000..bc2ad06 --- /dev/null +++ b/src/render/globaldata.rs @@ -0,0 +1,76 @@ +use bytemuck; +use std::mem; +use wgpu; + +pub struct GlobalData { + pub buffer: wgpu::Buffer, + pub bind_group: wgpu::BindGroup, + pub bind_group_layout: wgpu::BindGroupLayout, + pub content: GlobalDataContent, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)] +pub struct GlobalDataContent { + /// Camera position, in game units + pub camera_position: [f32; 2], + + /// Camera zoom value, in game units + pub camera_zoom: f32, + + /// Aspect ratio of window (width / height) + pub window_aspect: f32, + + /// Texture index of starfield sprites + pub starfield_texture: u32, + + // Size of (square) starfield tile, in game units + pub starfield_tile_size: f32, + + pub padding: [f32; 3], +} + +impl GlobalDataContent { + const SIZE: u64 = mem::size_of::() as wgpu::BufferAddress; +} + +impl GlobalData { + pub fn new(device: &wgpu::Device) -> Self { + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: None, + size: GlobalDataContent::SIZE, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + 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("camera_bind_group_layout"), + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + label: Some("camera_bind_group"), + }); + + return Self { + buffer, + bind_group, + bind_group_layout, + content: GlobalDataContent::default(), + }; + } +} diff --git a/src/render/gpustate.rs b/src/render/gpustate.rs index f3eeb88..9c706e3 100644 --- a/src/render/gpustate.rs +++ b/src/render/gpustate.rs @@ -1,6 +1,6 @@ use anyhow::Result; use bytemuck; -use cgmath::{EuclideanSpace, Point2, Vector2}; +use cgmath::{EuclideanSpace, Matrix2, Point2}; use std::{iter, rc::Rc}; use wgpu; use winit::{self, dpi::PhysicalSize, window::Window}; @@ -8,12 +8,12 @@ use winit::{self, dpi::PhysicalSize, window::Window}; use crate::Game; use super::{ + globaldata::{GlobalData, GlobalDataContent}, pipeline::PipelineBuilder, texturearray::TextureArray, - util::Transform, vertexbuffer::{ data::{SPRITE_INDICES, SPRITE_VERTICES}, - types::{PlainVertex, SpriteInstance, StarfieldInstance, TexturedVertex}, + types::{SpriteInstance, StarfieldInstance, TexturedVertex}, VertexBuffer, }, }; @@ -32,6 +32,7 @@ pub struct GPUState { starfield_pipeline: wgpu::RenderPipeline, texture_array: TextureArray, + global_data: GlobalData, vertex_buffers: VertexBuffers, } @@ -44,7 +45,7 @@ impl GPUState { // We can draw at most this many sprites on the screen. // TODO: compile-time option pub const SPRITE_INSTANCE_LIMIT: u64 = 100; - pub const STARFIELD_INSTANCE_LIMIT: u64 = 100; + pub const STARFIELD_INSTANCE_LIMIT: u64 = 501 * 9; pub async fn new(window: Window) -> Result { let window_size = window.inner_size(); @@ -118,18 +119,25 @@ impl GPUState { Self::SPRITE_INSTANCE_LIMIT, )), - starfield: Rc::new(VertexBuffer::new::( + starfield: Rc::new(VertexBuffer::new::( "starfield", &device, - None, - None, + Some(SPRITE_VERTICES), + Some(SPRITE_INDICES), Self::STARFIELD_INSTANCE_LIMIT, )), }; - // Load textures + // Load uniforms + let global_data = GlobalData::new(&device); let texture_array = TextureArray::new(&device, &queue)?; + // Make sure these match the indices in each shader + let bind_group_layouts = &[ + &texture_array.bind_group_layout, + &global_data.bind_group_layout, + ]; + // Create render pipelines let sprite_pipeline = PipelineBuilder::new("sprite", &device) .set_shader(include_str!(concat!( @@ -140,7 +148,7 @@ impl GPUState { .set_format(config.format) .set_triangle(true) .set_vertex_buffer(&vertex_buffers.sprite) - .set_bind_group_layouts(&[&texture_array.bind_group_layout]) + .set_bind_group_layouts(bind_group_layouts) .build(); let starfield_pipeline = PipelineBuilder::new("starfield", &device) @@ -150,8 +158,9 @@ impl GPUState { "starfield.wgsl" ))) .set_format(config.format) - .set_triangle(false) + .set_triangle(true) .set_vertex_buffer(&vertex_buffers.starfield) + .set_bind_group_layouts(bind_group_layouts) .build(); return Ok(Self { @@ -168,6 +177,7 @@ impl GPUState { starfield_pipeline, texture_array, + global_data, vertex_buffers, }); } @@ -229,19 +239,12 @@ impl GPUState { // // We can't use screen_pos to exclude off-screen sprites because // it can't account for height and width. - let scale = height / game.camera.zoom; - let screen_pos: Point2 = pos / game.camera.zoom; instances.push(SpriteInstance { - transform: Transform { - pos: screen_pos, - sprite_aspect: texture.aspect, - window_aspect: self.window_aspect, - rotate: s.angle, - scale, - } - .to_matrix() - .into(), + position: pos.into(), + aspect: texture.aspect, + rotation: Matrix2::from_angle(s.angle).into(), + height, texture_index: texture.index, }) } @@ -259,22 +262,25 @@ impl GPUState { /// Will panic if STARFIELD_INSTANCE_LIMIT is exceeded. /// /// This is only called inside self.render() - fn make_starfield_instances(&self, _game: &Game) -> Vec { - let instances: Vec = (-10..10) - .map(|x| StarfieldInstance { - position: Vector2 { - x: x as f32 / 10.0, - y: 0.0, - } - .into(), - }) - .collect(); + fn make_starfield_instances(&self, game: &Game) -> Vec { + let mut instances = Vec::new(); + for s in &game.system.starfield { + // TODO: minimize operations here + //let pos = Sprite::post_parallax(s.parallax, s.pos * 500.0, game.camera) + // - game.camera.pos.to_vec(); + instances.push(StarfieldInstance { + position: s.pos.into(), + parallax: s.parallax, + height: 2.0, + }); + } // Enforce starfield limit if instances.len() as u64 > Self::STARFIELD_INSTANCE_LIMIT { // TODO: no panic, handle this better. unreachable!("Starfield limit exceeded!") } + return instances; } @@ -311,6 +317,20 @@ impl GPUState { timestamp_writes: None, }); + // Update global values + self.queue.write_buffer( + &self.global_data.buffer, + 0, + bytemuck::cast_slice(&[GlobalDataContent { + camera_position: game.camera.pos.into(), + camera_zoom: game.camera.zoom, + window_aspect: self.window_aspect, + starfield_texture: 1, + starfield_tile_size: 1000.0, + padding: Default::default(), + }]), + ); + // Create sprite instances let sprite_instances = self.make_sprite_instances(game); self.queue.write_buffer( @@ -327,12 +347,21 @@ impl GPUState { bytemuck::cast_slice(&starfield_instances), ); + // These should match the indices in each shader, + // and should each have a corresponding bind group layout. render_pass.set_bind_group(0, &self.texture_array.bind_group, &[]); + render_pass.set_bind_group(1, &self.global_data.bind_group, &[]); // Starfield pipeline self.vertex_buffers.starfield.set_in_pass(&mut render_pass); render_pass.set_pipeline(&self.starfield_pipeline); + render_pass.draw(0..1, 0..starfield_instances.len() as _); + render_pass.draw_indexed( + 0..SPRITE_INDICES.len() as u32, + 0, + 0..starfield_instances.len() as _, + ); // Sprite pipeline self.vertex_buffers.sprite.set_in_pass(&mut render_pass); diff --git a/src/render/mod.rs b/src/render/mod.rs index 479cc43..6877b1a 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -1,18 +1,17 @@ +mod globaldata; mod gpustate; mod pipeline; mod rawtexture; mod texturearray; -mod util; mod vertexbuffer; pub use gpustate::GPUState; pub use texturearray::Texture; -use cgmath::Matrix4; - -/// API correction matrix. -/// cgmath uses OpenGL's matrix format, which -/// needs to be converted to wgpu's matrix format. +// API correction matrix. +// cgmath uses OpenGL's matrix format, which +// needs to be converted to wgpu's matrix format. +/* #[rustfmt::skip] const OPENGL_TO_WGPU_MATRIX: Matrix4 = Matrix4::new( 1.0, 0.0, 0.0, 0.0, @@ -20,3 +19,4 @@ const OPENGL_TO_WGPU_MATRIX: Matrix4 = Matrix4::new( 0.0, 0.0, 0.5, 0.5, 0.0, 0.0, 0.0, 1.0, ); +*/ diff --git a/src/render/shaders/sprite.wgsl b/src/render/shaders/sprite.wgsl index c6e8532..446064d 100644 --- a/src/render/shaders/sprite.wgsl +++ b/src/render/shaders/sprite.wgsl @@ -1,10 +1,11 @@ // Vertex shader struct InstanceInput { - @location(2) transform_matrix_0: vec4, - @location(3) transform_matrix_1: vec4, - @location(4) transform_matrix_2: vec4, - @location(5) transform_matrix_3: vec4, - @location(6) texture_idx: u32, + @location(2) rotation_matrix_0: vec2, + @location(3) rotation_matrix_1: vec2, + @location(4) position: vec2, + @location(5) height: f32, + @location(6) aspect: f32, + @location(7) texture_idx: u32, }; struct VertexInput { @@ -13,26 +14,57 @@ struct VertexInput { } struct VertexOutput { - @builtin(position) clip_position: vec4, + @builtin(position) position: vec4, @location(0) texture_coords: vec2, @location(1) index: u32, } + +@group(1) @binding(0) +var global: GlobalUniform; +struct GlobalUniform { + camera_position: vec2, + camera_zoom: f32, + window_aspect: f32, + starfield_texture: u32, + starfield_tile_size: f32 +}; + + @vertex fn vertex_shader_main( - model: VertexInput, + vertex: VertexInput, instance: InstanceInput, ) -> VertexOutput { - let transform_matrix = mat4x4( - instance.transform_matrix_0, - instance.transform_matrix_1, - instance.transform_matrix_2, - instance.transform_matrix_3, + + // Apply sprite aspect ratio & scale factor + // This must be done *before* rotation. + let scale = instance.height / global.camera_zoom; + var pos: vec2 = vec2( + vertex.position.x * instance.aspect * scale, + vertex.position.y * scale + ); + + // Rotate + pos = mat2x2( + instance.rotation_matrix_0, + instance.rotation_matrix_1, + ) * pos; + + // Apply screen aspect ratio, again preserving height. + // This must be done AFTER rotation... think about it! + pos = pos / vec2(global.window_aspect, 1.0); + + // Translate + pos = pos + ( + // Don't forget to correct distance for screen aspect ratio too! + (instance.position / global.camera_zoom) + / vec2(global.window_aspect, 1.0) ); var out: VertexOutput; - out.texture_coords = model.texture_coords; - out.clip_position = transform_matrix * vec4(model.position, 1.0); + out.position = vec4(pos, 0.0, 1.0); + out.texture_coords = vertex.texture_coords; out.index = instance.texture_idx; return out; } diff --git a/src/render/shaders/starfield.wgsl b/src/render/shaders/starfield.wgsl index c63e44c..f97171a 100644 --- a/src/render/shaders/starfield.wgsl +++ b/src/render/shaders/starfield.wgsl @@ -1,25 +1,94 @@ // Vertex shader struct InstanceInput { @location(2) position: vec2, + @location(3) parallax: f32, + @location(4) height: f32, }; +struct VertexInput { + @location(0) position: vec3, + @location(1) texture_coords: vec2, +} + struct VertexOutput { - @builtin(position) clip_position: vec4, + @builtin(position) position: vec4, + @location(0) texture_coords: vec2, +} + +@group(1) @binding(0) +var global: GlobalUniform; +struct GlobalUniform { + camera_position: vec2, + camera_zoom: f32, + window_aspect: f32, + starfield_texture: u32, + starfield_tile_size: f32 +}; + + +fn fmod(x: vec2, m: f32) -> vec2 { + return x - floor(x * (1.0 / m)) * m; } @vertex fn vertex_shader_main( + vertex: VertexInput, instance: InstanceInput, ) -> VertexOutput { - // Stars consist of only one vertex, so we don't need to pass a buffer into this shader. - // We need one instance per star, and that's it! + + // Center of the tile the camera is currently in, in game coordinates. + // x div y = x - (x mod y) + let tile_center = ( + global.camera_position + - ( + fmod( + global.camera_position + global.starfield_tile_size/2.0, + global.starfield_tile_size + ) - global.starfield_tile_size/2.0 + ) + ); + + // Apply sprite aspect ratio & scale factor + // also applies screen aspect ratio + let scale = instance.height / global.camera_zoom; + var pos: vec2 = vec2( + vertex.position.x * scale / global.window_aspect, + vertex.position.y * scale + ); + + // World position relative to camera + // (Note that instance position is in a different + // coordinate system than usual) + let camera_pos = (instance.position + tile_center) - global.camera_position; + let parallax_vector = -camera_pos * instance.parallax * 0.1; + + // Translate + pos = pos + ( + // Don't forget to correct distance for screen aspect ratio too! + // The minus in "minus parallax_vector" is important. + ((camera_pos + parallax_vector) / global.camera_zoom) + / vec2(global.window_aspect, 1.0) + ); + var out: VertexOutput; - out.clip_position = vec4(instance.position, 0.0, 1.0); + out.position = vec4(pos, 0.0, 1.0); + out.texture_coords = vertex.texture_coords; return out; } + +@group(0) @binding(0) +var texture_array: binding_array>; +@group(0) @binding(1) +var sampler_array: binding_array; + // Fragment shader @fragment fn fragment_shader_main(in: VertexOutput) -> @location(0) vec4 { - return vec4(1.0, 1.0, 1.0, 1.0); + return textureSampleLevel( + texture_array[1], + sampler_array[1], + in.texture_coords, + 0.0 + ).rgba * vec4(0.5, 0.5, 0.5, 1.0); } \ No newline at end of file diff --git a/src/render/util.rs b/src/render/util.rs deleted file mode 100644 index 73f9b84..0000000 --- a/src/render/util.rs +++ /dev/null @@ -1,53 +0,0 @@ -use cgmath::{Deg, Matrix4, Point2, Vector3}; - -use super::OPENGL_TO_WGPU_MATRIX; - -// Represents a sprite tranformation. -// -// This produces a single matrix we can apply to a brand-new sprite instance -// to put it in the right place, at the right angle, with the right scale. -pub struct Transform { - pub pos: Point2, // position on screen - pub window_aspect: f32, // width / height. Screen aspect ratio. - pub sprite_aspect: f32, // width / height. Sprite aspect ratio. - pub scale: f32, // if scale = 1, this sprite will be as tall as the screen. - pub rotate: Deg, // Around this object's center, in degrees measured ccw from vertical -} - -impl Transform { - /// Build the matrix that corresponds to this transformation. - pub fn to_matrix(&self) -> Matrix4 { - // 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(self.sprite_aspect * self.scale, self.scale, 1.0); - - // Apply rotation - let rotate = Matrix4::from_angle_z(self.rotate); - - // 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 op, translate. - // This must be done last, all other operations - // require us to be at (0, 0). - let translate = Matrix4::from_translation(Vector3 { - x: self.pos.x, - y: self.pos.y, - z: 0.0, - }); - - // Order matters! - // The rightmost matrix is applied first. - return OPENGL_TO_WGPU_MATRIX - * translate * screen_aspect - * rotate * sprite_aspect_and_scale; - } -} diff --git a/src/render/vertexbuffer/types.rs b/src/render/vertexbuffer/types.rs index a57e158..50b9a94 100644 --- a/src/render/vertexbuffer/types.rs +++ b/src/render/vertexbuffer/types.rs @@ -32,34 +32,20 @@ impl BufferObject for TexturedVertex { } } -// Represents a plain vertex in WGSL -#[repr(C)] -#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] -pub struct PlainVertex { - pub position: [f32; 3], -} - -impl BufferObject for PlainVertex { - fn layout() -> wgpu::VertexBufferLayout<'static> { - wgpu::VertexBufferLayout { - array_stride: Self::SIZE, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &[wgpu::VertexAttribute { - offset: 0, - shader_location: 0, - format: wgpu::VertexFormat::Float32x3, - }], - } - } -} - // Represents a point in the starfield instance in WGSL #[repr(C)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] pub struct StarfieldInstance { - // All transformations we need to put this dot in its place - // TODO: we don't need that much data + /// Position in origin field tile. + /// note that this is DIFFERENT from + /// the way we provide sprite positions! pub position: [f32; 2], + + /// Parallax factor (same unit as usual) + pub parallax: f32, + + /// Height of (unrotated) sprite in world units + pub height: f32, } impl BufferObject for StarfieldInstance { @@ -67,11 +53,26 @@ impl BufferObject for StarfieldInstance { wgpu::VertexBufferLayout { array_stride: Self::SIZE, step_mode: wgpu::VertexStepMode::Instance, - attributes: &[wgpu::VertexAttribute { - offset: 0, - shader_location: 2, - format: wgpu::VertexFormat::Float32x2, - }], + attributes: &[ + // Position + wgpu::VertexAttribute { + offset: 0, + shader_location: 2, + format: wgpu::VertexFormat::Float32x2, + }, + // Parallax + wgpu::VertexAttribute { + offset: mem::size_of::<[f32; 2]>() as wgpu::BufferAddress, + shader_location: 3, + format: wgpu::VertexFormat::Float32, + }, + // Height + wgpu::VertexAttribute { + offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, + shader_location: 4, + format: wgpu::VertexFormat::Float32, + }, + ], } } } @@ -80,11 +81,19 @@ impl BufferObject for StarfieldInstance { #[repr(C)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] pub struct SpriteInstance { - // All transformations we need to put this sprite in its place - pub transform: [[f32; 4]; 4], + /// Rotation matrix for this sprite + pub rotation: [[f32; 2]; 2], - // Which texture we should use for this sprite - // (see TextureArray) + /// World position, relative to camera + pub position: [f32; 2], + + /// Height of (unrotated) sprite in world units + pub height: f32, + + // Sprite aspect ratio (width / height) + pub aspect: f32, + + // What texture to use for this sprite pub texture_index: u32, } @@ -97,29 +106,39 @@ impl BufferObject for SpriteInstance { // instance when the shader starts processing a new instance step_mode: wgpu::VertexStepMode::Instance, attributes: &[ + // 2 arrays = 1 2x2 matrix wgpu::VertexAttribute { offset: 0, shader_location: 2, - format: wgpu::VertexFormat::Float32x4, + format: wgpu::VertexFormat::Float32x2, }, + wgpu::VertexAttribute { + offset: mem::size_of::<[f32; 2]>() as wgpu::BufferAddress, + shader_location: 3, + format: wgpu::VertexFormat::Float32x2, + }, + // Position wgpu::VertexAttribute { offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress, - shader_location: 3, - format: wgpu::VertexFormat::Float32x4, + shader_location: 4, + format: wgpu::VertexFormat::Float32x2, }, + // Height + wgpu::VertexAttribute { + offset: mem::size_of::<[f32; 6]>() as wgpu::BufferAddress, + shader_location: 5, + format: wgpu::VertexFormat::Float32, + }, + // Aspect + wgpu::VertexAttribute { + offset: mem::size_of::<[f32; 7]>() as wgpu::BufferAddress, + shader_location: 6, + format: wgpu::VertexFormat::Float32, + }, + // Texture 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, - }, - wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 16]>() as wgpu::BufferAddress, - shader_location: 6, + shader_location: 7, format: wgpu::VertexFormat::Uint32, }, ], diff --git a/src/system.rs b/src/system.rs index 37303bf..c20c584 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,13 +1,37 @@ -use crate::{physics::Polar, Doodad, Sprite, Spriteable}; -use cgmath::Deg; +use crate::{ + physics::{Pfloat, Polar}, + Doodad, Sprite, Spriteable, +}; +use cgmath::{Deg, Point2}; +use rand::{self, Rng}; + +pub struct StarfieldStar { + // Star coordinates, in world space. + // These are relative to the center of a starfield tile. + pub pos: Point2, + pub parallax: Pfloat, +} pub struct System { bodies: Vec, + pub starfield: Vec, } impl System { pub fn new() -> Self { - let mut s = System { bodies: Vec::new() }; + let mut rng = rand::thread_rng(); + let mut s = System { + bodies: Vec::new(), + starfield: (0..500) + .map(|_| StarfieldStar { + pos: Point2 { + x: rng.gen_range(-500.0..500.0), + y: rng.gen_range(-500.0..500.0), + }, + parallax: rng.gen_range(10.0..11.0), + }) + .collect(), + }; s.bodies.push(Doodad { pos: (0.0, 0.0).into(),