Compare commits

...

9 Commits

Author SHA1 Message Date
Mark 7334ebd00e
Added basic planet ui 2024-01-17 14:50:44 -08:00
Mark 0570a13f1b
Added ui assets 2024-01-17 14:50:14 -08:00
Mark 62d63be55e
Added ui utilities 2024-01-17 13:36:14 -08:00
Mark 1ae8e6c5a8
Added ui sprite masking 2024-01-17 13:27:32 -08:00
Mark 81dc7b170c
Minor edits 2024-01-17 10:25:03 -08:00
Mark fa46c76fd2
Updated TODO 2024-01-17 10:24:18 -08:00
Mark 3c5ede28e4
Added basic planet descriptions 2024-01-17 10:23:42 -08:00
Mark df7baa338c
Updated glyphon 2024-01-17 10:17:27 -08:00
Mark 3561baa99c
Minor reorganization 2024-01-17 10:17:18 -08:00
30 changed files with 973 additions and 526 deletions

9
Cargo.lock generated
View File

@ -777,8 +777,9 @@ dependencies = [
[[package]]
name = "glyphon"
version = "0.3.0"
source = "git+https://github.com/grovesNL/glyphon.git?rev=b15437b87f4082e7a96a2ba05ed5063a6047cf95#b15437b87f4082e7a96a2ba05ed5063a6047cf95"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17e87235dcba36007d4990db6253ac04e4efbd16df1f0812c2fefb1217aa2644"
dependencies = [
"cosmic-text",
"etagere",
@ -1043,9 +1044,9 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "lru"
version = "0.11.1"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21"
checksum = "2994eeba8ed550fd9b47a0b38f0242bc3344e496483c6180b69139cc2fa5d1d7"
dependencies = [
"hashbrown",
]

View File

@ -63,6 +63,4 @@ anyhow = "1.0"
rand = "0.8.5"
walkdir = "2.4.0"
toml = "0.8.8"
# Glyphon's crates.io release doesn't support wgpu 0.18 yet
glyphon = { git = "https://github.com/grovesNL/glyphon.git", rev = "b15437b87f4082e7a96a2ba05ed5063a6047cf95" }
glyphon = "0.4.1"

View File

@ -6,6 +6,7 @@
## Small jobs
- 🌟 Better planet desc formatting
- Procedural suns
- 🌟 Back arrow -> reverse
- No wobble for ai ships & autopilot
@ -43,6 +44,7 @@
- Projectile performance
- Starfield clusters, shader instead of an array?
- Collider groups for factions? (projectile optimization)
- Better error when sprite is missing from atlas
## 🌟 Player selection
- Planet name, ring, and distance

2
assets

@ -1 +1 @@
Subproject commit 9746acb16c6e2c5f6f232e8d1b53e27fb9ef486e
Subproject commit b4fe8111c7b1cb17987bbff095bd50e61e2d8998

View File

@ -61,6 +61,16 @@ file = "ui/radarframe.png"
[sprite."ui::centerarrow"]
file = "ui/center-arrow.png"
[sprite."ui::planet"]
file = "ui/planet.png"
[sprite."ui::landscape::test"]
file = "ui/landscape/test.png"
[sprite."ui::landscapemask"]
file = "ui/landscape-mask.png"
[sprite."particle::blaster"]
timing.duration = 0.15
repeat = "once"

View File

@ -15,6 +15,20 @@ object.earth.size = 1000
# TODO: satisfy conditions to land
object.earth.landable = true
object.earth.name = "Earth"
object.earth.desc = """
The ancestral home world of humanity, Earth has a population twice that of any other inhabited planet.
Sprawling cities cover large portions of its surface, many of them overcrowded and dangerous.
Some people work to scrape together enough money to leave, while at the same time others, born
on distant worlds, make a pilgrimage of sorts to see this planet that once cradled the entirety
of the human species.
<br><br>
Earth is also the capital of the Republic. Representative government becomes complicated when
one planet has a greater population than a hundred planets elsewhere. As a result,
settlements of less than a million are grouped together into planetary districts that
elect a single representative between them - a source of much frustration in the frontier worlds.
"""
object.luna.sprite = "planet::luna"
object.luna.position.center = "earth"

View File

@ -29,6 +29,8 @@ pub(crate) mod syntax {
pub radius: Option<f32>,
pub angle: Option<f32>,
pub landable: Option<bool>,
pub name: Option<String>,
pub desc: Option<String>,
}
#[derive(Debug, Deserialize)]
@ -122,6 +124,12 @@ pub struct SystemObject {
/// If true, ships may land on this object
pub landable: bool,
/// The display name of this object
pub name: String,
/// The description of this object
pub desc: String,
}
/// Helper function for resolve_position, never called on its own.
@ -223,6 +231,19 @@ impl crate::Build for System {
body_index: 0,
},
landable: obj.landable.unwrap_or(false),
name: obj
.name
.as_ref()
.map(|x| x.clone())
.unwrap_or("".to_string()),
// TODO: better linebreaks, handle double spaces
// Tabs
desc: obj
.desc
.as_ref()
.map(|x| x.replace("\n", " ").replace("<br>", "\n"))
.unwrap_or("".to_string()),
});
}

View File

@ -44,7 +44,7 @@ impl<'a> Game {
ShipHandle { index: 0 },
FactionHandle { index: 0 },
ShipPersonality::Player,
Point2::new(0.0, 0.0),
Point2::new(0.0, 4000.0),
);
let s = self.state.systemsim.get_ship_mut(&player).unwrap();

View File

