Open GL主要是渲染图形的,有时候需要绘制文字,网上搜了一下,基本思路都是把文字转成位图,再使用Open GL纹理进行渲染。加载纹理在特定阶段才能成功(在onSurfaceCreated中加载),这样就无法动态的绘制字符串,一种方式是把可能用到的字符都加载到一个位图,渲染纹理的时候不同的字符就渲染纹理的特定区域,另一种方式就是每个字符生成一个位图(本文提供的代码就是这种方式)。
1、集成FreeType
这里我们直接使用源码集成 下载FreeType源码
新建一个 Android Native Library 类型的 Module 或者点击 File -> Add C++ to Module,下载的FreeType源码解压后文件夹改成 freetype,然后把整个文件夹复制到 cpp 目录,在 cpp 目录下的 CMakeLists.txt 中添加 freetype:
add_subdirectory(freetype)
target_link_libraries(${CMAKE_PROJECT_NAME} # List libraries link to the target library android log freetype)
我建的Module名称是 jfreetype,实现的代码主要有:
jfreetype.cpp
#include <jni.h>
#include <string>
#include <android//log.h>
#include "ft2build.h"
#include FT_FREETYPE_H
#define LOG_I(...) __android_log_print(ANDROID_LOG_INFO, "NDK FT", __VA_ARGS__)
#define LOG_W(...) __android_log_print(ANDROID_LOG_WARN, "NDK FT", __VA_ARGS__)
#define LOG_E(...) __android_log_print(ANDROID_LOG_ERROR, "NDK FT", __VA_ARGS__)
// https://freetype.org/freetype2/docs/tutorial
FT_Library library; /* handle to library */
FT_Face face; /* handle to face object */
extern "C" JNIEXPORT jint JNICALL
Java_site_feiyuliuxing_jfreetype_JFreeType_init(
JNIEnv *env,
jobject, jobject face_buffer) {
std::string hello = "Hello from C++";
FT_Error error = FT_Init_FreeType(&library);
if (error) {
LOG_E("an error occurred during library initialization, error: %d", error);
return error;
}
jbyte *buffer = (jbyte *) (env->GetDirectBufferAddress(face_buffer));
jlong size = env->GetDirectBufferCapacity(face_buffer);
error = FT_New_Memory_Face(library,
(FT_Byte *) buffer, /* first byte in memory */
size, /* size in bytes */
0, /* face_index */
&face);
if (error) {
LOG_E("an error occurred during FT_New_Memory_Face, error: %d", error);
return error;
}
error = FT_Set_Pixel_Sizes(
face, /* handle to face object */
0, /* pixel_width */
128); /* pixel_height */
if (error) {
LOG_E("an error occurred during FT_Set_Pixel_Sizes, error: %d", error);
return error;
}
return 0;
}
extern "C"
JNIEXPORT jint JNICALL
Java_site_feiyuliuxing_jfreetype_JFreeType_charBitmap(
JNIEnv *env, jobject thiz,
jobject ft_bitmap, jchar charcode) {
FT_UInt glyph_index = FT_Get_Char_Index(face, charcode);
FT_Error error = FT_Load_Glyph(
face, /* handle to face object */
glyph_index, /* glyph index */
FT_LOAD_DEFAULT); /* load flags, see below */
if (error) {
LOG_E("an error occurred during FT_Get_Char_Index, error: %d", error);
return error;
}
error = FT_Render_Glyph(face->glyph, /* glyph slot */
FT_RENDER_MODE_NORMAL); /* render mode */
if (error) {
LOG_E("an error occurred during FT_Render_Glyph, error: %d", error);
return error;
}
FT_Bitmap bitmap = face->glyph->bitmap;
LOG_I("--------------- %c ---------------", charcode);
LOG_I("FT_Bitmap size: %d x %d", bitmap.width, bitmap.rows);
LOG_I("FT_Bitmap pixel mode: %d", bitmap.pixel_mode);
LOG_I("FT_Bitmap bitmap top: %d", face->glyph->bitmap_top);
LOG_I("metrics.height: %ld", face->glyph->metrics.height);
LOG_I("metrics.horiBearingY: %ld", face->glyph->metrics.horiBearingY);
jclass bmpCls = env->GetObjectClass(ft_bitmap);
jfieldID rowsID = env->GetFieldID(bmpCls, "rows", "I");
jfieldID widthID = env->GetFieldID(bmpCls, "width", "I");
jfieldID bufferID = env->GetFieldID(bmpCls, "buffer", "[B");
jfieldID leftID = env->GetFieldID(bmpCls, "bitmapLeft", "I");
jfieldID topID = env->GetFieldID(bmpCls, "bitmapTop", "I");
env->SetIntField(ft_bitmap, rowsID, (int) bitmap.rows);
env->SetIntField(ft_bitmap, widthID, (int) bitmap.width);
env->SetIntField(ft_bitmap, leftID, face->glyph->bitmap_left);
env->SetIntField(ft_bitmap, topID, face->glyph->bitmap_top);
int dataLength = bitmap.rows * bitmap.width;
jbyteArray buf = env->NewByteArray(dataLength);
jbyte *data = env->GetByteArrayElements(buf, nullptr);
for (int i = 0; i < dataLength; ++i) {
data[i] = bitmap.buffer[i];
}
env->ReleaseByteArrayElements(buf, data, 0);
env->SetObjectField(ft_bitmap, bufferID, buf);
return 0;
}
extern "C"
JNIEXPORT void JNICALL
Java_site_feiyuliuxing_jfreetype_JFreeType_close(JNIEnv *env, jobject thiz) {
FT_Done_FreeType(library);
}
FTBitmap.kt
import android.graphics.Bitmap
import android.graphics.Color
class FTBitmap @JvmOverloads constructor(
var rows: Int = 0,
var width: Int = 0,
var buffer: ByteArray? = null,
var bitmapLeft: Int = 0,
var bitmapTop: Int = 0,
) {
fun toBitmap(maxAscent: Int, maxDescent: Int): Bitmap? {
if (buffer == null) return null
val xOffset = bitmapLeft
val yOffset = maxAscent - bitmapTop
val width = this.width + xOffset
val height = rows + yOffset + maxDescent - (rows - bitmapTop)
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
for (y in 0 until rows) {
for (x in 0 until this.width) {
val index = y * this.width + x
val pixelValue = buffer!![index].toInt() and 0xff
bitmap.setPixel(x + xOffset, y + yOffset, Color.rgb(pixelValue, pixelValue, pixelValue))
}
}
return bitmap
}
}
JFreeType.kt
package site.feiyuliuxing.jfreetype
import java.nio.ByteBuffer
class JFreeType {
/**
* A native method that is implemented by the 'jfreetype' native library,
* which is packaged with this application.
*/
external fun init(faceBuffer: ByteBuffer): Int
external fun charBitmap(ftBitmap: FTBitmap, char: Char): Int
external fun close()
companion object {
// Used to load the 'jfreetype' library on application startup.
init {
System.loadLibrary("jfreetype")
}
}
}
至此,我们需要的接口都已经准备好啦,继续~~
2、使用Open GL绘制文字
Android Open GL基础这里就不介绍了,如有需要,可以参考构建OpenGL ES环境
需要准备一个字体文件,可以自己搜索下载一个ttf,替换后面代码中的“SourceCodePro-Regular.ttf”
GLUtil.kt
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.opengl.GLES11Ext.GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT
import android.opengl.GLES11Ext.GL_TEXTURE_MAX_ANISOTROPY_EXT
import android.opengl.GLES30.*
import android.opengl.GLUtils
import android.util.Log
import androidx.annotation.DrawableRes
import java.nio.ByteBuffer
object GLUtil {
private const val TAG = "GLUtil"
fun createShaderProgram(vertexShaderSource: String, fragmentShaderSource: String): Int {
val vShader = glCreateShader(GL_VERTEX_SHADER)
val fShader = glCreateShader(GL_FRAGMENT_SHADER)
glShaderSource(vShader, vertexShaderSource)
glShaderSource(fShader, fragmentShaderSource)
val status = IntArray(1)
glCompileShader(vShader)
checkOpenGLError()
glGetShaderiv(vShader, GL_COMPILE_STATUS, status, 0)
if (status[0] != 1) {
Log.e(TAG, "vertex compilation failed")
printShaderLog(vShader)
}
glCompileShader(fShader)
checkOpenGLError()
glGetShaderiv(fShader, GL_COMPILE_STATUS, status, 0)
if (status[0] != 1) {
Log.e(TAG, "fragment compilation failed")
printShaderLog(fShader)
}
val vfProgram = glCreateProgram()
glAttachShader(vfProgram, vShader)
glAttachShader(vfProgram, fShader)
glLinkProgram(vfProgram)
checkOpenGLError()
glGetProgramiv(vfProgram, GL_LINK_STATUS, status, 0)
if (status[0] != 1) {
Log.e(TAG, "linking failed")
printProgramLog(vfProgram)
}
return vfProgram
}
private fun printShaderLog(shader: Int) {
val len = IntArray(1)
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, len, 0)
if (len[0] > 0) {
val log = glGetShaderInfoLog(shader)
Log.e(TAG, "Shader Info Log: $log")
}
}
private fun printProgramLog(prog: Int) {
val len = IntArray(1)
glGetProgramiv(prog, GL_INFO_LOG_LENGTH, len, 0)
Log.e(TAG, "printProgramLog() - log length=${len[0]}")
if (len[0] > 0) {
val log = glGetProgramInfoLog(prog)
Log.e(TAG, "Program Info Log: $log")
}
}
private fun checkOpenGLError(): Boolean {
var foundError = false
var glErr = glGetError()
while (glErr != GL_NO_ERROR) {
Log.e(TAG, "glError: $glErr")
foundError = true
glErr = glGetError()
}
return foundError
}
fun Context.loadTexture(@DrawableRes img: Int): Int {
val options = BitmapFactory.Options()
options.inScaled = false
val bitmap = BitmapFactory.decodeResource(resources, img, options)
return loadTexture(bitmap)
}
fun loadTexture(bitmap: Bitmap): Int {
Log.d(TAG, "bitmap size: ${bitmap.width} x ${bitmap.height}")
val textures = IntArray(1)
glGenTextures(1, textures, 0)
val textureID = textures[0]
if (textureID == 0) {
Log.e(TAG, "Could not generate a new OpenGL textureId object.")
return 0
}
glBindTexture(GL_TEXTURE_2D, textureID)
// https://developer.android.google.cn/reference/android/opengl/GLES20#glTexImage2D(int,%20int,%20int,%20int,%20int,%20int,%20int,%20int,%20java.nio.Buffer)
/* int target,
int level,
int internalformat,
int width,
int height,
int border,
int format,
int type,
Buffer pixels */
val pixels = ByteBuffer.allocateDirect(bitmap.byteCount)
bitmap.copyPixelsToBuffer(pixels)
pixels.position(0)//这步比较关键,不然无法加载纹理数据
val internalformat = GLUtils.getInternalFormat(bitmap)
val type = GLUtils.getType(bitmap)
// Log.i(TAG, "internalformat=$internalformat, GL_RGBA=$GL_RGBA")
// Log.i(TAG, "type=$type, GL_UNSIGNED_BYTE=$GL_UNSIGNED_BYTE")
// glTexImage2D(GL_TEXTURE_2D, 0, internalformat, bitmap.width, bitmap.height, 0, internalformat, type, pixels)
GLUtils.texImage2D(GL_TEXTURE_2D, 0, bitmap, 0)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
glGenerateMipmap(GL_TEXTURE_2D)
val ext = glGetString(GL_EXTENSIONS)
// Log.e(TAG, ext)
if (ext.contains("GL_EXT_texture_filter_anisotropic")) {
val anisoset = FloatArray(1)
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, anisoset, 0)
Log.d(TAG, "anisoset=${anisoset[0]}")
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, anisoset[0])
}
bitmap.recycle()
return textureID
}
}
GLChar.kt
import android.graphics.Bitmap
import android.opengl.GLES30.*
import java.nio.ByteBuffer
import java.nio.ByteOrder
class GLChar(bitmap: Bitmap) {
private var positionVertex = FloatArray(15)
private val vertexBuffer = ByteBuffer.allocateDirect(positionVertex.size * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(positionVertex)
.apply{ position(0) }
private val texVertexBuffer = ByteBuffer.allocateDirect(TEX_VERTEX.size * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(TEX_VERTEX)
.position(0)
private val vertexIndexBuffer = ByteBuffer.allocateDirect(VERTEX_INDEX.size * 2)
.order(ByteOrder.nativeOrder())
.asShortBuffer()
.put(VERTEX_INDEX)
.position(0)
private var textureId = 0
var glWidth: Float = 0f
private set
var glHeight: Float = 0f
private set
init {
textureId = GLUtil.loadTexture(bitmap)
val cx = 0f
val cy = 0f
val xOffset = 0.0005f * bitmap.width
val yOffset = 0.0005f * bitmap.height
glWidth = xOffset * 2f
glHeight = yOffset * 2f
positionVertex = floatArrayOf(
cx, cy, 0f,
xOffset, yOffset, 0f,
-xOffset, yOffset, 0f,
-xOffset, -yOffset, 0f,
xOffset, -yOffset, 0f
)
vertexBuffer.position(0)
vertexBuffer.put(positionVertex)
vertexBuffer.position(0)
}
fun draw(vbo: IntArray) {
glBindBuffer(GL_ARRAY_BUFFER, vbo[0])
glBufferData(GL_ARRAY_BUFFER, vertexBuffer.capacity() * 4, vertexBuffer, GL_STATIC_DRAW)
glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0)
glEnableVertexAttribArray(0)
glBindBuffer(GL_ARRAY_BUFFER, vbo[1])
glBufferData(GL_ARRAY_BUFFER, texVertexBuffer.capacity() * 4, texVertexBuffer, GL_STATIC_DRAW)
glVertexAttribPointer(1, 2, GL_FLOAT, false, 0, 0)
glEnableVertexAttribArray(1)
//激活纹理
glActiveTexture(GL_TEXTURE0)
//绑定纹理
glBindTexture(GL_TEXTURE_2D, textureId)
// 绘制
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo[2])
glBufferData(GL_ELEMENT_ARRAY_BUFFER, VERTEX_INDEX.size * 2, vertexIndexBuffer, GL_STATIC_DRAW)
glDrawElements(GL_TRIANGLES, VERTEX_INDEX.size, GL_UNSIGNED_SHORT, 0)
}
companion object {
private const val TAG = "GLChar"
/**
* 绘制顺序索引
*/
private val VERTEX_INDEX = shortArrayOf(
0, 1, 2, //V0,V1,V2 三个顶点组成一个三角形
0, 2, 3, //V0,V2,V3 三个顶点组成一个三角形
0, 3, 4, //V0,V3,V4 三个顶点组成一个三角形
0, 4, 1 //V0,V4,V1 三个顶点组成一个三角形
)
/**
* 纹理坐标
* (s,t)
*/
private val TEX_VERTEX = floatArrayOf(
0.5f, 0.5f, //纹理坐标V0
1f, 0f, //纹理坐标V1
0f, 0f, //纹理坐标V2
0f, 1.0f, //纹理坐标V3
1f, 1.0f //纹理坐标V4
)
}
}
GLText.tk
class GLText(text: String, glChars: Map<Char, GLChar>) {
private val glCharList = mutableListOf<GLChar>()
init {
for (c in text) glChars[c]?.let(glCharList::add)
}
fun draw(vbo: IntArray, offsetBlock: (Float, Float)->Unit) {
val textWidth = glCharList.sumOf { it.glWidth.toDouble() }.toFloat()
var xOffset = -textWidth / 2f
for (glChar in glCharList) {
offsetBlock(xOffset, 0f)
glChar.draw(vbo)
xOffset += glChar.glWidth
}
}
}
RendererText.kt
import android.content.Context
import android.opengl.GLES30.*
import android.opengl.GLSurfaceView
import android.opengl.Matrix
import java.nio.ByteBuffer
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10
class RendererText(private val context: Context) : GLSurfaceView.Renderer, IShaderProvider {
private val numVAOs = 1
private val numVBOs = 3
private val vao = IntArray(numVAOs)
private val vbo = IntArray(numVBOs)
private var cameraX = 0f
private var cameraY = 0f
private var cameraZ = 2.5f
private var renderingProgram = 0
private var mvLoc = 0
private var projLoc = 0
private val pMat = FloatArray(16)
private val vMat = FloatArray(16)
private val mMat = FloatArray(16)
private val mvMat = FloatArray(16)
private val glChars = mutableMapOf<Char, GLChar>()
private var glText = GLText("", glChars)
private fun loadGLChars() {
val ft = JFreeType()
val faceBuffer = context.assets.open("fonts/SourceCodePro-Regular.ttf").use {
ByteBuffer.allocateDirect(it.available())
.put(it.readBytes()).apply { position(0) }
}
ft.init(faceBuffer)
val chars = mutableListOf<Char>()
fun putChar(char: Char) {
chars.add(char)
}
fun putChars(range: IntRange) {
for (charcode in range) putChar(charcode.toChar())
}
putChars('A'.code..'Z'.code)
putChars('a'.code..'z'.code)
putChars('0'.code..'9'.code)
putChar('!')
val ftBitmaps = chars.map {
val ftBitmap = FTBitmap()
ft.charBitmap(ftBitmap, it)
ftBitmap
}
var maxAscent = 0
var maxDescent = 0
for (ftBmp in ftBitmaps) {
if (ftBmp.bitmapTop > maxAscent) maxAscent = ftBmp.bitmapTop
if (ftBmp.rows - ftBmp.bitmapTop > maxDescent) maxDescent = ftBmp.rows - ftBmp.bitmapTop
}
for (i in chars.indices) {
ftBitmaps[i].toBitmap(maxAscent, maxDescent)?.let { bitmap ->
glChars[chars[i]] = GLChar(bitmap)
}
}
ft.close()
glText = GLText("HelloWorld!", glChars)
}
override fun onSurfaceCreated(p0: GL10?, p1: EGLConfig?) {
renderingProgram = GLUtil.createShaderProgram(vertexShaderSource(), fragmentShaderSource())
glUseProgram(renderingProgram)
mvLoc = glGetUniformLocation(renderingProgram, "mv_matrix")
projLoc = glGetUniformLocation(renderingProgram, "proj_matrix")
glGenVertexArrays(1, vao, 0)
glBindVertexArray(vao[0])
glGenBuffers(numVBOs, vbo, 0)
loadGLChars()
}
override fun onSurfaceChanged(p0: GL10?, width: Int, height: Int) {
glViewport(0, 0, width, height)
val aspect = width.toFloat() / height.toFloat()
Matrix.perspectiveM(pMat, 0, Math.toDegrees(1.0472).toFloat(), aspect, 0.1f, 1000f)
}
override fun onDrawFrame(p0: GL10?) {
glClearColor(0f, 0f, 0f, 1f)
glClear(GL_COLOR_BUFFER_BIT)
//下面两行代码,防止图片的透明部分被显示成黑色
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
Matrix.setIdentityM(vMat, 0)
Matrix.translateM(vMat, 0, -cameraX, -cameraY, -cameraZ)
Matrix.setIdentityM(mMat, 0)
Matrix.multiplyMM(mvMat, 0, vMat, 0, mMat, 0)
glUniformMatrix4fv(mvLoc, 1, false, mvMat, 0)
glUniformMatrix4fv(projLoc, 1, false, pMat, 0)
glText.draw(vbo) { xOffset, yOffset ->
Matrix.setIdentityM(mMat, 0)
Matrix.translateM(mMat, 0, xOffset, yOffset, 0f)
Matrix.multiplyMM(mvMat, 0, vMat, 0, mMat, 0)
glUniformMatrix4fv(mvLoc, 1, false, mvMat, 0)
}
}
override fun vertexShaderSource(): String {
return """
#version 300 es
layout (location = 0) in vec3 position;
layout (location = 1) in vec2 tex_coord;
out vec2 tc;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
uniform sampler2D s;
void main(void)
{
gl_Position = proj_matrix * mv_matrix * vec4(position, 1.0);
tc = tex_coord;
}
""".trimIndent()
}
override fun fragmentShaderSource(): String {
return """
#version 300 es
precision mediump float;
in vec2 tc;
out vec4 color;
uniform sampler2D s;
void main(void)
{
color = texture(s,tc);
}
""".trimIndent()
}
}
效果图
3、总结
字符转位图,照着FreeType的文档很容易就实现了,其中关于字符水平对齐稍微花了点时间,后结合文档Managing Glyphs以及观察打印的数据,确定 bitmap_left 就是 bearingX,bitmap_top 是 bearingY,这样很容易把水平方向的字符按照 baseline 对齐。