Compare commits

...

3 Commits

Author SHA1 Message Date
Mark f1dba0978e
Reworked rendering architecture 2024-01-08 15:17:20 -08:00
Mark 34c0065c2d
Added radialbar shader 2024-01-08 15:17:05 -08:00
Mark d294ca4974
Added status ring 2024-01-08 15:16:50 -08:00
29 changed files with 865 additions and 636 deletions

15
Cargo.lock generated
View File

@ -584,7 +584,6 @@ dependencies = [
"galactica-content", "galactica-content",
"galactica-gameobject", "galactica-gameobject",
"galactica-render", "galactica-render",
"galactica-ui",
"galactica-world", "galactica-world",
"pollster", "pollster",
"wgpu", "wgpu",
@ -624,7 +623,6 @@ version = "0.0.0"
dependencies = [ dependencies = [
"cgmath", "cgmath",
"galactica-content", "galactica-content",
"galactica-render",
"rand", "rand",
] ]
@ -650,23 +648,13 @@ dependencies = [
"galactica-constants", "galactica-constants",
"galactica-content", "galactica-content",
"galactica-packer", "galactica-packer",
"galactica-world",
"image", "image",
"rand", "rand",
"wgpu", "wgpu",
"winit", "winit",
] ]
[[package]]
name = "galactica-ui"
version = "0.0.0"
dependencies = [
"cgmath",
"galactica-content",
"galactica-gameobject",
"galactica-render",
"galactica-world",
]
[[package]] [[package]]
name = "galactica-world" name = "galactica-world"
version = "0.0.0" version = "0.0.0"
@ -675,7 +663,6 @@ dependencies = [
"crossbeam", "crossbeam",
"galactica-content", "galactica-content",
"galactica-gameobject", "galactica-gameobject",
"galactica-render",
"nalgebra", "nalgebra",
"rand", "rand",
"rapier2d", "rapier2d",

View File

@ -48,12 +48,12 @@ galactica-render = { path = "crates/render" }
galactica-world = { path = "crates/world" } galactica-world = { path = "crates/world" }
galactica-behavior = { path = "crates/behavior" } galactica-behavior = { path = "crates/behavior" }
galactica-gameobject = { path = "crates/gameobject" } galactica-gameobject = { path = "crates/gameobject" }
galactica-ui = { path = "crates/ui" }
galactica-packer = { path = "crates/packer" } galactica-packer = { path = "crates/packer" }
galactica = { path = "crates/galactica" } galactica = { path = "crates/galactica" }
image = { version = "0.24", features = ["png"] } image = { version = "0.24", features = ["png"] }
serde = { version = "1.0.193", features = ["derive"] } serde = { version = "1.0.193", features = ["derive"] }
# TODO: update winit, but wgpu seems to be tied to this version
winit = "0.28" winit = "0.28"
wgpu = "0.18" wgpu = "0.18"
bytemuck = { version = "1.12", features = ["derive"] } bytemuck = { version = "1.12", features = ["derive"] }

2
assets

@ -1 +1 @@
Subproject commit 95558076be1819b10d5a56c62274cdf7f61ea9a8 Subproject commit 1674e86c1edcbd119d94516950d2d274b46a19d4

View File

@ -43,6 +43,9 @@ frames = [
"ship/peregrine/11.png", "ship/peregrine/11.png",
] ]
[sprite."ui::status"]
file = "ui/status.png"
[sprite."ui::radar"] [sprite."ui::radar"]
file = "ui/radar.png" file = "ui/radar.png"

View File

@ -27,7 +27,6 @@ galactica-constants = { workspace = true }
galactica-world = { workspace = true } galactica-world = { workspace = true }
galactica-behavior = { workspace = true } galactica-behavior = { workspace = true }
galactica-gameobject = { workspace = true } galactica-gameobject = { workspace = true }
galactica-ui = { workspace = true }
winit = { workspace = true } winit = { workspace = true }
wgpu = { workspace = true } wgpu = { workspace = true }

View File

@ -1,5 +1,4 @@
use cgmath::Point2; use cgmath::Point2;
use content::SystemHandle;
use std::time::Instant; use std::time::Instant;
use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode}; use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode};
@ -10,9 +9,8 @@ use galactica_behavior::{behavior, ShipBehavior};
use galactica_constants; use galactica_constants;
use galactica_content as content; use galactica_content as content;
use galactica_gameobject as object; use galactica_gameobject as object;
use galactica_render::{ObjectSprite, ParticleBuilder, RenderState, UiSprite}; use galactica_render::RenderState;
use galactica_ui as ui; use galactica_world::{util, ParticleBuilder, ShipPhysicsHandle, World};
use galactica_world::{util, ShipPhysicsHandle, World};
pub struct Game { pub struct Game {
input: InputStatus, input: InputStatus,
@ -23,7 +21,8 @@ pub struct Game {
start_instant: Instant, start_instant: Instant,
camera: Camera, camera: Camera,
system: object::System, // TODO: include system in world
//system: object::System,
shipbehaviors: Vec<Box<dyn ShipBehavior>>, shipbehaviors: Vec<Box<dyn ShipBehavior>>,
playerbehavior: behavior::Player, playerbehavior: behavior::Player,
@ -98,8 +97,7 @@ impl Game {
zoom: 500.0, zoom: 500.0,
aspect: 1.0, aspect: 1.0,
}, },
system: object::System::new(&ct, SystemHandle { index: 0 }), //system: object::System::new(&ct, SystemHandle { index: 0 }),
paused: false, paused: false,
time_scale: 1.0, time_scale: 1.0,
world: physics, world: physics,
@ -179,41 +177,11 @@ impl Game {
RenderState { RenderState {
camera_pos: self.camera.pos, camera_pos: self.camera.pos,
camera_zoom: self.camera.zoom, camera_zoom: self.camera.zoom,
object_sprites: self.get_object_sprites(),
ui_sprites: self.get_ui_sprites(),
new_particles: &mut self.new_particles,
current_time: self.start_instant.elapsed().as_secs_f32(), current_time: self.start_instant.elapsed().as_secs_f32(),
content: &self.content, content: &self.content,
world: &self.world,
particles: &mut self.new_particles,
player: &self.player,
} }
} }
pub fn get_object_sprites(&self) -> Vec<ObjectSprite> {
let mut sprites: Vec<ObjectSprite> = Vec::new();
sprites.append(&mut self.system.get_sprites());
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)
//
// TODO: maybe use a gpu depth buffer instead?
// I've tried this, but it doesn't seem to work with transparent textures.
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.world.get_projectile_sprites());
return sprites;
}
pub fn get_ui_sprites(&self) -> Vec<UiSprite> {
return ui::build_radar(
&self.content,
self.player,
&self.world,
&self.system,
self.camera.zoom,
self.camera.aspect,
);
}
} }

View File

@ -17,7 +17,6 @@ readme = { workspace = true }
workspace = true workspace = true
[dependencies] [dependencies]
galactica-render = { workspace = true }
galactica-content = { workspace = true } galactica-content = { workspace = true }
cgmath = { workspace = true } cgmath = { workspace = true }

View File

