summaryrefslogtreecommitdiff
path: root/src/graphics/font.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/graphics/font.rs')
-rw-r--r--src/graphics/font.rs238
1 files changed, 238 insertions, 0 deletions
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<FontRef<'static>> = 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<u8>,
+ 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<Self, f32> {
+ 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,
+ })
+ }
+}