summaryrefslogtreecommitdiff
path: root/src/graphics/font.rs
diff options
context:
space:
mode:
authorTengu712 <genreihoutengu@gmail.com>2026-10-01 01:53:23 +0900
committerTengu712 <genreihoutengu@gmail.com>2026-10-01 01:53:23 +0900
commit2c104d7936193bb19eb52b3e4d47657ffb18a9d7 (patch)
tree6f90ea406a1506bbb37ee3aea52a29278524074a /src/graphics/font.rs
parente3835c956011c217e498b272490005dca457d402 (diff)
render text
Diffstat (limited to 'src/graphics/font.rs')
-rw-r--r--src/graphics/font.rs222
1 files changed, 138 insertions, 84 deletions
diff --git a/src/graphics/font.rs b/src/graphics/font.rs
index d05682d..78a28a3 100644
--- a/src/graphics/font.rs
+++ b/src/graphics/font.rs
@@ -4,34 +4,73 @@ use etagere::{AllocId, AtlasAllocator};
use glam::Vec4;
use lru::LruCache;
use std::sync::LazyLock;
-use wgpu::*;
+use ttf_parser::{
+ Face, Tag,
+ gpos::{PairAdjustment, PositioningSubtable},
+};
+use wgpu::{Device, Queue, TextureFormat, TextureUsages};
+
+static FONT_DATA: &[u8] = include_bytes!(concat!(
+ env!("CARGO_MANIFEST_DIR"),
+ "/assets/KazukiReiwa-Regular.ttf"
+));
+
+static FONT: LazyLock<FontRef<'static>> =
+ LazyLock::new(|| FontRef::try_from_slice(FONT_DATA).expect("fatal: failed to load font"));
-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")
+static KERN: LazyLock<Vec<PairAdjustment<'static>>> = LazyLock::new(|| {
+ let face = Face::parse(FONT_DATA, 0).expect("fatal: failed to load font");
+ let gpos = face.tables().gpos.expect("fatal: failed to load GPOS");
+ gpos.features
+ .into_iter()
+ .find(|v| v.tag == Tag::from_bytes(b"kern"))
+ .expect("fatal: failed to load kern")
+ .lookup_indices
+ .into_iter()
+ .filter_map(|v| gpos.lookups.get(v))
+ .flat_map(|v| v.subtables.into_iter::<PositioningSubtable>())
+ .filter_map(|v| match v {
+ PositioningSubtable::Pair(v) => Some(v),
+ _ => None,
+ })
+ .collect()
});
-/// スケールされたフォントのトップ-ベースライン間を取得するメソッド
-pub fn ascent(scale: u32) -> f32 {
- FONT.as_scaled(scale as f32).ascent()
+pub fn kern(scale: f32, left: GlyphId, right: GlyphId) -> f32 {
+ let left = ttf_parser::GlyphId(left.0);
+ let right = ttf_parser::GlyphId(right.0);
+ KERN.iter()
+ .find_map(|v| match v {
+ PairAdjustment::Format1 { coverage, sets } => sets.get(coverage.get(left)?)?.get(right),
+ PairAdjustment::Format2 {
+ coverage,
+ classes,
+ matrix,
+ } => {
+ coverage.get(left)?;
+ matrix.get((classes.0.get(left), classes.1.get(right)))
+ }
+ })
+ .map_or(0.0, |(v, _)| v.x_advance as f32 * factor(scale))
+}
+
+pub fn ascent(scale: f32) -> f32 {
+ FONT.ascent_unscaled() * factor(scale)
+}
+
+pub fn line_height(scale: f32) -> f32 {
+ (FONT.height_unscaled() + FONT.line_gap_unscaled()) * factor(scale)
}
-/// スケールされたフォントの行間を取得するメソッド
-pub fn line_gap(scale: u32) -> f32 {
- FONT.as_scaled(scale as f32).line_gap()
+pub fn h_advance(scale: f32, id: GlyphId) -> f32 {
+ FONT.h_advance_unscaled(id) * factor(scale)
}
-/// スケールされたフォントのカーニング量を取得するメソッド
-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)
+fn factor(scale: f32) -> f32 {
+ scale / FONT.units_per_em().unwrap()
}
-const WIDTH: u32 = 128;
+const WIDTH: u32 = 512;
const HEIGHT: u32 = WIDTH;
// NOTE: macOSではswizzleをゼロコストで使えないのでRGBAにする。
@@ -41,44 +80,53 @@ const FORMAT: TextureFormat = if cfg!(target_os = "macos") {
TextureFormat::R8Unorm
};
+/// ラスタライズ可能な最大文字サイズ (論理解像度)
+const MAX_CHAR_SIZE: f32 = 64.0;
/// 1文字テクスチャの1辺におけるマージン
///
-/// NOTE: 境界で隣の文字のピクセルを含まないように、テクスチャ上で四辺に2pxのマージンを取る。
+/// 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,
- },
+#[derive(Default)]
+pub struct CharInfo {
+ pub glyph_id: GlyphId,
+ pub alloc_id: Option<AllocId>,
+ pub width: f32,
+ pub height: f32,
+ pub x_offset: f32,
+ pub y_offset: f32,
+ pub uv: Vec4,
}
pub struct CharAtlas {
/// ウィンドウの倍率
///
- /// 物理解像度と論理解像度の差を考慮するため、この倍率に従ってテクスチャアトラス全体をスケールする。
+ /// 物理解像度と論理解像度の差を考慮するため、
+ /// この倍率に従ってテクスチャアトラス全体をスケールする。
///
/// NOTE: 実行中に変化しうるが、致命的ではないので初期化時の倍率に従うことにする。
scale_factor: f32,
+ /// テクスチャの物理幅 (uv計算用)
width: f32,
+ /// テクスチャの物理高 (uv計算用)
height: f32,
+
+ /// 文字テクスチャアトラス
+ ///
+ /// etagereが賢く文字テクスチャを配置してくれる。
atlas: AtlasAllocator,
/// 文字テクスチャアトラスに登録されている文字とそのID
///
- /// NOTE: 文字テクスチャアトラスが満ちている場合に最も古く使われた文字を消すためにLRUキャッシュを用いている。
+ /// NOTE: 文字テクスチャアトラスが満ちている場合に
+ // 最も古く使われた文字を消すために、LRUキャッシュを用いている。
registereds: LruCache<(char, u32), CharInfo>,
+ /// アロケーション回避目的のラスタライズ作業用バッファ
+ scratch: Vec<u8>,
+
pub texture: TextureWrapper,
}
@@ -87,12 +135,16 @@ impl CharAtlas {
let width = WIDTH as f32 * scale_factor;
let height = HEIGHT as f32 * scale_factor;
+ let max_side = (MAX_CHAR_SIZE * scale_factor).ceil() as usize + MARGIN_AXIS;
+ let channel_count = FORMAT.block_copy_size(None).unwrap() as usize;
+
Self {
scale_factor,
width,
height,
atlas: AtlasAllocator::new(etagere::size2(width as i32, height as i32)),
registereds: LruCache::unbounded(),
+ scratch: vec![0x00; channel_count * max_side * max_side],
texture: TextureWrapper::new(
device,
width as u32,
@@ -107,29 +159,39 @@ impl CharAtlas {
///
/// * queue - queue
/// * c - 対象文字
- /// * scale - 論理解像度上の倍率 (雑に1文字の最大高と思って良い)
+ /// * scale - 論理解像度上の文字サイズ
///
/// 対象文字がテクスチャアトラス上に存在しない場合はラスタライズ・登録を行う。
/// また、同時にLRUキャッシュの更新を行う。
///
+ /// 対象文字がフォントにない場合(豆腐)やグリフが存在しない場合(空白等)、
+ /// ラスタライズ・登録を行わず、`glyph_id`のみ有効で・無害な結果を返す。
+ /// そのため、`CharInfo.alloc_id`が`None`である場合、
+ /// その`glyph_id`で水平方向のadvanceを取得すること。
+ ///
/// WARN: アトラス上の全ての文字が同時に使用される場合、
/// 一部文字はテクスチャが更新されるため、意図しない結果を得る。
- pub fn acquire(&mut self, queue: &Queue, c: char, scale: u32) -> &CharInfo {
- if !self.registereds.contains(&(c, scale)) {
+ pub fn acquire(&mut self, queue: &Queue, c: char, scale: f32) -> &CharInfo {
+ let key = (c, scale.to_bits());
+ if !self.registereds.contains(&key) {
self.register_character(queue, c, scale);
}
- self.registereds.get(&(c, scale)).unwrap()
+ self.registereds.get(&key).unwrap()
}
- fn register_character(&mut self, queue: &Queue, c: char, scale: u32) {
+ fn register_character(&mut self, queue: &Queue, c: char, scale: f32) {
+ let id = FONT.glyph_id(c);
+
// ラスタライズ
- 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 Some(rasterized) =
+ RasterizedChar::new(id, scale * self.scale_factor, &mut self.scratch)
+ else {
+ let info = CharInfo {
+ glyph_id: id,
+ ..Default::default()
+ };
+ self.registereds.push((c, scale.to_bits()), info);
+ return;
};
// アロケート
@@ -142,7 +204,7 @@ impl CharAtlas {
.registereds
.pop_lru()
.expect("internal error: no character exists but character atlas is full");
- if let CharInfo::Rasterizable { alloc_id, .. } = least_used.1 {
+ if let Some(alloc_id) = least_used.1.alloc_id {
self.atlas.deallocate(alloc_id);
}
}
@@ -151,7 +213,7 @@ impl CharAtlas {
// アップロード
self.texture.write_partial(
queue,
- &rasterized.data,
+ &self.scratch,
// NOTE: 非負が保証されているため、`u32`にキャストして良い。
allocated.rectangle.min.x as u32,
// NOTE: 非負が保証されているため、`u32`にキャストして良い。
@@ -163,13 +225,13 @@ impl CharAtlas {
// 登録
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,
+ let info = CharInfo {
+ glyph_id: id,
+ alloc_id: Some(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,
@@ -177,62 +239,54 @@ impl CharAtlas {
height / self.height,
),
};
- self.registereds.push((c, scale), info);
+ self.registereds.push((c, scale.to_bits()), info);
}
}
struct RasterizedChar {
- data: Vec<u8>,
width: u32,
height: u32,
x_offset: f32,
y_offset: f32,
- advance: f32,
}
impl RasterizedChar {
/// 文字をラスタライズするコンストラクタ
///
- /// * font - フォント
- /// * c - 対象文字
- /// * scale - 論理解像度上の倍率 (雑に1文字の最大高と思って良い)
+ /// * id - 対象グリフ
+ /// * scale - 物理解像度上の文字サイズ
+ /// * data - アロケーション回避用の作業用バッファ
///
- /// 物理解像度と論理解像度の違いを考慮しない。
- /// ラスタライザブルでない文字に対しては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);
- };
+ /// ラスタライズ不可能なグリフに対しては`None`を返す。
+ fn new(id: GlyphId, scale: f32, data: &mut [u8]) -> Option<Self> {
+ let scale = PxScale::from(factor(scale) * FONT.height_unscaled());
+ let outlined = FONT.outline_glyph(id.with_scale(scale))?;
+ let bounds = outlined.px_bounds();
- 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 width = bounds.width() as usize + MARGIN_AXIS;
+ let height = bounds.height() 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;
+ // 0埋め
+ //
+ // NOTE: 描画ではマージンが初期化されないため。
+ // マージンだけ0埋めするとコードが煩雑になるため。
+ data[..channel_count * width * height].fill(0x00);
+
+ outlined.draw(|x, y, c| {
+ let i = channel_count * (width * (y as usize + MARGIN) + x as usize + MARGIN);
data[i] = (c * 255.0) as u8;
#[cfg(target_os = "macos")]
- for j in 1..4 {
- data[i + j] = 255;
- }
+ data[i + 1..i + 4].fill(255);
});
- Ok(Self {
- data,
+ Some(Self {
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,
+ x_offset: bounds.min.x,
+ y_offset: bounds.min.y,
})
}
}