@ -1,6 +1,5 @@
use cgmath::{Deg, Point3}; use content::{EnginePoint, SpriteHandle};
use galactica_content as content; use galactica_content as content;
use galactica_render::ObjectSubSprite;
/// Represents a gun attached to a specific ship at a certain gunpoint. /// Represents a gun attached to a specific ship at a certain gunpoint.
#[derive(Debug)] #[derive(Debug)]
@ -80,10 +79,6 @@ pub struct OutfitSet {
guns: Vec<ShipGun>, guns: Vec<ShipGun>,
enginepoints: Vec<content::EnginePoint>, enginepoints: Vec<content::EnginePoint>,
gunpoints: Vec<content::GunPoint>, gunpoints: Vec<content::GunPoint>,
// Minor performance optimization, since we
// rarely need to re-compute these.
engine_flare_sprites: Vec<ObjectSubSprite>,
} }
impl<'a> OutfitSet { impl<'a> OutfitSet {
@ -97,7 +92,6 @@ impl<'a> OutfitSet {
//total_space: content.space.clone(), //total_space: content.space.clone(),
enginepoints: content.engines.clone(), enginepoints: content.engines.clone(),
gunpoints: content.guns.clone(), gunpoints: content.guns.clone(),
engine_flare_sprites: vec![],
} }
} }
@ -124,7 +118,7 @@ impl<'a> OutfitSet {
self.available_space.occupy(&outfit.space); self.available_space.occupy(&outfit.space);
self.stats.add(&outfit); self.stats.add(&outfit);
self.outfits.push(o); self.outfits.push(o);
self.update_engine_flares(); //self.update_engine_flares();
return true; return true;
} }
@ -138,7 +132,7 @@ impl<'a> OutfitSet {
self.available_space.free(&outfit.space); self.available_space.free(&outfit.space);
self.outfits.remove(i); self.outfits.remove(i);
self.stats.remove(&outfit); self.stats.remove(&outfit);
self.update_engine_flares(); //self.update_engine_flares();
} }
/// Add a gun to this outfit set. /// Add a gun to this outfit set.
@ -181,34 +175,13 @@ impl<'a> OutfitSet {
.map(|(a, b)| (b, a)) .map(|(a, b)| (b, a))
} }
/// Update engine flare sprites // TODO: move to ship
pub fn update_engine_flares(&mut self) { /// Iterate over all ships in this physics system
// TODO: better way to pick flare texture pub fn iter_enginepoints(&self) -> impl Iterator<Item = &EnginePoint> + '_ {
self.engine_flare_sprites.clear(); self.enginepoints.iter()
let s = if let Some(e) = self.stats.engine_flare_sprites.iter().next() {
e
} else {
return;
};
self.engine_flare_sprites = self
.enginepoints
.iter()
.map(|p| ObjectSubSprite {
pos: Point3 {
x: p.pos.x,
y: p.pos.y - p.size / 2.0,
z: 1.0,
},
sprite: *s,
angle: Deg(0.0),
size: p.size,
})
.collect();
} }
/// Get the sprites we should show if this ship is firing its engines pub fn get_flare_sprite(&self) -> Option<SpriteHandle> {
pub fn get_engine_flares(&self) -> Vec<ObjectSubSprite> { self.stats.engine_flare_sprites.iter().next().map(|x| *x)
return self.engine_flare_sprites.clone();
} }
} }

View File

@ -1,6 +1,5 @@
use crate::SystemObject; use crate::SystemObject;
use galactica_content as content; use galactica_content as content;
use galactica_render::ObjectSprite;
pub struct System { pub struct System {
pub name: String, pub name: String,
@ -27,7 +26,7 @@ impl System {
return s; return s;
} }
pub fn get_sprites(&self) -> Vec<ObjectSprite> { //pub fn get_sprites(&self) -> Vec<ObjectSprite> {
return self.bodies.iter().map(|x| x.get_sprite()).collect(); // return self.bodies.iter().map(|x| x.get_sprite()).collect();
} //}
} }

View File

@ -1,7 +1,6 @@
use cgmath::{Deg, Point3}; use cgmath::{Deg, Point3};
use galactica_content as content; use galactica_content as content;
use galactica_render::ObjectSprite;
pub struct SystemObject { pub struct SystemObject {
pub sprite: content::SpriteHandle, pub sprite: content::SpriteHandle,
@ -11,13 +10,13 @@ pub struct SystemObject {
} }
impl SystemObject { impl SystemObject {
pub(crate) fn get_sprite(&self) -> ObjectSprite { //pub(crate) fn get_sprite(&self) -> ObjectSprite {
return ObjectSprite { // return ObjectSprite {
sprite: self.sprite, // sprite: self.sprite,
pos: self.pos, // pos: self.pos,
angle: self.angle, // angle: self.angle,
size: self.size, // size: self.size,
children: None, // children: None,
}; // };
} //}
} }

View File

@ -20,6 +20,7 @@ workspace = true
galactica-content = { workspace = true } galactica-content = { workspace = true }
galactica-constants = { workspace = true } galactica-constants = { workspace = true }
galactica-packer = { workspace = true } galactica-packer = { workspace = true }
galactica-world = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
cgmath = { workspace = true } cgmath = { workspace = true }

View File

@ -0,0 +1,109 @@
// INCLUDE: global uniform header
struct InstanceInput {
@location(2) anchor: u32,
@location(3) position: vec2<f32>,
@location(4) diameter: f32,
@location(5) stroke: f32,
@location(6) angle: f32,
@location(7) color: vec4<f32>,
};
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) texture_coords: vec2<f32>,
};
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(2) center: vec2<f32>,
@location(3) diameter: f32,
@location(4) stroke: f32,
@location(5) angle: f32,
@location(6) color: vec4<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.position = vec4(vertex.position, 1.0);
out.diameter = instance.diameter;
out.stroke = instance.stroke;
out.color = instance.color;
out.angle = instance.angle;
// Center of this radial bar, in logical pixels,
// with (0, 0) at the center of the screen.
if instance.anchor == u32(0) {
out.center = instance.position + (
(global_data.window_size / global_data.window_scale.x)
- vec2(instance.diameter, instance.diameter)
) / 2.0;
} else {
out.center = vec2(0.0, 0.0);
}
return out;
}
@fragment
fn fragment_main(in: VertexOutput) -> @location(0) vec4<f32> {
// Fragment position in logical pixels, relative to arc center
let p = (
vec2(1.0, -1.0) *
(in.position.xy - global_data.window_size / 2.0) /
global_data.window_scale.x
) - in.center;
let bar_width = in.stroke; // Width of filled bar
let bar_radius = in.diameter / 2.0 - bar_width / 2.0; // Middle radius of the bar
let angle = in.angle - floor(in.angle / 6.283) * 6.28318; // Sanely handle large angles (fmod(angle, 2pi))
let zero_vector = vec2(0.0, 1.0); // Draw bar clockwise from this vector
let frag_radius = distance(vec2(0.0, 0.0), p); // Radius of this fragment
let feather = 2.0; // Size of feather, in logical pixels
// Clockwise angle between zero_vector and fragment location
let frag_angle = atan2(
p.y*zero_vector.x - p.x*zero_vector.y,
-dot(p, zero_vector)
) + 3.14159;
// Line fill & feather
if abs(frag_radius - bar_radius) <= bar_width / 2.0 && frag_angle <= angle {
let x = (abs(frag_radius - bar_radius) - (bar_width/2.0 - feather)) / feather;
return in.color * vec4(1.0, 1.0, 1.0, clamp(1.0 - x, 0.0, 1.0));
}
// Round cap centers
let cap_start_center = zero_vector * (in.diameter / 2.0 - (bar_width / 2.0));
let cap_end_center = mat2x2(
vec2(cos(-angle), sin(-angle)),
vec2(-sin(-angle), cos(-angle))
) * cap_start_center;
// Cap fill & feather
let cap_start_d = distance(p, cap_start_center);
let cap_end_d = distance(p, cap_end_center);
if (
cap_start_d <= bar_width / 2.0 ||
cap_end_d <= bar_width / 2.0
) {
let x = (
min(cap_start_d, cap_end_d)
- (bar_width/2.0 - feather)
) / feather;
return in.color * vec4(1.0, 1.0, 1.0, clamp(1.0 - x, 0.0, 1.0));
}
discard;
}

View File

