✅作者简介:大家好,我是 Meteors., 向往着更加简洁高效的代码写法与编程方式,持续分享Java技术内容。
🍎个人主页:Meteors.的博客
💞当前专栏:知识分享
✨特色专栏:知识分享
🥭本文内容:安卓进阶------OpenGL ES
📚 ** ps ** :阅读文章如果有问题或者疑惑,欢迎在评论区提问或指出。
目录
[(二) 渲染核心概念](#(二) 渲染核心概念)
[1. 顶点(Vertices)](#1. 顶点(Vertices))
[2. 纹理坐标(Texture Coordinates)](#2. 纹理坐标(Texture Coordinates))
[3. 着色器(Shaders)](#3. 着色器(Shaders))
[4. 程序(Program)](#4. 程序(Program))
[(三)在 Android 中的基本使用步骤](#(三)在 Android 中的基本使用步骤)
[1. 创建渲染表面(GLSurfaceView)](#1. 创建渲染表面(GLSurfaceView))
[2. 实现渲染器(GLSurfaceView.Renderer)](#2. 实现渲染器(GLSurfaceView.Renderer))
[(四)进阶 2D 技巧](#(四)进阶 2D 技巧)
[1. 3D 坐标系](#1. 3D 坐标系)
[2. 矩阵变换](#2. 矩阵变换)
[3. 深度测试](#3. 深度测试)
[4. 3D 模型数据](#4. 3D 模型数据)
[(二)核心 3D 技术](#(二)核心 3D 技术)
[1. 光照(Lighting)](#1. 光照(Lighting))
[2. 纹理映射](#2. 纹理映射)
[3. 混合(Blending)](#3. 混合(Blending))
[(三)在 Android 中的实现步骤](#(三)在 Android 中的实现步骤)
[(四)进阶 3D 技术](#(四)进阶 3D 技术)
一、2D
(一)优点
- 极高的性能:OpenGL 直接利用 GPU 进行硬件加速。对于需要频繁、大量重绘的界面(如复杂游戏、动态壁纸、高速图表、图片编辑器、视频播放器),它的性能远超基于 CPU 的 Canvas 绘制。
- 高效的图像处理:OpenGL 的片段着色器可以轻松实现对图像/纹理的实时处理,如美颜滤镜、颜色调整、模糊、边缘检测等。这些效果在 Canvas 上实现要么很慢,要么无法实现。
- 平滑的动画和特效:可以实现复杂的 2D 变换(旋转、缩放、平移)、粒子系统、光照和阴影效果,并且能保持极高的帧率。
- 跨平台基础:OpenGL ES 是一个跨平台的图形库。掌握了它在 Android 上的 2D 应用,对理解 iOS、Web(WebGL)的图形编程也大有裨益。
(二) 渲染核心概念
要将一个 2D 图片(例如一张 PNG)绘制到屏幕上,需要理解以下几个关键概念和流程。下图清晰地展示了从2D图像数据到最终屏幕渲染的完整管线流程
1. 顶点(Vertices)
2D 图形本质上是由三角形构成的。一个矩形需要两个三角形(6 个顶点)。每个顶点不仅包含它在坐标系中的位置(x, y),还可以包含其他信息,如纹理坐标、颜色等。
float[] vertices = { -1.0f, -1.0f, // 左下角 - 三角形1 1.0f, -1.0f, // 右下角 - 三角形1 -1.0f, 1.0f, // 左上角 - 三角形1 -1.0f, 1.0f, // 左上角 - 三角形2 1.0f, -1.0f, // 右下角 - 三角形2 1.0f, 1.0f // 右上角 - 三角形2 };
2. 纹理坐标(Texture Coordinates)
纹理坐标定义了顶点在纹理图像(你的 2D 图片)上的对应位置。范围是
[0, 0]到[1, 1]。
java// 与顶点数组顺序对应的纹理坐标 (s, t 或 u, v) float[] textureVertices = { 0.0f, 1.0f, // 纹理左下角 - 对应顶点左下角 1.0f, 1.0f, // 纹理右下角 - 对应顶点右下角 0.0f, 0.0f, // 纹理左上角 - 对应顶点左上角 0.0f, 0.0f, // 纹理左上角 - 对应顶点左上角 1.0f, 1.0f, // 纹理右下角 - 对应顶点右下角 1.0f, 0.0f // 纹理右上角 - 对应顶点右上角 };3. 着色器(Shaders)
这是 OpenGL ES 2.0 及以上版本的核心。着色器是运行在 GPU 上的小程序
(1)顶点着色器
作用:处理每个顶点。在这里可以进行坐标变换(如平移、旋转、缩放)。
示例:一个简单的直通着色器,不做任何变换。
java// plain_vertex_shader.glsl attribute vec4 a_Position; attribute vec2 a_TexCoord; varying vec2 v_TexCoord; // 传递给片段着色器 void main() { gl_Position = a_Position; // 设置顶点的最终位置 v_TexCoord = a_TexCoord; }(2)片段着色器
作用:处理每个像素(片段)的颜色。在这里进行纹理采样(从图片上取颜色)。
示例:
java // plain_fragment_shader.glsl precision mediump float; // 设置精度 varying vec2 v_TexCoord; // 从顶点着色器传来 uniform sampler2D u_TextureUnit; // 纹理采样器 void main() { // texture2D 函数,根据纹理坐标从纹理上取样颜色 gl_FragColor = texture2D(u_TextureUnit, v_TexCoord); } 4. 程序(Program)
将顶点着色器和片段着色器链接成一个完整的 OpenGL ES 程序,供 GPU 执行。
(三)在 Android 中的基本使用步骤
1. 创建渲染表面(GLSurfaceView)
GLSurfaceView是 Android 提供的专门用于显示 OpenGL 画面的 View,它管理着EGLContext和渲染线程。
XML<!-- activity_main.xml --> <android.opengl.GLSurfaceView android:id="@+id/gl_surface_view" android:layout_width="match_parent" android:layout_height="match_parent" />
java// MainActivity.java public class MainActivity extends AppCompatActivity { private GLSurfaceView mGLSurfaceView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mGLSurfaceView = findViewById(R.id.gl_surface_view); // 设置 OpenGL ES 2.0 上下文 mGLSurfaceView.setEGLContextClientVersion(2); // 设置自定义的渲染器 MyRenderer renderer = new MyRenderer(this); mGLSurfaceView.setRenderer(renderer); // 设置渲染模式:持续渲染(RENDERMODE_CONTINUOUSLY) // 或按需渲染(RENDERMODE_WHEN_DIRTY) mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); } @Override protected void onResume() { super.onResume(); mGLSurfaceView.onResume(); } @Override protected void onPause() { super.onPause(); mGLSurfaceView.onPause(); } }2. 实现渲染器(GLSurfaceView.Renderer)
javapublic class MyRenderer implements GLSurfaceView.Renderer { private Context mContext; private int mProgramId; private int mTextureId; // ... 其他变量,如顶点缓冲对象(VBO)的句柄 public MyRenderer(Context context) { mContext = context; } @Override public void onSurfaceCreated(GL10 glUnused, EGLConfig config) { // 1. 设置清屏颜色 GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // 2. 编译和链接着色器,创建 OpenGL 程序 int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER_CODE); int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER_CODE); mProgramId = GLES20.glCreateProgram(); GLES20.glAttachShader(mProgramId, vertexShader); GLES20.glAttachShader(mProgramId, fragmentShader); GLES20.glLinkProgram(mProgramId); // 3. 创建纹理并加载图片(例如从 Resources) mTextureId = loadTexture(mContext, R.drawable.my_image); // 4. 将顶点数据和纹理坐标数据加载到 ByteBuffer 中,并传入 OpenGL // ... (代码略长,涉及 FloatBuffer 和 glVertexAttribPointer) } @Override public void onSurfaceChanged(GL10 glUnused, int width, int height) { // 设置视口(Viewport),当 Surface 尺寸改变时调用(如屏幕旋转) GLES20.glViewport(0, 0, width, height); } @Override public void onDrawFrame(GL10 glUnused) { // 每一帧的绘制工作 // 1. 清空颜色缓冲区 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); // 2. 使用我们创建的程序 GLES20.glUseProgram(mProgramId); // 3. 启用顶点属性并传递数据 // ... (代码略长,涉及 glEnableVertexAttribArray 和 glVertexAttribPointer) // 4. 绑定纹理到纹理单元 GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId); // 5. 告诉着色器使用哪个纹理单元(这里是 0) int textureUniformHandle = GLES20.glGetUniformLocation(mProgramId, "u_TextureUnit"); GLES20.glUniform1i(textureUniformHandle, 0); // 6. 执行绘制! GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6); // 绘制 6 个顶点(2个三角形) } // 辅助方法:加载和编译着色器 private int loadShader(int type, String shaderCode) { ... } // 辅助方法:从资源加载图片生成 OpenGL 纹理 private int loadTexture(Context context, int resourceId) { ... } }
(四)进阶 2D 技巧
纹理滤镜(Filtering) :当纹理被放大或缩小时,如何采样。
GL_LINEAR(平滑)和GL_NEAREST(像素化)。纹理环绕(Wrapping) :当纹理坐标超出
[0, 1]时如何处理。GL_REPEAT(重复)和GL_CLAMP_TO_EDGE(拉伸边缘)。矩阵变换(Matrix Transformations) :在顶点着色器中使用模型(Model)、视图(View)、投影(Projection)矩阵来控制物体的位置、旋转、缩放以及相机视角。这是实现 2D 动画和复杂场景的基石。可以借助
android.opengl.Matrix类进行矩阵计算。粒子系统(Particle Systems):用于实现火焰、烟雾、爆炸等效果,通过一次提交大量顶点并利用着色器进行高效计算。
帧缓冲区(FrameBuffer Objects - FBOs):离屏渲染。可以先将场景渲染到一个纹理上,然后对这个纹理进行二次处理(如全屏模糊),实现高级后期处理效果。
二、3D
(一)介绍
1. 3D 坐标系
从 2D 的平面坐标系,扩展为三维空间坐标系。
右手坐标系 :在 OpenGL 中通常使用右手坐标系。伸出右手,食指指向 x 轴正方向(右),中指指向 y 轴正方向(上),大拇指则指向 z 轴正方向(从屏幕向外指向你)。
模型坐标 -> 世界坐标 -> 视图坐标 -> 裁剪坐标:一个顶点的最终位置需要经过一系列变换
2. 矩阵变换
这是 3D 编程中最核心、最重要的概念。所有 3D 空间中的移动、旋转、缩放以及最终的透视效果,都是通过矩阵乘法完成的。下图清晰地展示了顶点从本地坐标到最终屏幕坐标的变换全过程:
下面解释图中的变换阶段:
模型矩阵(Model Matrix):
作用 :将顶点从模型坐标(本地坐标) 变换到世界坐标。它定义了模型(一个物体)在世界空间中的位置、大小和朝向。
示例:一个放在世界坐标 (2, 0, -5) 位置,放大 2 倍的立方体。
视图矩阵(View Matrix):
作用 :相当于"相机"的放置。它将所有顶点从世界坐标变换到相机坐标(视图坐标)。视图矩阵定义了相机的位置、观察方向和相机的向上方向。
理解 :想象一下,不是你移动了场景中的所有物体,而是你扛着相机在移动和旋转来取景。
Matrix.setLookAtM方法可以方便地创建视图矩阵。投影矩阵(Projection Matrix):
作用 :将 3D 坐标投影到 2D 屏幕上,并创建透视效果,使远处的物体看起来更小。这是实现 3D 感的关键!
透视投影 :模拟人眼看到的真实世界,有"近大远小"的效果。通过
Matrix.frustumM或Matrix.perspectiveM创建。正射投影 :物体的大小不受距离影响,常用于 CAD 绘图或 2.5D 游戏(如模拟城市)。通过
Matrix.orthoM创建。这三种变换的矩阵通常在顶点着色器中相乘,共同组成一个 MVP 矩阵(Model-View-Projection Matrix)。
顶点着色器示例
java// vertex_shader_3d.glsl uniform mat4 u_MVPMatrix; // 组合好的 Model-View-Projection 矩阵 attribute vec4 a_Position; attribute vec2 a_TexCoord; varying vec2 v_TexCoord; void main() { // 将顶点位置与 MVP 矩阵相乘,得到最终的裁剪坐标 gl_Position = u_MVPMatrix * a_Position; v_TexCoord = a_TexCoord; }在 Java 代码中,你需要计算这个矩阵:
java// 在 onDrawFrame 中计算 MVP 矩阵 float[] modelMatrix = new float[16]; float[] viewMatrix = new float[16]; float[] projectionMatrix = new float[16]; float[] mvpMatrix = new float[16]; // 初始化为单位矩阵 Matrix.setIdentityM(modelMatrix, 0); // 将模型向后移动 5 个单位,否则相机可能会在物体内部 Matrix.translateM(modelMatrix, 0, 0f, 0f, -5f); // 让模型绕 y 轴旋转 Matrix.rotateM(modelMatrix, 0, angleInDegrees, 0.0f, 1.0f, 0.0f); // 设置相机位置:眼睛在 (0,0,0),看向 z 轴负方向,向上向量为 y 轴 Matrix.setLookAtM(viewMatrix, 0, 0, 0, 0, 0f, 0f, -5f, 0f, 1.0f, 0.0f); // 设置透视投影:45度视野,宽高比,近裁剪面距离 1,远裁剪面距离 10 float ratio = (float) width / height; Matrix.frustumM(projectionMatrix, 0, -ratio, ratio, -1, 1, 1, 10); // 计算 MVP = P * V * M (注意顺序,从右向左读) Matrix.multiplyMM(mvpMatrix, 0, projectionMatrix, 0, viewMatrix, 0); Matrix.multiplyMM(mvpMatrix, 0, mvpMatrix, 0, modelMatrix, 0); // 将 mvpMatrix 传递到着色器中的 u_MVPMatrix3. 深度测试
在 3D 空间中,物体有前后关系。OpenGL 使用深度缓冲区来记录每个像素的深度值(z 值)。在绘制一个片段时,会检查其深度值是否比缓冲区中已有值更小(离相机更近)。如果是,则绘制并更新缓冲区;如果不是(被遮挡),则丢弃。
开启深度测试:
java@Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { ... // 开启深度测试 GLES20.glEnable(GLES20.GL_DEPTH_TEST); } @Override public void onDrawFrame(GL10 gl) { // 清空颜色和深度缓冲区 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); ... }4. 3D 模型数据
一个复杂的 3D 模型(如游戏角色)由成千上万个三角形组成。通常我们不会在代码中硬定义这些顶点,而是从
.obj等模型文件中加载顶点、纹理坐标和法线数据。
- 法线:一个垂直于三角形表面的向量。用于计算光照。
(二)核心 3D 技术
1. 光照(Lighting)
光照是让 3D 场景具有真实感的关键。基本的光照模型(Phong)包含三个部分:
环境光:模拟场景中的间接光,均匀地照亮所有物体。
漫反射:模拟来自一个方向的光源,其强度与表面法线和光线方向的夹角有关。这是物体呈现颜色的主要部分。
镜面反射:模拟物体表面的高光,产生亮斑,与观察视角有关。
计算光照需要在顶点或片段着色器中进行,需要提供法线、光源位置、相机位置等信息。
一个简单的漫反射光照的顶点着色器示例:
javauniform mat4 u_MVPMatrix; uniform mat4 u_ModelMatrix; // 需要单独的法线矩阵来变换法线 attribute vec4 a_Position; attribute vec3 a_Normal; // 顶点法线 varying vec3 v_Color; void main() { gl_Position = u_MVPMatrix * a_Position; // 计算光照(在世界空间) vec3 lightPos = vec3(5.0, 5.0, 5.0); // 光源位置 vec3 modelPos = vec3(u_ModelMatrix * a_Position); // 顶点在世界空间的位置 vec3 normal = normalize(vec3(u_ModelMatrix * vec4(a_Normal, 0.0))); // 变换法线 vec3 lightDir = normalize(lightPos - modelPos); float diffuse = max(dot(normal, lightDir), 0.1); // 计算漫反射强度,0.1是环境光 v_Color = vec3(1.0, 0.5, 0.0) * diffuse; // 物体的颜色乘以光照强度 }2. 纹理映射
与 2D 类似,但需要为 3D 模型的每个顶点指定正确的纹理坐标。这通常在 3D 建模软件中完成。
3. 混合(Blending)
用于实现透明效果(如玻璃、水)。需要开启混合并设置混合函数
javaGLES20.glEnable(GLES20.GL_BLEND); GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); // 注意:绘制透明物体时,通常需要从远到近排序,否则混合结果可能不正确。
(三)在 Android 中的实现步骤
框架与 2D 完全相同(
GLSurfaceView+Renderer),主要区别在于onDrawFrame中的逻辑。
数据:顶点数据从 2D 矩形变为 3D 模型(如立方体、球体)。一个立方体至少需要 36 个顶点(6个面 * 2个三角形 * 3个顶点)。
着色器:顶点着色器必须包含 MVP 矩阵变换。通常会加入光照计算。
绘制:清屏时必须同时清空颜色和深度缓冲区。
矩阵管理 :需要在 Java 端(使用
android.opengl.Matrix)为每个物体计算其模型矩阵、视图矩阵和投影矩阵,并组合成 MVP 矩阵传入着色器。一个简单的 3D 立方体渲染器
onDrawFrame示例:
java@Override public void onDrawFrame(GL10 gl) { GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); // 计算相机视角和投影(通常放在 onSurfaceChanged 或当屏幕旋转时计算) Matrix.setLookAtM(mViewMatrix, 0, 0, 0, 0, 0f, 0f, -5f, 0f, 1.0f, 0.0f); Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 1, 10); // 为当前绘制的立方体计算模型矩阵(例如,让它旋转) Matrix.setIdentityM(mModelMatrix, 0); Matrix.translateM(mModelMatrix, 0, 0, 0, -5.0f); // 移到视野内 Matrix.rotateM(mModelMatrix, 0, mAngle, 0.0f, 1.0f, 0.0f); // 绕 y 轴旋转 mAngle += 1.0f; // 更新旋转角度 // 计算 MVP 矩阵 Matrix.multiplyMM(mMVPMatrix, 0, mViewMatrix, 0, mModelMatrix, 0); Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mMVPMatrix, 0); // ... 其余部分(使用程序、传递顶点数据、传递 MVP 矩阵、绑定纹理、绘制)与 2D 类似 GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 36); // 立方体有 36 个顶点 }
(四)进阶 3D 技术
加载复杂模型 :使用
AssetManager读取.obj文件,解析顶点、法线、纹理坐标。多遍渲染:先渲染场景到纹理(使用 FBO),再进行后处理(如模糊、Bloom 效果)。
天空盒:一个巨大的立方体包围整个场景,模拟天空或远山。
粒子系统:用于火焰、烟雾、魔法效果。
骨骼动画:让 3D 角色动起来。
着色器特效:如法线贴图(增加表面细节)、凹凸贴图、卡通渲染等。

