Android使用OpenGL和FreeType绘制文字

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

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

Kotlin 复制代码
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

java 复制代码
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

Kotlin 复制代码
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

Kotlin 复制代码
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

Kotlin 复制代码
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

Kotlin 复制代码
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 对齐。

相关推荐
安卓理事人6 小时前
安卓LinkedBlockingQueue消息队列
android
万能的小裴同学7 小时前
Android M3U8视频播放器
android·音视频
q***57747 小时前
MySql的慢查询(慢日志)
android·mysql·adb
JavaNoober8 小时前
Android 前台服务 "Bad Notification" 崩溃机制分析文档
android
城东米粉儿8 小时前
关于ObjectAnimator
android
zhangphil9 小时前
Android渲染线程Render Thread的RenderNode与DisplayList,引用Bitmap及Open GL纹理上传GPU
android
火柴就是我10 小时前
从头写一个自己的app
android·前端·flutter
lichong95111 小时前
XLog debug 开启打印日志,release 关闭打印日志
android·java·前端
用户693717500138412 小时前
14.Kotlin 类:类的形态(一):抽象类 (Abstract Class)
android·后端·kotlin
火柴就是我12 小时前
NekoBoxForAndroid 编译libcore.aar
android