@ -23,6 +23,10 @@ pub struct GlobalDataContent {
/// Size ratio of window, in physical pixels /// Size ratio of window, in physical pixels
pub window_size: [f32; 2], pub window_size: [f32; 2],
/// Physical pixel to logical pixel conversion factor.
/// Second component is ignored.
pub window_scale: [f32; 2],
/// Aspect ratio of window /// Aspect ratio of window
/// Second component is ignored. /// Second component is ignored.
pub window_aspect: [f32; 2], pub window_aspect: [f32; 2],

View File

@ -27,6 +27,7 @@ impl GlobalUniform {
camera_zoom: vec2<f32>, camera_zoom: vec2<f32>,
camera_zoom_limits: vec2<f32>, camera_zoom_limits: vec2<f32>,
window_size: vec2<f32>, window_size: vec2<f32>,
window_scale: vec2<f32>,
window_aspect: vec2<f32>, window_aspect: vec2<f32>,
starfield_sprite: vec2<u32>, starfield_sprite: vec2<u32>,
starfield_tile_size: vec2<f32>, starfield_tile_size: vec2<f32>,

View File

@ -0,0 +1,224 @@
//! GPUState routines for drawing HUD elements
use cgmath::{Deg, InnerSpace, Point2, Vector2};
use galactica_world::util;
use crate::{
sprite::UiSprite, vertexbuffer::types::UiInstance, AnchoredUiPosition, GPUState, RenderState,
};
impl GPUState {
pub(super) fn hud_add_radar(&mut self, state: &RenderState, instances: &mut Vec<UiInstance>) {
let radar_range = 4000.0;
let radar_size = 300.0;
let hide_range = 0.85;
let shrink_distance = 20.0;
//let system_object_scale = 1.0 / 600.0;
let ship_scale = 1.0 / 15.0;
let (_, player_body) = state.world.get_ship_body(*state.player).unwrap();
let player_position = util::rigidbody_position(player_body);
//let planet_sprite = state.content.get_sprite_handle("ui::planetblip");
let ship_sprite = state.content.get_sprite_handle("ui::shipblip");
let arrow_sprite = state.content.get_sprite_handle("ui::centerarrow");
self.push_ui_sprite(
instances,
&UiSprite {
sprite: state.content.get_sprite_handle("ui::status"),
pos: AnchoredUiPosition::NeNe(Point2 { x: -10.0, y: -10.0 }),
dimensions: Point2 {
x: radar_size,
y: radar_size,
},
angle: Deg(0.0),
color: None,
},
);
self.push_ui_sprite(
instances,
&UiSprite {
sprite: state.content.get_sprite_handle("ui::radar"),
pos: AnchoredUiPosition::NwNw(Point2 { x: 10.0, y: -10.0 }),
dimensions: Point2 {
x: radar_size,
y: radar_size,
},
angle: Deg(0.0),
color: None,
},
);
/*
// Draw system objects
for o in &system.bodies {
let size = (o.size / o.pos.z) / (radar_range * system_object_scale);
let p = Point2 {
x: o.pos.x,
y: o.pos.y,
};
let d = (p - player_position) / radar_range;
// Add half the blip sprite's height to distance
let m = d.magnitude() + (size / (2.0 * radar_size));
if m < hide_range {
// Shrink blips as they get closeto the edge
let size = size.min((hide_range - m) * size * shrink_distance);
if size <= 2.0 {
// Don't draw super tiny sprites, they flicker
continue;
}
out.push(UiSprite {
sprite: planet_sprite,
pos: AnchoredUiPosition::NwC(
Point2 {
x: radar_size / 2.0 + 10.0,
y: radar_size / -2.0 - 10.0,
} + (d * (radar_size / 2.0)),
),
dimensions: Point2 {
x: planet_sprite.aspect,
y: 1.0,
} * size,
angle: o.angle,
color: Some([0.5, 0.5, 0.5, 1.0]),
});
}
}
*/
// Draw ships
for (s, r) in state.world.iter_ship_body() {
let ship = state.content.get_ship(s.ship.handle);
let size = (ship.size * ship.sprite.aspect) * ship_scale;
let p = util::rigidbody_position(r);
let d = (p - player_position) / radar_range;
let m = d.magnitude() + (size / (2.0 * radar_size));
if m < hide_range {
let size = size.min((hide_range - m) * size * shrink_distance);
if size < 2.0 {
continue;
}
let angle: Deg<f32> = util::rigidbody_rotation(r)
.angle(Vector2 { x: 0.0, y: 1.0 })
.into();
let f = state.content.get_faction(s.ship.faction).color;
let f = [f[0], f[1], f[2], 1.0];
self.push_ui_sprite(
instances,
&UiSprite {
sprite: ship_sprite,
pos: AnchoredUiPosition::NwC(
Point2 {
x: radar_size / 2.0 + 10.0,
y: radar_size / -2.0 - 10.0,
} + (d * (radar_size / 2.0)),
),
dimensions: Point2 {
x: ship_sprite.aspect,
y: 1.0,
} * size,
angle: -angle,
color: Some(f),
},
);
}
}
// Draw viewport frame
let d = Vector2 {
x: (state.camera_zoom / 2.0) * self.window_aspect,
y: state.camera_zoom / 2.0,
} / radar_range;
let m = d.magnitude();
let d = d * (radar_size / 2.0);
let color = Some([0.3, 0.3, 0.3, 1.0]);
if m < 0.8 {
let sprite = state.content.get_sprite_handle("ui::radarframe");
let dimensions = Point2 {
x: sprite.aspect,
y: 1.0,
} * 7.0f32.min((0.8 - m) * 70.0);
self.push_ui_sprite(
instances,
&UiSprite {
sprite,
pos: AnchoredUiPosition::NwNw(Point2 {
x: (radar_size / 2.0 + 10.0) - d.x,
y: (radar_size / -2.0 - 10.0) + d.y,
}),
dimensions,
angle: Deg(0.0),
color,
},
);
self.push_ui_sprite(
instances,
&UiSprite {
sprite,
pos: AnchoredUiPosition::NwSw(Point2 {
x: (radar_size / 2.0 + 10.0) - d.x,
y: (radar_size / -2.0 - 10.0) - d.y,
}),
dimensions,
angle: Deg(90.0),
color,
},
);
self.push_ui_sprite(
instances,
&UiSprite {
sprite,
pos: AnchoredUiPosition::NwSe(Point2 {
x: (radar_size / 2.0 + 10.0) + d.x,
y: (radar_size / -2.0 - 10.0) - d.y,
}),
dimensions,
angle: Deg(180.0),
color,
},
);
self.push_ui_sprite(
instances,
&UiSprite {
sprite,
pos: AnchoredUiPosition::NwNe(Point2 {
x: (radar_size / 2.0 + 10.0) + d.x,
y: (radar_size / -2.0 - 10.0) + d.y,
}),
dimensions,
angle: Deg(270.0),
color,
},
);
}
// Arrow to center of system
let q = Point2 { x: 0.0, y: 0.0 } - player_position;
let m = q.magnitude();
if m > 200.0 {
let player_angle: Deg<f32> = q.angle(Vector2 { x: 0.0, y: 1.0 }).into();
self.push_ui_sprite(
instances,
&UiSprite {
sprite: arrow_sprite,
pos: AnchoredUiPosition::NwC(
Point2 {
x: radar_size / 2.0 + 10.0,
y: radar_size / -2.0 - 10.0,
} + ((q.normalize() * 0.865) * (radar_size / 2.0)),
),
dimensions: Point2 {
x: arrow_sprite.aspect,
y: 1.0,
} * 10.0,
angle: -player_angle,
color: Some([1.0, 1.0, 1.0, 1f32.min((m - 200.0) / 400.0)]),
},
);
}
}
}

View File

@ -1,7 +1,8 @@
use anyhow::Result; use anyhow::Result;
use bytemuck; use bytemuck;
use cgmath::{EuclideanSpace, Matrix4, Point2, Rad, Vector3}; use cgmath::{Matrix4, Point2, Vector3};
use galactica_constants; use galactica_constants;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use std::{iter, rc::Rc}; use std::{iter, rc::Rc};
use wgpu; use wgpu;
@ -9,18 +10,26 @@ use winit::{self, dpi::LogicalSize, window::Window};
use crate::{ use crate::{
content, content,
globaluniform::{GlobalDataContent, GlobalUniform, ObjectData}, globaluniform::{GlobalDataContent, GlobalUniform},
pipeline::PipelineBuilder, pipeline::PipelineBuilder,
sprite::UiSprite,
starfield::Starfield, starfield::Starfield,
texturearray::TextureArray, texturearray::TextureArray,
vertexbuffer::{ vertexbuffer::{
consts::{SPRITE_INDICES, SPRITE_VERTICES}, consts::{SPRITE_INDICES, SPRITE_VERTICES},
types::{ObjectInstance, ParticleInstance, StarfieldInstance, TexturedVertex, UiInstance}, types::{
ObjectInstance, ParticleInstance, RadialBarInstance, StarfieldInstance, TexturedVertex,
UiInstance,
},
BufferObject, VertexBuffer, BufferObject, VertexBuffer,
}, },
ObjectSprite, RenderState, UiSprite, OPENGL_TO_WGPU_MATRIX, RenderState, OPENGL_TO_WGPU_MATRIX,
}; };
// Additional implementaitons for GPUState
mod hud;
mod world;
/// A high-level GPU wrapper. Consumes game state, /// A high-level GPU wrapper. Consumes game state,
/// produces pretty pictures. /// produces pretty pictures.
pub struct GPUState { pub struct GPUState {
@ -41,6 +50,7 @@ pub struct GPUState {
starfield_pipeline: wgpu::RenderPipeline, starfield_pipeline: wgpu::RenderPipeline,
particle_pipeline: wgpu::RenderPipeline, particle_pipeline: wgpu::RenderPipeline,
ui_pipeline: wgpu::RenderPipeline, ui_pipeline: wgpu::RenderPipeline,
radialbar_pipeline: wgpu::RenderPipeline,
starfield: Starfield, starfield: Starfield,
texture_array: TextureArray, texture_array: TextureArray,
@ -58,6 +68,8 @@ struct VertexBuffers {
/// of the particle instance array. /// of the particle instance array.
particle_array_head: u64, particle_array_head: u64,
particle: Rc<VertexBuffer>, particle: Rc<VertexBuffer>,
radialbar: Rc<VertexBuffer>,
} }
/// Basic wgsl preprocesser /// Basic wgsl preprocesser
@ -183,6 +195,14 @@ impl GPUState {
Some(SPRITE_INDICES), Some(SPRITE_INDICES),
galactica_constants::PARTICLE_SPRITE_INSTANCE_LIMIT, galactica_constants::PARTICLE_SPRITE_INSTANCE_LIMIT,
)), )),
radialbar: Rc::new(VertexBuffer::new::<TexturedVertex, RadialBarInstance>(
"radial bar",
&device,
Some(SPRITE_VERTICES),
Some(SPRITE_INDICES),
10,
)),
}; };
// Load uniforms // Load uniforms
@ -256,6 +276,22 @@ impl GPUState {
.set_bind_group_layouts(bind_group_layouts) .set_bind_group_layouts(bind_group_layouts)
.build(); .build();
let radialbar_pipeline = PipelineBuilder::new("radialbar", &device)
.set_shader(&preprocess_shader(
&include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/shaders/",
"radialbar.wgsl"
)),
&global_uniform,
1,
))
.set_format(config.format)
.set_triangle(true)
.set_vertex_buffer(&vertex_buffers.radialbar)
.set_bind_group_layouts(bind_group_layouts)
.build();
let mut starfield = Starfield::new(); let mut starfield = Starfield::new();
starfield.regenerate(); starfield.regenerate();
@ -273,6 +309,7 @@ impl GPUState {
starfield_pipeline, starfield_pipeline,
ui_pipeline, ui_pipeline,
particle_pipeline, particle_pipeline,
radialbar_pipeline,
starfield, starfield,
texture_array, texture_array,
@ -300,90 +337,7 @@ impl GPUState {
self.update_starfield_buffer() self.update_starfield_buffer()
} }
/// Create a ObjectInstance for an object and add it to `instances`. // TODO:remove
fn push_object_sprite(
&self,
state: &RenderState,
instances: &mut Vec<ObjectInstance>,
clip_ne: Point2<f32>,
clip_sw: Point2<f32>,
s: &ObjectSprite,
) {
// Position adjusted for parallax
// TODO: adjust parallax for zoom?
let pos: Point2<f32> = {
(Point2 {
x: s.pos.x,
y: s.pos.y,
} - state.camera_pos.to_vec())
/ s.pos.z
};
// Game dimensions of this sprite post-scale.
// 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 sprites that are off the screen
if pos.x < clip_ne.x - m
|| pos.y > clip_ne.y + m
|| pos.x > clip_sw.x + m
|| pos.y < clip_sw.y - m
{
return;
}
let idx = instances.len();
// Write this object's location data
self.queue.write_buffer(
&self.global_uniform.object_buffer,
ObjectData::SIZE * idx as u64,
bytemuck::cast_slice(&[ObjectData {
xpos: s.pos.x,
ypos: s.pos.y,
zpos: s.pos.z,
angle: Rad::from(s.angle).0,
size: s.size,
parent: 0,
is_child: 0,
_padding: Default::default(),
}]),
);
// Push this object's instance
instances.push(ObjectInstance {
sprite_index: s.sprite.get_index(),
object_index: idx as u32,
});
// Add children
if let Some(children) = &s.children {
for c in children {
self.queue.write_buffer(
&self.global_uniform.object_buffer,
ObjectData::SIZE * instances.len() as u64,
bytemuck::cast_slice(&[ObjectData {
xpos: c.pos.x,
ypos: c.pos.y,
zpos: c.pos.z,
angle: Rad::from(c.angle).0,
size: c.size,
parent: idx as u32,
is_child: 1,
_padding: Default::default(),
}]),
);
instances.push(ObjectInstance {
sprite_index: c.sprite.get_index(),
object_index: instances.len() as u32,
});
}
}
}
/// Create a UiInstance for a ui sprite and add it to `instances` /// Create a UiInstance for a ui sprite and add it to `instances`
fn push_ui_sprite(&self, instances: &mut Vec<UiInstance>, s: &UiSprite) { fn push_ui_sprite(&self, instances: &mut Vec<UiInstance>, s: &UiSprite) {
let logical_size: LogicalSize<f32> = let logical_size: LogicalSize<f32> =
@ -427,6 +381,11 @@ impl GPUState {
y: 1.0 + (height / 2.0 + p.y) / (logical_size.height / 2.0), y: 1.0 + (height / 2.0 + p.y) / (logical_size.height / 2.0),
z: 0.0, z: 0.0,
}, },
super::AnchoredUiPosition::NeNe(p) => Vector3 {
x: 1.0 - (width / 2.0 - p.x) / (logical_size.width / 2.0),
y: 1.0 - (height / 2.0 - p.y) / (logical_size.height / 2.0),
z: 0.0,
},
}); });
let screen_aspect = Matrix4::from_nonuniform_scale(1.0 / self.window_aspect, 1.0, 1.0); let screen_aspect = Matrix4::from_nonuniform_scale(1.0 / self.window_aspect, 1.0, 1.0);
@ -440,7 +399,7 @@ impl GPUState {
/// Make an instance for all the game's sprites /// Make an instance for all the game's sprites
/// (Objects and UI) /// (Objects and UI)
/// This will Will panic if any X_SPRITE_INSTANCE_LIMIT is exceeded. /// This will Will panic if any X_SPRITE_INSTANCE_LIMIT is exceeded.
fn update_sprite_instances(&self, state: &RenderState) -> (usize, usize) { fn update_sprite_instances(&mut self, state: &RenderState) -> (usize, usize) {
let mut object_instances: Vec<ObjectInstance> = Vec::new(); let mut object_instances: Vec<ObjectInstance> = Vec::new();
// Game coordinates (relative to camera) of ne and sw corners of screen. // Game coordinates (relative to camera) of ne and sw corners of screen.
@ -448,8 +407,13 @@ impl GPUState {
let clip_ne = Point2::from((-self.window_aspect, 1.0)) * state.camera_zoom; let clip_ne = Point2::from((-self.window_aspect, 1.0)) * state.camera_zoom;
let clip_sw = Point2::from((self.window_aspect, -1.0)) * state.camera_zoom; let clip_sw = Point2::from((self.window_aspect, -1.0)) * state.camera_zoom;
for s in &state.object_sprites { // TODO:sort. Order matters.
self.push_object_sprite(state, &mut object_instances, clip_ne, clip_sw, &s); for s in state.world.iter_ships() {
self.world_push_ship(state, (clip_ne, clip_sw), &s, &mut object_instances);
}
for p in state.world.iter_projectiles() {
self.world_push_projectile(state, (clip_ne, clip_sw), &p, &mut object_instances);
} }
// Enforce sprite limit // Enforce sprite limit
@ -464,11 +428,10 @@ impl GPUState {
bytemuck::cast_slice(&object_instances), bytemuck::cast_slice(&object_instances),
); );
// TODO: we don't need an array, just use a counter
let mut ui_instances: Vec<UiInstance> = Vec::new(); let mut ui_instances: Vec<UiInstance> = Vec::new();
for s in &state.ui_sprites { self.hud_add_radar(state, &mut ui_instances);
self.push_ui_sprite(&mut ui_instances, &s);
}
if ui_instances.len() as u64 > galactica_constants::UI_SPRITE_INSTANCE_LIMIT { if ui_instances.len() as u64 > galactica_constants::UI_SPRITE_INSTANCE_LIMIT {
panic!("Ui sprite limit exceeded!") panic!("Ui sprite limit exceeded!")
@ -515,9 +478,7 @@ impl GPUState {
/// Main render function. Draws sprites on a window. /// Main render function. Draws sprites on a window.
pub fn render(&mut self, state: RenderState) -> Result<(), wgpu::SurfaceError> { pub fn render(&mut self, state: RenderState) -> Result<(), wgpu::SurfaceError> {
let output = self.surface.get_current_texture()?; let output = self.surface.get_current_texture()?;
let view = output let view = output.texture.create_view(&Default::default());
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self let mut encoder = self
.device .device
@ -560,6 +521,7 @@ impl GPUState {
self.window_size.width as f32, self.window_size.width as f32,
self.window_size.height as f32, self.window_size.height as f32,
], ],
window_scale: [self.window.scale_factor() as f32, 0.0],
window_aspect: [self.window_aspect, 0.0], window_aspect: [self.window_aspect, 0.0],
starfield_sprite: [s.get_index(), 0], starfield_sprite: [s.get_index(), 0],
starfield_tile_size: [galactica_constants::STARFIELD_SIZE as f32, 0.0], starfield_tile_size: [galactica_constants::STARFIELD_SIZE as f32, 0.0],
@ -572,8 +534,8 @@ impl GPUState {
); );
// Write all new particles to GPU buffer // Write all new particles to GPU buffer
state.new_particles.shuffle(&mut rand::thread_rng()); state.particles.shuffle(&mut rand::thread_rng());
for i in state.new_particles.iter() { for i in state.particles.iter() {
self.queue.write_buffer( self.queue.write_buffer(
&self.vertex_buffers.particle.instances, &self.vertex_buffers.particle.instances,
ParticleInstance::SIZE * self.vertex_buffers.particle_array_head, ParticleInstance::SIZE * self.vertex_buffers.particle_array_head,
@ -596,7 +558,7 @@ impl GPUState {
self.vertex_buffers.particle_array_head = 0; self.vertex_buffers.particle_array_head = 0;
} }
} }
state.new_particles.clear(); state.particles.clear();
// Create sprite instances // Create sprite instances
let (n_object, n_ui) = self.update_sprite_instances(&state); let (n_object, n_ui) = self.update_sprite_instances(&state);
@ -634,6 +596,53 @@ impl GPUState {
render_pass.set_pipeline(&self.ui_pipeline); render_pass.set_pipeline(&self.ui_pipeline);
render_pass.draw_indexed(0..SPRITE_INDICES.len() as u32, 0, 0..n_ui as _); render_pass.draw_indexed(0..SPRITE_INDICES.len() as u32, 0, 0..n_ui as _);
/*
let mut i = 0;
for b in &state.render_elements.radial_bars {
self.queue.write_buffer(
&self.vertex_buffers.radialbar.instances,
RadialBarInstance::SIZE * i,
bytemuck::cast_slice(&[RadialBarInstance {
position: b.pos.position().clone().into(),
anchor: b.pos.to_anchor_int(),
diameter: b.diameter,
stroke: b.stroke,
color: b.color,
angle: b.angle.0,
}]),
);
i += 1;
}
*/
/*
self.queue.write_buffer(
&self.vertex_buffers.radialbar.instances,
0,
bytemuck::cast_slice(&[
RadialBarInstance {
position: [-23.0, -23.0],
diameter: 274.0,
stroke: 10.0,
color: [0.3, 0.6, 0.8, 1.0],
angle: -state.current_time / 2.0,
},
RadialBarInstance {
position: [-35.0, -35.0],
diameter: 250.0,
stroke: 10.0,
color: [0.8, 0.7, 0.5, 1.0],
angle: -state.current_time / 5.0,
},
]),
);*/
// Ui pipeline
// TODO: do we need to do this every time?
self.vertex_buffers.radialbar.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&self.radialbar_pipeline);
//render_pass.draw_indexed(0..SPRITE_INDICES.len() as u32, 0, 0..2);
// begin_render_pass borrows encoder mutably, so we can't call finish() // begin_render_pass borrows encoder mutably, so we can't call finish()
// without dropping this variable. // without dropping this variable.
drop(render_pass); drop(render_pass);

View File

@ -0,0 +1,162 @@
//! GPUState routines for drawing the world
use bytemuck;
use cgmath::{EuclideanSpace, InnerSpace, Point2, Vector2};
use galactica_world::{
objects::{ProjectileWorldObject, ShipWorldObject},
util,
};
use crate::{
globaluniform::ObjectData, vertexbuffer::types::ObjectInstance, GPUState, RenderState,
};
impl GPUState {
pub(super) fn world_push_ship(
&self,
state: &RenderState,
// NE and SW corners of screen
screen_clip: (Point2<f32>, Point2<f32>),
s: &ShipWorldObject,
instances: &mut Vec<ObjectInstance>,
) {
let (_, r) = state.world.get_ship_body(s.physics_handle).unwrap();
let ship_pos = util::rigidbody_position(&r);
let ship_rot = util::rigidbody_rotation(r);
let ship_ang = -ship_rot.angle(Vector2 { x: 0.0, y: 1.0 }); // TODO: inconsistent angles. Fix!
let ship_cnt = state.content.get_ship(s.ship.handle);
// Position adjusted for parallax
// TODO: adjust parallax for zoom?
// 1.0 is z-coordinate, which is constant for ships
let pos: Point2<f32> = (ship_pos - state.camera_pos.to_vec()) / 1.0;
// Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger.
// This is in game units.
//
// We take the maximum to account for rotated sprites.
let m = (ship_cnt.size / 1.0) * ship_cnt.sprite.aspect.max(1.0);
// Don't draw sprites that are off the screen
if pos.x < screen_clip.0.x - m
|| pos.y > screen_clip.0.y + m
|| pos.x > screen_clip.1.x + m
|| pos.y < screen_clip.1.y - m
{
return;
}
let idx = instances.len();
// Write this object's location data
self.queue.write_buffer(
&self.global_uniform.object_buffer,
ObjectData::SIZE * idx as u64,
bytemuck::cast_slice(&[ObjectData {
xpos: ship_pos.x,
ypos: ship_pos.y,
zpos: 1.0,
angle: ship_ang.0,
size: ship_cnt.size,
parent: 0,
is_child: 0,
_padding: Default::default(),
}]),
);
// Push this object's instance
instances.push(ObjectInstance {
sprite_index: ship_cnt.sprite.get_index(),
object_index: idx as u32,
});
// Draw engine flares if necessary
if s.controls.thrust {
for f in s.ship.outfits.iter_enginepoints() {
let flare = match s.ship.outfits.get_flare_sprite() {
None => continue,
Some(s) => s,
};
self.queue.write_buffer(
&self.global_uniform.object_buffer,
ObjectData::SIZE * instances.len() as u64,
bytemuck::cast_slice(&[ObjectData {
xpos: f.pos.x,
ypos: f.pos.y - f.size / 2.0,
zpos: 1.0,
angle: 0.0,
size: f.size,
parent: idx as u32,
is_child: 1,
_padding: Default::default(),
}]),
);
instances.push(ObjectInstance {
sprite_index: flare.get_index(),
object_index: instances.len() as u32,
});
}
}
}
pub(super) fn world_push_projectile(
&self,
state: &RenderState,
// NE and SW corners of screen
screen_clip: (Point2<f32>, Point2<f32>),
p: &ProjectileWorldObject,
instances: &mut Vec<ObjectInstance>,
) {
let r = state.world.get_rigid_body(p.rigid_body).unwrap();
let proj_pos = util::rigidbody_position(&r);
let proj_rot = util::rigidbody_rotation(r);
let proj_ang = -proj_rot.angle(Vector2 { x: 1.0, y: 0.0 });
let proj_cnt = &p.projectile.content; // TODO: don't clone this?
// Position adjusted for parallax
// TODO: adjust parallax for zoom?
// 1.0 is z-coordinate, which is constant for ships
let pos: Point2<f32> = (proj_pos - state.camera_pos.to_vec()) / 1.0;
// Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger.
// This is in game units.
//
// We take the maximum to account for rotated sprites.
let m = (proj_cnt.size / 1.0) * proj_cnt.sprite.aspect.max(1.0);
// Don't draw sprites that are off the screen
if pos.x < screen_clip.0.x - m
|| pos.y > screen_clip.0.y + m
|| pos.x > screen_clip.1.x + m
|| pos.y < screen_clip.1.y - m
{
return;
}
let idx = instances.len();
// Write this object's location data
self.queue.write_buffer(
&self.global_uniform.object_buffer,
ObjectData::SIZE * idx as u64,
bytemuck::cast_slice(&[ObjectData {
xpos: proj_pos.x,
ypos: proj_pos.y,
zpos: 1.0,
angle: proj_ang.0,
size: 0f32.max(proj_cnt.size + p.size_rng),
parent: 0,
is_child: 0,
_padding: Default::default(),
}]),
);
// Push this object's instance
instances.push(ObjectInstance {
sprite_index: proj_cnt.sprite.get_index(),
object_index: idx as u32,
});
}
}

View File

@ -19,7 +19,7 @@ mod vertexbuffer;
use galactica_content as content; use galactica_content as content;
pub use gpustate::GPUState; pub use gpustate::GPUState;
pub use renderstate::RenderState; pub use renderstate::RenderState;
pub use sprite::{AnchoredUiPosition, ObjectSprite, ObjectSubSprite, ParticleBuilder, UiSprite}; pub use sprite::AnchoredUiPosition;
use cgmath::Matrix4; use cgmath::Matrix4;
@ -28,6 +28,7 @@ pub(crate) const SHADER_MAIN_VERTEX: &'static str = "vertex_main";
pub(crate) const SHADER_MAIN_FRAGMENT: &'static str = "fragment_main"; pub(crate) const SHADER_MAIN_FRAGMENT: &'static str = "fragment_main";
#[rustfmt::skip] #[rustfmt::skip]
#[allow(dead_code)]
pub(crate) const OPENGL_TO_WGPU_MATRIX: Matrix4<f32> = Matrix4::new( pub(crate) const OPENGL_TO_WGPU_MATRIX: Matrix4<f32> = Matrix4::new(
1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0,

View File

@ -1,25 +1,20 @@
use cgmath::Point2; use cgmath::Point2;
use galactica_content::Content; use galactica_content::Content;
use galactica_world::{ParticleBuilder, ShipPhysicsHandle, World};
use crate::{ObjectSprite, ParticleBuilder, UiSprite};
/// Bundles parameters passed to a single call to GPUState::render /// Bundles parameters passed to a single call to GPUState::render
pub struct RenderState<'a> { pub struct RenderState<'a> {
/// Camera position, in world units /// Camera position, in world units
pub camera_pos: Point2<f32>, pub camera_pos: Point2<f32>,
/// Player ship
pub player: &'a ShipPhysicsHandle,
/// Height of screen, in world units /// Height of screen, in world units
pub camera_zoom: f32, pub camera_zoom: f32,
/// World object sprites /// The world state to render
pub object_sprites: Vec<ObjectSprite>, pub world: &'a World,
/// UI sprites
pub ui_sprites: Vec<UiSprite>,
/// Particles to create during this frame.
/// this array will be cleared.
pub new_particles: &'a mut Vec<ParticleBuilder>,
// TODO: handle overflow // TODO: handle overflow
/// The current time, in seconds /// The current time, in seconds
@ -27,4 +22,7 @@ pub struct RenderState<'a> {
/// Game content /// Game content
pub content: &'a Content, pub content: &'a Content,
/// Particles to spawn during this frame
pub particles: &'a mut Vec<ParticleBuilder>,
} }

View File

@ -1,90 +1,5 @@
use crate::content; use crate::content;
use cgmath::{Deg, Matrix2, Point2, Point3, Rad, Vector2}; use cgmath::{Deg, Point2};
use rand::Rng;
/// Instructions to create a new particle
pub struct ParticleBuilder {
/// The sprite to use for this particle
pub sprite: content::SpriteHandle,
/// This object's center, in world coordinates.
pub pos: Point2<f32>,
/// This particle's velocity, in world coordinates
pub velocity: Vector2<f32>,
/// This particle's angle
pub angle: Rad<f32>,
/// This particle's angular velocity (rad/sec)
pub angvel: Rad<f32>,
/// This particle's lifetime, in seconds
pub lifetime: f32,
/// The size of this particle,
/// given as height in world units.
pub size: f32,
/// Fade this particle out over this many seconds as it expires
pub fade: f32,
}
impl ParticleBuilder {
/// Create a ParticleBuilder from an Effect
pub fn from_content(
effect: &content::Effect,
pos: Point2<f32>,
parent_angle: Rad<f32>,
parent_velocity: Vector2<f32>,
target_velocity: Vector2<f32>,
) -> Self {
let mut rng = rand::thread_rng();
let velocity = {
let a =
rng.gen_range(-effect.velocity_scale_parent_rng..=effect.velocity_scale_parent_rng);
let b =
rng.gen_range(-effect.velocity_scale_target_rng..=effect.velocity_scale_target_rng);
let velocity = ((effect.velocity_scale_parent + a) * parent_velocity)
+ ((effect.velocity_scale_target + b) * target_velocity);
Matrix2::from_angle(Rad(
rng.gen_range(-effect.direction_rng..=effect.direction_rng)
)) * velocity
};
// Rad has odd behavior when its angle is zero, so we need extra checks here
let angvel = if effect.angvel_rng == 0.0 {
effect.angvel
} else {
Rad(effect.angvel.0 + rng.gen_range(-effect.angvel_rng..=effect.angvel_rng))
};
let angle = if effect.angle_rng == 0.0 {
parent_angle + effect.angle
} else {
parent_angle + effect.angle + Rad(rng.gen_range(-effect.angle_rng..=effect.angle_rng))
};
ParticleBuilder {
sprite: effect.sprite,
pos,
velocity,
angle,
angvel,
lifetime: 0f32
.max(effect.lifetime + rng.gen_range(-effect.lifetime_rng..=effect.lifetime_rng)),
// Make sure size isn't negative. This check should be on EVERY rng!
size: 0f32.max(effect.size + rng.gen_range(-effect.size_rng..=effect.size_rng)),
fade: 0f32.max(effect.fade + rng.gen_range(-effect.fade_rng..=effect.fade_rng)),
}
}
}
/// The location of a UI element, in one of a few /// The location of a UI element, in one of a few
/// possible coordinate systems. /// possible coordinate systems.
@ -112,8 +27,37 @@ pub enum AnchoredUiPosition {
/// Position of this sprite's se corner, /// Position of this sprite's se corner,
/// relative to the nw corner of the window. /// relative to the nw corner of the window.
NwSe(Point2<f32>), NwSe(Point2<f32>),
/// Position of this sprite's ne corner,
/// relative to the ne corner of the window.
NeNe(Point2<f32>),
} }
impl AnchoredUiPosition {
pub fn to_anchor_int(&self) -> u32 {
match self {
Self::NwC(_) => 0,
Self::NwNw(_) => 1,
Self::NwNe(_) => 2,
Self::NwSw(_) => 3,
Self::NwSe(_) => 4,
Self::NeNe(_) => 5,
}
}
pub fn position(&self) -> &Point2<f32> {
match self {
Self::NwC(x)
| Self::NwNw(x)
| Self::NwNe(x)
| Self::NwSw(x)
| Self::NwSe(x)
| Self::NeNe(x) => x,
}
}
}
// TODO: remove
/// A sprite that represents a ui element /// A sprite that represents a ui element
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct UiSprite { pub struct UiSprite {
@ -133,47 +77,3 @@ pub struct UiSprite {
/// This sprite's rotation, measured ccw /// This sprite's rotation, measured ccw
pub angle: Deg<f32>, pub angle: Deg<f32>,
} }
/// A sprite that represents a world object:
/// Ships, planets, debris, etc
#[derive(Debug, Clone)]
pub struct ObjectSprite {
/// The sprite to draw
pub sprite: content::SpriteHandle,
/// This object's center, in world coordinates.
pub pos: Point3<f32>,
/// The size of this sprite,
/// given as height in world units.
pub size: f32,
/// This sprite's rotation
/// (relative to north, measured ccw)
/// Note that this is different from the angle used by our physics system.
pub angle: Deg<f32>,
/// Sprites that should be drawn relative to this sprite.
pub children: Option<Vec<ObjectSubSprite>>,
}
/// A sprite that is drawn relative to an ObjectSprite.
#[derive(Debug, Clone)]
pub struct ObjectSubSprite {
/// The sprite to draw
pub sprite: content::SpriteHandle,
/// This object's position, in world coordinates.
/// This is relative to this sprite's parent.
pub pos: Point3<f32>,
/// The size of this sprite,
/// given as height in world units.
pub size: f32,
/// This sprite's rotation
/// (relative to north, measured ccw)
/// Just as position, this is relative to this
/// subsprite's parent sprite.
pub angle: Deg<f32>,
}

View File

@ -122,6 +122,7 @@ impl BufferObject for ObjectInstance {
pub struct UiInstance { pub struct UiInstance {
/// Extra transformations this sprite /// Extra transformations this sprite
/// (rotation, etc) /// (rotation, etc)
/// TODO: remove
pub transform: [[f32; 4]; 4], pub transform: [[f32; 4]; 4],
/// This lets us color ui sprites dynamically. /// This lets us color ui sprites dynamically.
@ -277,3 +278,75 @@ impl BufferObject for ParticleInstance {
} }
} }
} }
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct RadialBarInstance {
/// How to interpret this object's coordinates.
/// this should be generated from an AnchoredUiPosition
pub anchor: u32,
/// Position of this particle in logical pixels
pub position: [f32; 2],
/// The diameter of this bar, in logical pixels
/// No part of the bar will be outside this diameter.
/// Stroke is grown inwards.
pub diameter: f32,
// The stroke width of this bar, in logical pixels
pub stroke: f32,
/// Angle of this radial bar, in radians
pub angle: f32,
/// Color of this bar
pub color: [f32; 4],
}
impl BufferObject for RadialBarInstance {
fn layout() -> wgpu::VertexBufferLayout<'static> {
wgpu::VertexBufferLayout {
array_stride: Self::SIZE,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
// Anchor
wgpu::VertexAttribute {
offset: 0,
shader_location: 2,
format: wgpu::VertexFormat::Uint32,
},
// Position
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 1]>() as wgpu::BufferAddress,
shader_location: 3,
format: wgpu::VertexFormat::Float32x2,
},
// Diameter
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
shader_location: 4,
format: wgpu::VertexFormat::Float32,
},
// Width
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress,
shader_location: 5,
format: wgpu::VertexFormat::Float32,
},
// Angle
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 5]>() as wgpu::BufferAddress,
shader_location: 6,
format: wgpu::VertexFormat::Float32,
},
// Color
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 6]>() as wgpu::BufferAddress,
shader_location: 7,
format: wgpu::VertexFormat::Float32x4,
},
],
}
}
}