@ -28,6 +28,12 @@ fn anchor(
} else if anchor == 5u { // NE NE
trans += vec2(window_dim.x, window_dim.y) / 2.0;
trans += vec2(-dim.x, -dim.y) / 2.0;
} else if anchor == 6u { // C C
trans += vec2(0.0, 0.0) / 2.0;
trans += vec2(0.0, 0.0) / 2.0;
} else if anchor == 7u { // C NW
trans += vec2(0.0, 0.0) / 2.0;
trans += vec2(dim.x, -dim.y) / 2.0;
} else { // center / center as default, since it's the most visible variant.
trans += vec2(0.0, 0.0) / 2.0;
trans += vec2(0.0, 0.0) / 2.0;

View File

@ -7,6 +7,7 @@ struct InstanceInput {
@location(5) size: f32,
@location(6) color_transform: vec4<f32>,
@location(7) sprite_index: u32,
@location(8) mask_index: vec2<u32>,
};
struct VertexInput {
@ -18,7 +19,9 @@ struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) texture_coords: vec2<f32>,
@location(1) texture_index: u32,
@location(2) color_transform: vec4<f32>,
@location(2) mask_coords: vec2<f32>,
@location(3) mask_index: vec2<u32>,
@location(4) color_transform: vec4<f32>,
}
@group(0) @binding(0)
@ -72,7 +75,7 @@ fn vertex_main(
// TODO: animate
// TODO: function to get texture from sprite
// Pick texture frame
let t = global_atlas[u32(animate(instance.sprite_index, global_data.current_time.x, 0.0))];
out.texture_index = u32(t.atlas_texture);
@ -84,16 +87,55 @@ fn vertex_main(
out.texture_coords = vec2(out.texture_coords.x, out.texture_coords.y + t.height);
}
// Pick mask image if mask is enabled
// x coordinate of mask index is either 0 or 1, telling us whether or not to use a mask.
// y coordinate is mask sprite index
if instance.mask_index.x == 1u {
let ms = global_sprites[instance.mask_index.y].first_frame;
let m = global_atlas[ms];
out.mask_index = vec2(1u, u32(m.atlas_texture));
out.mask_coords = vec2(m.xpos, m.ypos);
if vertex.texture_coords.x == 1.0 {
out.mask_coords = vec2(out.mask_coords.x + m.width, out.mask_coords.y);
}
if vertex.texture_coords.y == 1.0 {
out.mask_coords = vec2(out.mask_coords.x, out.mask_coords.y + m.height);
}
} else {
out.mask_coords = vec2(0.0, 0.0);
out.mask_index = vec2(0u, 0u);
}
return out;
}
@fragment
fn fragment_main(in: VertexOutput) -> @location(0) vec4<f32> {
return textureSampleLevel(
var mask: f32 = 1.0;
if in.mask_index.x == 1u {
mask = textureSampleLevel(
texture_array[in.mask_index.y],
sampler_array[0],
in.mask_coords,
0.0
).a;
}
var color: vec4<f32> = textureSampleLevel(
texture_array[in.texture_index],
sampler_array[0],
in.texture_coords,
0.0
).rgba * in.color_transform;
// Apply mask and discard fully transparent pixels
color = vec4(color.rgb, color.a *mask);
if color.a == 0.0 {
discard;
}
return color;
}

View File

@ -1,78 +0,0 @@
use galactica_content::{Content, SystemHandle};
use galactica_playeragent::PlayerAgent;
use galactica_system::phys::{ParticleBuilder, PhysSim};
use galactica_util::timing::Timing;
use glyphon::{FontSystem, SwashCache, TextAtlas, TextRenderer};
use nalgebra::Vector2;
use std::rc::Rc;
use wgpu::BufferAddress;
use winit::window::Window;
use crate::{globaluniform::GlobalUniform, vertexbuffer::VertexBuffer};
/// Bundles parameters passed to a single call to GPUState::render
pub struct RenderInput<'a> {
/// Camera position, in world units
pub camera_pos: Vector2<f32>,
/// Player ship data
pub player: &'a PlayerAgent,
/// The system we're currently in
pub current_system: SystemHandle,
/// Height of screen, in world units
pub camera_zoom: f32,
/// The world state to render
pub systemsim: &'a PhysSim,
// TODO: handle overflow. is it a problem?
/// The current time, in seconds
pub current_time: f32,
/// Game content
pub ct: &'a Content,
/// Particles to spawn during this frame
pub particles: &'a Vec<ParticleBuilder>,
/// Time we spent in each part of the game loop
pub timing: Timing,
}
/// Renderer state. A reference to this struct is often passed to helper functions.
pub(crate) struct RenderState {
pub window: Window,
pub window_size: winit::dpi::PhysicalSize<u32>,
pub window_aspect: f32,
pub queue: wgpu::Queue,
pub global_uniform: GlobalUniform,
pub vertex_buffers: VertexBuffers,
pub text_font_system: FontSystem,
pub text_cache: SwashCache,
pub text_atlas: TextAtlas,
pub text_renderer: TextRenderer,
}
/// Vertex buffers
pub(crate) struct VertexBuffers {
// Keeps track of length of each buffer
// Most of these are reset on each frame.
//
// The exception is particle_counter, which
// is never reset, and loops back to zero once
// it exceeds buffer length
pub object_counter: BufferAddress,
pub ui_counter: BufferAddress,
pub particle_counter: BufferAddress,
pub radialbar_counter: BufferAddress,
pub object: Rc<VertexBuffer>,
pub starfield: Rc<VertexBuffer>,
pub ui: Rc<VertexBuffer>,
pub particle: Rc<VertexBuffer>,
pub radialbar: Rc<VertexBuffer>,
}

View File

@ -3,9 +3,7 @@ use galactica_content::Content;
use wgpu;
use winit;
use crate::{
datastructs::RenderState, starfield::Starfield, texturearray::TextureArray, ui::UiManager,
};
use crate::{starfield::Starfield, texturearray::TextureArray, ui::UiManager, RenderState};
/// GPUState is very big, so its methods have been split
/// among the following files.

View File

