Moved `render` into `crates`

master
Mark 2023-12-31 18:39:37 -08:00
parent fa8a0f5761
commit fc728eaf0e
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
26 changed files with 192 additions and 101 deletions

15
Cargo.lock generated
View File

@ -578,10 +578,10 @@ name = "galactica"
version = "0.1.0"
dependencies = [
"anyhow",
"bytemuck",
"cgmath",
"crossbeam",
"galactica-content",
"galactica-render",
"image",
"nalgebra",
"pollster",
@ -605,6 +605,19 @@ dependencies = [
"walkdir",
]
[[package]]
name = "galactica-render"
version = "0.0.0"
dependencies = [
"anyhow",
"bytemuck",
"cgmath",
"galactica-content",
"image",
"wgpu",
"winit",
]
[[package]]
name = "getrandom"
version = "0.2.11"

View File

@ -28,26 +28,27 @@ rpath = false
[workspace]
members = ["crates/content"]
members = ["crates/content", "crates/render"]
[dependencies]
# Internal crates
galactica-content = { path = "crates/content" }
galactica-render = { path = "crates/render" }
# Files
image = { version = "0.24", features = ["png"] }
# Graphics
winit = "0.28"
wgpu = "0.18"
bytemuck = { version = "1.12", features = ["derive"] }
# Physics
rapier2d = { version = "0.17.2" } #, features = ["parallel"] }
rapier2d = { version = "0.17.2" }
nalgebra = "0.32.3"
crossbeam = "0.8.3"
# Misc helpers
pollster = "0.3"
anyhow = "1.0"
# TODO: migrate to nalgebra
cgmath = "0.18.0"
rand = "0.8.5"
walkdir = "2.4.0"

17
crates/render/Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "galactica-render"
version = "0.0.0"
edition = "2021"
[dependencies]
galactica-content = { path = "../content" }
# Misc helpers
anyhow = "1.0"
cgmath = "0.18.0"
# Files
image = { version = "0.24", features = ["png"] }
# Graphics
winit = "0.28"
wgpu = "0.18"
bytemuck = { version = "1.12", features = ["derive"] }

View File

@ -1,4 +1,4 @@
use crate::consts;
use crate::consts_main;
use cgmath::Matrix4;
// We can draw at most this many sprites on the screen.
@ -6,7 +6,7 @@ use cgmath::Matrix4;
pub const SPRITE_INSTANCE_LIMIT: u64 = 500;
// Must be small enough to fit in an i32
pub const STARFIELD_INSTANCE_LIMIT: u64 = consts::STARFIELD_COUNT * 24;
pub const STARFIELD_INSTANCE_LIMIT: u64 = consts_main::STARFIELD_COUNT * 24;
/// Shader entry points
pub const SHADER_MAIN_VERTEX: &'static str = "vertex_main";

View File

@ -0,0 +1,26 @@
pub const ZOOM_MIN: f32 = 200.0;
pub const ZOOM_MAX: f32 = 2000.0;
/// Z-axis range for starfield stars
/// This does not affect scale.
pub const STARFIELD_Z_MIN: f32 = 100.0;
pub const STARFIELD_Z_MAX: f32 = 200.0;
/// Size range for starfield stars, in game units.
/// This is scaled for zoom, but NOT for distance.
pub const STARFIELD_SIZE_MIN: f32 = 0.2;
pub const STARFIELD_SIZE_MAX: f32 = 1.8;
/// Size of a square starfield tile, in game units.
/// A tile of size STARFIELD_Z_MAX * screen-size-in-game-units
/// will completely cover a (square) screen. This depends on zoom!
///
/// Use a value smaller than zoom_max for debug.
pub const STARFIELD_SIZE: u64 = STARFIELD_Z_MAX as u64 * ZOOM_MAX as u64;
/// Average number of stars per game unit
pub const STARFIELD_DENSITY: f64 = 0.01;
/// Number of stars in one starfield tile
/// Must fit inside an i32
pub const STARFIELD_COUNT: u64 = (STARFIELD_SIZE as f64 * STARFIELD_DENSITY) as u64;

View File

@ -12,6 +12,7 @@ use winit::{
use super::{
consts::{OPENGL_TO_WGPU_MATRIX, SPRITE_INSTANCE_LIMIT, STARFIELD_INSTANCE_LIMIT},
consts_main,
globaldata::{GlobalData, GlobalDataContent},
pipeline::PipelineBuilder,
sprite::ObjectSubSprite,
@ -21,9 +22,8 @@ use super::{
types::{SpriteInstance, StarfieldInstance, TexturedVertex},
VertexBuffer,
},
ObjectSprite, UiSprite,
ObjectSprite, StarfieldStar, UiSprite,
};
use crate::{consts, game::Game};
pub struct GPUState {
device: wgpu::Device,
@ -145,7 +145,7 @@ impl GPUState {
let sprite_pipeline = PipelineBuilder::new("sprite", &device)
.set_shader(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/render/shaders/",
"/shaders/",
"sprite.wgsl"
)))
.set_format(config.format)
@ -157,7 +157,7 @@ impl GPUState {
let starfield_pipeline = PipelineBuilder::new("starfield", &device)
.set_shader(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/render/shaders/",
"/shaders/",
"starfield.wgsl"
)))
.set_format(config.format)
@ -190,7 +190,7 @@ impl GPUState {
&self.window
}
pub fn resize(&mut self, game: &Game, new_size: PhysicalSize<u32>) {
pub fn resize(&mut self, starfield: &Vec<StarfieldStar>, new_size: PhysicalSize<u32>) {
if new_size.width > 0 && new_size.height > 0 {
self.window_size = new_size;
self.window_aspect = new_size.width as f32 / new_size.height as f32;
@ -198,18 +198,19 @@ impl GPUState {
self.config.height = new_size.height;
self.surface.configure(&self.device, &self.config);
}
self.update_starfield_buffer(game)
self.update_starfield_buffer(starfield)
}
/// Create a SpriteInstance for an object and add it to `instances`.
/// Also handles child sprites.
fn push_object_sprite(
&self,
game: &Game,
camera_zoom: f32,
camera_pos: Point2<f32>,
instances: &mut Vec<SpriteInstance>,
clip_ne: Point2<f32>,
clip_sw: Point2<f32>,
s: ObjectSprite,
s: &ObjectSprite,
) {
// Position adjusted for parallax
// TODO: adjust parallax for zoom?
@ -217,7 +218,7 @@ impl GPUState {
(Point2 {
x: s.pos.x,
y: s.pos.y,
} - game.camera.pos.to_vec())
} - camera_pos.to_vec())
/ s.pos.z
};
let texture = self.texture_array.get_texture(s.texture);
@ -241,7 +242,7 @@ impl GPUState {
}
// TODO: clean up
let scale = height / game.camera.zoom;
let scale = height / camera_zoom;
// Note that our mesh starts centered at (0, 0).
// This is essential---we do not want scale and rotation
@ -270,8 +271,8 @@ impl GPUState {
// The height of the viewport is `zoom` in game units,
// but it's 2 in screen units! (since coordinates range from -1 to 1)
let translate = Matrix4::from_translation(Vector3 {
x: pos.x / (game.camera.zoom / 2.0) / self.window_aspect,
y: pos.y / (game.camera.zoom / 2.0),
x: pos.x / (camera_zoom / 2.0) / self.window_aspect,
y: pos.y / (camera_zoom / 2.0),
z: 0.0,
});
@ -286,9 +287,9 @@ impl GPUState {
});
// Add children
if let Some(children) = s.children {
if let Some(children) = &s.children {
for c in children {
self.push_object_subsprite(game, instances, c, pos, s.angle);
self.push_object_subsprite(camera_zoom, instances, c, pos, s.angle);
}
}
}
@ -297,29 +298,29 @@ impl GPUState {
/// Only called by `self.push_object_sprite`.
fn push_object_subsprite(
&self,
game: &Game,
camera_zoom: f32,
instances: &mut Vec<SpriteInstance>,
s: ObjectSubSprite,
s: &ObjectSubSprite,
parent_pos: Point2<f32>,
parent_angle: Deg<f32>,
) {
let texture = self.texture_array.get_texture(s.texture);
let scale = s.size / (s.pos.z * game.camera.zoom);
let scale = s.size / (s.pos.z * camera_zoom);
let sprite_aspect_and_scale =
Matrix4::from_nonuniform_scale(texture.aspect * scale, scale, 1.0);
let rotate = Matrix4::from_angle_z(s.angle);
let screen_aspect = Matrix4::from_nonuniform_scale(1.0 / self.window_aspect, 1.0, 1.0);
let ptranslate = Matrix4::from_translation(Vector3 {
x: parent_pos.x / (game.camera.zoom / 2.0) / self.window_aspect,
y: parent_pos.y / (game.camera.zoom / 2.0),
x: parent_pos.x / (camera_zoom / 2.0) / self.window_aspect,
y: parent_pos.y / (camera_zoom / 2.0),
z: 0.0,
});
let protate = Matrix4::from_angle_z(parent_angle);
let translate = Matrix4::from_translation(Vector3 {
x: s.pos.x / (game.camera.zoom / 2.0) / self.window_aspect,
y: s.pos.y / (game.camera.zoom / 2.0),
x: s.pos.x / (camera_zoom / 2.0) / self.window_aspect,
y: s.pos.y / (camera_zoom / 2.0),
z: 0.0,
});
@ -337,7 +338,7 @@ impl GPUState {
}
/// Create a SpriteInstance for a ui sprite and add it to `instances`
fn push_ui_sprite(&self, instances: &mut Vec<SpriteInstance>, s: UiSprite) {
fn push_ui_sprite(&self, instances: &mut Vec<SpriteInstance>, s: &UiSprite) {
let logical_size: LogicalSize<f32> =
self.window_size.to_logical(self.window.scale_factor());
@ -375,19 +376,25 @@ impl GPUState {
/// Will panic if SPRITE_INSTANCE_LIMIT is exceeded.
///
/// This is only called inside self.render()
fn make_sprite_instances(&self, game: &Game) -> Vec<SpriteInstance> {
fn make_sprite_instances(
&self,
camera_zoom: f32,
camera_pos: Point2<f32>,
objects: &Vec<ObjectSprite>,
ui: &Vec<UiSprite>,
) -> Vec<SpriteInstance> {
let mut instances: Vec<SpriteInstance> = Vec::new();
// Game coordinates (relative to camera) of ne and sw corners of screen.
// Used to skip off-screen sprites.
let clip_ne = Point2::from((-self.window_aspect, 1.0)) * game.camera.zoom;
let clip_sw = Point2::from((self.window_aspect, -1.0)) * game.camera.zoom;
let clip_ne = Point2::from((-self.window_aspect, 1.0)) * camera_zoom;
let clip_sw = Point2::from((self.window_aspect, -1.0)) * camera_zoom;
for s in game.get_object_sprites() {
self.push_object_sprite(game, &mut instances, clip_ne, clip_sw, s);
for s in objects {
self.push_object_sprite(camera_zoom, camera_pos, &mut instances, clip_ne, clip_sw, s);
}
for s in game.get_ui_sprites() {
for s in ui {
self.push_ui_sprite(&mut instances, s);
}
@ -404,18 +411,19 @@ impl GPUState {
/// Will panic if STARFIELD_INSTANCE_LIMIT is exceeded.
///
/// Starfield data rarely changes, so this is called only when it's needed.
pub fn update_starfield_buffer(&mut self, game: &Game) {
let sz = consts::STARFIELD_SIZE as f32;
pub fn update_starfield_buffer(&mut self, starfield: &Vec<StarfieldStar>) {
let sz = consts_main::STARFIELD_SIZE as f32;
// Compute window size in starfield tiles
let mut nw_tile: Point2<i32> = {
// Game coordinates (relative to camera) of nw corner of screen.
let clip_nw = Point2::from((self.window_aspect, 1.0)) * consts::ZOOM_MAX;
// TODO: we probably don't need to re-compute this on resize
let clip_nw = Point2::from((self.window_aspect, 1.0)) * consts_main::ZOOM_MAX;
// Parallax correction.
// Also, adjust v for mod to work properly
// (v is centered at 0)
let v: Point2<f32> = clip_nw * consts::STARFIELD_Z_MIN;
let v: Point2<f32> = clip_nw * consts_main::STARFIELD_Z_MIN;
let v_adj: Point2<f32> = (v.x + (sz / 2.0), v.y + (sz / 2.0)).into();
#[rustfmt::skip]
@ -439,7 +447,7 @@ impl GPUState {
// Truncate tile grid to buffer size
// (The window won't be full of stars if our instance limit is too small)
while ((nw_tile.x * 2 + 1) * (nw_tile.y * 2 + 1) * consts::STARFIELD_COUNT as i32)
while ((nw_tile.x * 2 + 1) * (nw_tile.y * 2 + 1) * consts_main::STARFIELD_COUNT as i32)
> STARFIELD_INSTANCE_LIMIT as i32
{
nw_tile -= Vector2::from((1, 1));
@ -454,7 +462,7 @@ impl GPUState {
y: sz * y as f32,
z: 0.0,
};
for s in &game.system.starfield {
for s in starfield {
instances.push(StarfieldInstance {
position: (s.pos + offset).into(),
size: s.size,
@ -477,7 +485,13 @@ impl GPUState {
);
}
pub fn render(&mut self, game: &Game) -> Result<(), wgpu::SurfaceError> {
pub fn render(
&mut self,
camera_pos: Point2<f32>,
camera_zoom: f32,
object_sprites: &Vec<ObjectSprite>,
ui_sprites: &Vec<UiSprite>,
) -> Result<(), wgpu::SurfaceError> {
let output = self.surface.get_current_texture()?;
let view = output
.texture
@ -515,22 +529,26 @@ impl GPUState {
&self.global_data.buffer,
0,
bytemuck::cast_slice(&[GlobalDataContent {
camera_position: game.camera.pos.into(),
camera_zoom: [game.camera.zoom, 0.0],
camera_zoom_limits: [consts::ZOOM_MIN, consts::ZOOM_MAX],
camera_position: camera_pos.into(),
camera_zoom: [camera_zoom, 0.0],
camera_zoom_limits: [consts_main::ZOOM_MIN, consts_main::ZOOM_MAX],
window_size: [
self.window_size.width as f32,
self.window_size.height as f32,
],
window_aspect: [self.window_aspect, 0.0],
starfield_texture: [self.texture_array.get_starfield_texture().index, 0],
starfield_tile_size: [consts::STARFIELD_SIZE as f32, 0.0],
starfield_size_limits: [consts::STARFIELD_SIZE_MIN, consts::STARFIELD_SIZE_MAX],
starfield_tile_size: [consts_main::STARFIELD_SIZE as f32, 0.0],
starfield_size_limits: [
consts_main::STARFIELD_SIZE_MIN,
consts_main::STARFIELD_SIZE_MAX,
],
}]),
);
// Create sprite instances
let sprite_instances = self.make_sprite_instances(game);
let sprite_instances =
self.make_sprite_instances(camera_zoom, camera_pos, object_sprites, ui_sprites);
self.queue.write_buffer(
&self.vertex_buffers.sprite.instances,
0,

28
crates/render/src/lib.rs Normal file
View File

@ -0,0 +1,28 @@
mod consts;
mod globaldata;
mod gpustate;
mod pipeline;
mod sprite;
mod texturearray;
mod vertexbuffer;
// TODO: remove
mod consts_main;
use cgmath::{Point3, Vector2};
pub use gpustate::GPUState;
pub use sprite::{AnchoredUiPosition, ObjectSprite, ObjectSubSprite, UiSprite};
pub struct StarfieldStar {
/// Star coordinates, in world space.
/// These are relative to the center of a starfield tile.
pub pos: Point3<f32>,
/// Height in game units.
/// Will be scaled for zoom, but not for distance.
pub size: f32,
/// Color/brightness variation. Random between 0 and 1.
/// Used in starfield shader.
pub tint: Vector2<f32>,
}

View File

@ -1,15 +1,15 @@
use cgmath::{Deg, InnerSpace, Point2};
use galactica_content as content;
use galactica_content::FactionHandle;
use galactica_render::{AnchoredUiPosition, ObjectSprite, UiSprite};
use std::time::Instant;
use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode};
use super::{camera::Camera, outfits, system::System};
use crate::{
consts,
content::Content,
inputstatus::InputStatus,
physics::{util, Physics, ShipHandle},
render::{AnchoredUiPosition, ObjectSprite, UiSprite},
shipbehavior::{self, ShipBehavior},
};
@ -20,7 +20,12 @@ impl Ui {
Self {}
}
fn build_radar(&self, player: &ShipHandle, physics: &Physics, ct: &Content) -> Vec<UiSprite> {
fn build_radar(
&self,
player: &ShipHandle,
physics: &Physics,
ct: &content::Content,
) -> Vec<UiSprite> {
let mut out = Vec::new();
let radar_range = 2000.0;
@ -76,11 +81,11 @@ pub struct Game {
ui: Ui,
physics: Physics,
shipbehaviors: Vec<Box<dyn ShipBehavior>>,
content: Content,
content: content::Content,
}
impl Game {
pub fn new(ct: Content) -> Self {
pub fn new(ct: content::Content) -> Self {
let mut physics = Physics::new();
let mut o1 = outfits::ShipOutfits::new(&ct.ships[0]);

View File

@ -1,7 +1,6 @@
use cgmath::{Deg, Point3};
use content::{OutfitSpace, TextureHandle};
use crate::{content, render::ObjectSubSprite};
use galactica_content as content;
use galactica_render::ObjectSubSprite;
/// Represents a gun attached to a specific ship at a certain gunpoint.
#[derive(Debug)]
@ -28,7 +27,7 @@ impl ShipGun {
pub struct OutfitStatSum {
pub engine_thrust: f32,
pub steer_power: f32,
pub engine_flare_textures: Vec<TextureHandle>,
pub engine_flare_textures: Vec<content::TextureHandle>,
}
impl OutfitStatSum {
@ -67,9 +66,9 @@ impl OutfitStatSum {
#[derive(Debug)]
pub struct ShipOutfits {
pub stats: OutfitStatSum,
pub total_space: OutfitSpace,
pub total_space: content::OutfitSpace,
available_space: OutfitSpace,
available_space: content::OutfitSpace,
outfits: Vec<content::Outfit>,
guns: Vec<ShipGun>,
enginepoints: Vec<content::EnginePoint>,

View File

@ -1,22 +1,10 @@
use cgmath::{Point3, Vector2};
use galactica_content as content;
use galactica_render::{ObjectSprite, StarfieldStar};
use rand::{self, Rng};
use super::SystemObject;
use crate::{consts, content, render::ObjectSprite};
pub struct StarfieldStar {
/// Star coordinates, in world space.
/// These are relative to the center of a starfield tile.
pub pos: Point3<f32>,
/// Height in game units.
/// Will be scaled for zoom, but not for distance.
pub size: f32,
/// Color/brightness variation. Random between 0 and 1.
/// Used in starfield shader.
pub tint: Vector2<f32>,
}
use crate::consts;
pub struct System {
pub name: String,

View File

@ -1,7 +1,6 @@
use cgmath::{Deg, Point3};
use galactica_content::TextureHandle;
use crate::render::ObjectSprite;
use galactica_render::ObjectSprite;
pub struct SystemObject {
pub sprite_texture: TextureHandle,

View File

@ -3,13 +3,11 @@ mod game;
mod inputstatus;
mod objects;
mod physics;
mod render;
mod shipbehavior;
use std::path::PathBuf;
use anyhow::Result;
use galactica_content as content;
use winit::{
event::{Event, KeyboardInput, WindowEvent},
event_loop::{ControlFlow, EventLoop},
@ -18,7 +16,7 @@ use winit::{
fn main() -> Result<()> {
// TODO: error if missing
let content = content::Content::load_dir(
let content = galactica_content::Content::load_dir(
PathBuf::from(consts::CONTENT_ROOT),
PathBuf::from(consts::TEXTURE_ROOT),
consts::STARFIELD_TEXTURE_NAME.to_owned(),
@ -26,17 +24,24 @@ fn main() -> Result<()> {
let event_loop = EventLoop::new();
let window = WindowBuilder::new().build(&event_loop).unwrap();
let mut gpu = pollster::block_on(render::GPUState::new(window, &content))?;
let mut gpu = pollster::block_on(galactica_render::GPUState::new(window, &content))?;
let mut game = game::Game::new(content);
gpu.update_starfield_buffer(&game);
gpu.update_starfield_buffer(&game.system.starfield);
event_loop.run(move |event, _, control_flow| {
match event {
Event::RedrawRequested(window_id) if window_id == gpu.window().id() => {
match gpu.render(&game) {
match gpu.render(
game.camera.pos,
game.camera.zoom,
&game.get_object_sprites(),
&game.get_ui_sprites(),
) {
Ok(_) => {}
Err(wgpu::SurfaceError::Lost) => gpu.resize(&game, gpu.window_size),
Err(wgpu::SurfaceError::Lost) => {
gpu.resize(&game.system.starfield, gpu.window_size)
}
Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit,
// All other errors (Outdated, Timeout) should be resolved by the next frame
Err(e) => eprintln!("{:?}", e),
@ -72,10 +77,10 @@ fn main() -> Result<()> {
game.process_scroll(delta, phase);
}
WindowEvent::Resized(physical_size) => {
gpu.resize(&game, *physical_size);
gpu.resize(&game.system.starfield, *physical_size);
}
WindowEvent::ScaleFactorChanged { new_inner_size, .. } => {
gpu.resize(&game, **new_inner_size);
gpu.resize(&game.system.starfield, **new_inner_size);
}
_ => {}
},

View File

@ -1,11 +1,12 @@
use cgmath::{Deg, InnerSpace, Point3, Vector2};
use galactica_content::{FactionHandle, TextureHandle};
use galactica_render::ObjectSprite;
use rapier2d::{
dynamics::{RigidBody, RigidBodyBuilder, RigidBodyHandle},
geometry::{ColliderBuilder, ColliderHandle},
};
use crate::{physics::util, render::ObjectSprite};
use crate::physics::util;
pub struct ProjectileBuilder {
pub rigid_body: RigidBodyBuilder,

View File

@ -1,5 +1,7 @@
use cgmath::{Deg, EuclideanSpace, InnerSpace, Matrix2, Rad, Vector2};
use content::{FactionHandle, TextureHandle};
use galactica_content as content;
use galactica_render::ObjectSprite;
use nalgebra::vector;
use rand::Rng;
use rapier2d::{
@ -10,10 +12,8 @@ use rapier2d::{
use super::ProjectileBuilder;
use crate::{
content,
game::outfits,
physics::{util, ShipHandle},
render::ObjectSprite,
};
pub struct ShipTickResult {

View File

@ -1,6 +1,7 @@
use cgmath::Point2;
use content::{Content, FactionHandle};
use crossbeam::channel::Receiver;
use galactica_content as content;
use galactica_render::ObjectSprite;
use nalgebra::vector;
use rapier2d::{
dynamics::{RigidBody, RigidBodyBuilder, RigidBodyHandle},
@ -10,7 +11,7 @@ use rapier2d::{
use std::{collections::HashMap, f32::consts::PI};
use super::{wrapper::Wrapper, ShipHandle};
use crate::{content, game::outfits, objects, render::ObjectSprite};
use crate::{game::outfits, objects};
/// Keeps track of all objects in the world that we can interact with.
/// Also wraps our physics engine
@ -84,7 +85,7 @@ impl Physics {
ct: &content::Ship,
outfits: outfits::ShipOutfits,
position: Point2<f32>,
faction: FactionHandle,
faction: content::FactionHandle,
) -> ShipHandle {
let cl = ColliderBuilder::convex_decomposition(
&ct.collision.points[..],
@ -114,7 +115,7 @@ impl Physics {
return h;
}
pub fn step(&mut self, t: f32, ct: &Content) {
pub fn step(&mut self, t: f32, ct: &content::Content) {
// Run ship updates
let mut res = Vec::new();
let mut to_remove = Vec::new();

View File

@ -1,10 +0,0 @@
mod consts;
mod globaldata;
mod gpustate;
mod pipeline;
mod sprite;
mod texturearray;
mod vertexbuffer;
pub use gpustate::GPUState;
pub use sprite::{AnchoredUiPosition, ObjectSprite, ObjectSubSprite, UiSprite};