一、FreeType库

FreeType是一个能够提供多种字体相关的操作的软件开发库,往往使用它来做最简单的文字渲染:

OpenGL环境配置(超全整合版)FreeType库可以从这篇文章中的链接中下载到,也可以直接去他们的官方网站上获取

和其它环境配置方法一样,编译其builds文件夹的对应版本,并将include文件夹里面的所有文件全部放入老位置(C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.25.28610\include),编译成功后,应该就可以获得对应的dll和lib,dll放入老位置(C:\Windows\SysWOW64),lib放入老位置(C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.25.28610\lib\x86)

如果嫌编译麻烦,又是windows操作系统,可以直接从objs\Win32\Debug种获得已编译好的文件,你编译的结果也会在这里面

只要包含对应的头文件,就可以应用了:

#include <ft2build.h>
#include FT_FREETYPE_H  

二、字形加载

TrueType字体(TTF):

新型数学字形描述技术,它用数学函数描述字体轮廓外形,含有字形构造、颜色填充、数字描述函数、流程条件控制、栅格处理控制、附加提示控制等指令。通过数学公式描述字体意味着可以轻易渲染不同大小的字形而不造成任何质量损失,这也是当下使用的主流字体,FreeType正可以加载TrueType字体

Windows/Fonts下系统自带的TTF字体

要加载字体,需要先初始化FreeType库,在这里你可以加载你字体的TTF文件:

  • FT_Set_Pixel_Sizes(face, w, h):设置字体默认参数大小,最后两个参数为宽和高,如果宽填0则为通过高来动态计算
  • glPixelStorei(GL_UNPACK_ALIGNMENT, 1):设置纹理解压对齐参数

OpenGL要求所有的纹理都是4字节对齐的,即纹理的大小永远是4字节的倍数,通常这并不会出现什么问题,因为大部分纹理的宽度都为4的倍数并/或每像素使用4个字节,但是对于字体纹理,往往需要支持任意宽度,并且只需要一个字节(用于表示透明度,当前片元显示/不显示),所以这里需要将纹理解压对齐参数设为1,这样才能确保不会有对齐问题(可能会引起段错误)

FT_Library ft;
if (FT_Init_FreeType(&ft))std::cout << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl;
FT_Face face;
if (FT_New_Face(ft, "Fonts/Common.ttf", 0, &face))std::cout << "ERROR::FREETYPE: Failed to load font" << std::endl;
FT_Set_Pixel_Sizes(face, 0, 48);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

一个比较麻烦的事情是:每个字母/文字都会有不同的大小为中心点位置,例如"g", ".", "X"这3个字符在显示相同大小时,实际位图的大小依旧不同,因此每个文字FreeType都需要为每一个字符去单独计算对应的属性,也称作为度量值:

每一个字形都有一个水平的基准线(Baseline)上(即上图中水平箭头指示的那条线)
大多数的字体都会在基准线之上,而对于存在下伸部的字体,就会有一部分越过基准线(例如p和g)

这些度量值精确定义了摆放字形所需的每个字形距离基准线的偏移量,每个字形的大小,以及需要预留多少空间来渲染下一个字形。下面这个表列出了暂时需要的所有属性

属性 获取方式 生成位图描述
width face->glyph->bitmap.width 位图宽度(像素)
height face->glyph->bitmap.rows 位图高度(像素)
bearingX face->glyph->bitmap_left 水平距离,即位图相对于原点的水平位置(像素)
bearingY face->glyph->bitmap_top 垂直距离,即位图相对于基准线的垂直位置(像素)
advance face->glyph->advance.x 水平预留值,即原点到下一个字形原点的水平距离(单位:1/64像素)

在需要渲染字符时,需要先加载字符字形并获取它的度量值,对于重复渲染的字体,往往需要将这些属性存储起来以节省效率:

truct Character
{GLuint     TextureID;  // 字形纹理的IDglm::ivec2 Size;       // 字形大小glm::ivec2 Bearing;    // 从基准线到字形左部/顶部的偏移值GLuint     Advance;    // 原点距下一个字形原点的距离
};
std::map<GLchar, Character> Characters;

如果只考虑ASCLL字符集,那就更好办了,直接将所有的127个字符离线处理下:

  • FT_Load_Char(face, ch, FT_LOAD_RENDER):核心逻辑,将字形ch设置为激活字形以加载

代码中将纹理的internalFormat和format设置为GL_RED,上面也提到过:通过字形生成的位图是一个8位灰度图,它的每一个颜色都由一个字节来表示,因此需要将位图缓冲的每一字节都作为纹理的颜色值

for (GLubyte ch = 0; ch < 128; ch++)
{if (FT_Load_Char(face, ch, FT_LOAD_RENDER)){std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl;continue;}GLuint texture;glGenTextures(1, &texture);glBindTexture(GL_TEXTURE_2D, texture);glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, face->glyph->bitmap.width, face->glyph->bitmap.rows, 0, GL_RED, GL_UNSIGNED_BYTE, face->glyph->bitmap.buffer);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);Character character ={texture,glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows),glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top),face->glyph->advance.x};Characters.insert(std::pair<GLchar, Character>(ch, character));
}
glBindTexture(GL_TEXTURE_2D, 0);