@ -1,28 +1,11 @@
use anyhow::Result;
use galactica_content::Content;
use galactica_util::constants::{
OBJECT_SPRITE_INSTANCE_LIMIT, PARTICLE_SPRITE_INSTANCE_LIMIT, UI_SPRITE_INSTANCE_LIMIT,
};
use glyphon::{FontSystem, SwashCache, TextAtlas, TextRenderer};
use std::rc::Rc;
use crate::{
datastructs::{RenderState, VertexBuffers},
globaluniform::GlobalUniform,
pipeline::PipelineBuilder,
shaderprocessor::preprocess_shader,
starfield::Starfield,
texturearray::TextureArray,
ui::UiManager,
vertexbuffer::{
consts::{SPRITE_INDICES, SPRITE_VERTICES},
types::{
ObjectInstance, ParticleInstance, RadialBarInstance, StarfieldInstance, TexturedVertex,
UiInstance,
},
VertexBuffer,
},
GPUState,
globaluniform::GlobalUniform, pipeline::PipelineBuilder, shaderprocessor::preprocess_shader,
starfield::Starfield, texturearray::TextureArray, ui::UiManager, GPUState, RenderState,
VertexBuffers,
};
impl GPUState {
@ -90,52 +73,7 @@ impl GPUState {
surface.configure(&device, &config);
}
let vertex_buffers = VertexBuffers {
object_counter: 0,
ui_counter: 0,
particle_counter: 0,
radialbar_counter: 0,
object: Rc::new(VertexBuffer::new::<TexturedVertex, ObjectInstance>(
"object",
&device,
Some(SPRITE_VERTICES),
Some(SPRITE_INDICES),
OBJECT_SPRITE_INSTANCE_LIMIT,
)),
starfield: Rc::new(VertexBuffer::new::<TexturedVertex, StarfieldInstance>(
"starfield",
&device,
Some(SPRITE_VERTICES),
Some(SPRITE_INDICES),
ct.get_config().starfield_instance_limit,
)),
ui: Rc::new(VertexBuffer::new::<TexturedVertex, UiInstance>(
"ui",
&device,
Some(SPRITE_VERTICES),
Some(SPRITE_INDICES),
UI_SPRITE_INSTANCE_LIMIT,
)),
particle: Rc::new(VertexBuffer::new::<TexturedVertex, ParticleInstance>(
"particle",
&device,
Some(SPRITE_VERTICES),
Some(SPRITE_INDICES),
PARTICLE_SPRITE_INSTANCE_LIMIT,
)),
radialbar: Rc::new(VertexBuffer::new::<TexturedVertex, RadialBarInstance>(
"radial bar",
&device,
Some(SPRITE_VERTICES),
Some(SPRITE_INDICES),
10,
)),
};
let vertex_buffers = VertexBuffers::new(&device, ct);
// Load uniforms
let global_uniform = GlobalUniform::new(&device);
@ -197,7 +135,7 @@ impl GPUState {
))
.set_format(config.format)
.set_triangle(true)
.set_vertex_buffer(&vertex_buffers.object)
.set_vertex_buffer(vertex_buffers.get_object())
.set_bind_group_layouts(bind_group_layouts)
.build();
@ -213,7 +151,7 @@ impl GPUState {
))
.set_format(config.format)
.set_triangle(true)
.set_vertex_buffer(&vertex_buffers.starfield)
.set_vertex_buffer(vertex_buffers.get_starfield())
.set_bind_group_layouts(bind_group_layouts)
.build();
@ -225,7 +163,7 @@ impl GPUState {
))
.set_format(config.format)
.set_triangle(true)
.set_vertex_buffer(&vertex_buffers.ui)
.set_vertex_buffer(vertex_buffers.get_ui())
.set_bind_group_layouts(bind_group_layouts)
.build();
@ -241,7 +179,7 @@ impl GPUState {
))
.set_format(config.format)
.set_triangle(true)
.set_vertex_buffer(&vertex_buffers.particle)
.set_vertex_buffer(vertex_buffers.get_particle())
.set_bind_group_layouts(bind_group_layouts)
.build();
@ -257,7 +195,7 @@ impl GPUState {
))
.set_format(config.format)
.set_triangle(true)
.set_vertex_buffer(&vertex_buffers.radialbar)
.set_vertex_buffer(vertex_buffers.get_radialbar())
.set_bind_group_layouts(bind_group_layouts)
.build();
@ -281,7 +219,7 @@ impl GPUState {
};
return Ok(Self {
ui: UiManager::new(&mut state),
ui: UiManager::new(ct, &mut state),
device,
config,
surface,

View File

@ -2,13 +2,11 @@
use bytemuck;
use galactica_system::data::ShipState;
use galactica_util::{constants::OBJECT_SPRITE_INSTANCE_LIMIT, to_radians};
use galactica_util::to_radians;
use nalgebra::{Point2, Point3};
use crate::{
globaluniform::ObjectData,
vertexbuffer::{types::ObjectInstance, BufferObject},
GPUState, RenderInput,
globaluniform::ObjectData, vertexbuffer::types::ObjectInstance, GPUState, RenderInput,
};
impl GPUState {
@ -68,7 +66,7 @@ impl GPUState {
continue;
}
let idx = self.state.vertex_buffers.object_counter;
let idx = self.state.get_object_counter();
// Write this object's location data
self.state.queue.write_buffer(
&self.state.global_uniform.object_buffer,
@ -85,22 +83,11 @@ impl GPUState {
}]),
);
// Enforce buffer limit
if self.state.vertex_buffers.object_counter as u64 > OBJECT_SPRITE_INSTANCE_LIMIT {
// TODO: no panic, handle this better.
panic!("Sprite limit exceeded!")
}
// Push this object's instance
self.state.queue.write_buffer(
&self.state.vertex_buffers.object.instances,
ObjectInstance::SIZE * self.state.vertex_buffers.object_counter,
bytemuck::cast_slice(&[ObjectInstance {
sprite_index: ship_cnt.sprite.get_index(),
object_index: idx as u32,
}]),
);
self.state.vertex_buffers.object_counter += 1;
self.state.push_object_buffer(ObjectInstance {
sprite_index: ship_cnt.sprite.get_index(),
object_index: idx as u32,
});
let flare = ship.data.get_outfits().get_flare_sprite(state.ct);
if {
@ -115,7 +102,7 @@ impl GPUState {
for engine_point in &ship_cnt.engines {
self.state.queue.write_buffer(
&self.state.global_uniform.object_buffer,
ObjectData::SIZE * self.state.vertex_buffers.object_counter as u64,
ObjectData::SIZE * self.state.get_object_counter() as u64,
bytemuck::cast_slice(&[ObjectData {
// Note that we adjust the y-coordinate for half-height,
// not the x-coordinate, even though our ships point east
@ -135,23 +122,10 @@ impl GPUState {
}]),
);
// Enforce buffer limit
if self.state.vertex_buffers.object_counter as u64
> OBJECT_SPRITE_INSTANCE_LIMIT
{
// TODO: no panic, handle this better.
panic!("Sprite limit exceeded!")
}
self.state.queue.write_buffer(
&self.state.vertex_buffers.object.instances,
ObjectInstance::SIZE * self.state.vertex_buffers.object_counter,
bytemuck::cast_slice(&[ObjectInstance {
sprite_index: flare.unwrap().get_index(),
object_index: self.state.vertex_buffers.object_counter as u32,
}]),
);
self.state.vertex_buffers.object_counter += 1;
self.state.push_object_buffer(ObjectInstance {
sprite_index: flare.unwrap().get_index(),
object_index: self.state.get_object_counter() as u32,
});
}
}
}
@ -191,7 +165,7 @@ impl GPUState {
continue;
}
let idx = self.state.vertex_buffers.object_counter;
let idx = self.state.get_object_counter();
// Write this object's location data
self.state.queue.write_buffer(
&self.state.global_uniform.object_buffer,
@ -208,22 +182,11 @@ impl GPUState {
}]),
);
// Enforce buffer limit
if self.state.vertex_buffers.object_counter as u64 > OBJECT_SPRITE_INSTANCE_LIMIT {
// TODO: no panic, handle this better.
panic!("Sprite limit exceeded!")
}
// Push this object's instance
self.state.queue.write_buffer(
&self.state.vertex_buffers.object.instances,
ObjectInstance::SIZE * self.state.vertex_buffers.object_counter,
bytemuck::cast_slice(&[ObjectInstance {
sprite_index: proj_cnt.sprite.get_index(),
object_index: idx as u32,
}]),
);
self.state.vertex_buffers.object_counter += 1;
self.state.push_object_buffer(ObjectInstance {
sprite_index: proj_cnt.sprite.get_index(),
object_index: idx as u32,
});
}
}
@ -255,7 +218,7 @@ impl GPUState {
continue;
}
let idx = self.state.vertex_buffers.object_counter;
let idx = self.state.get_object_counter();
// Write this object's location data
self.state.queue.write_buffer(
&self.state.global_uniform.object_buffer,
@ -272,22 +235,11 @@ impl GPUState {
}]),
);
// Enforce buffer limit
if self.state.vertex_buffers.object_counter as u64 > OBJECT_SPRITE_INSTANCE_LIMIT {
// TODO: no panic, handle this better.
panic!("Sprite limit exceeded!")
}
// Push this object's instance
self.state.queue.write_buffer(
&self.state.vertex_buffers.object.instances,
ObjectInstance::SIZE * self.state.vertex_buffers.object_counter,
bytemuck::cast_slice(&[ObjectInstance {
sprite_index: o.sprite.get_index(),
object_index: idx as u32,
}]),
);
self.state.vertex_buffers.object_counter += 1;
self.state.push_object_buffer(ObjectInstance {
sprite_index: o.sprite.get_index(),
object_index: idx as u32,
});
}
}
}

View File

@ -8,7 +8,7 @@ use wgpu;
use crate::{
globaluniform::GlobalDataContent,
vertexbuffer::{consts::SPRITE_INDICES, types::ParticleInstance, BufferObject},
vertexbuffer::{consts::SPRITE_INDICES, types::ParticleInstance},
RenderInput,
};
@ -45,11 +45,7 @@ impl super::GPUState {
timestamp_writes: None,
});
self.state.vertex_buffers.object_counter = 0;
self.state.vertex_buffers.ui_counter = 0;
self.state.vertex_buffers.radialbar_counter = 0;
// Don't reset particle counter, it's special
self.state.frame_reset();
let s = input.ct.get_starfield_handle();
// Update global values
@ -81,25 +77,17 @@ impl super::GPUState {
// Write all new particles to GPU buffer
for i in input.particles.iter() {
self.state.queue.write_buffer(
&self.state.vertex_buffers.particle.instances,
ParticleInstance::SIZE * self.state.vertex_buffers.particle_counter,
bytemuck::cast_slice(&[ParticleInstance {
position: [i.pos.x, i.pos.y],
velocity: i.velocity.into(),
angle: i.angle,
angvel: i.angvel,
size: i.size,
sprite_index: i.sprite.get_index(),
created: input.current_time,
expires: input.current_time + i.lifetime,
fade: i.fade,
}]),
);
self.state.vertex_buffers.particle_counter += 1;
if self.state.vertex_buffers.particle_counter == PARTICLE_SPRITE_INSTANCE_LIMIT {
self.state.vertex_buffers.particle_counter = 0;
}
self.state.push_particle_buffer(ParticleInstance {
position: [i.pos.x, i.pos.y],
velocity: i.velocity.into(),
angle: i.angle,
angvel: i.angvel,
size: i.size,
sprite_index: i.sprite.get_index(),
created: input.current_time,
expires: input.current_time + i.lifetime,
fade: i.fade,
});
}
// Create sprite instances
@ -109,10 +97,6 @@ impl super::GPUState {
let clip_ne = Point2::new(-self.state.window_aspect, 1.0) * input.camera_zoom;
let clip_sw = Point2::new(self.state.window_aspect, -1.0) * input.camera_zoom;
// TODO: sorting. We don't need to sort ships, but we do need to sort system objects by z-level
// (which we don't yet draw)
// that should probably be done in iter_system().
// Order matters, it determines what is drawn on top.
// The order inside ships and projectiles doesn't matter,
// but ships should always be under projectiles.
@ -129,31 +113,31 @@ impl super::GPUState {
// Starfield pipeline
self.state
.vertex_buffers
.starfield
.get_starfield()
.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&self.starfield_pipeline);
render_pass.draw_indexed(
0..SPRITE_INDICES.len() as u32,
0,
0..self.starfield.instance_count,
0..self.state.get_starfield_counter(),
);
// Sprite pipeline
self.state
.vertex_buffers
.object
.get_object()
.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&self.object_pipeline);
render_pass.draw_indexed(
0..SPRITE_INDICES.len() as u32,
0,
0..self.state.vertex_buffers.object_counter as _,
0..self.state.get_object_counter(),
);
// Particle pipeline
self.state
.vertex_buffers
.particle
.get_particle()
.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&self.particle_pipeline);
render_pass.draw_indexed(
@ -163,27 +147,31 @@ impl super::GPUState {
);
// Ui pipeline
self.state.vertex_buffers.ui.set_in_pass(&mut render_pass);
self.state
.vertex_buffers
.get_ui()
.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&self.ui_pipeline);
render_pass.draw_indexed(
0..SPRITE_INDICES.len() as u32,
0,
0..self.state.vertex_buffers.ui_counter as _,
0..self.state.get_ui_counter(),
);
// Radial progress bars
// TODO: do we need to do this every time?
self.state
.vertex_buffers
.radialbar
.get_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..self.state.vertex_buffers.radialbar_counter as _,
0..self.state.get_radialbar_counter(),
);
let textareas = self.ui.get_textareas(&input, &self.state);
self.state
.text_renderer
.prepare(
@ -195,7 +183,7 @@ impl super::GPUState {
width: self.state.window_size.width,
height: self.state.window_size.height,
},
self.ui.get_textareas(),
textareas,
&mut self.state.text_cache,
)
.unwrap();

