diff options
| author | LLLL Colonq <llll@colonq> | 2025-01-15 21:21:22 -0500 |
|---|---|---|
| committer | LLLL Colonq <llll@colonq> | 2025-01-15 21:21:22 -0500 |
| commit | 82d1c94d999654bda5d40108393eade80b77342c (patch) | |
| tree | 2a05c0ffe34ba47933ca39ebef3c3609fb6a5d2a | |
| parent | f1b47ccb8a92280df51bb28b495829f8f7f8127e (diff) | |
Update
| -rw-r--r-- | Cargo.lock | 29 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | index.html | 2 | ||||
| -rw-r--r-- | src/assets/fonts/ComicNeue-Regular.ttf | bin | 0 -> 54848 bytes | |||
| -rw-r--r-- | src/assets/shaders/bitmap/frag.glsl (renamed from src/assets/shaders/text/frag.glsl) | 0 | ||||
| -rw-r--r-- | src/assets/shaders/bitmap/vert.glsl (renamed from src/assets/shaders/text/vert.glsl) | 0 | ||||
| -rw-r--r-- | src/assets/shaders/truetype/frag.glsl | 23 | ||||
| -rw-r--r-- | src/assets/shaders/truetype/vert.glsl | 26 | ||||
| -rw-r--r-- | src/font.rs | 137 | ||||
| -rw-r--r-- | src/lib.rs | 27 | ||||
| -rw-r--r-- | src/scene.rs | 79 | ||||
| -rw-r--r-- | src/texture.rs | 14 |
12 files changed, 321 insertions, 17 deletions
@@ -56,6 +56,12 @@ dependencies = [ ] [[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] name = "alsa" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -720,6 +726,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] +name = "fontdue" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efe23d02309319171d00d794c9ff48d4f903c0e481375b1b04b017470838af04" +dependencies = [ + "hashbrown", + "ttf-parser 0.21.1", +] + +[[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -942,6 +958,10 @@ name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", +] [[package]] name = "hermit-abi" @@ -1570,7 +1590,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4586edfe4c648c71797a74c84bacb32b52b212eff5dfe2bb9f2c599844023e7" dependencies = [ - "ttf-parser", + "ttf-parser 0.20.0", ] [[package]] @@ -2342,6 +2362,7 @@ dependencies = [ "console_log", "enum-map", "env_logger", + "fontdue", "getrandom", "glam", "glow", @@ -2612,6 +2633,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" [[package]] +name = "ttf-parser" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" + +[[package]] name = "unicode-bidi" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -20,6 +20,7 @@ tobj = "*" # loader for .obj meshes loader gltf = {path = "deps/gltf", features = ["extras", "names", "utils"]} # loader for .gltf scenes # gltf = {version = "*", features = ["extras", "names"]} # model loader image = "*" # texture loader +fontdue = "*" # truetype fonts glam = "*" # linear algebra log = "*" # logging rand = {version = "*", features = ["small_rng"]} # rng @@ -18,7 +18,7 @@ <script> addEventListener("TrunkApplicationStarted", async (event) => { console.log("initialized, starting..."); - window.wasmBindings.main_js(); + window.wasmBindings.main_js_test(); }); </script> <div id="teleia-parent"></canvas> diff --git a/src/assets/fonts/ComicNeue-Regular.ttf b/src/assets/fonts/ComicNeue-Regular.ttf Binary files differnew file mode 100644 index 0000000..d454f46 --- /dev/null +++ b/src/assets/fonts/ComicNeue-Regular.ttf diff --git a/src/assets/shaders/text/frag.glsl b/src/assets/shaders/bitmap/frag.glsl index 94d27c9..94d27c9 100644 --- a/src/assets/shaders/text/frag.glsl +++ b/src/assets/shaders/bitmap/frag.glsl diff --git a/src/assets/shaders/text/vert.glsl b/src/assets/shaders/bitmap/vert.glsl index 4005d75..4005d75 100644 --- a/src/assets/shaders/text/vert.glsl +++ b/src/assets/shaders/bitmap/vert.glsl diff --git a/src/assets/shaders/truetype/frag.glsl b/src/assets/shaders/truetype/frag.glsl new file mode 100644 index 0000000..b0e25bf --- /dev/null +++ b/src/assets/shaders/truetype/frag.glsl @@ -0,0 +1,23 @@ +#version 300 es +precision highp float; + +uniform sampler2D texture_data; +uniform int text[256]; +uniform int atlas_width; +uniform int cell_width; +uniform int text_width; + +in vec2 vertex_texcoord; +out vec4 frag_color; + +void main() +{ + vec2 inverted_texcoord = vec2(vertex_texcoord.x, 1.0 - vertex_texcoord.y); + float texcoord_pixels_x = inverted_texcoord.x * float(text_width); + int char_idx = int(floor(texcoord_pixels_x)) / cell_width; + int offset = text[char_idx]; + float cbase = float(offset); + float coff = mod(texcoord_pixels_x, float(cell_width)); + float val = texture(texture_data, vec2((cbase + coff) / float(atlas_width), inverted_texcoord.y)).r; + frag_color = vec4(val, val, val, 1.0); +} diff --git a/src/assets/shaders/truetype/vert.glsl b/src/assets/shaders/truetype/vert.glsl new file mode 100644 index 0000000..4005d75 --- /dev/null +++ b/src/assets/shaders/truetype/vert.glsl @@ -0,0 +1,26 @@ +#version 300 es +precision highp float; + +uniform mat4 view; +uniform mat4 position; + +out vec2 vertex_texcoord; + +void main() { + const vec2 positions[4] = vec2[]( + vec2(-1, -1), + vec2(+1, -1), + vec2(-1, +1), + vec2(+1, +1) + ); + const vec2 coords[4] = vec2[]( + vec2(0, 0), + vec2(1, 0), + vec2(0, 1), + vec2(1, 1) + ); + vec4 vertex = vec4(positions[gl_VertexID], 0.0, 1.0); + + vertex_texcoord = coords[gl_VertexID]; + gl_Position = view * position * vertex; +} diff --git a/src/font.rs b/src/font.rs index 393b1f5..fb8f133 100644 --- a/src/font.rs +++ b/src/font.rs @@ -1,21 +1,24 @@ +use std::collections::HashMap; + use crate::{context, texture, shader}; +use glow::HasContext; pub const CHAR_WIDTH: i32 = 7; pub const CHAR_HEIGHT: i32 = 9; pub const FONT_WIDTH: i32 = 112; pub const FONT_HEIGHT: i32 = 54; -pub struct Font { +pub struct Bitmap { pub shader: shader::Shader, pub font: texture::Texture, } -impl Font { +impl Bitmap { pub fn new(ctx: &context::Context) -> Self { let shader = shader::Shader::new_nolib( &ctx, - include_str!("assets/shaders/text/vert.glsl"), - include_str!("assets/shaders/text/frag.glsl"), + include_str!("assets/shaders/bitmap/vert.glsl"), + include_str!("assets/shaders/bitmap/frag.glsl"), ); let font = texture::Texture::new(ctx, include_bytes!("assets/fonts/simple.png")); Self { @@ -85,3 +88,129 @@ impl Font { self.render_text_helper(ctx, pos, text, &glam::Vec3::new(1.0, 1.0, 1.0)); } } + +pub struct AtlasInfo { + pub pos: usize, +} + +pub struct TrueType { + pub shader: shader::Shader, + pub font: fontdue::Font, + pub atlas: texture::Texture, + pub atlaswidth: usize, + pub cellwidth: usize, + pub cellheight: usize, + pub info: HashMap<char, AtlasInfo>, +} + +impl TrueType { + pub fn new(ctx: &context::Context) -> Self { + let shader = shader::Shader::new_nolib( + &ctx, + include_str!("assets/shaders/truetype/vert.glsl"), + include_str!("assets/shaders/truetype/frag.glsl"), + ); + let size = 20.0; + let font = fontdue::Font::from_bytes( + include_bytes!("assets/fonts/ComicNeue-Regular.ttf") as &[u8], + fontdue::FontSettings::default(), + ).expect("failed to load font"); + let mut chardata = HashMap::new(); + for ci in 0..128 { + if let Some(c) = char::from_u32(ci) { + if !c.is_ascii_graphic() { continue; } + let res = font.rasterize(c, size); + chardata.insert(c, res); + } + } + let mut cellwidth = 0; + let mut cellbase = 0; + let mut cellextra = 0; + for (_, (m, _)) in &chardata { + if m.width > cellwidth { cellwidth = m.width } + if m.height > cellbase { cellbase = m.height } + let extra = (-m.ymin.min(0)) as usize; + if extra > cellextra { cellextra = extra } + } + let mut cellheight = cellbase + cellextra; + cellwidth = cellwidth.next_power_of_two(); + cellheight = cellheight.next_power_of_two(); + let atlaswidth = (chardata.len() * cellwidth).next_power_of_two(); + let mut info = HashMap::new(); + let mut atlas_bmp: Vec<u8> = vec![0; atlaswidth * cellheight]; + for (i, (c, (m, bmp))) in chardata.iter().enumerate() { + let by = ((cellbase as i32) - (m.height as i32) - m.ymin) as usize; + let bx = cellwidth * i; + info.insert(*c, AtlasInfo { + pos: cellwidth * i, + }); + for x in 0..m.width { + for y in 0..m.height { + atlas_bmp[bx + x + (by + y) * atlaswidth] = bmp[x + y * m.width]; + } + } + } + let atlas = texture::Texture::new_empty(ctx); + unsafe { + ctx.gl.bind_texture(glow::TEXTURE_2D, Some(atlas.tex)); + ctx.gl.pixel_store_i32(glow::UNPACK_ALIGNMENT, 1); + ctx.gl.tex_image_2d( + glow::TEXTURE_2D, + 0, + glow::R8 as i32, + atlaswidth as i32, + cellheight as i32, + 0, + glow::RED, + glow::UNSIGNED_BYTE, + Some(&atlas_bmp), + ); + ctx.gl.generate_mipmap(glow::TEXTURE_2D); + } + Self { shader, font, atlas, atlaswidth, cellwidth, cellheight, info, } + } + + pub fn render_text(&self, ctx: &context::Context, pos: &glam::Vec2, text: &str) { + self.shader.bind(ctx); + unsafe { + ctx.gl.active_texture(glow::TEXTURE0); + ctx.gl.bind_texture(glow::TEXTURE_2D, Some(self.atlas.tex)); + } + self.shader.set_mat4( + ctx, "view", + &glam::Mat4::from_scale( + glam::Vec3::new( + 2.0 / context::RENDER_WIDTH, + 2.0 / context::RENDER_HEIGHT, + 1.0, + ), + ), + ); + let width = text.len() * self.cellwidth; + self.shader.set_mat4( + ctx, "position", + &glam::Mat4::from_scale_rotation_translation( + glam::Vec3::new(width as f32 / 2.0, self.cellheight as f32 / 2.0, 1.0), + glam::Quat::IDENTITY, + glam::Vec3::new( + -context::RENDER_WIDTH / 2.0 + pos.x + width as f32 / 2.0, + context::RENDER_HEIGHT / 2.0 - pos.y - self.cellheight as f32 / 2.0, + 0.0, + ), + ) + ); + let len = text.len().min(256); + let textvals: Vec<i32> = text.chars().take(len).map(|c| { + if let Some(i) = self.info.get(&c) { + i.pos as i32 + } else { + 0 + } + }).collect(); + self.shader.set_i32_array(ctx, "text[0]", &textvals); + self.shader.set_i32(ctx, "atlas_width", self.atlaswidth as i32); + self.shader.set_i32(ctx, "cell_width", self.cellwidth as i32); + self.shader.set_i32(ctx, "text_width", width as i32); + ctx.render_no_geometry(); + } +} @@ -11,6 +11,8 @@ pub mod font; pub mod shadow; pub mod audio; pub mod net; +use std::ops::Rem; + #[cfg(target_arch = "wasm32")] use winit::platform::web::EventLoopExtWebSys; @@ -272,7 +274,8 @@ use wasm_bindgen::prelude::*; #[cfg(target_arch = "wasm32")] struct TestGame { - font: font::Font, + font: font::Bitmap, + tt: font::TrueType, cube: mesh::Mesh, fox: scene::Scene, tex: texture::Texture, @@ -281,9 +284,9 @@ struct TestGame { impl TestGame { pub async fn new(ctx: &context::Context) -> Self { Self { - font: font::Font::new(ctx), + font: font::Bitmap::new(ctx), + tt: font::TrueType::new(ctx), cube: mesh::Mesh::from_obj(ctx, include_bytes!("assets/meshes/cube.obj")), - // fox: scene::Scene::from_gltf(ctx, include_bytes!("/home/llll/src/colonq/assets/lcolonq_flat.vrm")), fox: scene::Scene::from_gltf(ctx, include_bytes!("assets/scenes/fox.glb")), tex: texture::Texture::new(ctx, include_bytes!("assets/textures/test.png")), shader: shader::Shader::new(ctx, include_str!("assets/shaders/scene/vert.glsl"), include_str!("assets/shaders/scene/frag.glsl")), @@ -294,34 +297,36 @@ impl state::Game for TestGame { fn update(&mut self, ctx: &context::Context, st: &mut state::State) -> Option<()> { st.move_camera( ctx, - &glam::Vec3::new(0.0, 0.0, -10.0), + &glam::Vec3::new(0.0, 0.0, -1.0), &glam::Vec3::new(0.0, 0.0, 1.0), &glam::Vec3::new(0.0, 1.0, 0.0), ); Some(()) } fn render(&mut self, ctx: &context::Context, st: &mut state::State) -> Option<()> { - if let Some(n) = self.fox.nodes_by_name.get("b_Neck_04").and_then(|i| self.fox.nodes.get_mut(*i)) { - n.transform *= glam::Mat4::from_rotation_y(0.05); - } + // if let Some(n) = self.fox.nodes_by_name.get("J_Bip_C_Neck").and_then(|i| self.fox.nodes.get_mut(*i)) { + // n.transform *= glam::Mat4::from_rotation_z(0.05); + // } + self.fox.reflect_animation("Run", (st.tick as f32 / 60.0).rem(3.0)); st.bind_3d(ctx, &self.shader); self.shader.set_position_3d( ctx, &glam::Mat4::from_scale_rotation_translation( - glam::Vec3::new(0.1, 0.1, 0.1), + glam::Vec3::new(0.005, 0.005, 0.005), glam::Quat::from_rotation_y(st.tick as f32 / 60.0), - glam::Vec3::new(0.0, -5.0, 0.0), + glam::Vec3::new(0.0, -0.2, 0.0), ), ); self.tex.bind(ctx); self.fox.render(ctx, &self.shader); - self.font.render_text(ctx, &glam::Vec2::new(0.0, 0.0), "he's all fucked up"); + self.font.render_text(ctx, &glam::Vec2::new(0.0, 0.0), "he's all FIXED up"); + self.tt.render_text(ctx, &glam::Vec2::new(10.0, 10.0), "tESTge"); Some(()) } } #[cfg(target_arch = "wasm32")] #[wasm_bindgen] -pub async fn main_js() { +pub async fn main_js_test() { run(TestGame::new).await; } diff --git a/src/scene.rs b/src/scene.rs index 8c71a12..9e98936 100644 --- a/src/scene.rs +++ b/src/scene.rs @@ -37,6 +37,29 @@ pub struct Skin { pub joints: Vec<Index>, } +pub enum ChannelValues { + Translation(Vec<glam::Vec3>), + Rotation(Vec<glam::Quat>), + Scale(Vec<glam::Vec3>), +} + +pub enum Interpolation { + Linear, + Step, + CubicSpline, +} + +pub struct Channel { + pub target: Index, + pub interpolation: Interpolation, + pub keyframes: Vec<f32>, + pub values: ChannelValues, +} + +pub struct Animation { + pub channels: Vec<Channel>, +} + pub struct Node { pub children: Vec<Index>, pub object: Option<Index>, @@ -49,6 +72,7 @@ pub struct Scene { pub textures: Vec<texture::Texture>, pub materials: Vec<Material>, pub skins: Vec<Skin>, + pub animations: HashMap<String, Animation>, pub nodes: Vec<Node>, pub nodes_by_name: HashMap<String, Index>, pub scene_nodes: Vec<Index>, @@ -232,6 +256,31 @@ impl Scene { } }).collect(); + let animations = HashMap::from_iter(gltf.animations().filter_map(|a| { + let channels = a.channels().map(|c| { + let read = c.reader(get_buffer_data); + Channel { + target: c.target().node().index(), + interpolation: match c.sampler().interpolation() { + gltf::animation::Interpolation::Linear => Interpolation::Linear, + gltf::animation::Interpolation::Step => Interpolation::Step, + gltf::animation::Interpolation::CubicSpline => Interpolation::CubicSpline, + }, + keyframes: read.read_inputs().expect("channel has no inputs").collect(), + values: match read.read_outputs().expect("channel has no outputs") { + gltf::animation::util::ReadOutputs::Translations(ts) => + ChannelValues::Translation(ts.map(glam::Vec3::from_array).collect()), + gltf::animation::util::ReadOutputs::Rotations(ts) => + ChannelValues::Rotation(ts.into_f32().map(glam::Quat::from_array).collect()), + gltf::animation::util::ReadOutputs::Scales(ts) => + ChannelValues::Scale(ts.map(glam::Vec3::from_array).collect()), + _ => panic!("unsupport channel outputs"), + }, + } + }).collect(); + a.name().map(|nm| (nm.to_owned(), Animation { channels })) + })); + let nodes = gltf.nodes().map(|n| { Node { children: n.children().map(|c| c.index()).collect(), @@ -255,6 +304,7 @@ impl Scene { textures, materials, skins, + animations, nodes, nodes_by_name, scene_nodes, @@ -309,4 +359,33 @@ impl Scene { } } } + + pub fn reflect_animation(&mut self, nm: &str, time: f32) { + if let Some(anim) = self.animations.get(nm) { + for c in &anim.channels { + let (previ, nexti) = if let Some(nexti) = c.keyframes.iter().position(|x| *x > time) { + let previ = if nexti > 0 { nexti - 1 } else { c.keyframes.len() - 1 }; + (previ, nexti) + } else { + (c.keyframes.len() - 1, 0) + }; + match &c.values { + ChannelValues::Rotation(vs) => { + let prevt = c.keyframes[previ]; + let nextt = c.keyframes[nexti]; + let prev = vs[previ]; + let next = vs[nexti]; + let new = prev.slerp(next, (time - prevt) / (nextt - prevt)); + let (scale, _, trans) = self.nodes[c.target].transform.to_scale_rotation_translation(); + self.nodes[c.target].transform = glam::Mat4::from_scale_rotation_translation( + scale, + new, + trans, + ); + }, + _ => {}, + } + } + } + } } diff --git a/src/texture.rs b/src/texture.rs index 2fd9aab..68272cb 100644 --- a/src/texture.rs +++ b/src/texture.rs @@ -8,6 +8,20 @@ pub struct Texture { } impl Texture { + pub fn new_empty(ctx: &context::Context) -> Self { + unsafe { + let tex = ctx.gl.create_texture().expect("failed to create texture"); + ctx.gl.bind_texture(glow::TEXTURE_2D, Some(tex)); + ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32); + ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32); + ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as i32); + ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as i32); + Self { + tex, + } + } + } + pub fn new(ctx: &context::Context, bytes: &[u8]) -> Self { let rgba = image::io::Reader::new(std::io::Cursor::new(bytes)) .with_guessed_format() |
