OpenGL ES 渲染实战
使用 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.txtcinterop 配置
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 -llogGradle 配置
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++ JNI | 60 FPS | ⭐⭐ | ❌ |
| Kotlin/Native | 60 FPS | ⭐⭐⭐⭐⭐ | ✅ |