View File

@ -1,24 +0,0 @@
[package]
name = "galactica-ui"
description = "UI routines for Galactica"
categories = { workspace = true }
keywords = { workspace = true }
version = { workspace = true }
rust-version = { workspace = true }
authors = { workspace = true }
edition = { workspace = true }
homepage = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
documentation = { workspace = true }
readme = { workspace = true }
[lints]
workspace = true
[dependencies]
galactica-content = { workspace = true }
galactica-world = { workspace = true }
galactica-render = { workspace = true }
galactica-gameobject = { workspace = true }
cgmath = { workspace = true }

View File

@ -1,3 +0,0 @@
mod radar;
pub use radar::build_radar;

View File

@ -1,194 +0,0 @@
use cgmath::{Deg, InnerSpace, Point2, Vector2};
use galactica_content as content;
use galactica_gameobject as object;
use galactica_render::{AnchoredUiPosition, UiSprite};
use galactica_world::{util, ShipPhysicsHandle, World};
// TODO: args as one unit
pub fn build_radar(
ct: &content::Content,
player: ShipPhysicsHandle,
physics: &World,
system: &object::System,
camera_zoom: f32,
camera_aspect: f32,
) -> Vec<UiSprite> {
let mut out = Vec::new();
let radar_range = 4000.0;
let radar_size = 300.0;
let hide_range = 0.85;
let shrink_distance = 20.0;
let system_object_scale = 1.0 / 600.0;
let ship_scale = 1.0 / 15.0;
let (_, player_body) = physics.get_ship_body(player).unwrap();
let player_position = util::rigidbody_position(player_body);
let planet_sprite = ct.get_sprite_handle("ui::planetblip");
let ship_sprite = ct.get_sprite_handle("ui::shipblip");
let arrow_sprite = ct.get_sprite_handle("ui::centerarrow");
out.push(UiSprite {
sprite: ct.get_sprite_handle("ui::radar"),
pos: AnchoredUiPosition::NwNw(Point2 { x: 10.0, y: -10.0 }),
dimensions: Point2 {
x: radar_size,
y: radar_size,
},
angle: Deg(0.0),
color: None,
});
// Draw system objects
for o in &system.bodies {
let size = (o.size / o.pos.z) / (radar_range * system_object_scale);
let p = Point2 {
x: o.pos.x,
y: o.pos.y,
};
let d = (p - player_position) / radar_range;
// Add half the blip sprite's height to distance
let m = d.magnitude() + (size / (2.0 * radar_size));
if m < hide_range {
// Shrink blips as they get closeto the edge
let size = size.min((hide_range - m) * size * shrink_distance);
if size <= 2.0 {
// Don't draw super tiny sprites, they flicker
continue;
}
out.push(UiSprite {
sprite: planet_sprite,
pos: AnchoredUiPosition::NwC(
Point2 {
x: radar_size / 2.0 + 10.0,
y: radar_size / -2.0 - 10.0,
} + (d * (radar_size / 2.0)),
),
dimensions: Point2 {
x: planet_sprite.aspect,
y: 1.0,
} * size,
angle: o.angle,
color: Some([0.5, 0.5, 0.5, 1.0]),
});
}
}
// Draw ships
for (s, r) in physics.iter_ship_body() {
let ship = ct.get_ship(s.ship.handle);
let size = (ship.size * ship.sprite.aspect) * ship_scale;
let p = util::rigidbody_position(r);
let d = (p - player_position) / radar_range;
let m = d.magnitude() + (size / (2.0 * radar_size));
if m < hide_range {
let size = size.min((hide_range - m) * size * shrink_distance);
if size < 2.0 {
continue;
}
let angle: Deg<f32> = util::rigidbody_rotation(r)
.angle(Vector2 { x: 0.0, y: 1.0 })
.into();
let f = ct.get_faction(s.ship.faction).color;
let f = [f[0], f[1], f[2], 1.0];
out.push(UiSprite {
sprite: ship_sprite,
pos: AnchoredUiPosition::NwC(
Point2 {
x: radar_size / 2.0 + 10.0,
y: radar_size / -2.0 - 10.0,
} + (d * (radar_size / 2.0)),
),
dimensions: Point2 {
x: ship_sprite.aspect,
y: 1.0,
} * size,
angle: -angle,
color: Some(f),
});
}
}
// Draw viewport frame
let d = Vector2 {
x: (camera_zoom / 2.0) * camera_aspect,
y: camera_zoom / 2.0,
} / radar_range;
let m = d.magnitude();
let d = d * (radar_size / 2.0);
let color = Some([0.3, 0.3, 0.3, 1.0]);
if m < 0.8 {
let sprite = ct.get_sprite_handle("ui::radarframe");
let dimensions = Point2 {
x: sprite.aspect,
y: 1.0,
} * 7.0f32.min((0.8 - m) * 70.0);
out.push(UiSprite {
sprite,
pos: AnchoredUiPosition::NwNw(Point2 {
x: (radar_size / 2.0 + 10.0) - d.x,
y: (radar_size / -2.0 - 10.0) + d.y,
}),
dimensions,
angle: Deg(0.0),
color,
});
out.push(UiSprite {
sprite,
pos: AnchoredUiPosition::NwSw(Point2 {
x: (radar_size / 2.0 + 10.0) - d.x,
y: (radar_size / -2.0 - 10.0) - d.y,
}),
dimensions,
angle: Deg(90.0),
color,
});
out.push(UiSprite {
sprite,
pos: AnchoredUiPosition::NwSe(Point2 {
x: (radar_size / 2.0 + 10.0) + d.x,
y: (radar_size / -2.0 - 10.0) - d.y,
}),
dimensions,
angle: Deg(180.0),
color,
});
out.push(UiSprite {
sprite,
pos: AnchoredUiPosition::NwNe(Point2 {
x: (radar_size / 2.0 + 10.0) + d.x,
y: (radar_size / -2.0 - 10.0) + d.y,
}),
dimensions,
angle: Deg(270.0),
color,
});
}
// Arrow to center of system
let q = Point2 { x: 0.0, y: 0.0 } - player_position;
let m = q.magnitude();
if m > 200.0 {
let player_angle: Deg<f32> = q.angle(Vector2 { x: 0.0, y: 1.0 }).into();
out.push(UiSprite {
sprite: arrow_sprite,
pos: AnchoredUiPosition::NwC(
Point2 {
x: radar_size / 2.0 + 10.0,
y: radar_size / -2.0 - 10.0,
} + ((q.normalize() * 0.865) * (radar_size / 2.0)),
),
dimensions: Point2 {
x: arrow_sprite.aspect,
y: 1.0,
} * 10.0,
angle: -player_angle,
color: Some([1.0, 1.0, 1.0, 1f32.min((m - 200.0) / 400.0)]),
});
}
return out;
}

