Skip to content

OpenGL ES 渲染实战

源:Android OpenGL ES Guide

使用 Kotlin/Native 通过 cinterop 直接调用 OpenGL ES 和 EGL API,实现高性能图形渲染。本文展示如何用纯 Kotlin 代码操作 GPU,而非传统的 C++ 实现。

Kotlin/Native vs 传统 JNI

传统 Android OpenGL 开发需要编写大量 C++ 代码,而 Kotlin/Native 可以直接调用这些 API:

kotlin
// ❌ 传统方式:C++ 代码
// extern "C" void Java_..._renderFrame(JNIEnv* env, ...) {
//     glClear(GL_COLOR_BUFFER_BIT);
//     eglSwapBuffers(display, surface);
// }

// ✅ Kotlin/Native:纯 Kotlin 调用 OpenGL
fun renderFrame() {
    glClear(GL_COLOR_BUFFER_BIT.toUInt())
    eglSwapBuffers(display, surface)
}

项目架构

OpenGLNativeDemo/
├── src/
│   ├── androidNativeArm64Main/kotlin/
│   │   ├── EGLContext.kt        # EGL 上下文管理
│   │   ├── ShaderProgram.kt     # 着色器编译
│   │   ├── Renderer.kt          # 渲染器
│   │   └── JniBridge.kt         # JNI 桥接
│   └── nativeInterop/cinterop/
│       └── gles.def             # OpenGL ES 定义
├── build.gradle.kts
└── CMakeLists.txt

cinterop 配置

gles.def 定义文件

