跳转至

GPU文字渲染之周边功能实现

背景色

在片元着色器当中,如果覆盖率为0,则赋值为背景色即可

void main() 
{
    float alpha = 0;

    //遍历字体的轮廓
    Glyph glyph = loadGlyph(vertex_glyph_index);
    for (int i = 0; i < glyph.count; i++) 
    {
        Curve curve = loadCurve(glyph.start + i);

        vec2 p0 = curve.p0 - vert_uv;
        vec2 p1 = curve.p1 - vert_uv;
        vec2 p2 = curve.p2 - vert_uv;

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

    alpha = clamp(alpha, 0.0, 1.0);
    if (alpha < 0.01)
    {
        frag_color = vert_backgroud_color;
    }
    else
    {
        frag_color = vert_color * alpha;
    }
}

文字布局

假设,字符串在编排之后的坐标系如下所示:

  • 坐标系原点在文字的左上角
  • 文字所在Box在坐标系的第四象限

![[字符串编排.png]]

那么文字布局算法如下,在获得偏移量之后,构造一个偏移矩阵即可

//获取布局的偏移值
static Vec3f getAlignmentOffset(
    const Box& box, //文字的Box
    const Alignment&  alignment //布局
)
{
    //# 布局调整
    Vec3f algin_offset{ 0,0,0 };

    //y方向上偏移
    auto y_len = box.max.y() - box.min.y();

    switch (alignment)
    {
    case LEFT_BOTTOM:
    case BOTTOM:
    case RIGHT_BOTTOM:
        //Bottom => 文字在放置点下方 => y小于等于0
        // 当前就是,无需转换
        break;

    case LEFT:
    case CENTER:
    case RIGHT:
        //文字中心与放置点重合 => 往上移一半
        algin_offset.y() = +y_len/2;
        break;

    case LEFT_TOP:
    case TOP:
    case RIGHT_TOP:
        //top => 上方
        algin_offset.y() = +y_len;
        break;

    default:
        break;
    }

    //x方向上的偏移
    auto x_len = box.max.x() - box.min.x();

    switch (alignment)
    {
    case RIGHT_TOP:
    case RIGHT:
    case RIGHT_BOTTOM:
        //right => 原先就在右边,无需转换
        break;

    case TOP:
    case CENTER:
    case BOTTOM:
        //中间 => 往左平移一半
        algin_offset.x() = -x_len/2;
        break;

    case LEFT_TOP:
    case LEFT:
    case LEFT_BOTTOM:
        //最左边 => 往左平移全部
        algin_offset.x() = -x_len;
        break;

    default:
        break;
    }

    return algin_offset;
}

粗体

FreeType本身支持粗体,只需在获取轮廓之前,调用FT_Outline_Embolden函数即可。

if (key.bold)
{
    if (_face->glyph->format == FT_GLYPH_FORMAT_OUTLINE)
    {
        FT_Outline_Embolden(&_face->glyph->outline, 10); //加粗
    }
}

但是,一个字符要准备两个轮廓,即粗体时的轮廓、非粗体时的轮廓,而且两种轮廓要区分开。

这里需要注意的是,要更新字符描述信息的规格属性。因为这些属性不会因为调用了FT_Outline_Embolden函数而改变,而实际上粗体之后,字体的宽、高是变化的。

// 字符的描述信息
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;
};

具体做法是:

//在将轮廓转成二阶贝塞尔曲线时,统计其新的宽度
FT_Pos width = 0;
for (int i = 0; i < _face->glyph->outline.n_contours; i++)
{
    // Note: The end indices in face->glyph->outline.contours are inclusive.
    _convertContour(
        //...
        width //将宽度作为引用传入
    );
    //...
}

//按照原先的方式,获取文字规格
GlyphDescriptionValue glyph;
glyph.glyphIndex = glyphIndex;
glyph.bufferIndex = bufferIndex;
glyph.curveCount = bufferGlyph.count;
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;

//字体加粗之后,字形的宽度其实发生了变换
// 但metrics内部是原先字体的规格,不会发生变换
// 因此需要特殊处理
auto oldWidth = _face->glyph->metrics.width;
if (width < oldWidth)
{
    width = oldWidth;
}
glyph.width = width;
glyph.advance += (width - oldWidth)/2;

斜体

虽然FreeType支持斜体,但不建议使用。如果使用FreeType的斜体,那也要像粗体一样,多保存和维护一份轮廓了,如此数据量就大了。

我们可以直接使用二维的“错切”仿射变换即可,它用一个2x2的矩阵就可以解决

//斜体
if (text_desc.italic)
{
    const static Matrixd shearMatrix(
        1, 0, 0, 0,
        0.30, 1, 0, 0, //tanθ=0.3
        0, 0, 1, 0,
        0, 0, 0, 1
    ); //沿X轴正方向错切θ角度
    textMesh.matrix *= shearMatrix; //赋值给模型矩阵即可
}

如下图所示

  1. 左上角文字:加粗、红色背景色
  2. 右下角文字:斜体、红色背景色