View File

@ -17,7 +17,6 @@ readme = { workspace = true }
workspace = true workspace = true
[dependencies] [dependencies]
galactica-render = { workspace = true }
galactica-content = { workspace = true } galactica-content = { workspace = true }
galactica-gameobject = { workspace = true } galactica-gameobject = { workspace = true }

View File

@ -8,6 +8,9 @@ pub mod util;
mod world; mod world;
mod wrapper; mod wrapper;
use cgmath::{Matrix2, Point2, Rad, Vector2};
use galactica_content as content;
use rand::Rng;
pub use world::World; pub use world::World;
use rapier2d::{dynamics::RigidBodyHandle, geometry::ColliderHandle}; use rapier2d::{dynamics::RigidBodyHandle, geometry::ColliderHandle};
@ -15,3 +18,87 @@ use rapier2d::{dynamics::RigidBodyHandle, geometry::ColliderHandle};
/// A lightweight handle for a specific ship in our physics system /// A lightweight handle for a specific ship in our physics system
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct ShipPhysicsHandle(pub RigidBodyHandle, ColliderHandle); pub struct ShipPhysicsHandle(pub RigidBodyHandle, ColliderHandle);
/// Instructions to create a new particle
pub struct ParticleBuilder {
/// The sprite to use for this particle
pub sprite: content::SpriteHandle,
/// This object's center, in world coordinates.
pub pos: Point2<f32>,
/// This particle's velocity, in world coordinates
pub velocity: Vector2<f32>,
/// This particle's angle
pub angle: Rad<f32>,
/// This particle's angular velocity (rad/sec)
pub angvel: Rad<f32>,
/// This particle's lifetime, in seconds
pub lifetime: f32,
/// The size of this particle,
/// given as height in world units.
pub size: f32,
/// Fade this particle out over this many seconds as it expires
pub fade: f32,
}
impl ParticleBuilder {
/// Create a ParticleBuilder from an Effect
pub fn from_content(
effect: &content::Effect,
pos: Point2<f32>,
parent_angle: Rad<f32>,
parent_velocity: Vector2<f32>,
target_velocity: Vector2<f32>,
) -> Self {
let mut rng = rand::thread_rng();
let velocity = {
let a =
rng.gen_range(-effect.velocity_scale_parent_rng..=effect.velocity_scale_parent_rng);
let b =
rng.gen_range(-effect.velocity_scale_target_rng..=effect.velocity_scale_target_rng);
let velocity = ((effect.velocity_scale_parent + a) * parent_velocity)
+ ((effect.velocity_scale_target + b) * target_velocity);
Matrix2::from_angle(Rad(
rng.gen_range(-effect.direction_rng..=effect.direction_rng)
)) * velocity
};
// Rad has odd behavior when its angle is zero, so we need extra checks here
let angvel = if effect.angvel_rng == 0.0 {
effect.angvel
} else {
Rad(effect.angvel.0 + rng.gen_range(-effect.angvel_rng..=effect.angvel_rng))
};
let angle = if effect.angle_rng == 0.0 {
parent_angle + effect.angle
} else {
parent_angle + effect.angle + Rad(rng.gen_range(-effect.angle_rng..=effect.angle_rng))
};
ParticleBuilder {
sprite: effect.sprite,
pos,
velocity,
angle,
angvel,
lifetime: 0f32
.max(effect.lifetime + rng.gen_range(-effect.lifetime_rng..=effect.lifetime_rng)),
// Make sure size isn't negative. This check should be on EVERY rng!
size: 0f32.max(effect.size + rng.gen_range(-effect.size_rng..=effect.size_rng)),
fade: 0f32.max(effect.fade + rng.gen_range(-effect.fade_rng..=effect.fade_rng)),
}
}
}

