Skip to content

字号的真相:字模与光栅化

把字“弄大一点”,手边有两个旋钮:TextFontfont_size,和第 12 章就认识的 Transform::scale。直觉上它们该是一回事——都让字变大。实际差得远,差距的根源在文字的渲染方式上。

字体文件里存的是矢量轮廓(数学曲线),GPU 不直接画曲线。每次排版,文本引擎把用到的字符按指定字号光栅化(rasterize,把轮廓画成像素图),存进一张专门的纹理图集——字形图集(font atlas),结构和第 15 章的精灵图集同源:一张大图,每个字形占一格。屏幕上的每个字,就是从图集里取出对应格子贴上去的小四边形。

文字渲染流水线示意:字符串与Font资产、font_size一起进入排版引擎,光栅化出字形图集,再按字形位置组装成屏幕上的四边形

Figure 16-4:从字符串到画面——字模提供轮廓,font_size 决定光栅化的精度,图集缓存成品字形

看清这条流水线,两个旋钮的分工就明白了:

  • font_size 在光栅化之前起作用——“按 64 号字刻字模”,刻出来的像素就是 64 号字的精度;
  • Transform::scale 在光栅化之后起作用——字已经刻成 16 号的像素图了,缩放只是把这张小图拉大,和把小照片放大成海报一样,糊。
rust
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.spawn(Camera2d);
    let zh_font = asset_server.load("fonts/book-sans-sc-regular.otf");

    // 字号阶梯:font_size 是"按多大字号刻字模"
    let mut y = 240.0;
    for size in [16.0, 24.0, 40.0, 64.0] {
        commands.spawn((
            Text2d::new(format!("渡口夜话 {size} 号")),
            TextFont {
                font: FontSource::Handle(zh_font.clone()),
                font_size: FontSize::Px(size),
                ..default()
            },
            Transform::from_xyz(0.0, y, 0.0),
        ));
        y -= 30.0 + size;
    }

    // 对照组:同样的视觉大小,左边用 64 号字模,右边用 16 号字模放大 4 倍
    commands.spawn((
        Text2d::new("会心"),
        TextFont {
            font: FontSource::Handle(zh_font.clone()),
            font_size: FontSize::Px(64.0),
            ..default()
        },
        Transform::from_xyz(-160.0, -240.0, 0.0),
    ));
    commands.spawn((
        Text2d::new("会心"),
        TextFont {
            font: FontSource::Handle(zh_font.clone()),
            font_size: FontSize::Px(16.0),
            ..default()
        },
        // 把 16 号小字模硬放大 4 倍——像把小照片拉成海报
        Transform::from_xyz(160.0, -240.0, 0.0).with_scale(Vec3::splat(4.0)),
    ));

    println!("小棠:左边是 64 号字模,右边是 16 号字模放大四倍——凑近看。");
}

Listing 16-4:字号阶梯,外加一组“64 号字模 vs 16 号字模放大四倍”的对照(examples/listing-16-04.rs)

console
cargo run -p ch16-text --example listing-16-04
text
小棠:左边是 64 号字模,右边是 16 号字模放大四倍——凑近看。

四行字号递增的字组成阶梯,下方两个会心字样大小相同,左侧锐利右侧模糊

Figure 16-5:同样的视觉大小——左边 font_size: 64.0 笔画锐利,右边 16 号放大四倍边缘发糊

所以正字法只有一条:要多大的字,就用多大的 font_sizescale 留给确实想要“拉伸一张字的图”的场合——比如飘字出场瞬间的弹跳挤压。官方 text2d 示例的注释也是这句话:缩放 Text2d 缩放的是渲染出的四边形,会带来像素化。

HiDPI 不用你操心:光栅化字号其实是 font_size × 窗口缩放系数。125% 缩放的屏幕上,64 号字实际按 80 号刻——这正是源码注释里写明的行为,也是高分屏下文字不糊的原因。但它Transform 的缩放与相机投影——镜头推近字会糊,道理同上。

这套缓存有一个性能账:每一种“字体 × 光栅化字号”的组合都要新开一张字形图集。十行字用同一个字号,图集只有一张;要是给伤害飘字按伤害值线性插值出 30.0、30.7、31.4……每个数字一个字号,图集就一张张地堆——源码注释专门警告过这会有可观的性能开销。字号要用档位(小/中/大/会心),别用连续值。

行高与磨边

TextFont 之外,还有两个独立组件管文字的“质感”。LineHeight(行高)管多行文本一行占多高,默认 RelativeToFont(1.2)——1.2 倍字号,也可以 Px(...) 写死像素:

rust
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.spawn(Camera2d);
    let zh_font = asset_server.load("fonts/book-sans-sc-regular.otf");
    let verse = "雁背驮霜\n橹声欸乃\n一篙点破满江星";

    // 三种行高:默认 1.2 倍字号 / 宽松 1.8 倍 / 用像素写死
    for (x, line_height) in [
        (-380.0, LineHeight::RelativeToFont(1.2)),
        (0.0, LineHeight::RelativeToFont(1.8)),
        (380.0, LineHeight::Px(34.0)),
    ] {
        commands.spawn((
            Text2d::new(verse),
            TextFont {
                font: FontSource::Handle(zh_font.clone()),
                font_size: FontSize::Px(32.0),
                ..default()
            },
            line_height,
            Transform::from_xyz(x, 110.0, 0.0),
        ));
    }

    // 磨边对比:默认灰度抗锯齿 vs 关掉抗锯齿(像素字)
    commands.spawn((
        Text2d::new("磨边的字"),
        TextFont {
            font: FontSource::Handle(zh_font.clone()),
            font_size: FontSize::Px(56.0),
            ..default()
        },
        Transform::from_xyz(-220.0, -220.0, 0.0),
    ));
    commands.spawn((
        Text2d::new("不磨的字"),
        TextFont {
            font: FontSource::Handle(zh_font.clone()),
            font_size: FontSize::Px(56.0),
            font_smoothing: FontSmoothing::None,
            ..default()
        },
        Transform::from_xyz(220.0, -220.0, 0.0),
    ));
}

Listing 16-5:三种行高排同一首词,外加磨边对照组(examples/listing-16-05.rs)

console
cargo run -p ch16-text --example listing-16-05

三栏同样的三行词分别以默认行高、1.8倍行高、34像素行高排版;下方磨边的字边缘平滑,不磨的字边缘呈硬像素阶梯

Figure 16-6:行高是排版的呼吸感;右下的 FontSmoothing::None 字形边缘是生硬的像素阶梯

FontSmoothing(磨边——抗锯齿)是 TextFont 的字段,默认 AntiAliased 灰度抗锯齿,矢量字体小字号也平滑。None 关掉抗锯齿,字形边缘出现硬像素阶梯——配像素风游戏用。源码注释提醒:想要彻底的像素观感,要配合专门设计的像素字体(点阵风格的 ttf),普通矢量字体关掉磨边后小字号会显得破碎;本书的 Noto 子集就属于后者,Figure 16-6 里看得出毛边。

下一节解决字幕框积压已久的需求:词太长,怎么换行?