📱 Android OpenGL ES 2.0 实战:3D旋转立方体完整实现
项目概述
这是一个完整的 Android OpenGL ES 2.0 示例项目,展示如何在 Android 平台上使用 Kotlin 和 OpenGL ES 2.0 渲染一个带有纹理贴图的 3D 旋转立方体。
完整代码实现
- 主 Activity (MainActivity.kt)
kotlin
package com.example.openglesdemo
import android.opengl.GLSurfaceView
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
private lateinit var glSurfaceView: MyGLSurfaceView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 创建自定义的 GLSurfaceView
glSurfaceView = MyGLSurfaceView(this)
// 设置全屏显示(可选)
glSurfaceView.systemUiVisibility =
android.view.View.SYSTEM_UI_FLAG_FULLSCREEN or
android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
setContentView(glSurfaceView)
}
override fun onPause() {
super.onPause()
glSurfaceView.onPause()
}
override fun onResume() {
super.onResume()
glSurfaceView.onResume()
}
}
- 自定义 GLSurfaceView (MyGLSurfaceView.kt)
kotlin
package com.example.openglesdemo
import android.content.Context
import android.opengl.GLSurfaceView
import android.view.MotionEvent
class MyGLSurfaceView(context: Context) : GLSurfaceView(context) {
private val renderer: CubeRenderer
// 触摸相关变量,用于实现立方体旋转
private var previousX: Float = 0f
private var previousY: Float = 0f
init {
// 设置 OpenGL ES 2.0 上下文
setEGLContextClientVersion(2)
// 创建渲染器
renderer = CubeRenderer(context)
setRenderer(renderer)
// 设置渲染模式为连续渲染
renderMode = RENDERMODE_CONTINUOUSLY
}
// 处理触摸事件,实现立方体旋转
override fun onTouchEvent(event: MotionEvent): Boolean {
val x = event.x
val y = event.y
when (event.action) {
MotionEvent.ACTION_MOVE -> {
val dx = x - previousX
val dy = y - previousY
// 根据滑动距离更新旋转角度
renderer.angleX += dy * 0.3f
renderer.angleY += dx * 0.3f
// 请求重绘
requestRender()
}
}
previousX = x
previousY = y
return true
}
}
- 渲染器 (CubeRenderer.kt)
kotlin
package com.example.openglesdemo
import android.content.Context
import android.opengl.GLES20
import android.opengl.GLSurfaceView
import android.opengl.Matrix
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10
class CubeRenderer(private val context: Context) : GLSurfaceView.Renderer {
// 旋转角度
var angleX: Float = 0f
var angleY: Float = 0f
// 矩阵变量
private val projectionMatrix = FloatArray(16)
private val viewMatrix = FloatArray(16)
private val modelMatrix = FloatArray(16)
private val mvpMatrix = FloatArray(16)
// 立方体对象
private lateinit var cube: Cube
// 当OpenGL ES表面被创建时调用
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
// 设置背景颜色(深灰色)
GLES20.glClearColor(0.2f, 0.2f, 0.2f, 1.0f)
// 启用深度测试,确保3D对象正确遮挡
GLES20.glEnable(GLES20.GL_DEPTH_TEST)
// 创建立方体对象
cube = Cube(context)
}
// 当表面大小改变时调用
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
// 设置视口大小
GLES20.glViewport(0, 0, width, height)
// 计算宽高比
val ratio = width.toFloat() / height.toFloat()
// 创建透视投影矩阵
Matrix.frustumM(projectionMatrix, 0, -ratio, ratio, -1f, 1f, 3f, 10f)
// 设置相机位置和视角
Matrix.setLookAtM(viewMatrix, 0,
0f, 0f, 5f, // 相机位置
0f, 0f, 0f, // 观察点
0f, 1f, 0f) // 上向量
}
// 每一帧绘制时调用
override fun onDrawFrame(gl: GL10?) {
// 清除颜色缓冲区和深度缓冲区
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)
// 创建模型矩阵并应用旋转
Matrix.setIdentityM(modelMatrix, 0)
Matrix.rotateM(modelMatrix, 0, angleX, 1f, 0f, 0f) // X轴旋转
Matrix.rotateM(modelMatrix, 0, angleY, 0f, 1f, 0f) // Y轴旋转
// 计算MVP矩阵:投影矩阵 × 视图矩阵 × 模型矩阵
Matrix.multiplyMM(mvpMatrix, 0, viewMatrix, 0, modelMatrix, 0)
Matrix.multiplyMM(mvpMatrix, 0, projectionMatrix, 0, mvpMatrix, 0)
// 绘制立方体
cube.draw(mvpMatrix)
}
}
- 立方体对象 (Cube.kt) - 已修复纹理加载问题
kotlin
package com.example.openglesdemo
import android.content.Context
import android.opengl.GLES20
import android.opengl.GLUtils
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.drawable.Drawable
import androidx.core.content.res.ResourcesCompat
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
import java.nio.ShortBuffer
class Cube(private val context: Context) {
// 顶点着色器 - 处理每个顶点的位置
private val vertexShaderCode = """
uniform mat4 uMVPMatrix;
attribute vec4 vPosition;
attribute vec2 aTexCoord;
varying vec2 vTexCoord;
void main() {
gl_Position = uMVPMatrix * vPosition;
vTexCoord = aTexCoord;
}
""".trimIndent()
// 片段着色器 - 处理每个像素的颜色
private val fragmentShaderCode = """
precision mediump float;
uniform sampler2D uTexture;
varying vec2 vTexCoord;
void main() {
gl_FragColor = texture2D(uTexture, vTexCoord);
}
""".trimIndent()
// 立方体的8个顶点坐标 (x, y, z)
private val vertices = floatArrayOf(
// 前平面
-0.5f, -0.5f, 0.5f, // 左下前
0.5f, -0.5f, 0.5f, // 右下前
0.5f, 0.5f, 0.5f, // 右上前
-0.5f, 0.5f, 0.5f, // 左上前
// 后平面
-0.5f, -0.5f, -0.5f, // 左下后
0.5f, -0.5f, -0.5f, // 右下后
0.5f, 0.5f, -0.5f, // 右上后
-0.5f, 0.5f, -0.5f // 左上后
)
// 每个顶点的纹理坐标 (u, v)
private val texCoords = floatArrayOf(
// 前平面
0.0f, 0.0f,
1.0f, 0.0f,
1.0f, 1.0f,
0.0f, 1.0f,
// 后平面
0.0f, 0.0f,
1.0f, 0.0f,
1.0f, 1.0f,
0.0f, 1.0f,
// 上平面(需要不同的纹理坐标)
0.0f, 0.0f,
1.0f, 0.0f,
1.0f, 1.0f,
0.0f, 1.0f
)
// 绘制顺序 - 使用索引绘制12个三角形
private val indices = shortArrayOf(
// 前平面
0, 1, 2, 0, 2, 3,
// 后平面
4, 6, 5, 4, 7, 6,
// 上平面
3, 2, 6, 3, 6, 7,
// 下平面
0, 4, 5, 0, 5, 1,
// 左平面
0, 3, 7, 0, 7, 4,
// 右平面
1, 5, 6, 1, 6, 2
)
// OpenGL程序句柄
private val program: Int
// 缓冲区对象
private val vertexBuffer: FloatBuffer
private val texCoordBuffer: FloatBuffer
private val indexBuffer: ShortBuffer
// 着色器变量位置
private var positionHandle: Int = 0
private var texCoordHandle: Int = 0
private var mvpMatrixHandle: Int = 0
private var textureHandle: Int = 0
// 纹理ID
private var textureId: Int = 0
init {
// 初始化顶点缓冲区
val vertexByteBuffer = ByteBuffer.allocateDirect(vertices.size * 4)
vertexByteBuffer.order(ByteOrder.nativeOrder())
vertexBuffer = vertexByteBuffer.asFloatBuffer()
vertexBuffer.put(vertices)
vertexBuffer.position(0)
// 初始化纹理坐标缓冲区
val texByteBuffer = ByteBuffer.allocateDirect(texCoords.size * 4)
texByteBuffer.order(ByteOrder.nativeOrder())
texCoordBuffer = texByteBuffer.asFloatBuffer()
texCoordBuffer.put(texCoords)
texCoordBuffer.position(0)
// 初始化索引缓冲区
val indexByteBuffer = ByteBuffer.allocateDirect(indices.size * 2)
indexByteBuffer.order(ByteOrder.nativeOrder())
indexBuffer = indexByteBuffer.asShortBuffer()
indexBuffer.put(indices)
indexBuffer.position(0)
// 编译着色器并创建OpenGL程序
val vertexShader = ShaderHelper.compileShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
val fragmentShader = ShaderHelper.compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
program = ShaderHelper.createProgram(vertexShader, fragmentShader)
// 加载纹理
textureId = createDefaultTexture()
}
/**
* 绘制立方体
* @param mvpMatrix 模型-视图-投影矩阵
*/
fun draw(mvpMatrix: FloatArray) {
// 使用着色器程序
GLES20.glUseProgram(program)
// 获取顶点属性位置
positionHandle = GLES20.glGetAttribLocation(program, "vPosition")
texCoordHandle = GLES20.glGetAttribLocation(program, "aTexCoord")
// 启用顶点属性数组
GLES20.glEnableVertexAttribArray(positionHandle)
GLES20.glEnableVertexAttribArray(texCoordHandle)
// 设置顶点数据
GLES20.glVertexAttribPointer(
positionHandle, 3,
GLES20.GL_FLOAT, false,
3 * 4, vertexBuffer
)
// 设置纹理坐标数据
GLES20.glVertexAttribPointer(
texCoordHandle, 2,
GLES20.GL_FLOAT, false,
2 * 4, texCoordBuffer
)
// 获取统一变量位置
mvpMatrixHandle = GLES20.glGetUniformLocation(program, "uMVPMatrix")
textureHandle = GLES20.glGetUniformLocation(program, "uTexture")
// 传递MVP矩阵到着色器
GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, mvpMatrix, 0)
// 激活纹理单元0并绑定纹理
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
GLES20.glUniform1i(textureHandle, 0)
// 使用索引绘制立方体
GLES20.glDrawElements(
GLES20.GL_TRIANGLES, indices.size,
GLES20.GL_UNSIGNED_SHORT, indexBuffer
)
// 禁用顶点属性数组
GLES20.glDisableVertexAttribArray(positionHandle)
GLES20.glDisableVertexAttribArray(texCoordHandle)
}
/**
* 创建默认纹理(解决纹理加载问题)
* 如果无法从资源加载,则创建程序生成的纹理
*/
private fun createDefaultTexture(): Int {
val textureHandle = IntArray(1)
GLES20.glGenTextures(1, textureHandle, 0)
if (textureHandle[0] == 0) {
return 0
}
// 尝试从资源加载纹理
var bitmap: Bitmap? = try {
// 尝试加载 R.drawable.texture
BitmapFactory.decodeResource(context.resources, R.drawable.texture)
} catch (e: Exception) {
null
}
// 如果资源加载失败,创建纯色纹理
if (bitmap == null) {
bitmap = createColorfulBitmap(256, 256)
}
// 绑定纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle[0])
// 设置纹理过滤参数
GLES20.glTexParameteri(
GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MIN_FILTER,
GLES20.GL_LINEAR
)
GLES20.glTexParameteri(
GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR
)
// 设置纹理环绕方式
GLES20.glTexParameteri(
GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_S,
GLES20.GL_CLAMP_TO_EDGE
)
GLES20.glTexParameteri(
GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_T,
GLES20.GL_CLAMP_TO_EDGE
)
// 加载位图到OpenGL
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0)
bitmap.recycle()
return textureHandle[0]
}
/**
* 创建彩色棋盘纹理
*/
private fun createColorfulBitmap(width: Int, height: Int): Bitmap {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val paint = Paint()
// 清除画布
canvas.drawColor(Color.DKGRAY)
// 绘制彩色方块
val cellSize = width / 8
for (i in 0 until 8) {
for (j in 0 until 8) {
// 交替使用不同颜色
if ((i + j) % 2 == 0) {
paint.color = Color.rgb(
(i * 32) % 256,
(j * 32) % 256,
((i + j) * 16) % 256
)
} else {
paint.color = Color.WHITE
}
canvas.drawRect(
i * cellSize.toFloat(),
j * cellSize.toFloat(),
(i + 1) * cellSize.toFloat(),
(j + 1) * cellSize.toFloat(),
paint
)
}
}
return bitmap
}
}
- 着色器辅助类 (ShaderHelper.kt)
kotlin
package com.example.openglesdemo
import android.opengl.GLES20
import android.util.Log
object ShaderHelper {
private const val TAG = "ShaderHelper"
/**
* 编译着色器
* @param type 着色器类型 (GL_VERTEX_SHADER 或 GL_FRAGMENT_SHADER)
* @param shaderCode 着色器源代码
* @return 着色器句柄
*/
fun compileShader(type: Int, shaderCode: String): Int {
// 创建着色器对象
val shader = GLES20.glCreateShader(type)
if (shader == 0) {
Log.e(TAG, "Failed to create shader")
return 0
}
// 加载着色器源代码
GLES20.glShaderSource(shader, shaderCode)
// 编译着色器
GLES20.glCompileShader(shader)
// 检查编译状态
val compileStatus = IntArray(1)
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0)
if (compileStatus[0] == 0) {
// 编译失败,输出错误信息
Log.e(TAG, "Shader compilation failed:\n${GLES20.glGetShaderInfoLog(shader)}")
GLES20.glDeleteShader(shader)
return 0
}
return shader
}
/**
* 创建OpenGL程序
* @param vertexShader 顶点着色器句柄
* @param fragmentShader 片段着色器句柄
* @return 程序句柄
*/
fun createProgram(vertexShader: Int, fragmentShader: Int): Int {
// 创建程序对象
val program = GLES20.glCreateProgram()
if (program == 0) {
Log.e(TAG, "Failed to create program")
return 0
}
// 附加着色器
GLES20.glAttachShader(program, vertexShader)
GLES20.glAttachShader(program, fragmentShader)
// 链接程序
GLES20.glLinkProgram(program)
// 检查链接状态
val linkStatus = IntArray(1)
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0)
if (linkStatus[0] == 0) {
// 链接失败,输出错误信息
Log.e(TAG, "Program linking failed:\n${GLES20.glGetProgramInfoLog(program)}")
GLES20.glDeleteProgram(program)
return 0
}
return program
}
}
- AndroidManifest.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.openglesdemo">
<!-- 声明需要OpenGL ES 2.0支持 -->
<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="OpenGL ES Demo"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
- 添加纹理资源 (可选)
在 res/drawable/ 目录下添加一个图片文件,命名为 texture.png。如果你没有合适的图片,程序会自动生成彩色棋盘纹理。
核心知识点解析
-
OpenGL ES 渲染管线
顶点数据 → 顶点着色器 → 图元组装 → 光栅化 → 片段着色器 → 深度测试 → 帧缓冲区
-
矩阵变换流程
kotlin
// 模型-视图-投影矩阵计算
MVP = Projection × View × Model
- 缓冲区管理
· 顶点缓冲区(VBO): 存储顶点坐标数据
· 纹理坐标缓冲区: 存储UV坐标
· 索引缓冲区(EBO): 存储绘制顺序,减少重复顶点
常见问题与解决方案
- 纹理加载失败
问题: BitmapFactory.decodeResource 返回 null
解决方案:
· 确保资源文件存在且格式正确
· 添加备用纹理生成机制
· 检查图片尺寸是否为2的幂次方(非必须,但推荐)
- 着色器编译失败
调试方法:
kotlin
// 获取编译日志
val infoLog = GLES20.glGetShaderInfoLog(shader)
Log.e(TAG, "Shader compile error: $infoLog")
- 立方体显示不正确
检查点:
· 顶点坐标是否在 -1 到 1 范围内
· 索引顺序是否正确
· 深度测试是否启用
· MVP矩阵计算是否正确