View File

@ -7,20 +7,24 @@
//! and the only one external code should interact with.
//! (Excluding data structs, like [`ObjectSprite`])
mod anchoredposition;
mod datastructs;
mod globaluniform;
mod gpustate;
mod pipeline;
mod positionanchor;
mod renderinput;
mod renderstate;
mod shaderprocessor;
mod starfield;
mod texturearray;
mod ui;
mod vertexbuffer;
pub use anchoredposition::PositionAnchor;
pub use datastructs::RenderInput;
use renderstate::*;
pub use gpustate::GPUState;
pub use positionanchor::PositionAnchor;
pub use renderinput::RenderInput;
use nalgebra::Matrix4;
/// Shader entry points

View File

@ -28,6 +28,14 @@ pub enum PositionAnchor {
/// Position of this sprite's ne corner,
/// relative to the ne corner of the window.
NeNe,
/// Position of this sprite's center,
/// relative to the center of the window.
CC,
/// Position of this sprite's NW corner,
/// relative to the center of the window.
CNw,
}
// These offsets are implemented in wgsl shaders.
@ -42,6 +50,8 @@ impl PositionAnchor {
Self::NwSw => 3,
Self::NwSe => 4,
Self::NeNe => 5,
Self::CC => 6,
Self::CNw => 7,
}
}
}

View File

@ -0,0 +1,36 @@
use galactica_content::{Content, SystemHandle};
use galactica_playeragent::PlayerAgent;
use galactica_system::phys::{ParticleBuilder, PhysSim};
use galactica_util::timing::Timing;
use nalgebra::Vector2;
/// Bundles parameters passed to a single call to GPUState::render
pub struct RenderInput<'a> {
/// Camera position, in world units
pub camera_pos: Vector2<f32>,
/// Player ship data
pub player: &'a PlayerAgent,
/// The system we're currently in
pub current_system: SystemHandle,
/// Height of screen, in world units
pub camera_zoom: f32,
/// The world state to render
pub systemsim: &'a PhysSim,
// TODO: handle overflow. is it a problem?
/// The current time, in seconds
pub current_time: f32,
/// Game content
pub ct: &'a Content,
/// Particles to spawn during this frame
pub particles: &'a Vec<ParticleBuilder>,
/// Time we spent in each part of the game loop
pub timing: Timing,
}

View File

