================================ 前序===============================
AndroidLearnOpenGL是本博主自己实现的LearnOpenGL练习集合:
Github地址:https://github.com/wangyongyao1989/AndroidLearnOpenGL
系列文章:
6、LearnOpenGL之文字渲染
============================== 显示效果 ===============================
OpenGL文本绘制
========================================================================
由于OpenGL本身并没有包含任何的文本处理能力,我们必须自己定义一套全新的系统让OpenGL绘制文本到屏幕上。由于文本字符没有图元,我们必须要有点创造力才行。需要使用的一些技术可以是:通过GL_LINES来绘制字形,创建文本的3D网格(Mesh),或在3D环境中将字符纹理渲染到2D四边形上。
一、经典文本渲染:位图字体
早期的时候,渲染文本是通过选择一个需要的字体(Font)(或者自己创建一个),并提取这个字体中所有相关的字符,将它们放到一个单独的大纹理中来实现的。这样一张纹理叫做位图字体(Bitmap Font),它在纹理的预定义区域中包含了我们想要使用的所有字符。字体的这些字符被称为字形(Glyph)。每个字形都关联着一个特定的纹理坐标区域。当你想要渲染一个字符的时候,你只需要通过渲染这一块特定的位图字体区域到2D四边形上即可。
我们取一张位图字体,(通过仔细选择纹理坐标)从纹理中采样对应的字形,并渲染它们到多个2D四边形上,最终渲染出“OpenGL”文本。通过启用混合,让背景保持透明,最终就能渲染一个字符串到屏幕上。这个位图字体是通过Codehead的位图字体生成器生成的。
使用这种方式绘制文本有许多优势也有很多缺点。首先,它相对来说很容易实现,并且因为位图字体已经预光栅化了,它的效率也很高。然而,这种方式不够灵活。当你想要使用不同的字体时,你需要重新编译一套全新的位图字体,而且你的程序会被限制在一个固定的分辨率。如果你对这些文本进行缩放的话你会看到文本的像素边缘。此外,这种方式通常会局限于非常小的字符集,如果你想让它来支持Extended或者Unicode字符的话就很不现实了。
二、现代文本渲染:FreeType
FreeType是一个能够用于加载字体并将他们渲染到位图以及提供多种字体相关的操作的软件开发库。它是一个非常受欢迎的跨平台字体库,它被用于Mac OS X、Java、PlayStation主机、Linux、Android等平台。FreeType的真正吸引力在于它能够加载TrueType字体。
TrueType字体不是用像素或其他不可缩放的方式来定义的,它是通过数学公式(曲线的组合)来定义的。类似于矢量图像,这些光栅化后的字体图像可以根据需要的字体高度来生成。通过使用TrueType字体,你可以轻易渲染不同大小的字形而不造成任何质量损失。
FreeType可以在他们的官方网站中下载到。你可以选择自己用源码编译这个库,如果支持你的平台的话,你也可以使用他们预编译好的库。请确认你将freetype.lib添加到你项目的链接库中,并且确认编译器知道头文件的位置。
1、FreeType头文件导入:
#include <ft2build.h>
#include FT_FREETYPE_H
FreeType所做的事就是加载TrueType字体并为每一个字形生成位图以及计算几个度量值(Metric)。我们可以提取出它生成的位图作为字形的纹理,并使用这些度量值定位字符的字形。
2、加载arial.ttf:
要加载一个字体,我们只需要初始化FreeType库,并且将这个字体加载为一个FreeType称之为面(Face)的东西,加载TrueType字体文件arial.ttf。
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/arial.ttf", 0, &face))
std::cout << "ERROR::FREETYPE: Failed to load font" << std::endl;
3、设置字体大小:
当面加载完成之后,我们需要定义字体大小,这表示着我们要从字体面中生成多大的字形
FT_Set_Pixel_Sizes(face, 0, 48);
此函数设置了字体面的宽度和高度,将宽度值设为0表示我们要从字体面通过给定的高度中动态计算出字形的宽度。
4、FreeType的8位的灰度位图:
通过将FT_LOAD_RENDER设为加载标记之一,我们告诉FreeType去创建一个8位的灰度位图,我们可以通过face->glyph->bitmap
来访问这个位图。
使用FreeType加载的每个字形没有相同的大小(不像位图字体那样)。使用FreeType生成的位图的大小恰好能包含这个字符可见区域。例如生成用于表示’.’的位图的大小要比表示’X’的小得多。因此,FreeType同样也加载了一些度量值来指定每个字符的大小和位置。下面这张图展示了FreeType对每一个字符字形计算的所有度量值。
每一个字形都放在一个水平的基准线(Baseline)上(即上图中水平箭头指示的那条线)。一些字形恰好位于基准线上(如’X’),而另一些则会稍微越过基准线以下(如’g’或’p’)(译注:即这些带有下伸部的字母,可以见这里)。这些度量值精确定义了摆放字形所需的每个字形距离基准线的偏移量,每个字形的大小,以及需要预留多少空间来渲染下一个字形。
下面这个表列出了我们需要的所有属性:
属性 | 获取方式 | 生成位图描述 |
---|---|---|
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像素) |
5、字符结构体:
在需要渲染字符时,我们可以加载一个字符字形,获取它的度量值,并生成一个纹理,但每一帧都这样做会非常没有效率。我们应将这些生成的数据储存在程序的某一个地方,在需要渲染字符的时候再去调用。我们会定义一个非常方便的结构体,并将这些结构体存储在一个map中。
struct Character {
GLuint TextureID; // 字形纹理的ID
glm::ivec2 Size; // 字形大小
glm::ivec2 Bearing; // 从基准线到字形左部/顶部的偏移值
GLuint Advance; // 原点距下一个字形原点的距离
};
std::map<GLchar, Character> Characters;
6、字符加载生成纹理:
生成ASCII字符集的前128个字符。对每一个字符,我们生成一个纹理并保存相关数据至Character结构体中,之后再添加至Characters这个映射表中。这样子,渲染一个字符所需的所有数据就都被储存下来备用了。
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); //禁用字节对齐限制
for (GLubyte c = 0; c < 128; c++)
{
// 加载字符的字形
if (FT_Load_Char(face, c, 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>(c, character));
}
7、顶点着色器:
这个顶点着色器将位置坐标与一个投影矩阵相乘,并将纹理坐标传递给片段着色器
#version 330 core
in vec2 TexCoords;
out vec4 color;
uniform sampler2D text;
uniform vec3 textColor;
void main()
{
vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r);
color = vec4(textColor, 1.0) * sampled;
}
8、片段着色器:
片段着色器有两个uniform变量:一个是单颜色通道的字形位图纹理,另一个是颜色uniform,它可以用来调整文本的最终颜色。我们首先从位图纹理中采样颜色值,由于纹理数据中仅存储着红色分量,我们就采样纹理的r分量来作为取样的alpha值。通过变换颜色的alpha值,最终的颜色在字形背景颜色上会是透明的,而在真正的字符像素上是不透明的。我们也将RGB颜色与textColor这个uniform相乘,来变换文本颜色。
#version 330 core
in vec2 TexCoords;
out vec4 color;
uniform sampler2D text;
uniform vec3 textColor;
void main()
{
vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r);
color = vec4(textColor, 1.0) * sampled;
}
9、创建一个VBO和VAO用来渲染四边形:
最后要做的事是创建一个VBO和VAO用来渲染四边形。现在我们在初始化VBO时分配足够的内存,这样我们可以在渲染字符的时候再来更新VBO的内存
GLuint VAO, VBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
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);
每个2D四边形需要6个顶点,每个顶点又是由一个4float向量(译注:一个纹理坐标和一个顶点坐标)组成,因此我们将VBO的内存分配为6 * 4个float的大小。由于我们会在绘制字符时经常更新VBO的内存,所以我们将内存类型设置为GL_DYNAMIC_DRAW。
10、渲染一行文本:
要渲染一个字符,我们从之前创建的Characters映射表中取出对应的Character结构体,并根据字符的度量值来计算四边形的维度。根据四边形的维度我们就能动态计算出6个描述四边形的顶点,并使用glBufferSubData函数更新VBO所管理内存的内容。
void RenderText(Shader &s, std::string text, GLfloat x, GLfloat y, GLfloat scale, glm::vec3 color)
{
// 激活对应的渲染状态
s.Use();
glUniform3f(glGetUniformLocation(s.Program, "textColor"), color.x, color.y, color.z);
glActiveTexture(GL_TEXTURE0);
glBindVertexArray(VAO);
// 遍历文本中所有的字符
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;
// 对每个字符更新VBO
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);
// 更新VBO内存的内容
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
glBindBuffer(GL_ARRAY_BUFFER, 0);
// 绘制四边形
glDrawArrays(GL_TRIANGLES, 0, 6);
// 更新位置到下一个字形的原点,注意单位是1/64像素
x += (ch.Advance >> 6) * scale; // 位偏移6个单位来获取单位为像素的值 (2^6 = 64)
}
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
}
三、Android端的代码实现:
1、Java端的代码:
通过继承GLSurfaceView构建OpenGL的运行环境,给Native层传入本地存储的着色器、arial.ttf的文件路径、窗体宽高。之后在OnDrawFrame方法内渲染刷新。
package com.wangyongyao.gl3d.view;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import com.wangyongyao.gl3d.GL3DCallJni;
import com.wangyongyao.gl3d.utils.GL3DShowUtil;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class GLDrawTextView extends GLSurfaceView implements GLSurfaceView.Renderer {
private static String TAG = GLDrawTextView.class.getSimpleName();
private GL3DCallJni mJniCall;
private Context mContext;
public GLDrawTextView(Context context, GL3DCallJni jniCall) {
super(context);
mContext = context;
mJniCall = jniCall;
init();
}
public GLDrawTextView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
init();
}
private void init() {
getHolder().addCallback(this);
setEGLContextClientVersion(3);
setEGLConfigChooser(8, 8, 8, 8, 16, 0);
String fragPath = GL3DShowUtil.getModelFilePath(mContext
, "gl_draw_text_fragment.glsl");
String vertexPath = GL3DShowUtil.getModelFilePath(mContext
, "gl_draw_text_vertex.glsl");
if (mJniCall != null) {
mJniCall.setGLDrawTextSLPath(fragPath, vertexPath);
}
setRenderer(this);
}
public void onDrawFrame(GL10 gl) {
// Log.e(TAG, "onDrawFrame: ");
if (mJniCall != null)
mJniCall.glDrawTextRenderFrame();
}
public void onSurfaceChanged(GL10 gl, int width, int height) {
Log.e(TAG, "onSurfaceChanged: ");
if (mJniCall != null) {
String modelPath = GL3DShowUtil.getModelFilePath(mContext
, "arial.ttf");
mJniCall.initGLDrawText(width, height,modelPath);
}
}
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
// String modelPath = GL3DShowUtil.getModelFilePath(mContext
// , "jianti.ttf");
// if (mJniCall != null)
// mJniCall.setGLDrawTextTypePath(modelPath);
}
}
2、Native层代码:
extern "C"
JNIEXPORT jboolean JNICALL
cpp_draw_text_init_opengl(JNIEnv *env, jobject thiz, jint width, jint height, jstring path) {
if (glDrawText == nullptr)
glDrawText = new GLDrawText();
const char *typePath = env->GetStringUTFChars(path, nullptr);
glDrawText->setupGraphics(width, height, typePath);
env->ReleaseStringUTFChars(path, typePath);
return 0;
}
extern "C"
JNIEXPORT void JNICALL
cpp_draw_text_render_frame(JNIEnv *env, jobject thiz) {
if (glDrawText == nullptr) return;
glDrawText->renderFrame();
}
extern "C"
JNIEXPORT void JNICALL
cpp_draw_text_frag_vertex_path(JNIEnv *env, jobject thiz, jstring frag, jstring vertex) {
const char *fragPath = env->GetStringUTFChars(frag, nullptr);
const char *vertexPath = env->GetStringUTFChars(vertex, nullptr);
if (glDrawText == nullptr) {
glDrawText = new GLDrawText();
}
glDrawText->setSharderPath(vertexPath, fragPath);
env->ReleaseStringUTFChars(frag, fragPath);
env->ReleaseStringUTFChars(vertex, vertexPath);
}
// 重点:定义类名和函数签名,如果有多个方法要动态注册,在数组里面定义即可
static const JNINativeMethod methods[] = {
/*********************** GL 文本绘制********************/
{"native_draw_text_init_opengl", "(IILjava/lang/String;)Z", (void *) cpp_draw_text_init_opengl},
{"native_draw_text_render_frame", "()V", (void *) cpp_draw_text_render_frame},
{"native_draw_text_set_glsl_path", "(Ljava/lang/String;"
"Ljava/lang/String;"
")V",
(void *) cpp_draw_text_frag_vertex_path},
};
3、C++层文字绘制代码:
// Author : wangyongyao https://github.com/wangyongyao1989
// Created by MMM on 2024/10/30.
//
#include "GLDrawText.h"
#include "freetype_2_9_1/freetype/freetype.h"
bool GLDrawText::setupGraphics(int w, int h, const char *path) {
screenW = w;
screenH = h;
LOGI("setupGraphics(%d, %d)", w, h);
LOGI("ttf: %s", path);
LOGI("Thread ID:%d", std::this_thread::get_id());
LoadFacesByASCII(path);
glViewport(0, 0, w, h);
checkGlError("glViewport");
//清屏
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
checkGlError("glClear");
// Set OpenGL options
glEnable(GL_CULL_FACE);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
GLuint createProgram = drawTextShader->createProgram();
checkGlError("createProgram");
if (!createProgram) {
LOGE("Could not create shaderId.");
return false;
} else {
LOGD("Shader createProgram succeed");
}
GLuint positionHandle = glGetAttribLocation(createProgram, "gl_Position");
checkGlError("glGetAttribLocation");
LOGE("positionHandle:%d", positionHandle);
if (!positionHandle) {
LOGE("Could not find gl_Position");
return false;
}
glm::vec2 viewport(screenW, screenH);
glm::mat4 projection = glm::ortho(0.0f, static_cast<GLfloat>(screenW), 0.0f,
static_cast<GLfloat>(screenH));
drawTextShader->use();
drawTextShader->setMat4("projection", projection);
checkGlError("drawTextShader->setMat4(projection");
// Configure VAO/VBO for texture quads
glGenVertexArrays(1, &VAO);
checkGlError("glGenVertexArrays(1, &VAO);");
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
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);
checkGlError("glBindVertexArray");
return false;
}
void GLDrawText::renderFrame() {
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // also clear the depth buffer now!
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); //禁用byte-alignment限制
glEnable(GL_BLEND);
//glEnable(GL_CULL_FACE);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
checkGlError("glBlendFunc");
glm::vec2 viewport(screenW, screenH);
RenderText("This is sample text", 300.0f, 500.0f, 2.0f, glm::vec3(0.8, 0.1f, 0.1f), viewport);
}
bool GLDrawText::setSharderPath(const char *vertexPath, const char *fragmentPath) {
drawTextShader->getSharderPath(vertexPath, fragmentPath);
return false;
}
GLDrawText::GLDrawText() {
drawTextShader = new GL3DShader();
}
GLDrawText::~GLDrawText() {
//析构函数中释放资源
if (drawTextShader) {
delete drawTextShader;
drawTextShader = nullptr;
}
screenH = 0;
screenW = 0;
if (VAO) {
glDeleteVertexArrays(1, &VAO);
}
if (VBO) {
glDeleteBuffers(1, &VBO);
}
std::map<GLchar, Character>::const_iterator iter;
for (iter = Characters.begin(); iter != Characters.end(); iter++) {
glDeleteTextures(1, &Characters[iter->first].TextureID);
}
}
void GLDrawText::printGLString(const char *name, GLenum s) {
const char *v = (const char *) glGetString(s);
LOGI("OpenGL %s = %s\n", name, v);
}
void GLDrawText::checkGlError(const char *op) {
for (GLint error = glGetError(); error; error = glGetError()) {
LOGI("after %s() glError (0x%x)\n", op, error);
}
}
void
GLDrawText::RenderText(std::string text, GLfloat x, GLfloat y, GLfloat scale,
glm::vec3 color, glm::vec2 viewport) {
// 激活对应的渲染状态
drawTextShader->use();
checkGlError("drawTextShader->use()");
drawTextShader->setVec3("textColor", color.x, color.y, color.z);
glActiveTexture(GL_TEXTURE0);
checkGlError("glActiveTexture");
glBindVertexArray(VAO);
checkGlError("glBindVertexArray(VAO)");
// 遍历文本中所有的字符
std::string::const_iterator c;
LOGE("RenderText x:%f == y:%f", x, y);
LOGE("RenderText viewportX:%f == viewportY:%f", viewport.x, viewport.y);
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;
// LOGE("TextRenderSample::RenderText [xpos,ypos,w,h]=[%f, %f, %f, %f], ch.advance >> 6 = %d"
// , xpos, ypos, w, h, ch.Advance >> 6);
// 对每个字符更新VBO
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);
checkGlError("glBindTexture");
// 更新VBO内存的内容
glBindBuffer(GL_ARRAY_BUFFER, VBO);
checkGlError("glBindBuffer");
// Be sure to use glBufferSubData and not glBufferData
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
checkGlError("glBufferSubData");
glBindBuffer(GL_ARRAY_BUFFER, 0);
checkGlError("glBindBuffer(GL_ARRAY_BUFFER");
// 绘制四边形
glDrawArrays(GL_TRIANGLES, 0, 6);
checkGlError("glDrawArrays(GL_TRIANGLES");
// 更新位置到下一个字形的原点,注意单位是1/64像素
// 位偏移6个单位来获取单位为像素的值 (2^6 = 64)
x += (ch.Advance >> 6) * scale;
}
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
}
void GLDrawText::LoadFacesByASCII(const char *path) {
// FreeType
FT_Library ft;
// All functions return a value different than 0 whenever an error occurred
if (FT_Init_FreeType(&ft))
LOGE("ERROR::FREETYPE: Could not init FreeType Library");
// Load font as face
FT_Face face;
if (FT_New_Face(ft, path, 0, &face))
LOGE("ERROR::FREETYPE: Failed to load font");
// Set size to load glyphs as
FT_Set_Pixel_Sizes(face, 0, 48);
// Disable byte-alignment restriction
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
// Load first 128 characters of ASCII set
for (GLubyte c = 0; c < 128; c++) {
// Load character glyph
if (FT_Load_Char(face, c, FT_LOAD_RENDER)) {
LOGE("ERROR::FREETYTPE: Failed to load Glyph");
continue;
}
// Generate texture
GLuint texture;
glGenTextures(1, &texture);
checkGlError("LoadFacesByASCII glGenTextures");
// LOGE("fore c= %d",c);
// LOGE("texture === %d",texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(
GL_TEXTURE_2D,
0,
//选择GL_LUMINANCE来于:
// https://stackoverflow.com/questions/70285879/android12-opengles3-0-glteximage2d-0x502-error
GL_LUMINANCE,
face->glyph->bitmap.width,
face->glyph->bitmap.rows,
0,
GL_LUMINANCE,
GL_UNSIGNED_BYTE,
face->glyph->bitmap.buffer
);
// Set texture options
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);
// Now store character for later use
Character character = {
texture,
glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows),
glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top),
static_cast<GLuint>(face->glyph->advance.x)
};
Characters.insert(std::pair<GLchar, Character>(c, character));
}
glBindTexture(GL_TEXTURE_2D, 0);
checkGlError("glBindTexture(GL_TEXTURE_2D, 0);");
// Destroy FreeType once we're finished
FT_Done_Face(face);
FT_Done_FreeType(ft);
LOGE("FT_Done_FreeType");
}
参考资料:中文版LearnOpenGL