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 libraryandroidlogfreetype)
我建的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/tutorialFT_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.Colorclass 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 nullval xOffset = bitmapLeftval yOffset = maxAscent - bitmapTopval width = this.width + xOffsetval 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 + xval pixelValue = buffer!![index].toInt() and 0xffbitmap.setPixel(x + xOffset, y + yOffset, Color.rgb(pixelValue, pixelValue, pixelValue))}}return bitmap}
}
JFreeType.kt
package site.feiyuliuxing.jfreetypeimport java.nio.ByteBufferclass JFreeType {/*** A native method that is implemented by the 'jfreetype' native library,* which is packaged with this application.*/external fun init(faceBuffer: ByteBuffer): Intexternal fun charBitmap(ftBitmap: FTBitmap, char: Char): Intexternal 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.ByteBufferobject 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 = falsevar glErr = glGetError()while (glErr != GL_NO_ERROR) {Log.e(TAG, "glError: $glErr")foundError = trueglErr = glGetError()}return foundError}fun Context.loadTexture(@DrawableRes img: Int): Int {val options = BitmapFactory.Options()options.inScaled = falseval 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.ByteOrderclass 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 = 0var glWidth: Float = 0fprivate setvar glHeight: Float = 0fprivate setinit {textureId = GLUtil.loadTexture(bitmap)val cx = 0fval cy = 0fval xOffset = 0.0005f * bitmap.widthval yOffset = 0.0005f * bitmap.heightglWidth = xOffset * 2fglHeight = yOffset * 2fpositionVertex = 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, //纹理坐标V01f, 0f, //纹理坐标V10f, 0f, //纹理坐标V20f, 1.0f, //纹理坐标V31f, 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 / 2ffor (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.GL10class RendererText(private val context: Context) : GLSurfaceView.Renderer, IShaderProvider {private val numVAOs = 1private val numVBOs = 3private val vao = IntArray(numVAOs)private val vbo = IntArray(numVBOs)private var cameraX = 0fprivate var cameraY = 0fprivate var cameraZ = 2.5fprivate var renderingProgram = 0private var mvLoc = 0private var projLoc = 0private 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 = 0var maxDescent = 0for (ftBmp in ftBitmaps) {if (ftBmp.bitmapTop > maxAscent) maxAscent = ftBmp.bitmapTopif (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 eslayout (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 esprecision 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 对齐。