From a5b3932e9dbdf31025a4703067beaaf924ee66aa Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 25 Dec 2023 15:56:27 -0800 Subject: [PATCH] Renamed "parallax" into 3D coordinates, polished starfield hiding. --- content/system.toml | 7 ++- src/consts.rs | 34 +++++++----- src/content/syntax/system.rs | 86 ++++++++++++++++++++++++------- src/game/doodad.rs | 6 +-- src/game/game.rs | 5 +- src/game/ship.rs | 3 +- src/game/system.rs | 20 ++++--- src/render/globaldata.rs | 14 +++-- src/render/gpustate.rs | 28 ++++++---- src/render/shaders/sprite.wgsl | 6 ++- src/render/shaders/starfield.wgsl | 47 ++++++++++++----- src/render/sprite.rs | 9 +--- src/render/vertexbuffer/types.rs | 38 +++++++------- 13 files changed, 192 insertions(+), 111 deletions(-) diff --git a/content/system.toml b/content/system.toml index 71eac99..97ccc4b 100644 --- a/content/system.toml +++ b/content/system.toml @@ -4,9 +4,8 @@ name = "12 Autumn above" [object.star] sprite = "star::star" -position = [0.0, 0.0] +position = [0.0, 0.0, 20.0] size = 1000 -parallax = 20.0 [object.earth] @@ -14,8 +13,8 @@ sprite = "planet::earth" position.center = "star" position.radius = 4000 position.angle = 0 +position.z = 10.0 size = 1000 -parallax = 10.0 [object.luna] @@ -23,6 +22,6 @@ sprite = "planet::luna" position.center = "earth" position.radius = 1600 position.angle = 135 +position.z = 7.8 size = 500 angle = -45 -parallax = 7.8 diff --git a/src/consts.rs b/src/consts.rs index 8a959d1..d7b64a0 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -3,19 +3,29 @@ use crate::physics::Pfloat; pub const ZOOM_MIN: Pfloat = 200.0; pub const ZOOM_MAX: Pfloat = 2000.0; -// Z-axis range for starfield stars -pub const STARFIELD_PARALLAX_MIN: f32 = 100.0; -pub const STARFIELD_PARALLAX_MAX: f32 = 200.0; -// Size of a square starfield tile, in game units. -// A tile of size PARALLAX_MAX * screen-size-in-game-units -// will completely cover a (square) screen. This depends on zoom! -// -// Use a value smaller than zoom_max for debug. -pub const STARFIELD_SIZE: u64 = STARFIELD_PARALLAX_MAX as u64 * ZOOM_MAX as u64; -// Average number of stars per game unit +/// Z-axis range for starfield stars +/// This does not affect scale. +pub const STARFIELD_Z_MIN: f32 = 100.0; +pub const STARFIELD_Z_MAX: f32 = 200.0; + +/// Size range for starfield stars, in game units. +/// This is scaled for zoom, but NOT for distance. +pub const STARFIELD_SIZE_MIN: f32 = 0.2; +pub const STARFIELD_SIZE_MAX: f32 = 1.8; + +/// Size of a square starfield tile, in game units. +/// A tile of size STARFIELD_Z_MAX * screen-size-in-game-units +/// will completely cover a (square) screen. This depends on zoom! +/// +/// Use a value smaller than zoom_max for debug. +pub const STARFIELD_SIZE: u64 = STARFIELD_Z_MAX as u64 * ZOOM_MAX as u64; + +/// Average number of stars per game unit pub const STARFIELD_DENSITY: f64 = 0.01; -// Number of stars in one starfield tile -// Must fit inside an i32 + +/// Number of stars in one starfield tile +/// Must fit inside an i32 pub const STARFIELD_COUNT: u64 = (STARFIELD_SIZE as f64 * STARFIELD_DENSITY) as u64; +/// Root directory of game content pub const CONTENT_ROOT: &'static str = "./content"; diff --git a/src/content/syntax/system.rs b/src/content/syntax/system.rs index cd0859c..9d7a2fe 100644 --- a/src/content/syntax/system.rs +++ b/src/content/syntax/system.rs @@ -1,5 +1,5 @@ use anyhow::{bail, Context, Result}; -use cgmath::{Deg, Point2}; +use cgmath::{Deg, Point3}; use std::collections::{HashMap, HashSet}; use crate::physics::{Pfloat, Polar}; @@ -27,7 +27,6 @@ pub(in crate::content) mod toml { pub position: Position, pub size: Pfloat, - pub parallax: Pfloat, pub radius: Option, pub angle: Option, @@ -37,24 +36,25 @@ pub(in crate::content) mod toml { #[serde(untagged)] pub enum Position { Polar(PolarCoords), - Cartesian(Coordinates), + Cartesian(CoordinatesThree), } #[derive(Debug, Deserialize)] pub struct PolarCoords { - pub center: Coordinates, + pub center: CoordinatesTwo, pub radius: Pfloat, pub angle: Pfloat, + pub z: Pfloat, } #[derive(Debug, Deserialize)] #[serde(untagged)] - pub enum Coordinates { + pub enum CoordinatesTwo { Label(String), Coords([Pfloat; 2]), } - impl ToString for Coordinates { + impl ToString for CoordinatesTwo { fn to_string(&self) -> String { match self { Self::Label(s) => s.to_owned(), @@ -62,6 +62,44 @@ pub(in crate::content) mod toml { } } } + + impl CoordinatesTwo { + /// Transform a CoordinatesThree into a CoordinatesTwo by adding a NaN z component. + /// Labels are not changed. + pub fn to_three(&self) -> CoordinatesThree { + match self { + Self::Label(s) => CoordinatesThree::Label(s.clone()), + Self::Coords(v) => CoordinatesThree::Coords([v[0], v[1], f32::NAN]), + } + } + } + + #[derive(Debug, Deserialize)] + #[serde(untagged)] + pub enum CoordinatesThree { + Label(String), + Coords([Pfloat; 3]), + } + + impl ToString for CoordinatesThree { + fn to_string(&self) -> String { + match self { + Self::Label(s) => s.to_owned(), + Self::Coords(v) => format!("{:?}", v), + } + } + } + + impl CoordinatesThree { + /// Transform a CoordinatesThree into a CoordinatesTwo by deleting z component. + /// Labels are not changed. + pub fn to_two(&self) -> CoordinatesTwo { + match self { + Self::Label(s) => CoordinatesTwo::Label(s.clone()), + Self::Coords(v) => CoordinatesTwo::Coords([v[0], v[1]]), + } + } + } } #[derive(Debug)] @@ -73,20 +111,20 @@ pub struct System { #[derive(Debug)] pub struct Object { pub sprite: String, - pub position: Point2, + pub position: Point3, pub size: Pfloat, - pub parallax: Pfloat, pub angle: Deg, } +// Helper function for resolve_position, never called on its own. fn resolve_coordinates( objects: &HashMap, - cor: &toml::Coordinates, + cor: &toml::CoordinatesThree, mut cycle_detector: HashSet, -) -> Result> { +) -> Result> { match cor { - toml::Coordinates::Coords(c) => Ok((*c).into()), - toml::Coordinates::Label(l) => { + toml::CoordinatesThree::Coords(c) => Ok((*c).into()), + toml::CoordinatesThree::Label(l) => { if cycle_detector.contains(l) { bail!( "Found coordinate cycle: `{}`", @@ -111,19 +149,28 @@ fn resolve_coordinates( } } +/// Given an object, resolve it's position as a Point3. fn resolve_position( objects: &HashMap, obj: &toml::Object, cycle_detector: HashSet, -) -> Result> { +) -> Result> { match &obj.position { - toml::Position::Cartesian(c) => Ok(resolve_coordinates(objects, c, cycle_detector)?), - toml::Position::Polar(p) => Ok(Polar { - center: resolve_coordinates(&objects, &p.center, cycle_detector)?, - radius: p.radius, - angle: Deg(p.angle), + toml::Position::Cartesian(c) => Ok(resolve_coordinates(objects, &c, cycle_detector)?), + toml::Position::Polar(p) => { + let r = resolve_coordinates(&objects, &p.center.to_three(), cycle_detector)?; + let plane = Polar { + center: (r.x, r.y).into(), + radius: p.radius, + angle: Deg(p.angle), + } + .to_cartesian(); + Ok(Point3 { + x: plane.x, + y: plane.y, + z: p.z, + }) } - .to_cartesian()), } } @@ -140,7 +187,6 @@ impl System { position: resolve_position(&value.object, &obj, cycle_detector) .with_context(|| format!("In object {:#?}", label))?, size: obj.size, - parallax: obj.parallax, angle: Deg(obj.angle.unwrap_or(0.0)), }); } diff --git a/src/game/doodad.rs b/src/game/doodad.rs index bfc3df1..f2534f1 100644 --- a/src/game/doodad.rs +++ b/src/game/doodad.rs @@ -1,11 +1,10 @@ -use cgmath::{Deg, Point2}; +use cgmath::{Deg, Point3}; use crate::{physics::Pfloat, render::Sprite, render::SpriteTexture, render::Spriteable}; pub struct Doodad { pub sprite: SpriteTexture, - pub pos: Point2, - pub parallax: Pfloat, + pub pos: Point3, pub size: Pfloat, pub angle: Deg, } @@ -18,7 +17,6 @@ impl Spriteable for Doodad { pos: self.pos, angle: self.angle, size: self.size, - parallax: self.parallax, }; } } diff --git a/src/game/game.rs b/src/game/game.rs index 3f0c42b..3dcc344 100644 --- a/src/game/game.rs +++ b/src/game/game.rs @@ -75,9 +75,8 @@ impl Game { // Make sure sprites are drawn in the correct order // (note the reversed a, b in the comparator) // - // TODO: use a gpu depth buffer with parallax as z-coordinate? - // Might be overkill. - sprites.sort_by(|a, b| b.parallax.total_cmp(&a.parallax)); + // TODO: use a gpu depth buffer instead. + sprites.sort_by(|a, b| b.pos.z.total_cmp(&a.pos.z)); return sprites; } diff --git a/src/game/ship.rs b/src/game/ship.rs index ef6cf63..8e33ab2 100644 --- a/src/game/ship.rs +++ b/src/game/ship.rs @@ -41,11 +41,10 @@ impl Ship { impl Spriteable for Ship { fn get_sprite(&self) -> Sprite { return Sprite { - pos: self.body.pos, + pos: (self.body.pos.x, self.body.pos.y, 1.0).into(), texture: self.kind.sprite(), angle: self.body.angle, scale: 1.0, - parallax: 1.0, size: self.kind.size(), }; } diff --git a/src/game/system.rs b/src/game/system.rs index e7c8641..8d782b7 100644 --- a/src/game/system.rs +++ b/src/game/system.rs @@ -1,4 +1,4 @@ -use cgmath::{Point2, Vector2}; +use cgmath::{Point3, Vector2}; use rand::{self, Rng}; use super::Doodad; @@ -9,14 +9,14 @@ use crate::{ pub struct StarfieldStar { /// Star coordinates, in world space. /// These are relative to the center of a starfield tile. - pub pos: Point2, + pub pos: Point3, - // TODO: z-coordinate? - pub parallax: Pfloat, + /// Height in game units. + /// Will be scaled for zoom, but not for distance. pub size: Pfloat, - /// Color/brightness variation. - /// See shader. + /// Color/brightness variation. Random between 0 and 1. + /// Used in starfield shader. pub tint: Vector2, } @@ -35,13 +35,12 @@ impl System { bodies: Vec::new(), starfield: (0..consts::STARFIELD_COUNT) .map(|_| StarfieldStar { - pos: Point2 { + pos: Point3 { x: rng.gen_range(-sz..=sz), y: rng.gen_range(-sz..=sz), + z: rng.gen_range(consts::STARFIELD_Z_MIN..consts::STARFIELD_Z_MAX), }, - parallax: rng - .gen_range(consts::STARFIELD_PARALLAX_MIN..consts::STARFIELD_PARALLAX_MAX), - size: rng.gen_range(0.2..0.8), // TODO: configurable + size: rng.gen_range(consts::STARFIELD_SIZE_MIN..consts::STARFIELD_SIZE_MAX), tint: Vector2 { x: rng.gen_range(0.0..=1.0), y: rng.gen_range(0.0..=1.0), @@ -55,7 +54,6 @@ impl System { pos: o.position, sprite: SpriteTexture(o.sprite.to_owned()), size: o.size, - parallax: o.parallax, angle: o.angle, }); } diff --git a/src/render/globaldata.rs b/src/render/globaldata.rs index 8bbc4bf..d2ab328 100644 --- a/src/render/globaldata.rs +++ b/src/render/globaldata.rs @@ -21,23 +21,29 @@ pub struct GlobalDataContent { pub camera_position: [f32; 2], /// Camera zoom value, in game units. - /// Only first component has meaning. + /// Second component is ignored. pub camera_zoom: [f32; 2], + /// Camera zoom min and max. + pub camera_zoom_limits: [f32; 2], + /// Size ratio of window, in physical pixels pub window_size: [f32; 2], // Aspect ration of window - /// Only first component has meaning. + /// Second component is ignored. pub window_aspect: [f32; 2], /// Texture index of starfield sprites - /// Only first component has meaning. + /// Second component is ignored. pub starfield_texture: [u32; 2], // Size of (square) starfield tiles, in game units - /// Only first component has meaning. + /// Second component is ignored. pub starfield_tile_size: [f32; 2], + + // Min and max starfield star size, in game units + pub starfield_size_limits: [f32; 2], } impl GlobalDataContent { diff --git a/src/render/gpustate.rs b/src/render/gpustate.rs index 11cc71a..46e4c00 100644 --- a/src/render/gpustate.rs +++ b/src/render/gpustate.rs @@ -1,6 +1,6 @@ use anyhow::Result; use bytemuck; -use cgmath::{EuclideanSpace, Matrix2, Point2, Vector2}; +use cgmath::{EuclideanSpace, Matrix2, Point2, Vector2, Vector3}; use std::{iter, rc::Rc}; use wgpu; use winit::{self, dpi::PhysicalSize, window::Window}; @@ -16,7 +16,7 @@ use super::{ VertexBuffer, }, }; -use crate::{consts, game::Game}; +use crate::{consts, game::Game, physics::Pfloat}; pub struct GPUState { device: wgpu::Device, @@ -209,14 +209,20 @@ impl GPUState { let clip_sw = Point2::from((self.window_aspect, -1.0)) * game.camera.zoom; for s in game.get_sprites() { - // Parallax is computed here, so we can check if this sprite is visible. - let pos = (s.pos - game.camera.pos.to_vec()) - / (s.parallax + game.camera.zoom / consts::ZOOM_MIN); + // Compute post-parallax position and distance-adjusted scale. + // We do this here so we can check if a sprite is on the screen. + let pos: Point2 = { + (Point2 { + x: s.pos.x, + y: s.pos.y, + } - game.camera.pos.to_vec()) + / (s.pos.z + game.camera.zoom / consts::ZOOM_MIN) + }; let texture = self.texture_array.get_sprite_texture(s.texture); // Game dimensions of this sprite post-scale. // Don't divide by 2, we use this later. - let height = s.size * s.scale / s.parallax; + let height = s.size * s.scale / s.pos.z; let width = height * texture.aspect; // Don't draw (or compute matrices for) @@ -233,7 +239,7 @@ impl GPUState { position: pos.into(), aspect: texture.aspect, rotation: Matrix2::from_angle(s.angle).into(), - height, + size: height, texture_index: texture.index, }) } @@ -262,7 +268,7 @@ impl GPUState { // Parallax correction. // Also, adjust v for mod to work properly // (v is centered at 0) - let v: Point2 = clip_nw * consts::STARFIELD_PARALLAX_MIN; + let v: Point2 = clip_nw * consts::STARFIELD_Z_MIN; let v_adj: Point2 = (v.x + (sz / 2.0), v.y + (sz / 2.0)).into(); #[rustfmt::skip] @@ -296,14 +302,14 @@ impl GPUState { let mut instances = Vec::new(); for x in (-nw_tile.x)..=nw_tile.x { for y in (-nw_tile.y)..=nw_tile.y { - let offset = Vector2 { + let offset = Vector3 { x: sz * x as f32, y: sz * y as f32, + z: 0.0, }; for s in &game.system.starfield { instances.push(StarfieldInstance { position: (s.pos + offset).into(), - parallax: s.parallax, size: s.size, tint: s.tint.into(), }) @@ -364,6 +370,7 @@ impl GPUState { bytemuck::cast_slice(&[GlobalDataContent { camera_position: game.camera.pos.into(), camera_zoom: [game.camera.zoom, 0.0], + camera_zoom_limits: [consts::ZOOM_MIN, consts::ZOOM_MAX], window_size: [ self.window_size.width as f32, self.window_size.height as f32, @@ -371,6 +378,7 @@ impl GPUState { window_aspect: [self.window_aspect, 0.0], starfield_texture: [self.texture_array.get_starfield_texture().index, 0], starfield_tile_size: [consts::STARFIELD_SIZE as f32, 0.0], + starfield_size_limits: [consts::STARFIELD_SIZE_MIN, consts::STARFIELD_SIZE_MAX], }]), ); diff --git a/src/render/shaders/sprite.wgsl b/src/render/shaders/sprite.wgsl index de0c32c..a4fac1d 100644 --- a/src/render/shaders/sprite.wgsl +++ b/src/render/shaders/sprite.wgsl @@ -2,7 +2,7 @@ struct InstanceInput { @location(2) rotation_matrix_0: vec2, @location(3) rotation_matrix_1: vec2, @location(4) position: vec2, - @location(5) height: f32, + @location(5) size: f32, @location(6) aspect: f32, @location(7) texture_idx: u32, }; @@ -24,10 +24,12 @@ var global: GlobalUniform; struct GlobalUniform { camera_position: vec2, camera_zoom: vec2, + camera_zoom_limits: vec2, window_size: vec2, window_aspect: vec2, starfield_texture: vec2, starfield_tile_size: vec2, + starfield_size_limits: vec2, }; @@ -47,7 +49,7 @@ fn vertex_main( // Apply sprite aspect ratio & scale factor // This must be done *before* rotation. - let scale = instance.height / global.camera_zoom.x; + let scale = instance.size / global.camera_zoom.x; var pos: vec2 = vec2( vertex.position.x * instance.aspect * scale, vertex.position.y * scale diff --git a/src/render/shaders/starfield.wgsl b/src/render/shaders/starfield.wgsl index 707af0e..4d5b49a 100644 --- a/src/render/shaders/starfield.wgsl +++ b/src/render/shaders/starfield.wgsl @@ -1,8 +1,7 @@ struct InstanceInput { - @location(2) position: vec2, - @location(3) parallax: f32, - @location(4) size: f32, - @location(5) tint: vec2, + @location(2) position: vec3, + @location(3) size: f32, + @location(4) tint: vec2, }; struct VertexInput { @@ -21,10 +20,12 @@ var global: GlobalUniform; struct GlobalUniform { camera_position: vec2, camera_zoom: vec2, + camera_zoom_limits: vec2, window_size: vec2, window_aspect: vec2, starfield_texture: vec2, starfield_tile_size: vec2, + starfield_size_limits: vec2, }; @@ -62,19 +63,37 @@ fn vertex_main( ) ); + + let zoom_min_times = ( + global.camera_zoom.x / global.camera_zoom_limits.x + ); + + // Hide n% of the smallest stars + // If we wanted a constant number of stars on the screen, we would do + // `let hide_fraction = 1.0 - 1.0 / (zoom_min_times * zoom_min_times);` + // We, however, don't want this: a bigger screen should have more stars, + // but not *too* many. We thus scale linearly. + let hide_fraction = 1.0 - 1.0 / (zoom_min_times * 0.8); + + // Hide some stars at large zoom levels. + if ( + instance.size < ( + hide_fraction * (global.starfield_size_limits.y - global.starfield_size_limits.x) + + (global.starfield_size_limits.x) + ) + ) { + out.position = vec4(2.0, 2.0, 0.0, 1.0); + return out; + } + + // Apply sprite aspect ratio & scale factor // also applies screen aspect ratio + // Note that we do NOT scale for distance here---this is intentional. var scale: f32 = instance.size / global.camera_zoom.x; // Minimum scale to prevent flicker at large zoom levels var real_size = scale * global.window_size.xy; - // TODO: configurable. - // Uniform distribution! - if (real_size.x < 0.5 || real_size.y < 0.5) { - // If this star is too small, don't even show it - out.position = vec4(2.0, 2.0, 0.0, 1.0); - return out; - } if (real_size.x < 2.0 || real_size.y < 2.0) { // Otherwise, clamp to a minimum scale scale = 2.0 / max(global.window_size.x, global.window_size.y); @@ -89,16 +108,16 @@ fn vertex_main( // 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.xy; + let camera_pos = (instance.position.xy + tile_center) - global.camera_position.xy; // Translate pos = pos + ( // Don't forget to correct distance for screen aspect ratio too! - (camera_pos / (global.camera_zoom.x * (instance.parallax))) + (camera_pos / (global.camera_zoom.x * (instance.position.z))) / vec2(global.window_aspect.x, 1.0) ); - out.position = vec4(pos, 0.0, 1.0) * instance.parallax; + out.position = vec4(pos, 0.0, 1.0) * instance.position.z; return out; } diff --git a/src/render/sprite.rs b/src/render/sprite.rs index e3edb97..a0e0785 100644 --- a/src/render/sprite.rs +++ b/src/render/sprite.rs @@ -1,4 +1,4 @@ -use cgmath::{Deg, Point2}; +use cgmath::{Deg, Point3}; use super::SpriteTexture; use crate::physics::Pfloat; @@ -8,7 +8,7 @@ pub struct Sprite { pub texture: SpriteTexture, /// This object's position, in world coordinates. - pub pos: Point2, + pub pos: Point3, /// The size of this sprite, /// given as height in world units. @@ -21,11 +21,6 @@ pub struct Sprite { /// This sprite's rotation /// (relative to north, measured ccw) pub angle: Deg, - - /// Parallax factor. - /// Corresponds to z-distance, and affects - /// position and scale. - pub parallax: Pfloat, } pub trait Spriteable { diff --git a/src/render/vertexbuffer/types.rs b/src/render/vertexbuffer/types.rs index 94ede28..3ba17ab 100644 --- a/src/render/vertexbuffer/types.rs +++ b/src/render/vertexbuffer/types.rs @@ -36,15 +36,20 @@ impl BufferObject for TexturedVertex { #[repr(C)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] pub struct StarfieldInstance { - /// 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, + /// Position in the starfield. + /// + /// This is NOT world position, i.e, different from sprite positioning! + /// The x and y coordinates here represent position relative to the center + /// of a starfield tile in world units, which is converted to world position + /// by the starfield vertex shader. + pub position: [f32; 3], + /// Star size, in world units. This does NOT scale with distance, + /// unlike sprite size. pub size: f32, + + /// Parameters for this star's color variation, + /// see the starfield fragment shader. pub tint: [f32; 2], } @@ -58,24 +63,18 @@ impl BufferObject for StarfieldInstance { 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, + format: wgpu::VertexFormat::Float32x3, }, // Size wgpu::VertexAttribute { offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, - shader_location: 4, + shader_location: 3, format: wgpu::VertexFormat::Float32, }, // Tint wgpu::VertexAttribute { offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress, - shader_location: 5, + shader_location: 4, format: wgpu::VertexFormat::Float32x2, }, ], @@ -91,10 +90,13 @@ pub struct SpriteInstance { pub rotation: [[f32; 2]; 2], /// World position, relative to camera + /// Note that this does NOT contain z-distance, + /// since sprite parallax and distance scaling + /// is applied beforehand. pub position: [f32; 2], /// Height of (unrotated) sprite in world units - pub height: f32, + pub size: f32, // Sprite aspect ratio (width / height) pub aspect: f32, @@ -129,7 +131,7 @@ impl BufferObject for SpriteInstance { shader_location: 4, format: wgpu::VertexFormat::Float32x2, }, - // Height + // Size wgpu::VertexAttribute { offset: mem::size_of::<[f32; 6]>() as wgpu::BufferAddress, shader_location: 5,