8.0 KiB
8.0 KiB
TTF Font Design
Overview
자체 TTF 파서 + 래스터라이저를 구현하고, 온디맨드 글리프 캐시 아틀라스로 가변폭 유니코드 텍스트를 렌더링한다. 기존 FontAtlas를 유지하면서 TtfFont를 병행 추가.
Scope
- TTF 바이너리 파싱 (head, hhea, maxp, cmap, loca, glyf, hmtx)
- Quadratic bezier 아웃라인 → 비트맵 래스터라이징
- 온디맨드 글리프 캐시 (1024x1024 R8Unorm 아틀라스)
- 가변폭 텍스트 렌더링 (ASCII + 한글 + 유니코드)
- UiContext에 옵션 통합
TtfParser
TTF 바이너리에서 필요한 테이블을 파싱한다.
pub struct TtfParser {
data: Vec<u8>,
// 테이블 오프셋 캐시
head_offset: usize,
hhea_offset: usize,
maxp_offset: usize,
cmap_offset: usize,
loca_offset: usize,
glyf_offset: usize,
hmtx_offset: usize,
// 헤더 정보
pub units_per_em: u16,
pub num_glyphs: u16,
pub ascender: i16,
pub descender: i16,
pub line_gap: i16,
num_h_metrics: u16,
loca_format: i16, // 0=short, 1=long
}
파싱 순서
- Offset Table: sfVersion, numTables → 테이블 디렉토리 순회
- 각 테이블 태그(4바이트 ASCII)와 offset/length 기록
- head: units_per_em, indexToLocFormat
- hhea: ascender, descender, lineGap, numberOfHMetrics
- maxp: numGlyphs
- cmap: Format 4 (BMP) 또는 Format 12 (full Unicode) 서브테이블
주요 메서드
parse(data: &[u8]) -> Result<TtfParser, String>glyph_index(codepoint: u32) -> u16— cmap 조회glyph_outline(glyph_id: u16) -> GlyphOutline— glyf 테이블에서 아웃라인 추출glyph_metrics(glyph_id: u16) -> GlyphMetrics— hmtx에서 advance/bearing
GlyphOutline
pub struct GlyphOutline {
pub contours: Vec<Vec<OutlinePoint>>,
pub x_min: i16,
pub y_min: i16,
pub x_max: i16,
pub y_max: i16,
}
pub struct OutlinePoint {
pub x: f32,
pub y: f32,
pub on_curve: bool,
}
pub struct GlyphMetrics {
pub advance_width: u16,
pub left_side_bearing: i16,
}
glyf 테이블 파싱
- Simple glyph: numberOfContours >= 0
- endPtsOfContours, flags, x/y 좌표 (delta 인코딩)
- on_curve 플래그로 직선/곡선 구분
- off-curve 점 사이에 암시적 on-curve 점 삽입
- Compound glyph: numberOfContours < 0
- 여러 simple glyph를 변환(translate/scale)하여 합성
- 재귀적 처리
cmap Format 4
BMP(U+0000~U+FFFF) 매핑:
- segCount, endCode[], startCode[], idDelta[], idRangeOffset[] 배열
- 이진 탐색으로 codepoint가 속하는 세그먼트 찾기
- idRangeOffset == 0: glyph = (codepoint + idDelta) % 65536
- idRangeOffset != 0: glyphIdArray에서 조회
Rasterizer
글리프 아웃라인을 알파 비트맵으로 변환한다.
입력
- GlyphOutline (contours of points)
- font_size (pixel height)
- units_per_em
스케일링
scale = font_size / units_per_em
pixel_x = (point.x - x_min) * scale
pixel_y = (y_max - point.y) * scale // Y 뒤집기 (TTF는 Y-up)
Bezier Flattening
Quadratic bezier (P0, P1, P2)를 선분으로 분할:
- 재귀 분할: 중간점과 직선 거리가 threshold(0.5px) 이하이면 직선으로 근사
- 결과: Vec<(f32, f32)> 선분 목록
스캔라인 래스터라이징
- 모든 contour를 선분 목록으로 변환
- 각 스캔라인 (y = 0..height):
- 모든 선분과 교차점 계산
- 교차점을 x 기준 정렬
- winding rule (non-zero): 교차할 때 winding number 증감
- winding != 0인 구간을 채움
안티앨리어싱
- 서브픽셀 없이 단순 coverage: 픽셀 경계에서의 부분 커버리지 계산
- 또는 4x 수직 슈퍼샘플링 (y를 4등분, 평균)
출력
pub fn rasterize(outline: &GlyphOutline, scale: f32) -> RasterResult
pub struct RasterResult {
pub width: u32,
pub height: u32,
pub bitmap: Vec<u8>, // R8 알파맵
pub offset_x: f32, // bearing
pub offset_y: f32, // baseline offset
}
GlyphCache
온디맨드 아틀라스를 관리한다.
pub struct GlyphCache {
atlas_data: Vec<u8>, // CPU측 R8 버퍼 (텍스처 업로드용)
atlas_width: u32, // 1024
atlas_height: u32, // 1024
glyphs: HashMap<char, GlyphInfo>,
cursor_x: u32,
cursor_y: u32,
row_height: u32,
dirty: bool, // GPU 업로드 필요 여부
}
pub struct GlyphInfo {
pub uv: [f32; 4], // u0, v0, u1, v1
pub width: f32,
pub height: f32,
pub advance: f32,
pub bearing_x: f32,
pub bearing_y: f32,
}
글리프 삽입 로직
glyphs.get(ch)로 캐시 확인- 없으면: parser.glyph_index → parser.glyph_outline → rasterize
- 아틀라스에 비트맵 복사 (cursor_x, cursor_y 위치)
- cursor_x 전진. 줄 넘김 시 cursor_y += row_height, cursor_x = 0
- GlyphInfo 생성 (UV 좌표 계산)
- dirty = true
GPU 업로드
upload_if_dirty(queue, texture)— dirty면 queue.write_texture로 전체 아틀라스 업로드- 또는 변경된 영역만 부분 업로드 (최적화, 나중에)
TtfFont
통합 인터페이스.
pub struct TtfFont {
parser: TtfParser,
cache: GlyphCache,
pub font_size: f32,
pub line_height: f32,
pub ascender: f32,
pub descender: f32,
scale: f32,
}
메서드
new(data: &[u8], font_size: f32) -> Result<Self, String>glyph(&mut self, ch: char) -> &GlyphInfotext_width(&mut self, text: &str) -> f32atlas_data(&self) -> &[u8]— CPU 아틀라스 데이터atlas_size(&self) -> (u32, u32)is_dirty(&self) -> boolclear_dirty(&mut self)
UI 통합
UiContext 변경
pub struct UiContext {
// 기존 필드...
pub font: FontAtlas, // 기존 비트맵 폰트 유지
pub ttf_font: Option<TtfFont>, // TTF 폰트 (옵션)
}
텍스트 렌더링 변경
기존 위젯(text, button, slider 등)에서 텍스트를 그릴 때:
ttf_font이 Some이면 → TTF 글리프 사용 (가변폭)- None이면 → 기존 비트맵 폰트 폴백
이를 위한 헬퍼:
impl UiContext {
/// 텍스트 너비 계산 (TTF or 비트맵)
pub fn text_width(&mut self, text: &str) -> f32;
/// 텍스트 렌더링 (TTF or 비트맵)
pub fn draw_text(&mut self, text: &str, x: f32, y: f32, color: [u8; 4]);
}
기존 위젯 코드에서 인라인 글리프 렌더링을 draw_text() 호출로 교체하면 TTF/비트맵 전환이 투명해짐.
UiRenderer 변경
- TTF 아틀라스 텍스처 생성 + 바인드 그룹
- 매 프레임 dirty 체크 → 업로드
- 기존 ui_shader.wgsl 재사용 가능 (R8 알파 텍스처 동일 패턴)
- DrawCommand에 텍스처 종류 플래그 추가 (bitmap vs ttf) 또는 별도 draw call
File Structure
crates/voltex_editor/src/ttf_parser.rs— TTF 바이너리 파싱crates/voltex_editor/src/rasterizer.rs— 아웃라인 → 비트맵crates/voltex_editor/src/glyph_cache.rs— 온디맨드 아틀라스crates/voltex_editor/src/ttf_font.rs— 통합 인터페이스crates/voltex_editor/src/lib.rs— 모듈 추가
Testing
TtfParser (순수 바이트 파싱, GPU 불필요)
- 테이블 디렉토리 파싱
- cmap 매핑: 'A'(0x41) → 올바른 glyph index
- 글리프 아웃라인: 알려진 글리프의 contour 수, 점 수 검증
- hmtx: advance width 검증
- 에러 케이스: 잘못된 데이터
Rasterizer (순수 수학, GPU 불필요)
- 직선 contour (사각형) → 비트맵 검증
- Bezier flatten: 곡선이 선분으로 올바르게 분할되는지
- 빈 글리프(space 등) → 0x0 비트맵
- 스케일링 정확성
GlyphCache (GPU 불필요 — CPU측만 테스트)
- 글리프 삽입 → UV 좌표 검증
- 중복 삽입 → 캐시 히트
- 아틀라스 가득 참 → 줄 넘김 동작
TtfFont (통합)
- text_width: "Hello" 너비 > 0
- 한글 글리프 래스터라이즈 성공
테스트 TTF 파일
NotoSansMono-Regular.ttf 등 오픈소스 폰트를 assets/fonts/ 에 포함하거나, 테스트에서 시스템 폰트 경로 사용.