@ -0,0 +1,237 @@
use galactica_content::Content;
use galactica_util::constants::{
OBJECT_SPRITE_INSTANCE_LIMIT, PARTICLE_SPRITE_INSTANCE_LIMIT, RADIALBAR_SPRITE_INSTANCE_LIMIT,
UI_SPRITE_INSTANCE_LIMIT,
};
use glyphon::{FontSystem, SwashCache, TextAtlas, TextRenderer};
use std::rc::Rc;
use wgpu::BufferAddress;
use winit::window::Window;
use crate::{
globaluniform::GlobalUniform,
vertexbuffer::{
consts::{SPRITE_INDICES, SPRITE_VERTICES},
types::{
ObjectInstance, ParticleInstance, RadialBarInstance, StarfieldInstance, TexturedVertex,
UiInstance,
},
BufferObject, VertexBuffer,
},
};
/// Vertex buffers
pub(crate) struct VertexBuffers {
// Keeps track of length of each buffer
// Most of these are reset on each frame.
//
// The exception is particle_counter, which
// is never reset, and loops back to zero once
// it exceeds buffer length
object_counter: BufferAddress,
ui_counter: BufferAddress,
particle_counter: BufferAddress,
radialbar_counter: BufferAddress,
starfield_counter: BufferAddress,
starfield_limit: BufferAddress,
object: Rc<VertexBuffer>,
starfield: Rc<VertexBuffer>,
ui: Rc<VertexBuffer>,
particle: Rc<VertexBuffer>,
radialbar: Rc<VertexBuffer>,
}
impl<'a> VertexBuffers {
pub fn new(device: &wgpu::Device, ct: &Content) -> Self {
Self {
object_counter: 0,
ui_counter: 0,
particle_counter: 0,
radialbar_counter: 0,
starfield_counter: 0,
starfield_limit: ct.get_config().starfield_instance_limit,
object: Rc::new(VertexBuffer::new::<TexturedVertex, ObjectInstance>(
"object",
&device,
Some(SPRITE_VERTICES),
Some(SPRITE_INDICES),
OBJECT_SPRITE_INSTANCE_LIMIT,
)),
starfield: Rc::new(VertexBuffer::new::<TexturedVertex, StarfieldInstance>(
"starfield",
&device,
Some(SPRITE_VERTICES),
Some(SPRITE_INDICES),
ct.get_config().starfield_instance_limit,
)),
ui: Rc::new(VertexBuffer::new::<TexturedVertex, UiInstance>(
"ui",
&device,
Some(SPRITE_VERTICES),
Some(SPRITE_INDICES),
UI_SPRITE_INSTANCE_LIMIT,
)),
particle: Rc::new(VertexBuffer::new::<TexturedVertex, ParticleInstance>(
"particle",
&device,
Some(SPRITE_VERTICES),
Some(SPRITE_INDICES),
PARTICLE_SPRITE_INSTANCE_LIMIT,
)),
radialbar: Rc::new(VertexBuffer::new::<TexturedVertex, RadialBarInstance>(
"radial bar",
&device,
Some(SPRITE_VERTICES),
Some(SPRITE_INDICES),
10,
)),
}
}
pub fn get_ui(&'a self) -> &'a Rc<VertexBuffer> {
&self.ui
}
pub fn get_object(&'a self) -> &'a Rc<VertexBuffer> {
&self.object
}
pub fn get_particle(&'a self) -> &'a Rc<VertexBuffer> {
&self.particle
}
pub fn get_radialbar(&'a self) -> &'a Rc<VertexBuffer> {
&self.radialbar
}
pub fn get_starfield(&'a self) -> &'a Rc<VertexBuffer> {
&self.starfield
}
}
/// Renderer state. A reference to this struct is often passed to helper functions.
pub(crate) struct RenderState {
pub window: Window,
pub window_size: winit::dpi::PhysicalSize<u32>,
pub window_aspect: f32,
pub queue: wgpu::Queue,
pub global_uniform: GlobalUniform,
pub vertex_buffers: VertexBuffers,
pub text_font_system: FontSystem,
pub text_cache: SwashCache,
pub text_atlas: TextAtlas,
pub text_renderer: TextRenderer,
}
impl RenderState {
/// Prepare this state for a new frame
pub fn frame_reset(&mut self) {
self.vertex_buffers.object_counter = 0;
self.vertex_buffers.ui_counter = 0;
self.vertex_buffers.radialbar_counter = 0
}
pub fn push_ui_buffer(&mut self, instance: UiInstance) {
// Enforce buffer limit
if self.vertex_buffers.ui_counter as u64 > UI_SPRITE_INSTANCE_LIMIT {
// TODO: no panic, handle this better.
panic!("UI limit exceeded!")
}
self.queue.write_buffer(
&self.vertex_buffers.ui.instances,
UiInstance::SIZE * self.vertex_buffers.ui_counter,
bytemuck::cast_slice(&[instance]),
);
self.vertex_buffers.ui_counter += 1;
}
pub fn get_ui_counter(&self) -> u32 {
self.vertex_buffers.ui_counter as u32
}
pub fn push_object_buffer(&mut self, instance: ObjectInstance) {
// Enforce buffer limit
if self.vertex_buffers.object_counter as u64 > OBJECT_SPRITE_INSTANCE_LIMIT {
// TODO: no panic, handle this better.
panic!("Object limit exceeded!")
}
self.queue.write_buffer(
&self.vertex_buffers.object.instances,
ObjectInstance::SIZE * self.vertex_buffers.object_counter,
bytemuck::cast_slice(&[instance]),
);
self.vertex_buffers.object_counter += 1;
}
pub fn get_object_counter(&self) -> u32 {
self.vertex_buffers.object_counter as u32
}
pub fn push_radialbar_buffer(&mut self, instance: RadialBarInstance) {
// Enforce buffer limit
if self.vertex_buffers.radialbar_counter as u64 > RADIALBAR_SPRITE_INSTANCE_LIMIT {
// TODO: no panic, handle this better.
panic!("Radialbar sprite limit exceeded!")
}
self.queue.write_buffer(
&self.vertex_buffers.radialbar.instances,
RadialBarInstance::SIZE * self.vertex_buffers.radialbar_counter,
bytemuck::cast_slice(&[instance]),
);
self.vertex_buffers.radialbar_counter += 1;
}
pub fn get_radialbar_counter(&self) -> u32 {
self.vertex_buffers.radialbar_counter as u32
}
pub fn reset_starfield_counter(&mut self) {
self.vertex_buffers.starfield_counter = 0;
}
pub fn push_starfield_buffer(&mut self, instance: StarfieldInstance) {
// Enforce buffer limit
// This should never happen, since starfield generator checks array size
if self.vertex_buffers.starfield_counter as u64 > self.vertex_buffers.starfield_limit {
panic!("Starfield sprite limit exceeded!")
}
self.queue.write_buffer(
&self.vertex_buffers.starfield.instances,
StarfieldInstance::SIZE * self.vertex_buffers.starfield_counter,
bytemuck::cast_slice(&[instance]),
);
self.vertex_buffers.starfield_counter += 1;
}
pub fn get_starfield_counter(&self) -> u32 {
self.vertex_buffers.starfield_counter as u32
}
pub fn push_particle_buffer(&mut self, instance: ParticleInstance) {
self.queue.write_buffer(
&self.vertex_buffers.particle.instances,
ParticleInstance::SIZE * self.vertex_buffers.particle_counter,
bytemuck::cast_slice(&[instance]),
);
self.vertex_buffers.particle_counter += 1;
if self.vertex_buffers.particle_counter == PARTICLE_SPRITE_INSTANCE_LIMIT {
self.vertex_buffers.particle_counter = 0;
}
}
//pub fn get_particle_counter(&self) -> u32 {
// self.vertex_buffers.particle_counter as u32
//}
}

View File

@ -2,10 +2,7 @@ use galactica_content::Content;
use nalgebra::{Point2, Point3, Vector2, Vector3};
use rand::{self, Rng};
use crate::{
datastructs::RenderState,
vertexbuffer::{types::StarfieldInstance, BufferObject},
};
use crate::{vertexbuffer::types::StarfieldInstance, RenderState};
pub(crate) struct StarfieldStar {
/// Star coordinates, in world space.
@ -23,15 +20,11 @@ pub(crate) struct StarfieldStar {
pub(crate) struct Starfield {
stars: Vec<StarfieldStar>,
pub instance_count: u32,
}
impl Starfield {
pub fn new() -> Self {
Self {
stars: Vec::new(),
instance_count: 0,
}
Self { stars: Vec::new() }
}
pub fn regenerate(&mut self, ct: &Content) {
@ -97,24 +90,19 @@ impl Starfield {
}
// Add all tiles to buffer
self.instance_count = 0; // Keep track of buffer index
state.reset_starfield_counter();
for x in (-nw_tile.x)..=nw_tile.x {
for y in (-nw_tile.y)..=nw_tile.y {
let offset = Vector3::new(sz * x as f32, sz * y as f32, 0.0);
for s in &self.stars {
state.queue.write_buffer(
&state.vertex_buffers.starfield.instances,
StarfieldInstance::SIZE * self.instance_count as u64,
bytemuck::cast_slice(&[StarfieldInstance {
position: (s.pos + offset).into(),
size: s.size,
tint: s.tint.into(),
}]),
);
self.instance_count += 1;
// instance_count is guaranteed to stay within buffer limits,
// This shouldn't ever panic.
// instance count is guaranteed to stay within buffer limits,
// this is guaranteed by previous checks.
state.push_starfield_buffer(StarfieldInstance {
position: (s.pos + offset).into(),
size: s.size,
tint: s.tint.into(),
});
}
}
}

View File

@ -1,6 +1,6 @@
use glyphon::{Attrs, Buffer, Color, Family, Metrics, Shaping, TextArea, TextBounds};
use crate::{datastructs::RenderState, RenderInput};
use crate::{RenderInput, RenderState};
pub(super) struct FpsIndicator {
buffer: Buffer,

View File

@ -1,17 +1,21 @@
use galactica_content::Content;
use galactica_system::{data::ShipState, phys::PhysSimShipHandle};
use glyphon::TextArea;
use super::{fpsindicator::FpsIndicator, radar::Radar, status::Status};
use crate::{datastructs::RenderState, RenderInput};
use super::{fpsindicator::FpsIndicator, planet::Planet, radar::Radar, status::Status};
use crate::{RenderInput, RenderState};
pub struct UiManager {
radar: Radar,
status: Status,
fps: FpsIndicator,
planet: Planet,
}
impl UiManager {
pub fn new(state: &mut RenderState) -> Self {
pub fn new(ct: &Content, state: &mut RenderState) -> Self {
Self {
planet: Planet::new(ct, state),
radar: Radar::new(),
status: Status::new(),
fps: FpsIndicator::new(state),
@ -20,12 +24,48 @@ impl UiManager {
/// Draw all ui elements
pub fn draw(&mut self, input: &RenderInput, state: &mut RenderState) {
self.radar.draw(input, state);
self.status.draw(input, state);
let ship_handle = input.player.ship.unwrap();
let ship = input
.systemsim
.get_ship(&PhysSimShipHandle(ship_handle))
.unwrap();
self.fps.update(input, state);
match ship.data.get_state() {
ShipState::Collapsing
| ShipState::Dead
| ShipState::Flying { .. }
| ShipState::Landing { .. }
| ShipState::UnLanding { .. } => {
self.radar.draw(input, state);
self.status.draw(input, state);
}
ShipState::Landed { .. } => {
self.planet.draw(input, state);
}
}
}
pub fn get_textareas(&self) -> [TextArea; 1] {
[self.fps.get_textarea()]
pub fn get_textareas(&self, input: &RenderInput, state: &RenderState) -> Vec<TextArea> {
let mut v = Vec::with_capacity(5);
let ship_handle = input.player.ship.unwrap();
let ship = input
.systemsim
.get_ship(&PhysSimShipHandle(ship_handle))
.unwrap();
match ship.data.get_state() {
ShipState::Collapsing
| ShipState::Dead
| ShipState::Flying { .. }
| ShipState::Landing { .. }
| ShipState::UnLanding { .. } => v.push(self.fps.get_textarea()),
ShipState::Landed { .. } => v.extend(self.planet.get_textarea(input, state)),
};
return v;
}
}

View File

@ -1,6 +1,8 @@
mod fpsindicator;
mod manager;
mod planet;
mod radar;
mod status;
mod util;
pub use manager::UiManager;

View File

@ -0,0 +1,141 @@
use galactica_content::{Content, SystemObject, SystemObjectHandle};
use galactica_system::{data::ShipState, phys::PhysSimShipHandle};
use galactica_util::to_radians;
use glyphon::{cosmic_text::Align, Attrs, Color, Metrics, TextArea, Weight};
use nalgebra::{Point2, Vector2};
use super::util::{SpriteRect, UiImage, UiTextArea};
use crate::{vertexbuffer::types::UiInstance, PositionAnchor, RenderInput, RenderState};
pub(super) struct Planet {
// UI elements
planet_desc: UiTextArea,
planet_name: UiTextArea,
landscape: UiImage,
// Height of whole element,in logical pixels
size: f32,
/// What object we're displaying currently.
/// Whenever this changes, we need to reflow text.
current_object: Option<SystemObjectHandle>,
}
// TODO: no scroll
// TODO: animate in/out
impl Planet {
pub fn new(ct: &Content, state: &mut RenderState) -> Self {
let size = 800.0;
let s = Self {
// height of element in logical pixels
size,
current_object: None,
planet_desc: UiTextArea::new(
state,
ct.get_sprite_handle("ui::planet"),
Point2::new(0.0, 0.0),
size,
SpriteRect {
pos: Point2::new(25.831, 284.883) / 512.0,
dim: Vector2::new(433.140, 97.220) / 512.0,
},
Metrics::new(16.0, 18.0),
Color::rgb(255, 255, 255),
Align::Left,
),
planet_name: UiTextArea::new(
state,
ct.get_sprite_handle("ui::planet"),
Point2::new(0.0, 0.0),
size,
SpriteRect {
pos: Point2::new(165.506, 82.0) / 512.0,
dim: Vector2::new(74.883, 17.0) / 512.0,
},
Metrics::new(19.0, 19.0),
Color::rgb(255, 255, 255),
Align::Center,
),
landscape: UiImage::new(
ct.get_sprite_handle("ui::planet"),
Point2::new(0.0, 0.0),
size,
ct.get_sprite_handle("ui::landscape::test"),
ct.get_sprite_handle("ui::landscapemask"),
SpriteRect {
pos: Point2::new(32.031, 75.587) / 512.0,
dim: Vector2::new(342.811, 171.402) / 512.0,
},
),
};
return s;
}
}
impl Planet {
fn reflow(&mut self, planet: &SystemObject, state: &mut RenderState) {
self.planet_desc.set_text(
state,
&planet.desc,
Attrs::new()
.weight(Weight::NORMAL)
.family(glyphon::Family::SansSerif),
);
self.planet_name.set_text(
state,
&planet.name,
Attrs::new()
.weight(Weight::BOLD)
.family(glyphon::Family::Serif),
);
self.current_object = Some(planet.handle);
}
pub fn draw(&mut self, input: &RenderInput, state: &mut RenderState) {
// Get required data
let ship_handle = input.player.ship.unwrap();
let ship_data = input
.systemsim
.get_ship(&PhysSimShipHandle(ship_handle))
.unwrap();
let planet_handle = match ship_data.data.get_state() {
ShipState::Landed { target } => *target,
_ => unreachable!("tried to draw planet interface while not landed!"),
};
let planet = input.ct.get_system_object(planet_handle);
// Reconfigure for new planet if necessary
if self
.current_object
.map(|x| x != planet_handle)
.unwrap_or(true)
{
self.reflow(planet, state);
}
// Draw elements
self.landscape.push_to_buffer(state);
state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::CC.to_int(),
position: [0.0, 0.0],
angle: to_radians(90.0),
size: self.size,
color: [1.0, 1.0, 1.0, 1.0],
sprite_index: input.ct.get_sprite_handle("ui::planet").get_index(),
mask_index: [0, 0],
});
}
pub fn get_textarea(&self, _input: &RenderInput, state: &RenderState) -> [TextArea; 2] {
[
self.planet_desc.get_textarea(state),
self.planet_name.get_textarea(state),
]
}
}

View File

@ -1,12 +1,8 @@
use galactica_system::data::ShipState;
use galactica_util::{clockwise_angle, constants::UI_SPRITE_INSTANCE_LIMIT, to_radians};
use galactica_util::{clockwise_angle, to_radians};
use nalgebra::{Point2, Rotation2, Vector2};
use crate::{
datastructs::RenderState,
vertexbuffer::{types::UiInstance, BufferObject},
PositionAnchor, RenderInput,
};
use crate::{vertexbuffer::types::UiInstance, PositionAnchor, RenderInput, RenderState};
pub(super) struct Radar {
last_player_position: Point2<f32>,
@ -64,26 +60,16 @@ impl Radar {
let ship_sprite = input.ct.get_sprite_handle("ui::shipblip");
let arrow_sprite = input.ct.get_sprite_handle("ui::centerarrow");
// Enforce buffer limit
if state.vertex_buffers.ui_counter as u64 > UI_SPRITE_INSTANCE_LIMIT {
// TODO: no panic, handle this better.
panic!("UI limit exceeded!")
}
// Push this object's instance
state.queue.write_buffer(
&state.vertex_buffers.ui.instances,
UiInstance::SIZE * state.vertex_buffers.ui_counter,
bytemuck::cast_slice(&[UiInstance {
anchor: PositionAnchor::NwNw.to_int(),
position: [10.0, -10.0],
angle: 0.0,
size: radar_size,
color: [1.0, 1.0, 1.0, 1.0],
sprite_index: input.ct.get_sprite_handle("ui::radar").get_index(),
}]),
);
state.vertex_buffers.ui_counter += 1;
state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::NwNw.to_int(),
position: [10.0, -10.0],
angle: 0.0,
size: radar_size,
color: [1.0, 1.0, 1.0, 1.0],
sprite_index: input.ct.get_sprite_handle("ui::radar").get_index(),
mask_index: [0, 0],
});
// Draw system objects
let system = input.ct.get_system(input.current_system);
@ -101,29 +87,19 @@ impl Radar {
continue;
}
// Enforce buffer limit
if state.vertex_buffers.ui_counter as u64 > UI_SPRITE_INSTANCE_LIMIT {
// TODO: no panic, handle this better.
panic!("UI limit exceeded!")
}
// Push this object's instance
state.queue.write_buffer(
&state.vertex_buffers.ui.instances,
UiInstance::SIZE * state.vertex_buffers.ui_counter,
bytemuck::cast_slice(&[UiInstance {
anchor: PositionAnchor::NwC.to_int(),
position: (Point2::new(radar_size / 2.0 + 10.0, radar_size / -2.0 - 10.0)
+ (d * (radar_size / 2.0)))
.into(),
angle: o.angle,
size,
color: [0.5, 0.5, 0.5, 1.0],
sprite_index: planet_sprite.get_index(),
}]),
);
state.vertex_buffers.ui_counter += 1;
}
state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::NwC.to_int(),
position: (Point2::new(radar_size / 2.0 + 10.0, radar_size / -2.0 - 10.0)
+ (d * (radar_size / 2.0)))
.into(),
angle: o.angle,
size,
color: [0.5, 0.5, 0.5, 1.0],
sprite_index: planet_sprite.get_index(),
mask_index: [0, 0],
})
};
}
// Draw ships
@ -164,32 +140,19 @@ impl Radar {
if size < 2.0 {
continue;
}
let angle = r.rotation().angle();
let position = Point2::new(radar_size / 2.0 + 10.0, radar_size / -2.0 - 10.0)
+ (d * (radar_size / 2.0));
// Enforce buffer limit
// TODO: cleaner solution. don't do this everywhere.
if state.vertex_buffers.ui_counter as u64 > UI_SPRITE_INSTANCE_LIMIT {
// TODO: no panic, handle this better.
panic!("UI limit exceeded!")
}
// Push this object's instance
state.queue.write_buffer(
&state.vertex_buffers.ui.instances,
UiInstance::SIZE * state.vertex_buffers.ui_counter,
bytemuck::cast_slice(&[UiInstance {
anchor: PositionAnchor::NwC.to_int(),
position: position.into(),
angle: -angle, // TODO: consistent angles
size,
color,
sprite_index: ship_sprite.get_index(),
}]),
);
state.vertex_buffers.ui_counter += 1;
state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::NwC.to_int(),
position: position.into(),
angle: r.rotation().angle(),
size,
color,
sprite_index: ship_sprite.get_index(),
mask_index: [0, 0],
});
}
}
@ -205,83 +168,61 @@ impl Radar {
let sprite = input.ct.get_sprite_handle("ui::radarframe");
let size = 7.0f32.min((0.8 - m) * 70.0);
// Enforce buffer limit (this section adds four items)
if state.vertex_buffers.ui_counter as u64 + 4 > UI_SPRITE_INSTANCE_LIMIT {
// TODO: no panic, handle this better.
panic!("UI limit exceeded!")
}
state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::NwNw.to_int(),
position: Point2::new(
(radar_size / 2.0 + 10.0) - d.x,
(radar_size / -2.0 - 10.0) + d.y,
)
.into(),
angle: to_radians(90.0),
size,
color,
sprite_index: sprite.get_index(),
mask_index: [0, 0],
});
state.queue.write_buffer(
&state.vertex_buffers.ui.instances,
UiInstance::SIZE * state.vertex_buffers.ui_counter,
bytemuck::cast_slice(&[UiInstance {
anchor: PositionAnchor::NwNw.to_int(),
position: Point2::new(
(radar_size / 2.0 + 10.0) - d.x,
(radar_size / -2.0 - 10.0) + d.y,
)
.into(),
angle: to_radians(90.0),
size,
color,
sprite_index: sprite.get_index(),
}]),
);
state.vertex_buffers.ui_counter += 1;
state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::NwSw.to_int(),
position: Point2::new(
(radar_size / 2.0 + 10.0) - d.x,
(radar_size / -2.0 - 10.0) - d.y,
)
.into(),
angle: to_radians(180.0),
size,
color,
sprite_index: sprite.get_index(),
mask_index: [0, 0],
});
state.queue.write_buffer(
&state.vertex_buffers.ui.instances,
UiInstance::SIZE * state.vertex_buffers.ui_counter,
bytemuck::cast_slice(&[UiInstance {
anchor: PositionAnchor::NwSw.to_int(),
position: Point2::new(
(radar_size / 2.0 + 10.0) - d.x,
(radar_size / -2.0 - 10.0) - d.y,
)
.into(),
angle: to_radians(180.0),
size,
color,
sprite_index: sprite.get_index(),
}]),
);
state.vertex_buffers.ui_counter += 1;
state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::NwSe.to_int(),
position: Point2::new(
(radar_size / 2.0 + 10.0) + d.x,
(radar_size / -2.0 - 10.0) - d.y,
)
.into(),
angle: to_radians(270.0),
size,
color,
sprite_index: sprite.get_index(),
mask_index: [0, 0],
});
state.queue.write_buffer(
&state.vertex_buffers.ui.instances,
UiInstance::SIZE * state.vertex_buffers.ui_counter,
bytemuck::cast_slice(&[UiInstance {
anchor: PositionAnchor::NwSe.to_int(),
position: Point2::new(
(radar_size / 2.0 + 10.0) + d.x,
(radar_size / -2.0 - 10.0) - d.y,
)
.into(),
angle: to_radians(270.0),
size,
color,
sprite_index: sprite.get_index(),
}]),
);
state.vertex_buffers.ui_counter += 1;
state.queue.write_buffer(
&state.vertex_buffers.ui.instances,
UiInstance::SIZE * state.vertex_buffers.ui_counter,
bytemuck::cast_slice(&[UiInstance {
anchor: PositionAnchor::NwNe.to_int(),
position: Point2::new(
(radar_size / 2.0 + 10.0) + d.x,
(radar_size / -2.0 - 10.0) + d.y,
)
.into(),
angle: to_radians(0.0),
size,
color,
sprite_index: sprite.get_index(),
}]),
);
state.vertex_buffers.ui_counter += 1;
state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::NwNe.to_int(),
position: Point2::new(
(radar_size / 2.0 + 10.0) + d.x,
(radar_size / -2.0 - 10.0) + d.y,
)
.into(),
angle: to_radians(0.0),
size,
color,
sprite_index: sprite.get_index(),
mask_index: [0, 0],
});
}
// Arrow to center of system
@ -292,24 +233,15 @@ impl Radar {
let position = Point2::new(10.0 + (radar_size / 2.0), -10.0 - (radar_size / 2.0))
+ Rotation2::new(angle) * Vector2::new(0.915 * (radar_size / 2.0), 0.0);
if state.vertex_buffers.ui_counter as u64 > UI_SPRITE_INSTANCE_LIMIT {
// TODO: no panic, handle this better.
panic!("UI limit exceeded!")
}
state.queue.write_buffer(
&state.vertex_buffers.ui.instances,
UiInstance::SIZE * state.vertex_buffers.ui_counter,
bytemuck::cast_slice(&[UiInstance {
anchor: PositionAnchor::NwC.to_int(),
position: position.into(),
angle,
size: 10.0,
color: [1.0, 1.0, 1.0, 1f32.min((m - 200.0) / 400.0)],
sprite_index: arrow_sprite.get_index(),
}]),
);
state.vertex_buffers.ui_counter += 1;
state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::NwC.to_int(),
position: position.into(),
angle,
size: 10.0,
color: [1.0, 1.0, 1.0, 1f32.min((m - 200.0) / 400.0)],
sprite_index: arrow_sprite.get_index(),
mask_index: [0, 0],
});
}
}
}