View File

@ -1,13 +1,7 @@
use cgmath::{Deg, InnerSpace, Point3, Vector2};
use rand::Rng; use rand::Rng;
use rapier2d::{ use rapier2d::{dynamics::RigidBodyHandle, geometry::ColliderHandle};
dynamics::{RigidBody, RigidBodyHandle},
geometry::ColliderHandle,
};
use crate::util;
use galactica_gameobject as object; use galactica_gameobject as object;
use galactica_render::ObjectSprite;
/// A single projectile in the world /// A single projectile in the world
#[derive(Debug)] #[derive(Debug)]
@ -41,25 +35,4 @@ impl ProjectileWorldObject {
size_rng: rng.gen_range(-size_rng..=size_rng), size_rng: rng.gen_range(-size_rng..=size_rng),
} }
} }
/// Get this projectiles' sprite
pub fn get_sprite(&self, r: &RigidBody) -> ObjectSprite {
let pos = util::rigidbody_position(r);
let rot = util::rigidbody_rotation(r);
// Sprites point north at 0 degrees
let ang: Deg<f32> = rot.angle(Vector2 { x: 1.0, y: 0.0 }).into();
ObjectSprite {
sprite: self.projectile.content.sprite,
pos: Point3 {
x: pos.x,
y: pos.y,
z: 1.0,
},
size: 0f32.max(self.projectile.content.size + self.size_rng),
angle: -ang,
children: None,
}
}
} }

