在数据可视化时,渲染字符是必不可少的,如果只渲染0-9数字和26个英文字母,那么 LearnOpenGL CN 的文字渲染教程就足够了,但涉及到中文字符渲染就有些麻烦了。
解决思路:统一使用 std::string 初始化需要渲染的单双字节混合字符串,创建两个 std::map<wchar_t, Character>,分别储存需要渲染的单字节字符和双字节字符,创建map的方法与教程类似。
string 单双字节混合字符串逐字分割
c++ 的 std::string 是 GBK 编码的(在我电脑上是这样的),GBK 编码中,单字节字符的第一位为0,双字节字符第一个字节的第一位为1,由此可进行单双字节字符判断及分割:
void splitGBK(std::string chars, std::vector<std::string>& words, std::vector<int>& sizes){
int size, i = 0, len = chars.length();
while (i < len) {
if ((chars[i] & 0x80) == 0x00)
size = 1;
else
size = 2;
words.push_back(chars.substr(i, size));
sizes.push_back(size);
i += size;
}
}
示例代码:
setlocale(LC_ALL, "chs");
std::string str = "文abc字";
std::vector <std::string> words;
std::vector <int> sizes;
splitGBK(str, words, sizes);
wchar_t* w = (wchar_t*)calloc(2, sizeof(wchar_t));
for (int i = 0;i < words.size();i++) {
MultiByteToWideChar(CP_ACP, 0, (LPCSTR)words[i].c_str(), -1, (LPWSTR)w, sizes[i]);
wprintf(L"%d: %s\tsize: %d \n", i, w, sizes[i]);
}
运行结果:
0: 文 size: 2
1: a size: 1
2: b size: 1
3: c size: 1
4: 字 size: 2
创建两个分别储存单双字节的 map
// 单双字节字体
FT_Face face_cn;
FT_Face face_en;
FT_New_Face(ft, font_cn_path, 0, &face_cn);
FT_New_Face(ft, font_en_path, 0, &face_en);
// 单双字节map
std::map<wchar_t, Character> Characters;
std::map<wchar_t, Character> wCharacters;
// 将单双字节混合字符串str逐字添加进相应map
splitGBK(str, words, sizes);
wchar_t* ch = (wchar_t*)calloc(1, sizeof(wchar_t));
for (int i = 0;i < words.size();i++) {
MultiByteToWideChar(CP_ACP, 0, (LPCSTR)words[i].c_str(), -1, (LPWSTR)ch, sizes[i]);
if (sizes[i] == 1)
FT_Load_Char(face_en, *ch, FT_LOAD_RENDER);
else
FT_Load_Char(face_cn, *ch, FT_LOAD_RENDER);
}
多纹理同时渲染
由于字符串占用一个纹理,可视化数据占用一个纹理,至少需要渲染两个纹理,此时需要在片段着色器中使用 uniform 变量决定渲染哪一个纹理,片段着色器:
#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D texture1;
uniform sampler2D text1;
uniform vec3 textColor;
uniform bool istext;
void main(){
if (istext)
FragColor = vec4(textColor, texture(text1, TexCoord).r);
else
FragColor = texture(texture1, TexCoord);
}
在程序中,设置该 uniform 变量的值来实现多纹理同时渲染,类似这样:
// 渲染循环
while (!glfwWindowShouldClose(window)){
// 省略------
// 渲染可视化数据的纹理
ourShader.setBool("istext", false);
/* ---省略--- */
// 渲染字符串的纹理
ourShader.setBool("istext", true);
/* ---省略--- */
}
设置渲染图像保持比例、保持居中
改变窗口大小,渲染图像的比例也会随之变化,文字就会变形
解决方法在于修改 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); 中的 framebuffer_size_callback 函数:
const unsigned int SCR_WIDTH = 600;
const unsigned int SCR_HEIGHT = 300;
const float ratio = SCR_HEIGHT / (float)SCR_WIDTH;
void framebuffer_size_callback(GLFWwindow* window, int width, int height){
float new_ratio = height / (float)width;
if (new_ratio > ratio)
glViewport(0, abs(height - width * ratio)/2, width, width * ratio);
else if (new_ratio < ratio)
glViewport(abs(width - height / ratio) / 2, 0, height/ratio, height);
else
glViewport(0, 0, width, height);
}
"有趣"的事
有时候会遇到这样的问题:将 PDF 中的文字复制进 word 时,英文字母很宽,给它设置西文字体(如 Times)也设置不了,令人头疼。
问题的原因:复制的是全角字符,Times 字体中没有全角字符,故无法设置。
GBK 编码是从 0x81 开始的,不含ASCII码,但是 GBK 码中也有英文字母和阿拉伯数字,即全角英文字母和全角阿拉伯数字。对于英文字母和阿拉伯数字,全角字符的宽度大约为半角字符的 2 倍,下图可以看出它们的宽度不同(没有打空格哦)。或者试着复制:全角abc半角abc,也能发现这个问题。