处理完字形后顺便清理FreeType的资源:

FT_Done_Face(face);
FT_Done_FreeType(ft);

三、文字渲染

搞定了预处理后,就可以开始渲染文字了:着色器非常简单

在顶点着色器中,需要给一个投影矩阵,这里直接用最简单的正射投影矩阵,并将视口范围设定为投影的范围,这样你渲染出来的实际纹理位置就正对应了屏幕位置

在片段着色器中,需要给予一个字体颜色

#version 330 core
layout (location = 0) in vec4 vertex;
out vec2 TexIn;
uniform mat4 projection;
void main()
{gl_Position = projection * vec4(vertex.xy, 0.0, 1.0);TexIn = vertex.zw;
}#version 330 core
in vec2 TexIn;
out vec4 color;
uniform sampler2D text;
uniform vec3 textColor;
void main()
{    vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexIn).r);color = vec4(textColor, 1.0) * sampled;
}

对于VAO和VBO,没有什么差别,不过这里只需要给VBO时分配足够的内存而暂时不去填充它,等到渲染字符的时候再用glBufferSubData来更新它的内存(别忘了要将内存类型设置为GL_DYNAMIC_DRAW),之所以预分配的大小是6 * 4,是因为每个2D四边形需要6个顶点,每个顶点又有一个4float向量(二维空间坐标xy和纹理坐标)

if (textVAO == 0)
{glGenVertexArrays(1, &textVAO);glGenBuffers(1, &textVBO);glBindVertexArray(textVAO);glBindBuffer(GL_ARRAY_BUFFER, textVBO);glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 6 * 4, NULL, GL_DYNAMIC_DRAW);glEnableVertexAttribArray(0);glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), 0);glBindBuffer(GL_ARRAY_BUFFER, 0);glBindVertexArray(0);
}

别忘了传递参数到着色器,并且开启混合,关闭深度测试:

开启混合的作用正是为了显示字体,本质上,文字渲染就是判断一个矩形区域内,哪些像素点需要显示颜色:

右图就是未开启混合的渲染效果