View File

@ -4,10 +4,9 @@ use nalgebra::{point, vector};
use rand::{rngs::ThreadRng, Rng}; use rand::{rngs::ThreadRng, Rng};
use rapier2d::{dynamics::RigidBody, geometry::Collider}; use rapier2d::{dynamics::RigidBody, geometry::Collider};
use crate::{util, ShipPhysicsHandle}; use crate::{util, ParticleBuilder, ShipPhysicsHandle};
use galactica_content as content; use galactica_content as content;
use galactica_gameobject as object; use galactica_gameobject as object;
use galactica_render::{ObjectSprite, ParticleBuilder};
pub struct ShipControls { pub struct ShipControls {
pub left: bool, pub left: bool,
@ -289,28 +288,4 @@ impl ShipWorldObject {
i.cooldown -= t; i.cooldown -= t;
} }
} }
/// Get this ship's sprite
pub fn get_sprite(&self, ct: &content::Content, r: &RigidBody) -> ObjectSprite {
let ship_pos = util::rigidbody_position(r);
let ship_rot = util::rigidbody_rotation(r);
// Sprites point north at 0 degrees
let ship_ang: Deg<f32> = ship_rot.angle(Vector2 { x: 0.0, y: 1.0 }).into();
let s = ct.get_ship(self.ship.handle);
ObjectSprite {
pos: (ship_pos.x, ship_pos.y, 1.0).into(),
sprite: s.sprite,
angle: -ship_ang,
size: s.size,
children: if self.controls.thrust {
Some(self.ship.outfits.get_engine_flares())
} else {
None
},
}
}
} }

