From e3835c956011c217e498b272490005dca457d402 Mon Sep 17 00:00:00 2001 From: Tengu712 Date: Sun, 27 Sep 2026 03:51:41 +0900 Subject: render font --- src/graphics/font.rs | 238 ++++++++++++++++++++++++++++++++++++++++++++++++ src/graphics/texture.rs | 10 +- 2 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 src/graphics/font.rs (limited to 'src/graphics') diff --git a/src/graphics/font.rs b/src/graphics/font.rs new file mode 100644 index 0000000..d05682d --- /dev/null +++ b/src/graphics/font.rs @@ -0,0 +1,238 @@ +use super::texture::TextureWrapper; +use ab_glyph::*; +use etagere::{AllocId, AtlasAllocator}; +use glam::Vec4; +use lru::LruCache; +use std::sync::LazyLock; +use wgpu::*; + +static FONT: LazyLock> = LazyLock::new(|| { + FontRef::try_from_slice(include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/KazukiReiwa-Regular.ttf" + ))) + .expect("fatal: failed to load font") +}); + +/// スケールされたフォントのトップ-ベースライン間を取得するメソッド +pub fn ascent(scale: u32) -> f32 { + FONT.as_scaled(scale as f32).ascent() +} + +/// スケールされたフォントの行間を取得するメソッド +pub fn line_gap(scale: u32) -> f32 { + FONT.as_scaled(scale as f32).line_gap() +} + +/// スケールされたフォントのカーニング量を取得するメソッド +pub fn kern(scale: u32, first: char, second: char) -> f32 { + let first = FONT.glyph_id(first); + let second = FONT.glyph_id(second); + FONT.as_scaled(scale as f32).kern(first, second) +} + +const WIDTH: u32 = 128; +const HEIGHT: u32 = WIDTH; + +// NOTE: macOSではswizzleをゼロコストで使えないのでRGBAにする。 +const FORMAT: TextureFormat = if cfg!(target_os = "macos") { + TextureFormat::Rgba8Unorm +} else { + TextureFormat::R8Unorm +}; + +/// 1文字テクスチャの1辺におけるマージン +/// +/// NOTE: 境界で隣の文字のピクセルを含まないように、テクスチャ上で四辺に2pxのマージンを取る。 +/// 文字情報(UV)はこのマージンを除いた領域を指定する。 +const MARGIN: usize = 2; +/// 1文字テクスチャの1方向におけるマージン +const MARGIN_AXIS: usize = MARGIN * 2; + +#[derive(Clone, Copy)] +pub enum CharInfo { + Rasterizable { + alloc_id: AllocId, + width: f32, + height: f32, + x_offset: f32, + y_offset: f32, + advance: f32, + uv: Vec4, + }, + Unrasterizable { + advance: f32, + }, +} + +pub struct CharAtlas { + /// ウィンドウの倍率 + /// + /// 物理解像度と論理解像度の差を考慮するため、この倍率に従ってテクスチャアトラス全体をスケールする。 + /// + /// NOTE: 実行中に変化しうるが、致命的ではないので初期化時の倍率に従うことにする。 + scale_factor: f32, + width: f32, + height: f32, + atlas: AtlasAllocator, + /// 文字テクスチャアトラスに登録されている文字とそのID + /// + /// NOTE: 文字テクスチャアトラスが満ちている場合に最も古く使われた文字を消すためにLRUキャッシュを用いている。 + registereds: LruCache<(char, u32), CharInfo>, + pub texture: TextureWrapper, +} + +impl CharAtlas { + pub fn new(scale_factor: f32, device: &Device) -> Self { + let width = WIDTH as f32 * scale_factor; + let height = HEIGHT as f32 * scale_factor; + + Self { + scale_factor, + width, + height, + atlas: AtlasAllocator::new(etagere::size2(width as i32, height as i32)), + registereds: LruCache::unbounded(), + texture: TextureWrapper::new( + device, + width as u32, + height as u32, + FORMAT, + TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + ), + } + } + + /// 文字情報を取得するメソッド + /// + /// * queue - queue + /// * c - 対象文字 + /// * scale - 論理解像度上の倍率 (雑に1文字の最大高と思って良い) + /// + /// 対象文字がテクスチャアトラス上に存在しない場合はラスタライズ・登録を行う。 + /// また、同時にLRUキャッシュの更新を行う。 + /// + /// WARN: アトラス上の全ての文字が同時に使用される場合、 + /// 一部文字はテクスチャが更新されるため、意図しない結果を得る。 + pub fn acquire(&mut self, queue: &Queue, c: char, scale: u32) -> &CharInfo { + if !self.registereds.contains(&(c, scale)) { + self.register_character(queue, c, scale); + } + self.registereds.get(&(c, scale)).unwrap() + } + + fn register_character(&mut self, queue: &Queue, c: char, scale: u32) { + // ラスタライズ + let rasterized = match RasterizedChar::new(&FONT, c, scale as f32 * self.scale_factor) { + Ok(n) => n, + Err(advance) => { + self.registereds + .push((c, scale), CharInfo::Unrasterizable { advance }); + return; + } + }; + + // アロケート + let size = etagere::size2(rasterized.width as i32, rasterized.height as i32); + let allocated = loop { + if let Some(alloc) = self.atlas.allocate(size) { + break alloc; + } else { + let least_used = self + .registereds + .pop_lru() + .expect("internal error: no character exists but character atlas is full"); + if let CharInfo::Rasterizable { alloc_id, .. } = least_used.1 { + self.atlas.deallocate(alloc_id); + } + } + }; + + // アップロード + self.texture.write_partial( + queue, + &rasterized.data, + // NOTE: 非負が保証されているため、`u32`にキャストして良い。 + allocated.rectangle.min.x as u32, + // NOTE: 非負が保証されているため、`u32`にキャストして良い。 + allocated.rectangle.min.y as u32, + rasterized.width, + rasterized.height, + ); + + // 登録 + let width = rasterized.width as f32 - MARGIN_AXIS as f32; + let height = rasterized.height as f32 - MARGIN_AXIS as f32; + let info = CharInfo::Rasterizable { + alloc_id: allocated.id, + width: width / self.scale_factor, + height: height / self.scale_factor, + x_offset: rasterized.x_offset / self.scale_factor, + y_offset: rasterized.y_offset / self.scale_factor, + advance: rasterized.advance / self.scale_factor, + uv: Vec4::new( + (allocated.rectangle.min.x as f32 + MARGIN as f32) / self.width, + (allocated.rectangle.min.y as f32 + MARGIN as f32) / self.height, + width / self.width, + height / self.height, + ), + }; + self.registereds.push((c, scale), info); + } +} + +struct RasterizedChar { + data: Vec, + width: u32, + height: u32, + x_offset: f32, + y_offset: f32, + advance: f32, +} + +impl RasterizedChar { + /// 文字をラスタライズするコンストラクタ + /// + /// * font - フォント + /// * c - 対象文字 + /// * scale - 論理解像度上の倍率 (雑に1文字の最大高と思って良い) + /// + /// 物理解像度と論理解像度の違いを考慮しない。 + /// ラスタライザブルでない文字に対してはadvanceと共に`Err`を返す。 + fn new(font: &FontRef<'static>, c: char, scale: f32) -> Result { + let font = font.as_scaled(scale); + let glyph = font.scaled_glyph(c); + let advance = font.h_advance(glyph.id); + + let Some(outlined_glyph) = font.outline_glyph(glyph) else { + return Err(advance); + }; + + let width = outlined_glyph.px_bounds().width().ceil() as usize + MARGIN_AXIS; + let height = outlined_glyph.px_bounds().height().ceil() as usize + MARGIN_AXIS; + let channel_count = FORMAT.block_copy_size(None).unwrap() as usize; + let mut data = vec![0x00; channel_count * width * height]; + + outlined_glyph.draw(|x, y, c| { + let x = x as usize + MARGIN; + let y = y as usize + MARGIN; + let i = channel_count * width * y + channel_count * x; + + data[i] = (c * 255.0) as u8; + + #[cfg(target_os = "macos")] + for j in 1..4 { + data[i + j] = 255; + } + }); + + Ok(Self { + data, + width: width as u32, + height: height as u32, + x_offset: outlined_glyph.px_bounds().min.x, + y_offset: outlined_glyph.px_bounds().min.y, + advance, + }) + } +} diff --git a/src/graphics/texture.rs b/src/graphics/texture.rs index 6273b6e..95b9727 100644 --- a/src/graphics/texture.rs +++ b/src/graphics/texture.rs @@ -51,14 +51,15 @@ impl TextureWrapper { /// /// `data`がテクスチャサイズと一致しない場合は、クラッシュしないように不足分が白色で埋められる。 pub fn write(&self, queue: &Queue, data: &[u8]) { - self.write_partial(queue, data, Origin3d::ZERO, self.width, self.height); + self.write_partial(queue, data, 0, 0, self.width, self.height); } /// テクスチャを部分的に更新するメソッド /// /// * queue - queue /// * data - 更新データ - /// * origin - 更新領域の開始座標 + /// * x - 更新領域の開始X座標 + /// * y - 更新領域の開始Y座標 /// * width - 更新領域の幅 /// * height - 更新領域の高さ /// @@ -67,7 +68,8 @@ impl TextureWrapper { &self, queue: &Queue, data: &[u8], - origin: Origin3d, + x: u32, + y: u32, width: u32, height: u32, ) { @@ -89,7 +91,7 @@ impl TextureWrapper { TexelCopyTextureInfo { texture: &self.texture, mip_level: 0, - origin, + origin: Origin3d { x, y, z: 0 }, aspect: TextureAspect::All, }, data, -- cgit v1.3.1