diff --git a/Cargo.lock b/Cargo.lock index 93302de..a75679b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,15 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "ab_glyph" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26a685fe66654266f321a8b572660953f4df36a2135706503a4c89981d76e1a2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser 0.8.0", +] + [[package]] name = "ab_glyph_rasterizer" version = "0.1.4" @@ -46,6 +56,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000444226fcff248f2bc4c7625be32c63caccfecc2723a2b9f78a7487a49c407" +[[package]] +name = "approx" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" +dependencies = [ + "num-traits", +] + [[package]] name = "arrayvec" version = "0.5.2" @@ -709,6 +728,45 @@ dependencies = [ "gl_generator", ] +[[package]] +name = "glyph_brush" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afd3e2cfd503a5218dd56172a8bf7c8655a4a7cf745737c606a6edfeea1b343f" +dependencies = [ + "glyph_brush_draw_cache", + "glyph_brush_layout", + "log", + "ordered-float 1.1.0", + "rustc-hash", + "twox-hash", +] + +[[package]] +name = "glyph_brush_draw_cache" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cef969a091be5565c2c10b31fd2f115cbeed9f783a27c96ae240ff8ceee067c" +dependencies = [ + "ab_glyph", + "crossbeam-channel", + "crossbeam-deque", + "linked-hash-map", + "rayon", + "rustc-hash", +] + +[[package]] +name = "glyph_brush_layout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10bc06d530bf20c1902f1b02799ab7372ff43f6119770c49b0bc3f21bd148820" +dependencies = [ + "ab_glyph", + "approx", + "xi-unicode", +] + [[package]] name = "hashbrown" version = "0.7.2" @@ -827,6 +885,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "linked-hash-map" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" + [[package]] name = "lock_api" version = "0.4.2" @@ -1041,6 +1105,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.13.0" @@ -1088,6 +1161,24 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" +[[package]] +name = "ordered-float" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3741934be594d77de1c8461ebcbbe866f585ea616a9753aa78f2bdc69f0e4579" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-float" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fe9037165d7023b1228bc4ae9a2fa1a2b0095eca6c2998c624723dfd01314a5" +dependencies = [ + "num-traits", +] + [[package]] name = "osmesa-sys" version = "0.1.2" @@ -1103,7 +1194,16 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f923fb806c46266c02ab4a5b239735c144bdeda724a50ed058e5226f594cde3" dependencies = [ - "ttf-parser", + "ttf-parser 0.6.2", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb477c7fd2a3a6e04e1dc6ca2e4e9b04f2df702021dc5a5d1cf078c587dc59f7" +dependencies = [ + "ttf-parser 0.8.3", ] [[package]] @@ -1320,6 +1420,12 @@ version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rusttype" version = "0.9.2" @@ -1327,7 +1433,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc7c727aded0be18c5b80c1640eae0ac8e396abf6fa8477d96cb37d18ee5ec59" dependencies = [ "ab_glyph_rasterizer", - "owned_ttf_parser", + "owned_ttf_parser 0.6.0", ] [[package]] @@ -1428,8 +1534,10 @@ dependencies = [ "gl", "glc", "glutin", + "glyph_brush", "log", "num_cpus", + "ordered-float 2.0.0", "rand", "shrev", "specs", @@ -1455,6 +1563,12 @@ dependencies = [ "tuple_utils", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.9.3" @@ -1535,12 +1649,29 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e5d7cd7ab3e47dda6e56542f4bbf3824c15234958c6e1bd6aaa347e93499fdc" +[[package]] +name = "ttf-parser" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7622061403fd00f0820df288e5a580e87d3ce15a1c4313c59fd1ffb77129903f" + [[package]] name = "tuple_utils" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44834418e2c5b16f47bedf35c28e148db099187dd5feee6367fb2525863af4f1" +[[package]] +name = "twox-hash" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f8ab788026715fa63b31960869617cba39117e520eb415b0139543e325ab59" +dependencies = [ + "cfg-if 0.1.10", + "rand", + "static_assertions", +] + [[package]] name = "tynm" version = "0.1.6" @@ -1793,6 +1924,12 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" +[[package]] +name = "xi-unicode" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" + [[package]] name = "xml-rs" version = "0.8.3" diff --git a/glc/src/error.rs b/glc/src/error.rs index 9530c68..3ef101b 100644 --- a/glc/src/error.rs +++ b/glc/src/error.rs @@ -24,9 +24,12 @@ pub enum Error { #[error("Error while linking shader program: {0}")] ShaderLink(String), - #[error("Vertex Array Index is already in use: {0}!")] + #[error("Vertex Array: Index is already in use: {0}!")] VertexArrayIndexAlreadyInUse(gl::GLuint), + #[error("Vertex Array: Expected pointer!")] + VertexArrayExpectedPointer, + #[error("Invalid Parameter!")] InvalidParameter, diff --git a/glc/src/matrix.rs b/glc/src/matrix.rs index 000d9c0..fa8ab8a 100644 --- a/glc/src/matrix.rs +++ b/glc/src/matrix.rs @@ -419,6 +419,54 @@ where } } +impl Matrix4 +where + T: Element, +{ + #[inline] + pub fn ortho(left: T, right: T, bottom: T, top: T, near: T, far: T) -> Self { + let one = T::one(); + let two = one + one; + let zero = T::zero(); + + Matrix4::new( + Vector4::new(two / (right - left), zero, zero, zero), + Vector4::new(zero, two / (top - bottom), zero, zero), + Vector4::new(zero, zero, -zero / (far - near), zero), + Vector4::new( + -(right + left) / (right - left), + -(top + bottom) / (top - bottom), + -(far + near) / (far - near), + one, + ), + ) + } + + #[inline] + pub fn perspective(fov: Angle, ratio: T, near: T, far: T) -> Self { + let one = T::one(); + let two = one + one; + let zero = T::zero(); + + let top = near * fov.into_rad().into_inner().tan(); + let bottom = -top; + let right = ratio * top; + let left = -right; + + Matrix4::new( + Vector4::new(two * near / (right - left), zero, zero, zero), + Vector4::new(zero, two * near / (top - bottom), zero, zero), + Vector4::new( + (right + left) / (right - left), + (top + bottom) / (top - bottom), + -(far + near) / (far - near), + -one, + ), + Vector4::new(zero, zero, -two * far * near / (far - near), zero), + ) + } +} + impl Mul> for Matrix4 where T: Element, diff --git a/glc/src/texture.rs b/glc/src/texture.rs index f8e1050..9d2e63b 100644 --- a/glc/src/texture.rs +++ b/glc/src/texture.rs @@ -38,6 +38,10 @@ impl Texture { }) } + pub fn id(&self) -> gl::GLuint { + self.id + } + pub fn upload(&mut self, data: &Data, generate_mipmap: bool) -> Result<(), Error> { let info = data.format.info(); diff --git a/glc/src/vector.rs b/glc/src/vector.rs index eae4f40..05adab0 100644 --- a/glc/src/vector.rs +++ b/glc/src/vector.rs @@ -494,6 +494,7 @@ pub trait Element: fn sin(self) -> Self; fn cos(self) -> Self; fn sqrt(self) -> Self; + fn tan(self) -> Self::AsFloat; fn acos(self) -> Self::AsFloat; fn atan(self) -> Self::AsFloat; fn atan2(a: Self, b: Self) -> Self::AsFloat; @@ -530,6 +531,11 @@ impl Element for gl::GLfloat { f32::sqrt(self) } + #[inline] + fn tan(self) -> Self::AsFloat { + f32::tan(self) + } + #[inline] fn acos(self) -> Self::AsFloat { f32::acos(self) @@ -589,6 +595,11 @@ impl Element for gl::GLdouble { f64::sqrt(self) } + #[inline] + fn tan(self) -> Self::AsFloat { + f64::tan(self) + } + #[inline] fn acos(self) -> Self::AsFloat { f64::acos(self) @@ -648,6 +659,11 @@ impl Element for gl::GLint { f64::sqrt(self as f64) as i32 } + #[inline] + fn tan(self) -> Self::AsFloat { + f32::tan(self as f32) + } + #[inline] fn acos(self) -> Self::AsFloat { f32::acos(self as f32) diff --git a/glc/src/vertex_array.rs b/glc/src/vertex_array.rs index 55b09e9..5f27108 100644 --- a/glc/src/vertex_array.rs +++ b/glc/src/vertex_array.rs @@ -77,8 +77,9 @@ impl Builder { let guard = BindGuard::new(&binding.buffer); for pointer in binding.pointers { + Error::checked(|| gl::enable_vertex_attrib_array(pointer.index))?; + Error::checked(|| { - gl::enable_vertex_attrib_array(pointer.index); gl::vertex_attrib_pointer( pointer.index, pointer.size, @@ -92,6 +93,10 @@ impl Builder { pointer.offset as *const _, ) })?; + + if let Some(divisor) = pointer.divisor { + Error::checked(|| gl::vertex_attrib_divisor(pointer.index, divisor))?; + } } drop(guard); @@ -142,6 +147,7 @@ impl BindingBuilder { normalize, stride, offset, + divisor: None, }; self.builder @@ -154,6 +160,18 @@ impl BindingBuilder { Ok(self) } + pub fn vertex_attrib_divisor(mut self, divisor: gl::GLuint) -> Result { + let binding = self.builder.bindings.last_mut().unwrap(); + let pointer = binding + .pointers + .last_mut() + .ok_or(Error::VertexArrayExpectedPointer)?; + + pointer.divisor = Some(divisor); + + Ok(self) + } + pub fn build(self) -> Result { self.builder.build() } @@ -214,4 +232,5 @@ struct Pointer { normalize: bool, stride: gl::GLsizei, offset: gl::GLsizei, + divisor: Option, } diff --git a/space-crush/Cargo.toml b/space-crush/Cargo.toml index f0b9f06..b0004b5 100644 --- a/space-crush/Cargo.toml +++ b/space-crush/Cargo.toml @@ -10,8 +10,10 @@ futures = "0.3" gl = { version = "0.1", features = [ "use_log_crate" ] } glc = "0.1" glutin = "0.25" +glyph_brush = "0.7" log = { version = "0.4", features = [ "max_level_trace", "release_max_level_warn" ] } num_cpus = "1.13" +ordered-float = "2.0" rand = "0.7" shrev = "1.1" specs = "0.16" diff --git a/space-crush/resources/fonts/DroidSansMono.ttf b/space-crush/resources/fonts/DroidSansMono.ttf new file mode 100644 index 0000000..a007071 Binary files /dev/null and b/space-crush/resources/fonts/DroidSansMono.ttf differ diff --git a/space-crush/resources/shader/noise.frag b/space-crush/resources/shader/noise.frag index 40d92ab..5e4ea1d 100644 --- a/space-crush/resources/shader/noise.frag +++ b/space-crush/resources/shader/noise.frag @@ -3,7 +3,8 @@ in vec4 gl_FragCoord; layout (std140, binding = 0) uniform Camera { - mat4 projection; + mat4 proj_world; + mat4 proj_ui; mat4 view; } camera; diff --git a/space-crush/resources/shader/quad.vert b/space-crush/resources/shader/quad.vert index ee0caa0..02bd7c9 100644 --- a/space-crush/resources/shader/quad.vert +++ b/space-crush/resources/shader/quad.vert @@ -3,7 +3,8 @@ layout (location = 0) in vec3 position; layout (std140, binding = 0) uniform Camera { - mat4 projection; + mat4 proj_world; + mat4 proj_ui; mat4 view; } camera; @@ -15,5 +16,5 @@ out FragmentData { void main() { data.tex_coords = position.xy + vec2(0.5); - gl_Position = camera.projection * camera.view * model * vec4(position, 1.0); + gl_Position = camera.proj_world * camera.view * model * vec4(position, 1.0); } diff --git a/space-crush/resources/shader/text.frag b/space-crush/resources/shader/text.frag new file mode 100644 index 0000000..b8a952d --- /dev/null +++ b/space-crush/resources/shader/text.frag @@ -0,0 +1,20 @@ +#version 450 core + +in FragmentData { + vec2 tex_coord; + vec4 color; +} data; + +uniform sampler2D tex; + +out vec4 color; + +void main() { + float alpha = texture(tex, data.tex_coord).r; + + if (alpha <= 0.0) { + discard; + } + + color = data.color * vec4(1.0, 1.0, 1.0, alpha); +} diff --git a/space-crush/resources/shader/text.vert b/space-crush/resources/shader/text.vert new file mode 100644 index 0000000..a86a266 --- /dev/null +++ b/space-crush/resources/shader/text.vert @@ -0,0 +1,52 @@ +#version 450 core + +layout (location = 0) in vec2 pos_min; +layout (location = 1) in vec2 pos_max; +layout (location = 2) in vec2 tex_min; +layout (location = 3) in vec2 tex_max; +layout (location = 4) in vec4 color; + +layout (std140, binding = 0) uniform Camera { + mat4 proj_world; + mat4 proj_ui; + mat4 view; +} camera; + +layout (location = 1) uniform mat4 model; + +out FragmentData { + vec2 tex_coord; + vec4 color; +} data; + +void main() { + vec2 position = vec2(0.0); + vec2 tex_coord = vec2(0.0); + + switch (gl_VertexID) { + case 0: + position = vec2(pos_min.x, pos_max.y); + tex_coord = vec2(tex_min.x, tex_max.y); + break; + + case 1: + position = vec2(pos_min.x, pos_min.y); + tex_coord = vec2(tex_min.x, tex_min.y); + break; + + case 2: + position = vec2(pos_max.x, pos_max.y); + tex_coord = vec2(tex_max.x, tex_max.y); + break; + + case 3: + position = vec2(pos_max.x, pos_min.y); + tex_coord = vec2(tex_max.x, tex_min.y); + break; + } + + gl_Position = camera.proj_ui * vec4(position, 0.0, 1.0); + + data.tex_coord = tex_coord; + data.color = color; +} diff --git a/space-crush/src/app/misc/camera.rs b/space-crush/src/app/misc/camera.rs index 8a04745..8a663e2 100644 --- a/space-crush/src/app/misc/camera.rs +++ b/space-crush/src/app/misc/camera.rs @@ -3,8 +3,7 @@ use glc::{ array_buffer::{ArrayBuffer, Target, Usage}, error::Error, - matrix::{Angle, Matrix4f}, - vector::Vector4f, + matrix::Matrix4f, }; pub struct Camera { @@ -17,7 +16,8 @@ impl Camera { buffer.buffer_data( Usage::StaticDraw, &[Data { - projection: Matrix4f::identity(), + proj_world: Matrix4f::identity(), + proj_ui: Matrix4f::identity(), view: Matrix4f::identity(), }], )?; @@ -25,94 +25,15 @@ impl Camera { Ok(Self { buffer }) } - pub fn ortho( - &mut self, - left: f32, - right: f32, - bottom: f32, - top: f32, - near: f32, - far: f32, - ) -> Result<(), Error> { + pub fn resize(&mut self, w: f32, h: f32) -> Result<(), Error> { let mut data = self.buffer.map_mut::(true)?; - data[0].projection = Matrix4f::new( - Vector4f::new(2.0 / (right - left), 0.0, 0.0, 0.0), - Vector4f::new(0.0, 2.0 / (top - bottom), 0.0, 0.0), - Vector4f::new(0.0, 0.0, -2.0 / (far - near), 0.0), - Vector4f::new( - -(right + left) / (right - left), - -(top + bottom) / (top - bottom), - -(far + near) / (far - near), - 1.0, - ), - ); - Ok(()) - } - - pub fn perspective( - &mut self, - fov: Angle, - ratio: f32, - near: f32, - far: f32, - ) -> Result<(), Error> { - let top = near * fov.into_rad().into_inner().tan(); - let bottom = -top; - let right = ratio * top; - let left = -right; - - let mut data = self.buffer.map_mut::(true)?; - data[0].projection = Matrix4f::new( - Vector4f::new(2.0 * near / (right - left), 0.0, 0.0, 0.0), - Vector4f::new(0.0, 2.0 * near / (top - bottom), 0.0, 0.0), - Vector4f::new( - (right + left) / (right - left), - (top + bottom) / (top - bottom), - -(far + near) / (far - near), - -1.0, - ), - Vector4f::new(0.0, 0.0, -2.0 * far * near / (far - near), 0.0), - ); - - Ok(()) - } - - pub fn set_projection(&mut self, m: Matrix4f) -> Result<(), Error> { - let mut data = self.buffer.map_mut::(false)?; - data[0].projection = m; + data[0].proj_world = Matrix4f::ortho(-w / 2.0, w / 2.0, -h / 2.0, h / 2.0, -100.0, 100.0); + data[0].proj_ui = Matrix4f::ortho(0.0, w, h, 0.0, -100.0, 100.0); Ok(()) } - pub fn set_view(&mut self, m: Matrix4f) -> Result<(), Error> { - let mut data = self.buffer.map_mut::(false)?; - data[0].view = m; - - Ok(()) - } - - pub fn update_view(&mut self, m: Matrix4f) -> Result<(), Error> { - let mut data = self.buffer.map_mut::(false)?; - data[0].view = data[0].view * m; - - Ok(()) - } - - pub fn projection(&self) -> Result { - let data = self.buffer.map::()?; - let ret = data[0].view; - - Ok(ret) - } - - pub fn view(&self) -> Result { - let data = self.buffer.map::()?; - let ret = data[0].view; - - Ok(ret) - } - pub fn bind(&self, index: gl::GLuint) -> Result<(), Error> { Error::checked(|| self.buffer.bind_buffer_base(index)) } @@ -120,6 +41,7 @@ impl Camera { #[repr(C, packed)] struct Data { - projection: Matrix4f, + proj_world: Matrix4f, + proj_ui: Matrix4f, view: Matrix4f, } diff --git a/space-crush/src/app/misc/frame_counter.rs b/space-crush/src/app/misc/frame_counter.rs index b5e8ead..61cd229 100644 --- a/space-crush/src/app/misc/frame_counter.rs +++ b/space-crush/src/app/misc/frame_counter.rs @@ -9,9 +9,14 @@ pub struct FrameCounter { count: usize, fps: usize, delta: f32, + updated: bool, } impl FrameCounter { + pub fn updated(&self) -> bool { + self.updated + } + #[inline] pub fn fps(&self) -> usize { self.fps @@ -31,7 +36,7 @@ impl FrameCounter { self.time += self.delta; self.count += 1; - if self.time >= 2.0 { + self.updated = if self.time >= 2.0 { self.time = 0.0; self.fps = 0; self.count = 0; @@ -44,7 +49,9 @@ impl FrameCounter { true } else { false - } + }; + + self.updated } } @@ -56,6 +63,7 @@ impl Default for FrameCounter { count: 0, fps: 0, delta: 0.0, + updated: false, } } } diff --git a/space-crush/src/app/misc/mod.rs b/space-crush/src/app/misc/mod.rs index 273e78d..c821bac 100644 --- a/space-crush/src/app/misc/mod.rs +++ b/space-crush/src/app/misc/mod.rs @@ -2,6 +2,7 @@ pub mod camera; pub mod events; pub mod frame_counter; pub mod geometry; +pub mod text; pub mod vfs; pub mod window; diff --git a/space-crush/src/app/misc/text.rs b/space-crush/src/app/misc/text.rs new file mode 100644 index 0000000..399fb75 --- /dev/null +++ b/space-crush/src/app/misc/text.rs @@ -0,0 +1,643 @@ +#![allow(dead_code)] + +use std::cell::RefCell; +use std::cmp::PartialEq; +use std::collections::HashMap; +use std::fmt::Display; +use std::hash::{Hash, Hasher}; +use std::mem::size_of; +use std::ptr::null; +use std::rc::{Rc, Weak}; + +use glc::{ + array_buffer::{ArrayBuffer, Target, Usage}, + error::Error as GlcError, + misc::BindGuard, + shader::{Program, Type}, + texture::{FilterMag, FilterMin, Target as TextureTarget, Texture, Wrap}, + vector::{Vector2f, Vector4f}, + vertex_array::{DataType, VertexArray}, +}; +use glyph_brush::{ + ab_glyph::{FontArc, FontVec}, + BrushAction, BrushError, FontId, GlyphBrush, GlyphBrushBuilder, GlyphVertex, OwnedSection, + OwnedText, Rectangle, +}; +use log::warn; +use ordered_float::OrderedFloat; + +use crate::Error; + +use super::{load_program, vfs::Vfs}; + +/* TextManager */ + +#[derive(Clone)] +pub struct TextManager(Rc>); + +struct TextManagerInner { + vfs: Vfs, + program: Rc, + fonts: HashMap, +} + +impl TextManager { + pub fn new(vfs: &Vfs) -> Result { + let vfs = vfs.clone(); + let program = load_program( + &vfs, + vec![ + (Type::Vertex, "resources/shader/text.vert"), + (Type::Fragment, "resources/shader/text.frag"), + ], + )?; + let program = Rc::new(program); + + Ok(Self(Rc::new(RefCell::new(TextManagerInner { + vfs, + program, + fonts: HashMap::new(), + })))) + } + + pub fn create_cache(&self) -> Result { + TextCache::new(self) + } + + pub fn font_by_name(&self, name: &str) -> Result { + if let Some(font) = self.0.borrow().fonts.get(name) { + return Ok(font.clone()); + } + + let path = format!("resources/fonts/{}", name); + self.font_by_path(&path) + } + + pub fn font_by_path(&self, path: &str) -> Result { + if let Some(font) = self.0.borrow().fonts.get(path) { + return Ok(font.clone()); + } + + let path = self.0.borrow().vfs.join(path)?; + + let mut data = Vec::new(); + path.open_file()?.read_to_end(&mut data)?; + + let font = FontVec::try_from_vec(data)?; + let font = FontArc::new(font); + + { + let mut this = self.0.borrow_mut(); + this.fonts.insert(path.filename(), font.clone()); + this.fonts.insert(path.as_str().to_owned(), font.clone()); + } + + Ok(font) + } +} + +/* TextCache */ + +#[derive(Clone)] +pub struct TextCache(Rc>); + +pub struct TextCacheInner { + manager: TextManager, + fonts: HashMap, + glyphs: Option>, + texture: Rc>, + texts: HashMap>>, + text_ids: Vec, + next_text_id: usize, +} + +impl TextCache { + fn new(manager: &TextManager) -> Result { + let mut texture = Texture::new(TextureTarget::Texture2D)?; + texture.set_filter(FilterMin::Linear, FilterMag::Linear)?; + texture.set_wrap(Wrap::ClampToEdge, Wrap::ClampToEdge, Wrap::ClampToEdge)?; + + Ok(Self(Rc::new(RefCell::new(TextCacheInner { + manager: manager.clone(), + fonts: HashMap::new(), + glyphs: None, + texture: Rc::new(RefCell::new(texture)), + texts: HashMap::new(), + text_ids: Vec::new(), + next_text_id: 0, + })))) + } + + pub fn new_text(&self) -> TextBuilder { + TextBuilder::new(self) + } + + fn font_by_name(&self, name: &str) -> Result { + let mut inner = self.0.borrow_mut(); + + if let Some(id) = inner.fonts.get(name) { + return Ok(*id); + } + + let font = inner.manager.font_by_name(name)?; + + inner.add_font(font) + } + + fn font_by_path(&self, path: &str) -> Result { + let mut inner = self.0.borrow_mut(); + + if let Some(id) = inner.fonts.get(path) { + return Ok(*id); + } + + let font = inner.manager.font_by_path(path)?; + + inner.add_font(font) + } + + fn next_text_id(&self) -> usize { + let mut inner = self.0.borrow_mut(); + + if let Some(id) = inner.text_ids.pop() { + return id; + } + + let ret = inner.next_text_id; + inner.next_text_id += 1; + + ret + } + + fn add_text(&self, text: Weak>) -> Result<(), Error> { + let text_id = self.next_text_id(); + for section in &mut text.upgrade().unwrap().borrow_mut().sections { + for t in &mut section.text { + t.extra.text_id = text_id; + } + } + + self.0.borrow_mut().texts.insert(text_id, text); + + self.update() + } + + fn update(&self) -> Result<(), Error> { + let mut inner = self.0.borrow_mut(); + let inner = &mut *inner; + + /* remove droped texts */ + let texts = &mut inner.texts; + let text_ids = &mut inner.text_ids; + texts.retain(|id, t| { + if t.strong_count() > 0 { + true + } else { + text_ids.push(*id); + + false + } + }); + + /* add sections to glyph queue */ + let glyphs = inner.glyphs.as_mut().unwrap(); + for text in texts.values_mut() { + for section in text.upgrade().unwrap().borrow_mut().sections.iter_mut() { + glyphs.queue(section.to_borrowed()); + } + } + + /* process glyph queue */ + let action = loop { + let glyphs = inner.glyphs.as_mut().unwrap(); + let texture = inner.texture.borrow(); + let ret = glyphs.process_queued( + move |rect, data| update_texture(&*texture, rect, data), + create_vertex, + ); + + match ret { + Ok(action) => break action, + Err(BrushError::TextureTooSmall { suggested, .. }) => { + let new_size = glyphs.texture_dimensions(); + let new_size = resize_texture(suggested, Some(new_size))?; + glyphs.resize_texture(new_size.0, new_size.1); + } + } + }; + + // upate vertex data + if let BrushAction::Draw(vertices) = action { + let mut map = Vec::>::new(); + map.resize_with(inner.next_text_id, Default::default); + + for vertex in vertices { + map[vertex.text_id].push(vertex.data); + } + + for (text_id, data) in map.into_iter().enumerate() { + if data.is_empty() { + continue; + } + + inner + .texts + .get(&text_id) + .unwrap() + .upgrade() + .unwrap() + .borrow_mut() + .update(data)?; + } + } + + Ok(()) + } +} + +impl TextCacheInner { + fn add_font(&mut self, font: FontArc) -> Result { + let (id, size) = match self.glyphs.as_mut() { + Some(glyphs) => (glyphs.add_font(font), None), + None => { + let glyphs = GlyphBrushBuilder::using_font(font) + .draw_cache_position_tolerance(2.0) + .build(); + let size = glyphs.texture_dimensions(); + + self.glyphs = Some(glyphs); + + (FontId(0), Some(size)) + } + }; + + if let Some(size) = size { + let texture = self.texture.borrow(); + let _guard = BindGuard::new(&*texture); + + let new_size = resize_texture(size, None)?; + + self.glyphs + .as_mut() + .unwrap() + .resize_texture(new_size.0, new_size.1); + } + + Ok(id) + } +} + +fn resize_texture(new_size: (u32, u32), cur_size: Option<(u32, u32)>) -> Result<(u32, u32), Error> { + let mut max_size = 0; + gl::get_integer_v(gl::MAX_TEXTURE_SIZE, &mut max_size); + + let max_size = max_size as u32; + let (w, h) = if let Some(cur_size) = cur_size { + if (new_size.0 > max_size || new_size.1 > max_size) + && (cur_size.0 < max_size || cur_size.1 < max_size) + { + (max_size, max_size) + } else { + new_size + } + } else { + new_size + }; + + GlcError::checked(|| { + gl::tex_image_2d( + gl::TEXTURE_2D, + 0, + gl::RED as _, + w as _, + h as _, + 0, + gl::RED, + gl::UNSIGNED_BYTE, + null(), + ) + })?; + + Ok(new_size) +} + +fn update_texture(texture: &Texture, rect: Rectangle, data: &[u8]) { + let ret = GlcError::checked(|| { + gl::texture_sub_image_2d( + texture.id(), + 0, + rect.min[0] as _, + rect.min[1] as _, + rect.width() as _, + rect.height() as _, + gl::RED, + gl::UNSIGNED_BYTE, + data.as_ptr() as _, + ) + }); + + if let Err(err) = ret { + warn!("Unable to update text texture: {}", err); + } +} + +fn create_vertex(data: GlyphVertex) -> Vertex { + let pos_min = Vector2f::new(data.pixel_coords.min.x, data.pixel_coords.min.y); + let pos_max = Vector2f::new(data.pixel_coords.max.x, data.pixel_coords.max.y); + let tex_min = Vector2f::new(data.tex_coords.min.x, data.tex_coords.min.y); + let tex_max = Vector2f::new(data.tex_coords.max.x, data.tex_coords.max.y); + + Vertex { + text_id: data.extra.text_id, + data: VertexData { + pos_min, + pos_max, + tex_min, + tex_max, + color: data.extra.color, + }, + } +} + +/* TextBuilder */ + +pub struct TextBuilder { + cache: TextCache, + items: Vec, +} + +enum BuilderItem { + Position(f32, f32), + FontName(String), + FontPath(String), + Color(Vector4f), + Text(String), + Scale(f32), +} + +impl TextBuilder { + fn new(cache: &TextCache) -> Self { + Self { + cache: cache.clone(), + items: Vec::new(), + } + } + + pub fn build(self) -> Result { + let mut sections = Vec::>::new(); + + let mut scale = 20.0; + let mut font_id = None; + let mut color = Vector4f::new(0.0, 0.0, 0.0, 1.0); + let mut position = None; + + for item in self.items { + match item { + BuilderItem::Text(text) => { + let font_id = font_id.ok_or(Error::FontNotSet)?; + let text = OwnedText::new(text) + .with_extra(Extra { text_id: 0, color }) + .with_scale(scale) + .with_font_id(font_id); + + match (sections.pop(), position.take()) { + (Some(section), Some(pos)) => { + sections.push(section); + sections.push( + OwnedSection::default() + .with_screen_position(pos) + .add_text(text), + ); + } + (Some(section), None) => { + sections.push(section.add_text(text)); + } + (None, Some(pos)) => sections.push( + OwnedSection::::default() + .with_screen_position(pos) + .add_text(text), + ), + (None, None) => { + sections.push(OwnedSection::::default().add_text(text)) + } + }; + } + BuilderItem::Position(x, y) => position = Some((x, y)), + BuilderItem::Color(c) => color = c, + BuilderItem::Scale(s) => scale = s, + BuilderItem::FontName(name) => font_id = Some(self.cache.font_by_name(&name)?), + BuilderItem::FontPath(path) => font_id = Some(self.cache.font_by_path(&path)?), + } + } + + let text = Text::new(&self.cache, sections)?; + + let weak = Rc::downgrade(&text.inner); + self.cache.add_text(weak)?; + + Ok(text) + } + + pub fn position(mut self, x: f32, y: f32) -> Self { + self.items.push(BuilderItem::Position(x, y)); + + self + } + + pub fn font_name(mut self, name: S) -> Self + where + S: Display, + { + self.items.push(BuilderItem::FontName(name.to_string())); + + self + } + + pub fn font_path(mut self, path: S) -> Self + where + S: Display, + { + self.items.push(BuilderItem::FontPath(path.to_string())); + + self + } + + pub fn color(self, r: f32, g: f32, b: f32, a: f32) -> Self { + self.color_vec(Vector4f::new(r, g, b, a)) + } + + pub fn color_vec(mut self, color: Vector4f) -> Self { + self.items.push(BuilderItem::Color(color)); + + self + } + + pub fn text(mut self, text: S) -> Self + where + S: Display, + { + self.items.push(BuilderItem::Text(text.to_string())); + + self + } + + pub fn scale(mut self, scale: f32) -> Self { + self.items.push(BuilderItem::Scale(scale)); + + self + } +} + +/* Text */ + +pub struct Text { + cache: TextCache, + inner: Rc>, +} + +struct TextInner { + array: VertexArray, + program: Rc, + texture: Rc>, + sections: Vec>, + vertex_count: usize, +} + +#[derive(Default, Debug, Clone)] +struct Extra { + text_id: usize, + color: Vector4f, +} + +#[derive(Default, Clone)] +struct Vertex { + text_id: usize, + data: VertexData, +} + +#[repr(C, packed)] +#[derive(Default, Debug, Clone)] +struct VertexData { + pos_min: Vector2f, + pos_max: Vector2f, + tex_min: Vector2f, + tex_max: Vector2f, + color: Vector4f, +} + +impl Text { + fn new(cache: &TextCache, sections: Vec>) -> Result { + const STRIDE: gl::GLsizei = size_of::() as _; + + const SIZE_VEC2: gl::GLsizei = size_of::() as _; + + const OFFSET_POS_MIN: gl::GLsizei = 0; + const OFFSET_POS_MAX: gl::GLsizei = OFFSET_POS_MIN + SIZE_VEC2; + const OFFSET_TEX_MIN: gl::GLsizei = OFFSET_POS_MAX + SIZE_VEC2; + const OFFSET_TEX_MAX: gl::GLsizei = OFFSET_TEX_MIN + SIZE_VEC2; + const OFFSET_COLOR: gl::GLsizei = OFFSET_TEX_MAX + SIZE_VEC2; + + let buffer = ArrayBuffer::new(Target::ArrayBuffer)?; + let array = VertexArray::builder() + .bind_buffer(buffer) + .vertex_attrib_pointer(0, 2, DataType::Float, false, STRIDE, OFFSET_POS_MIN)? + .vertex_attrib_divisor(1)? + .vertex_attrib_pointer(1, 2, DataType::Float, false, STRIDE, OFFSET_POS_MAX)? + .vertex_attrib_divisor(1)? + .vertex_attrib_pointer(2, 2, DataType::Float, false, STRIDE, OFFSET_TEX_MIN)? + .vertex_attrib_divisor(1)? + .vertex_attrib_pointer(3, 2, DataType::Float, false, STRIDE, OFFSET_TEX_MAX)? + .vertex_attrib_divisor(1)? + .vertex_attrib_pointer(4, 4, DataType::Float, false, STRIDE, OFFSET_COLOR)? + .vertex_attrib_divisor(1)? + .build()?; + + let cache = cache.clone(); + let program = cache.0.borrow().manager.0.borrow().program.clone(); + let texture = cache.0.borrow().texture.clone(); + + let inner = TextInner { + array, + program, + texture, + sections, + vertex_count: 0, + }; + let text = Text { + cache, + inner: Rc::new(RefCell::new(inner)), + }; + + Ok(text) + } + + pub fn render(&self, enable_blending: bool) { + let inner = self.inner.borrow(); + let texture = inner.texture.borrow(); + + if enable_blending { + gl::enable(gl::BLEND); + gl::blend_func(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA); + } + + let _guard = BindGuard::new(&*texture); + let _guard = BindGuard::new(&inner.array); + let _guard = BindGuard::new(&*inner.program); + gl::draw_arrays_instanced(gl::TRIANGLE_STRIP, 0, 4, inner.vertex_count as _); + + if enable_blending { + gl::disable(gl::BLEND); + } + } + + pub fn update(&mut self, mut index: usize, text: S) -> Result<(), Error> + where + S: Display, + { + let mut inner = self.inner.borrow_mut(); + + for section in &mut inner.sections { + if index < section.text.len() { + section.text[index].text = text.to_string(); + + break; + } else { + index -= section.text.len(); + } + } + + drop(inner); + + self.cache.update() + } +} + +impl TextInner { + fn update(&mut self, data: Vec) -> Result<(), Error> { + let buffers = self.array.buffers_mut(); + buffers[0].buffer_data(Usage::StaticDraw, &data)?; + + self.vertex_count = data.len(); + + Ok(()) + } +} + +impl Hash for Extra { + fn hash(&self, state: &mut H) + where + H: Hasher, + { + self.text_id.hash(state); + OrderedFloat::from(self.color[0]).hash(state); + OrderedFloat::from(self.color[1]).hash(state); + OrderedFloat::from(self.color[2]).hash(state); + OrderedFloat::from(self.color[3]).hash(state); + } +} + +impl PartialEq for Extra { + fn eq(&self, other: &Self) -> bool { + self.text_id.eq(&other.text_id) && self.color.eq(&other.color) + } +} diff --git a/space-crush/src/app/misc/vfs.rs b/space-crush/src/app/misc/vfs.rs index c3f25e7..9bec47a 100644 --- a/space-crush/src/app/misc/vfs.rs +++ b/space-crush/src/app/misc/vfs.rs @@ -13,6 +13,7 @@ use vfs_zip::ZipReadOnly as ZipFS; use crate::Error; +#[derive(Clone)] pub struct Vfs(pub VfsPath); impl Vfs { diff --git a/space-crush/src/app/misc/window.rs b/space-crush/src/app/misc/window.rs index 140108c..350c60a 100644 --- a/space-crush/src/app/misc/window.rs +++ b/space-crush/src/app/misc/window.rs @@ -54,6 +54,10 @@ impl Window { let context = unsafe { context.make_current().unwrap() }; gl::load_with(|s| context.get_proc_address(s)); + gl::disable(gl::CULL_FACE); + gl::disable(gl::DEPTH_TEST); + gl::pixel_store_i(gl::UNPACK_ALIGNMENT, 1); + Ok(Self { context }) } diff --git a/space-crush/src/app/mod.rs b/space-crush/src/app/mod.rs index cd687dd..9399a13 100644 --- a/space-crush/src/app/mod.rs +++ b/space-crush/src/app/mod.rs @@ -6,8 +6,8 @@ use specs::{Dispatcher, DispatcherBuilder, World}; use crate::Error; -use misc::{events::Events, geometry::Geometry, vfs::Vfs, window::Window}; -use render::{Init, Test}; +use misc::{events::Events, geometry::Geometry, text::TextManager, vfs::Vfs, window::Window}; +use render::{Debug, Init, Test}; use systems::{State, StateUpdate}; pub struct App<'a, 'b> { @@ -19,16 +19,19 @@ pub struct App<'a, 'b> { impl<'a, 'b> App<'a, 'b> { pub fn new(world: &mut World) -> Result { + let vfs = Vfs::new()?; let events = Events::new(world); let window = Window::new(events.handle())?; + let text_manager = TextManager::new(&vfs)?; - world.insert(Vfs::new()?); + world.insert(vfs); world.insert(Geometry::new()?); let mut dispatcher = DispatcherBuilder::new() .with(StateUpdate::new(world), "state_update", &[]) .with_thread_local(Init::new(world)?) .with_thread_local(Test::new(world)?) + .with_thread_local(Debug::new(text_manager)?) .build(); dispatcher.setup(world); diff --git a/space-crush/src/app/render/debug.rs b/space-crush/src/app/render/debug.rs new file mode 100644 index 0000000..320075c --- /dev/null +++ b/space-crush/src/app/render/debug.rs @@ -0,0 +1,52 @@ +use log::warn; +use specs::{ReadExpect, System}; + +use crate::Error; + +use super::{ + super::misc::text::{Text, TextManager}, + Global, +}; + +/* Debug */ + +pub struct Debug { + text: Text, +} + +impl Debug { + pub fn new(manager: TextManager) -> Result { + let text = manager + .create_cache()? + .new_text() + .scale(12.0) + .font_name("DroidSansMono.ttf") + .color(0.7, 0.7, 0.7, 1.0) + .position(5.0, 5.0) + .text(format!("Space Crush v{}\n", env!("CARGO_PKG_VERSION"))) + .text("\nFPS: ") + .text("-") + .text("\nResolution: ") + .text("1280 x 720") + .build()?; + + Ok(Self { text }) + } +} + +impl<'a> System<'a> for Debug { + type SystemData = ReadExpect<'a, Global>; + + fn run(&mut self, global: Self::SystemData) { + if global.frame_counter.updated() { + let ret = self + .text + .update(2, format!("{}", global.frame_counter.fps())); + if let Err(err) = ret { + warn!("Unable to update debug text: {}", err); + } + } + + self.text.render(true); + } +} diff --git a/space-crush/src/app/render/init.rs b/space-crush/src/app/render/init.rs index 24224e5..367ddc5 100644 --- a/space-crush/src/app/render/init.rs +++ b/space-crush/src/app/render/init.rs @@ -2,7 +2,7 @@ use glc::{ misc::Bindable, shader::{Program, Type}, }; -use log::{error, info}; +use log::error; use shrev::{EventChannel, ReaderId}; use specs::{ReadExpect, System, World, WriteExpect}; @@ -75,20 +75,14 @@ impl<'a> System<'a> for Init { let w = *w as f32; let h = *h as f32; - if let Err(err) = - global - .camera - .ortho(-w / 2.0, w / 2.0, -h / 2.0, h / 2.0, -100.0, 100.0) - { + if let Err(err) = global.camera.resize(w, h) { error!("Error while updating camera: {}", err); panic!("Error while updating camera: {}", err); } } } - if global.frame_counter.next() { - info!("FPS: {}", global.frame_counter.fps()); - } + global.frame_counter.next(); self.program.bind(); geometry.render_quad(); diff --git a/space-crush/src/app/render/mod.rs b/space-crush/src/app/render/mod.rs index bdbacf3..85687da 100644 --- a/space-crush/src/app/render/mod.rs +++ b/space-crush/src/app/render/mod.rs @@ -1,5 +1,7 @@ +mod debug; mod init; mod test; +pub use debug::Debug; pub use init::{Global, Init}; pub use test::Test; diff --git a/space-crush/src/error.rs b/space-crush/src/error.rs index 18bce4d..c092076 100644 --- a/space-crush/src/error.rs +++ b/space-crush/src/error.rs @@ -2,6 +2,7 @@ use std::io::Error as IoError; use glc::error::Error as GlcError; use glutin::{ContextError as GlutinContextError, CreationError as GlutinCreationError}; +use glyph_brush::ab_glyph::InvalidFont; use thiserror::Error; use vfs::VfsError; use vfs_zip::Error as VfsZipError; @@ -26,11 +27,17 @@ pub enum Error { #[error("glutin Creation Error: {0}")] GlutinCreationError(GlutinCreationError), - #[error("Unable to create OpenGL context")] + #[error("Invalid Font: {0}")] + InvalidFont(InvalidFont), + + #[error("Unable to create OpenGL context!")] CreateContext, - #[error("Unable to initialize VFS")] + #[error("Unable to initialize VFS!")] InitVFS, + + #[error("Font is not set!")] + FontNotSet, } impl From for Error { @@ -68,3 +75,9 @@ impl From for Error { Self::GlutinCreationError(err) } } + +impl From for Error { + fn from(err: InvalidFont) -> Self { + Self::InvalidFont(err) + } +}