View File

@ -1,14 +1,9 @@
use galactica_system::data::ShipState;
use galactica_util::constants::{RADIALBAR_SPRITE_INSTANCE_LIMIT, UI_SPRITE_INSTANCE_LIMIT};
use std::f32::consts::TAU;
use crate::{
datastructs::RenderState,
vertexbuffer::{
types::{RadialBarInstance, UiInstance},
BufferObject,
},
PositionAnchor, RenderInput,
vertexbuffer::types::{RadialBarInstance, UiInstance},
PositionAnchor, RenderInput, RenderState,
};
pub(super) struct Status {}
@ -20,11 +15,6 @@ impl Status {
impl Status {
pub fn draw(&mut self, input: &RenderInput, state: &mut RenderState) {
if state.vertex_buffers.ui_counter as u64 > UI_SPRITE_INSTANCE_LIMIT {
// TODO: no panic, handle this better.
panic!("UI limit exceeded!")
}
let max_shields;
let current_shields;
let current_hull;
@ -55,52 +45,32 @@ impl Status {
}
}
state.queue.write_buffer(
&state.vertex_buffers.ui.instances,
UiInstance::SIZE * state.vertex_buffers.ui_counter,
bytemuck::cast_slice(&[UiInstance {
anchor: PositionAnchor::NeNe.to_int(),
position: [-10.0, -10.0],
angle: 0.0,
size: 200.0,
color: [1.0, 1.0, 1.0, 1.0],
sprite_index: input.ct.get_sprite_handle("ui::status").get_index(),
}]),
);
state.vertex_buffers.ui_counter += 1;
state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::NeNe.to_int(),
position: [-10.0, -10.0],
angle: 0.0,
size: 200.0,
color: [1.0, 1.0, 1.0, 1.0],
sprite_index: input.ct.get_sprite_handle("ui::status").get_index(),
mask_index: [0, 0],
});
// We add two items here, so +2
if state.vertex_buffers.radialbar_counter as u64 + 2 > RADIALBAR_SPRITE_INSTANCE_LIMIT {
// TODO: no panic, handle this better.
panic!("Radialbar limit exceeded!")
}
state.push_radialbar_buffer(RadialBarInstance {
position: [-19.0, -19.0],
anchor: PositionAnchor::NeNe.to_int(),
diameter: 182.0,
stroke: 5.0,
color: [0.3, 0.6, 0.8, 1.0],
angle: (current_shields / max_shields) * TAU,
});
state.queue.write_buffer(
&state.vertex_buffers.radialbar.instances,
RadialBarInstance::SIZE * state.vertex_buffers.radialbar_counter,
bytemuck::cast_slice(&[RadialBarInstance {
position: [-19.0, -19.0],
anchor: PositionAnchor::NeNe.to_int(),
diameter: 182.0,
stroke: 5.0,
color: [0.3, 0.6, 0.8, 1.0],
angle: (current_shields / max_shields) * TAU,
}]),
);
state.vertex_buffers.radialbar_counter += 1;
state.queue.write_buffer(
&state.vertex_buffers.radialbar.instances,
RadialBarInstance::SIZE * state.vertex_buffers.radialbar_counter,
bytemuck::cast_slice(&[RadialBarInstance {
position: [-27.0, -27.0],
anchor: PositionAnchor::NeNe.to_int(),
diameter: 166.0,
stroke: 5.0,
color: [0.8, 0.7, 0.5, 1.0],
angle: (current_hull / max_hull) * TAU,
}]),
);
state.vertex_buffers.radialbar_counter += 1;
state.push_radialbar_buffer(RadialBarInstance {
position: [-27.0, -27.0],
anchor: PositionAnchor::NeNe.to_int(),
diameter: 166.0,
stroke: 5.0,
color: [0.8, 0.7, 0.5, 1.0],
angle: (current_hull / max_hull) * TAU,
});
}
}