# src/nativeInterop/cinterop/gles.def
headers = GLES3/gl3.h EGL/egl.h android/native_window.h android/native_window_jni.h
headerFilter = GLES3/* EGL/* android/*

libraryPaths = /path/to/ndk/platforms/android-24/arch-arm64/usr/lib

linkerOpts = -lGLESv3 -lEGL -landroid -llog

Gradle 配置

kotlin
// build.gradle.kts
kotlin {
    androidNativeArm64 {
        compilations.getByName("main") {
            cinterops {
                val gles by creating {
                    defFile(project.file("src/nativeInterop/cinterop/gles.def"))
                    packageName("platform.gles")
                }
            }
        }
        
        binaries {
            sharedLib {
                baseName = "glrenderer"
            }
        }
    }
}

EGL 上下文管理

Kotlin/Native 实现

kotlin
// src/androidNativeArm64Main/kotlin/EGLContext.kt
@file:OptIn(ExperimentalForeignApi::class)

package com.example.glrenderer

import kotlinx.cinterop.*
import platform.gles.*

class EGLContextManager {
    private var display: EGLDisplay? = null
    private var surface: EGLSurface? = null  
    private var context: EGLContext? = null
    private var config: EGLConfig? = null
    
    fun initialize(nativeWindow: COpaquePointer?): Boolean {
        // 获取默认显示
        display = eglGetDisplay(EGL_DEFAULT_DISPLAY.toLong().toCPointer())
        if (display == EGL_NO_DISPLAY) {
            logError("eglGetDisplay failed")
            return false
        }
        
        // 初始化 EGL
        memScoped {
            val major = alloc<EGLintVar>()
            val minor = alloc<EGLintVar>()
            
            if (eglInitialize(display, major.ptr, minor.ptr) == 0u) {
                logError("eglInitialize failed")
                return false
            }
            
            logInfo("EGL version: ${major.value}.${minor.value}")
        }
        
        // 配置属性
        val configAttribs = intArrayOf(
            EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
            EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
            EGL_RED_SIZE, 8,
            EGL_GREEN_SIZE, 8,
            EGL_BLUE_SIZE, 8,
            EGL_ALPHA_SIZE, 8,
            EGL_DEPTH_SIZE, 24,
            EGL_STENCIL_SIZE, 8,
            EGL_NONE
        )
        
        // 选择配置
        memScoped {
            val configPtr = alloc<EGLConfigVar>()
            val numConfigs = alloc<EGLintVar>()
            
            configAttribs.usePinned { pinned ->
                if (eglChooseConfig(
                    display,
                    pinned.addressOf(0),
                    configPtr.ptr,
                    1,
                    numConfigs.ptr
                ) == 0u || numConfigs.value == 0) {
                    logError("eglChooseConfig failed")
                    return false
                }
            }
            
            config = configPtr.value
        }
        
        // 创建窗口表面
        surface = eglCreateWindowSurface(display, config, nativeWindow, null)
        if (surface == EGL_NO_SURFACE) {
            logError("eglCreateWindowSurface failed: ${eglGetError()}")
            return false
        }
        
        // 上下文属性
        val contextAttribs = intArrayOf(
            EGL_CONTEXT_CLIENT_VERSION, 3,
            EGL_NONE
        )
        
        // 创建 OpenGL ES 3.0 上下文
        memScoped {
            contextAttribs.usePinned { pinned ->
                context = eglCreateContext(
                    display,
                    config,
                    EGL_NO_CONTEXT,
                    pinned.addressOf(0)
                )
            }
        }
        
        if (context == EGL_NO_CONTEXT) {
            logError("eglCreateContext failed: ${eglGetError()}")
            return false
        }
        
        makeCurrent()
        
        // 打印 OpenGL 信息
        logInfo("GL Vendor: ${glGetString(GL_VENDOR)?.toKString()}")
        logInfo("GL Renderer: ${glGetString(GL_RENDERER)?.toKString()}")
        logInfo("GL Version: ${glGetString(GL_VERSION)?.toKString()}")
        
        return true
    }
    
    fun makeCurrent() {
        if (eglMakeCurrent(display, surface, surface, context) == 0u) {
            logError("eglMakeCurrent failed: ${eglGetError()}")
        }
    }
    
    fun swapBuffers() {
        eglSwapBuffers(display, surface)
    }
    
    fun destroy() {
        display?.let { disp ->
            eglMakeCurrent(disp, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)
            
            context?.let { ctx ->
                eglDestroyContext(disp, ctx)
                context = null
            }
            
            surface?.let { surf ->
                eglDestroySurface(disp, surf)
                surface = null
            }
            
            eglTerminate(disp)
            display = null
        }
    }
}

着色器管理

Kotlin/Native 着色器编译

kotlin
// src/androidNativeArm64Main/kotlin/ShaderProgram.kt
@file:OptIn(ExperimentalForeignApi::class)

package com.example.glrenderer

import kotlinx.cinterop.*
import platform.gles.*

class ShaderProgram {
    private var program: GLuint = 0u
    private val uniformLocations = mutableMapOf<String, GLint>()
    
    fun loadFromSource(vertexSrc: String, fragmentSrc: String): Boolean {
        // 编译顶点着色器
        val vertexShader = compileShader(GL_VERTEX_SHADER, vertexSrc)
        if (vertexShader == 0u) return false
        
        // 编译片段着色器
        val fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentSrc)
        if (fragmentShader == 0u) {
            glDeleteShader(vertexShader)
            return false
        }
        
        // 链接程序
        program = glCreateProgram()
        glAttachShader(program, vertexShader)
        glAttachShader(program, fragmentShader)
        glLinkProgram(program)
        
        // 检查链接错误
        memScoped {
            val success = alloc<GLintVar>()
            glGetProgramiv(program, GL_LINK_STATUS, success.ptr)
            
            if (success.value == 0) {
                val logLength = alloc<GLintVar>()
                glGetProgramiv(program, GL_INFO_LOG_LENGTH, logLength.ptr)
                
                val log = allocArray<ByteVar>(logLength.value)
                glGetProgramInfoLog(program, logLength.value, null, log)
                
                logError("Program linking failed: ${log.toKString()}")
                
                glDeleteShader(vertexShader)
                glDeleteShader(fragmentShader)
                glDeleteProgram(program)
                program = 0u
                return false
            }
        }
        
        // 清理着色器对象
        glDeleteShader(vertexShader)
        glDeleteShader(fragmentShader)
        
        return true
    }
    
    private fun compileShader(type: GLenum, source: String): GLuint {
        val shader = glCreateShader(type)
        
        memScoped {
            val sourcePtr = source.cstr.ptr
            val sourcePtrPtr = alloc<CPointerVar<ByteVar>>()
            sourcePtrPtr.value = sourcePtr
            
            glShaderSource(shader, 1, sourcePtrPtr.ptr, null)
        }
        
        glCompileShader(shader)
        
        // 检查编译错误
        memScoped {
            val success = alloc<GLintVar>()
            glGetShaderiv(shader, GL_COMPILE_STATUS, success.ptr)
            
            if (success.value == 0) {
                val logLength = alloc<GLintVar>()
                glGetShaderiv(shader, GL_INFO_LOG_LENGTH, logLength.ptr)
                
                val log = allocArray<ByteVar>(logLength.value)
                glGetShaderInfoLog(shader, logLength.value, null, log)
                
                logError("Shader compilation failed: ${log.toKString()}")
                glDeleteShader(shader)
                return 0u
            }
        }
        
        return shader
    }
    
    fun use() {
        glUseProgram(program)
    }
    
    fun setFloat(name: String, value: Float) {
        glUniform1f(getUniformLocation(name), value)
    }
    
    fun setVec3(name: String, x: Float, y: Float, z: Float) {
        glUniform3f(getUniformLocation(name), x, y, z)
    }
    
    fun setMat4(name: String, matrix: FloatArray) {
        matrix.usePinned { pinned ->
            glUniformMatrix4fv(
                getUniformLocation(name),
                1,
                GL_FALSE.toUByte(),
                pinned.addressOf(0)
            )
        }
    }
    
    private fun getUniformLocation(name: String): GLint {
        return uniformLocations.getOrPut(name) {
            glGetUniformLocation(program, name)
        }
    }
    
    fun destroy() {
        if (program != 0u) {
            glDeleteProgram(program)
            program = 0u
        }
        uniformLocations.clear()
    }
}

渲染器实现

三角形渲染示例

kotlin
// src/androidNativeArm64Main/kotlin/Renderer.kt
@file:OptIn(ExperimentalForeignApi::class)

package com.example.glrenderer

import kotlinx.cinterop.*
import platform.gles.*

class TriangleRenderer {
    private val eglContext = EGLContextManager()
    private val shader = ShaderProgram()
    private var vao: GLuint = 0u
    private var vbo: GLuint = 0u
    private var time: Float = 0f
    
    private val vertexShaderSrc = """
        #version 300 es
        layout(location = 0) in vec3 aPos;
        layout(location = 1) in vec3 aColor;
        
        out vec3 vertexColor;
        uniform float uTime;
        
        void main() {
            float angle = uTime;
            mat3 rotation = mat3(
                cos(angle), -sin(angle), 0.0,
                sin(angle), cos(angle), 0.0,
                0.0, 0.0, 1.0
            );
            gl_Position = vec4(rotation * aPos, 1.0);
            vertexColor = aColor;
        }
    """.trimIndent()
    
    private val fragmentShaderSrc = """
        #version 300 es
        precision mediump float;
        
        in vec3 vertexColor;
        out vec4 FragColor;
        
        void main() {
            FragColor = vec4(vertexColor, 1.0);
        }
    """.trimIndent()
    
    // 顶点数据:位置 (xyz) + 颜色 (rgb)
    private val vertices = floatArrayOf(
        // 位置           // 颜色
         0.0f,  0.5f, 0.0f,  1.0f, 0.0f, 0.0f, // 顶部 - 红色
        -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f, // 左下 - 绿色
         0.5f, -0.5f, 0.0f,  0.0f, 0.0f, 1.0f  // 右下 - 蓝色
    )
    
    fun initialize(nativeWindow: COpaquePointer?): Boolean {
        if (!eglContext.initialize(nativeWindow)) {
            return false
        }
        
        if (!shader.loadFromSource(vertexShaderSrc, fragmentShaderSrc)) {
            return false
        }
        
        // 创建 VAO 和 VBO
        memScoped {
            val vaoPtr = alloc<GLuintVar>()
            val vboPtr = alloc<GLuintVar>()
            
            glGenVertexArrays(1, vaoPtr.ptr)
            glGenBuffers(1, vboPtr.ptr)
            
            vao = vaoPtr.value
            vbo = vboPtr.value
        }
        
        glBindVertexArray(vao)
        glBindBuffer(GL_ARRAY_BUFFER, vbo)
        
        // 上传顶点数据
        vertices.usePinned { pinned ->
            glBufferData(
                GL_ARRAY_BUFFER,
                (vertices.size * 4).toLong(),  // Float = 4 bytes
                pinned.addressOf(0),
                GL_STATIC_DRAW
            )
        }
        
        // 位置属性
        glVertexAttribPointer(0u, 3, GL_FLOAT, GL_FALSE.toUByte(), 24, null)
        glEnableVertexAttribArray(0u)
        
        // 颜色属性
        glVertexAttribPointer(1u, 3, GL_FLOAT, GL_FALSE.toUByte(), 24, 12L.toCPointer<COpaque>())
        glEnableVertexAttribArray(1u)
        
        glBindVertexArray(0u)
        
        // OpenGL 初始化
        glEnable(GL_DEPTH_TEST)
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f)
        
        return true
    }
    
    fun renderFrame(deltaTime: Float) {
        time += deltaTime
        
        eglContext.makeCurrent()
        
        glClear((GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT).toUInt())
        
        shader.use()
        shader.setFloat("uTime", time)
        
        glBindVertexArray(vao)
        glDrawArrays(GL_TRIANGLES, 0, 3)
        glBindVertexArray(0u)
        
        eglContext.swapBuffers()
    }
    
    fun destroy() {
        memScoped {
            val vaoPtr = alloc<GLuintVar>()
            val vboPtr = alloc<GLuintVar>()
            vaoPtr.value = vao
            vboPtr.value = vbo
            
            glDeleteVertexArrays(1, vaoPtr.ptr)
            glDeleteBuffers(1, vboPtr.ptr)
        }
        
        shader.destroy()
        eglContext.destroy()
    }
}

JNI 桥接

Kotlin/Native 导出 JNI 函数

kotlin
// src/androidNativeArm64Main/kotlin/JniBridge.kt
@file:OptIn(ExperimentalForeignApi::class)

package com.example.glrenderer

import kotlinx.cinterop.*
import platform.android.*

private var renderer: TriangleRenderer? = null

@CName("Java_com_example_glrenderer_NativeRenderer_nativeInit")
fun nativeInit(env: CPointer<JNIEnvVar>, thiz: jobject, surface: jobject): jboolean {
    val nativeWindow = ANativeWindow_fromSurface(env, surface)
    
    renderer = TriangleRenderer()
    val success = renderer?.initialize(nativeWindow) ?: false
    
    ANativeWindow_release(nativeWindow)
    
    return if (success) JNI_TRUE.toByte() else JNI_FALSE.toByte()
}

@CName("Java_com_example_glrenderer_NativeRenderer_nativeRender")
fun nativeRender(env: CPointer<JNIEnvVar>, thiz: jobject, deltaTime: jfloat) {
    renderer?.renderFrame(deltaTime)
}

@CName("Java_com_example_glrenderer_NativeRenderer_nativeDestroy")
fun nativeDestroy(env: CPointer<JNIEnvVar>, thiz: jobject) {
    renderer?.destroy()
    renderer = null
}

// 日志辅助函数
private fun logInfo(message: String) {
    __android_log_print(ANDROID_LOG_INFO.toInt(), "GLRenderer", "%s", message)
}

private fun logError(message: String) {
    __android_log_print(ANDROID_LOG_ERROR.toInt(), "GLRenderer", "%s", message)
}

Android Kotlin 层

kotlin
// NativeRenderer.kt (Android 模块)
package com.example.glrenderer

import android.view.Surface

class NativeRenderer {
    external fun nativeInit(surface: Surface): Boolean
    external fun nativeRender(deltaTime: Float)
    external fun nativeDestroy()
    
    companion object {
        init {
            System.loadLibrary("glrenderer")
        }
    }
}

// GLSurfaceView 使用
class MyGLSurfaceView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {
    private val renderer = NativeRenderer()
    private var renderThread: Thread? = null
    private var running = false
    
    init {
        holder.addCallback(this)
    }
    
    override fun surfaceCreated(holder: SurfaceHolder) {
        if (renderer.nativeInit(holder.surface)) {
            running = true
            renderThread = thread {
                var lastTime = System.nanoTime()
                while (running) {
                    val now = System.nanoTime()
                    val deltaTime = (now - lastTime) / 1_000_000_000f
                    lastTime = now
                    
                    renderer.nativeRender(deltaTime)
                }
            }
        }
    }
    
    override fun surfaceDestroyed(holder: SurfaceHolder) {
        running = false
        renderThread?.join()
        renderer.nativeDestroy()
    }
}

性能对比

实现方式帧率 (1080p)代码可维护性类型安全
纯 C++ JNI60 FPS⭐⭐
Kotlin/Native60 FPS⭐⭐⭐⭐⭐