glEnable(GL_BLEND);
glEnable(GL_PROGRAM_POINT_SIZE);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glDisable(GL_DEPTH_TEST);shader.Use();
glm::mat4 projection = glm::ortho(0.0f, static_cast<GLfloat>(WIDTH), 0.0f, static_cast<GLfloat>(HEIGHT));
glUniform3f(glGetUniformLocation(shader.Program, "textColor"), color.x, color.y, color.z);
glUniformMatrix4fv(glGetUniformLocation(shader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
glActiveTexture(GL_TEXTURE1);
glUniform1i(glGetUniformLocation(shader.Program, "text"), 1);
glBindVertexArray(textVAO);

最后就是主体部分:步骤如下

  1. 计算当前字符四边形的原点坐标(posx, posy)和大小(w, h)
  2. 赋予这个四边形位置属性和纹理属性
  3. 更新四边形内容
  4. 渲染

如果不了解四边形位置和宽高的计算,可以参考上面度量值那张图

std::string::const_iterator c;
for (c = text.begin(); c != text.end(); c++)
{Character ch = Characters[*c];GLfloat xpos = x + ch.Bearing.x * scale;GLfloat ypos = y - (ch.Size.y - ch.Bearing.y) * scale;GLfloat w = ch.Size.x * scale;GLfloat h = ch.Size.y * scale;GLfloat vertices[6][4] ={{ xpos,     ypos + h,   0.0, 0.0 },{ xpos,     ypos,       0.0, 1.0 },{ xpos + w, ypos,       1.0, 1.0 },{ xpos,     ypos + h,   0.0, 0.0 },{ xpos + w, ypos,       1.0, 1.0 },{ xpos + w, ypos + h,   1.0, 0.0 }};glBindTexture(GL_TEXTURE_2D, ch.TextureID);glBindBuffer(GL_ARRAY_BUFFER, textVBO);glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);glBindBuffer(GL_ARRAY_BUFFER, 0);glDrawArrays(GL_TRIANGLES, 0, 6);x += (ch.Advance >> 6) * scale;
}

如果没问题的话,就可以在任意地方显示文字了,而只需要更换ttf文件的路径,就可以实现字体的变更

四、扩展

有向距离场(Signed Distance Fields):

使用FreeType字体的问题是字形纹理是储存为一个固定的字体大小的,因此直接对其放大就会出现锯齿边缘。此外对字形进行旋转还会使它们看上去变得模糊,这可以通过储存每个像素距最近的字形轮廓的距离而不是光栅化的像素颜色来缓解,这项技术被称为有向距离场(Signed Distance Fields)

位图字体:

通俗易懂的讲解位图字体,就是直接将字符存储于图片,每一个字符都有一个实际的纹理图片,当你需要显示对应字符时直接加载对应的纹理,往往常用于显示数字1-9,字母A-Z,好处是可以实现各种艺术字效果,坏处就是样式和分辨率都被固定

在游戏设计中,伤害跳字,连招UI显示等往往用的都是位图字体

UI设计:

这个就不用说了,文字显示仅是UI设计的最简单的一部分,相当于HelloWorld级别,想要实现一个简单的UI框架都并非易事

OpenGL基础55:文字渲染相关推荐

  1. OpenGL text rendering文字渲染的实例

    OpenGL text rendering文字渲染 先上图,再解答. 完整主要的源代码 源代码剖析 先上图,再解答. 完整主要的源代码 #include <iostream> #inclu ...

  2. Android音视频学习系列(六) — 掌握视频基础知识并使用OpenGL ES 2.0渲染YUV数据

    系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...

  3. OpenGL基础33:帧缓冲(上)之离屏渲染

    在之前的章节,所有的物体都是中规中矩的显示的,只考虑了光照对物体的影响,那假设想要显示特殊的效果该怎么操作呢?例如马赛克风.将所有的物体都显示为黑白色,就像上世纪80年代的灰白电视一样,又或者说将整个 ...

  4. H5 canvas基础入门到捕鱼达人小游戏实现(3)-canvas运动入门,渐变,文字渲染,阴影

    上一篇主要讲解了矩形柱状图,弧形和饼图的绘制,但是离我们的目标还是有点远,不要紧,我们基础api都还没有学习完,今天继续. 本节主要内容 - canvas画板制作 - 块的直线运动 - 粒子运动 - ...

  5. OpenGL进阶(二十一) - 文字渲染

    经典文字渲染:位图字体 在早期渲染文字时,选择你应用程序的字体(或者创建你自己的字体)来绘制文字是通过将所有用到的文字加载在一张大纹理图中来实现的.这张纹理贴图我们把它叫做位图字体(Bitmap Fo ...

  6. OpenGL文字渲染

    一.简介 由于OpenGL本身并没有定义如何渲染文字到屏幕,也没有用于表示文字的基本图形,我们必须自己定义一套全新的方式才能让OpenGL来绘制文字.目前一些技术包括:通过GL_LINES来绘制字形. ...

  7. OpenGL基础54:点光源阴影

    前置: OpenGL基础53:阴影映射(下) 一.万象阴影贴图 之前成功实现了平行光阴影,生成阴影贴图时使用的矩阵是正交矩阵,若是想要实现点光源的阴影效果,那么理论上只需要修改投影矩阵为透视矩阵就好了 ...

  8. OpenGL基础53:阴影映射(下)

    接上文:OpenGL基础52:阴影映射(上) 五.阴影失真 按照上文的计算的结果,一个很明显的问题是:对于参与计算深度贴图的物体,其表面可以看到这样的栅格状的阴影,这种常见的错误表现也叫做阴影失真(S ...

  9. OpenGL基础50:HDR

    一.HDR与LDR 由于显示器只能显示值为0.0到1.0间的颜色,因此当数据存储在帧缓冲(Framebuffer)中时,亮度和颜色的值也是默认被限制在0.0到1.0之间的,这个颜色范围即是LDR(Lo ...

最新文章

  1. 使用hql动态创建对象问题
  2. go strconv
  3. 如何看待微软新开源的Service Fabric?
  4. 多线程创建方式 线程池、Future和CompletableFuture
  5. 黑马程序员——java语言基础——面向对象
  6. 停航63天!湖北复航了,机票预订火爆程度堪比春运
  7. 合肥工贸高级技工学校计算机系,合肥工贸高级技工学校
  8. 直接上手!不容错过的 Visual Studio Code 十大扩展组件
  9. 语言学博士、Kaggle数据分析师,她说:读研不是必选项,这4项技能学校不教
  10. 使用AppleScript批量删除Mac中的信息
  11. Python:对压缩包进行解压操作
  12. 妹妹呀,哥也是第一次当哥哥!
  13. 软件著作权可以更改名字吗?软著如何更改名字?
  14. 【OH】SET System Variable Summary SQLPLUS 系统变量设置
  15. 机械硬盘提示格式化的常见原因|3种数据恢复方法
  16. [ctf.show.reverse] 来一个派森,好好学习天天向上
  17. 脑洞文之去火星搞IT!
  18. 韦东山嵌入式第一期学习笔记DAY_1——2_0_安装ubuntu16.04虚拟机
  19. 这几年各种新零售模式层出不穷
  20. Docker run 容器处于created状态问题

热门文章

  1. python编程入门视频-2020年5个经典python编程入门视频教程推荐学习
  2. 0基础学python要多久-零基础学习python,要多久才可以学好并且找到工作?
  3. 5岁自学python编程-枣庄适合小学生学的少儿编程课程在哪里
  4. 独家 | 一文读懂语音识别(附学习资源)
  5. 各类手机开发平台介绍(转载)
  6. @JsonSerialize的使用
  7. java之备忘录模式,java设计模式之备忘录模式
  8. java request 封装对象_java request请求参数直接封装model对象
  9. PHP集锦点,php 函数集锦
  10. python笔记:random模块中的函数