View File

@ -0,0 +1,60 @@
use galactica_content::SpriteHandle;
use galactica_util::to_radians;
use nalgebra::{Point2, Vector2};
use super::SpriteRect;
use crate::{vertexbuffer::types::UiInstance, PositionAnchor, RenderState};
pub struct UiImage {
parent: SpriteHandle,
parent_position: Point2<f32>,
parent_size: f32,
inner: SpriteHandle,
mask: SpriteHandle,
rect: SpriteRect,
}
impl UiImage {
pub fn new(
parent: SpriteHandle,
parent_position: Point2<f32>,
parent_size: f32,
inner: SpriteHandle,
mask: SpriteHandle,
rect: SpriteRect,
) -> Self {
return Self {
parent,
parent_position,
parent_size,
inner,
mask,
rect,
};
}
/// Add this image to the gpu sprite buffer
pub fn push_to_buffer(&self, state: &mut RenderState) {
let h = self.parent_size;
let w = self.parent.aspect * h;
let zero = Point2::new(
self.parent_position.x - (w / 2.0),
self.parent_position.y + (h / 2.0),
);
let pos = zero + Vector2::new(self.rect.pos.x * w, -self.rect.pos.y * h);
let dim = Vector2::new(self.rect.dim.x * w, self.rect.dim.y * h);
state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::CNw.to_int(),
position: pos.into(),
angle: to_radians(90.0),
size: dim.y,
color: [1.0, 1.0, 1.0, 1.0],
sprite_index: self.inner.get_index(),
mask_index: [1, self.mask.get_index()],
});
}
}