View File

@ -9,10 +9,15 @@ use rapier2d::{
}; };
use std::{collections::HashMap, f32::consts::PI}; use std::{collections::HashMap, f32::consts::PI};
use crate::{objects, objects::ProjectileWorldObject, util, wrapper::Wrapper, ShipPhysicsHandle}; use crate::{
objects,
objects::{ProjectileWorldObject, ShipWorldObject},
util,
wrapper::Wrapper,
ParticleBuilder, ShipPhysicsHandle,
};
use galactica_content as content; use galactica_content as content;
use galactica_gameobject as object; use galactica_gameobject as object;
use galactica_render::{ObjectSprite, ParticleBuilder};
/// Keeps track of all objects in the world that we can interact with. /// Keeps track of all objects in the world that we can interact with.
/// Also wraps our physics engine /// Also wraps our physics engine
@ -379,20 +384,22 @@ impl<'a> World {
}) })
} }
/// Iterate over all ship sprites in this physics system /// Iterate over all ships in this physics system
pub fn get_ship_sprites( pub fn iter_ships(&'a self) -> impl Iterator<Item = &ShipWorldObject> + '_ {
&'a self, self.ships.values()
ct: &'a content::Content,
) -> impl Iterator<Item = ObjectSprite> + '_ {
self.ships
.values()
.map(|x| x.get_sprite(ct, &self.wrapper.rigid_body_set[x.physics_handle.0]))
} }
/// Iterate over all ships in this physics system
pub fn iter_projectiles(&'a self) -> impl Iterator<Item = &ProjectileWorldObject> + '_ {
self.projectiles.values()
}
/*
/// Iterate over all projectile sprites in this physics system /// Iterate over all projectile sprites in this physics system
pub fn get_projectile_sprites(&self) -> impl Iterator<Item = ObjectSprite> + '_ { pub fn get_projectile_sprites(&self) -> impl Iterator<Item = ObjectSprite> + '_ {
self.projectiles self.projectiles
.values() .values()
.map(|x| x.get_sprite(&self.wrapper.rigid_body_set[x.rigid_body])) .map(|x| x.get_sprite(&self.wrapper.rigid_body_set[x.rigid_body]))
} }
*/
} }