跳转至

基于字体轮廓的GPU文字渲染

本文介绍了一种实现文字渲染的新方法。此算法的主要思路在于,将字体轮廓传输到片元着色器当中,在GPU中对文字进行渲染。因此得名于“基于字体轮廓的GPU文字渲染”。

优势如下

  1. 高质量:在任意缩放比例下,仍然能够保持良好的抗锯齿
  2. 高效率、低内存

对比:十万字符

  1. 基于三角网绘制:稳定在90帧左右,显存占用2.5GB
  2. 基于字体轮廓的GPU文字渲染:稳定在140帧左右,显存占用0.1GB

代码:geodoer/gpu-font-rendering: 基于矢量轮廓的GPU文字渲染 (github.com)

算法概述

主要思想如下

  1. 将字符的轮廓统一存储在一个buffer当中,并传输到渲染管线当中
  2. 将每个像素简化为圆形窗口,我们只需计算这个圆被字体的覆盖率Coverage即可(而这一步是在片元着色器当中做的)
  3. 最后将文字颜色*Coverage作为片元颜色,这又可能达到抗锯齿的效果

算法步骤

以绘制I'm MM.为例

准备字符轮廓

当遇到新字符时,则需要准备该字符的轮廓,之后需要传输到渲染管线当中,在片元着色器中使用。

准备三角网

绘制时,需要根据字符的排版准备三角网(详情请看后文)。一个字符需用一个长方形来覆盖,一个长方形打散成两个三角形。

准备顶点数据

顶点数据需要包含哪些信息?

  1. 顶点的世界坐标无需赘述,这里使用的是二维的坐标,你也可以扩展成三维的
  2. UV坐标的物理意义,之后再展开细聊
  3. bufferIndex其实即是此字符所对应的BufferGlyph,不过传输的是array<BufferGlyph>中的下标。根据此,片元着色器就能取到此片元所属字符的轮廓
struct BufferVertex {
    float x, y;  //顶点的世界坐标,可以是三维的
    float u, v;  //UV坐标
    int32_t bufferIndex; //此字符所对应的BufferGlyph(array<BufferGlyph>的下标)
};

UV的物理意义

如下图所示,字体轮廓(即BufferCurve)是有一个字体坐标系的,BufferCurve中的点都是此坐标系的。

因此,我们需要标识,此顶点对应到字体坐标系上的坐标是多少,而这个信息就存在顶点的UV当中。

顶点着色器

顶点着色器很简单

  1. 顶点坐标做MVP变化即可
  2. UV坐标,一样输出到片元着色器即可,渲染管线会自动帮你插值
  3. 然而bufferIndex就要注意了,bufferIndex是不允许被插值的,因此要用flat来申明
#version 330 core

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

layout (location = 0) in vec2 vertexPosition;
layout (location = 1) in vec2 vertexUV;
layout (location = 2) in int  vertexIndex;

out vec2 uv;
flat out int bufferIndex;
//flat声明一个标量或向量变量是“平面的”(即不会被插值)

void main() {
    gl_Position = projection * view * model * vec4(vertexPosition, 0, 1);
    uv = vertexUV;
    bufferIndex = vertexIndex;
}

片元着色器

片元着色器是GPU文字渲染的关键,其主要思路是

  1. 据bufferIndex获取字形描述
  2. 根据字形描述,拿到该字符的所有轮廓曲线
  3. 遍历轮廓曲线,计算轮廓对片元的覆盖率
  4. 最后,片元颜色 = 覆盖率*文字颜色
#version 330 core

//字形描述
struct Glyph {
    int start, count; //Curves中的索引
};

//二次贝塞尔曲线
struct Curve {
    vec2 p0, p1, p2;
};

uniform isamplerBuffer glyphs;  //字形缓冲纹理(存储所有的字形描述)
uniform samplerBuffer curves;   //曲线缓冲纹理(存储所有字形的贝塞尔曲线)
uniform vec4 color; //字体颜色

in vec2 uv;
flat in int bufferIndex;

out vec4 result;

// 根据索引,从glyphs中加载Glyph
Glyph loadGlyph(int index) {
    Glyph result;
    ivec2 data = texelFetch(glyphs, index).xy; 
        //texelFetch直接从缓冲区中取出像素值,不进行插值处理
    result.start = data.x;
    result.count = data.y;
    return result;
}

// 根据索引,从curves中加载Curve
Curve loadCurve(int index) {
    Curve result;
    result.p0 = texelFetch(curves, 3*index+0).xy;
    result.p1 = texelFetch(curves, 3*index+1).xy;
    result.p2 = texelFetch(curves, 3*index+2).xy;
    return result;
}

// 计算覆盖率
float computeCoverage(vec2 p0, vec2 p1, vec2 p2) {
    //...计算覆盖率,并赋值给alpha
    return alpha;
}

void main() {
    float alpha = 0;

    //1. 根据bufferIndex获取字形描述
    Glyph glyph = loadGlyph(bufferIndex); 
    //2. 遍历该字符的所有轮廓曲线
    for (int i = 0; i < glyph.count; i++) {
        Curve curve = loadCurve(glyph.start + i);

        //一条二阶贝塞尔曲线
        vec2 p0 = curve.p0 - uv;
        vec2 p1 = curve.p1 - uv;
        vec2 p2 = curve.p2 - uv;

        //3. 计算覆盖率
        alpha += computeCoverage(p0, p1, p2);
    }

    alpha = clamp(alpha, 0.0, 1.0);
    result = color * alpha;
}

至于如何计算覆盖率,本文不再展开,可阅读参考链接中的文档。

文字编排

如上文“准备三角网”中所说,我们要根据String的情况,对单个字符进行编排,计算出三角网。

我们在生成文字轮廓buffer时,需要将每个字符的规格给记录下来,以便在编排文字时使用

  1. 在字符描述信息中,加上字符规格相关的属性
// 字符的描述信息
struct Glyph
{
    FT_UInt glyphIndex;     //可由FT_Get_Char_Index获得
    int32_t bufferIndex;    //在m_GlyphBuffer中的索引
    int32_t curveCount;     //该字形的曲线个数

    //此字符的规格
    // Important glyph metrics in font units.
    FT_Pos width, height;
    FT_Pos bearingX;
    FT_Pos bearingY;
    FT_Pos advance;
};
  1. 在生成文字轮廓buffer时,赋值这些属性
glyph.width = _face->glyph->metrics.width;
glyph.height = _face->glyph->metrics.height;
glyph.bearingX = _face->glyph->metrics.horiBearingX;
glyph.bearingY = _face->glyph->metrics.horiBearingY;
glyph.advance = _face->glyph->metrics.horiAdvance;
  1. 在绘制String时,只需要根据以上参数,编排字符串即可,具体可参照font.cpp中,Font类,void draw(float x, float y, const std::string& text)函数

参考链接

  1. 算法原文:《GPU Font Rendering: Current State of the Art
  2. 简化版本:GPU text rendering with vector textures
  3. C++ OpenGL示例代码:GreenLightning/gpu-font-rendering: GPU font rendering from vector outlines demonstration (github.com)