View File

@ -0,0 +1,18 @@
mod image;
mod textarea;
pub(super) use image::UiImage;
pub(super) use textarea::UiTextArea;
use nalgebra::{Point2, Vector2};
/// Represents a rectangular region inside a sprite.
pub(crate) struct SpriteRect {
/// The position of the top-left corner of this rectangle, in fractional units.
/// (0.0 is left edge of sprite, 1.0 is right edge)
pub pos: Point2<f32>,
/// The width and height of this rectangle, in fractional units.
/// 1.0 will be as tall as the sprite, 0.5 will be half as tall
pub dim: Vector2<f32>,
}

View File

@ -0,0 +1,106 @@
use galactica_content::SpriteHandle;
use glyphon::{cosmic_text::Align, Attrs, Buffer, Color, Metrics, Shaping, TextArea, TextBounds};
use nalgebra::{Point2, Vector2};
use super::SpriteRect;
use crate::RenderState;
/// Represents a text area inside a sprite.
pub(crate) struct UiTextArea {
/// Parent sprite
sprite: SpriteHandle,
/// Position of parent sprite's center, in logical pixels,
/// with 0, 0 at the center of the screen
sprite_position: Point2<f32>,
/// Height of parent sprite, in logical pixels
sprite_size: f32,
/// Bounds of text area
rect: SpriteRect,
/// Text buffer
buffer: Buffer,
/// Text color
color: Color,
/// Text alignment
align: Align,
}
impl UiTextArea {
pub fn new(
state: &mut RenderState,
sprite: SpriteHandle,
sprite_position: Point2<f32>,
sprite_size: f32,
rect: SpriteRect,
text_metrics: Metrics,
color: Color,
align: Align,
) -> Self {
let mut s = Self {
buffer: Buffer::new(&mut state.text_font_system, text_metrics),
sprite_size: f32::NAN,
sprite,
sprite_position,
rect,
align,
color,
};
s.set_size(state, sprite_size);
return s;
}
pub fn set_size(&mut self, state: &mut RenderState, sprite_size: f32) {
self.sprite_size = sprite_size;
self.buffer.set_size(
&mut state.text_font_system,
(self.rect.dim.x * self.sprite_size) * state.window.scale_factor() as f32,
(self.rect.dim.y * self.sprite_size * self.sprite.aspect)
* state.window.scale_factor() as f32,
);
}
pub fn set_text(&mut self, state: &mut RenderState, text: &str, attrs: Attrs) {
self.buffer
.set_text(&mut state.text_font_system, text, attrs, Shaping::Advanced);
for l in &mut self.buffer.lines {
l.set_align(Some(self.align));
}
self.buffer.shape_until_scroll(&mut state.text_font_system);
}
pub fn get_textarea(&self, state: &RenderState) -> TextArea {
let h = self.sprite_size;
let w = self.sprite.aspect * h;
// Glypon works with physical pixels, so we must convert
let fac = state.window.scale_factor() as f32;
// All the units below are in logical pixels
let zero = Vector2::new(
(state.window_size.width as f32 / (2.0 * fac)) - (w / 2.0) + self.sprite_position.x,
(state.window_size.height as f32 / (2.0 * fac)) - (h / 2.0) - self.sprite_position.y,
);
let corner_ne = zero + Vector2::new(self.rect.pos.x * w, self.rect.pos.y * h);
let corner_sw = corner_ne + Vector2::new(self.rect.dim.x * w, self.rect.dim.y * h);
TextArea {
buffer: &self.buffer,
top: corner_ne.y * fac,
left: corner_ne.x * fac,
scale: 1.0,
bounds: TextBounds {
top: (corner_ne.y * fac) as i32,
bottom: (corner_sw.y * fac) as i32,
left: (corner_ne.x * fac) as i32,
right: (corner_sw.x * fac) as i32,
},
default_color: self.color,
}
}
}

View File

@ -140,6 +140,11 @@ pub struct UiInstance {
/// What texture to use for this sprite
pub sprite_index: u32,
/// What mask to use for this sprite
/// If the first element is not 1, no mask is used.
/// Second element is sprite index of mask.
pub mask_index: [u32; 2],
}
impl BufferObject for UiInstance {
@ -187,6 +192,12 @@ impl BufferObject for UiInstance {
shader_location: 7,
format: wgpu::VertexFormat::Uint32,
},
// Mask
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 10]>() as wgpu::BufferAddress,
shader_location: 8,
format: wgpu::VertexFormat::Uint32x2,
},
],
}
}