安卓进阶——OpenGL ES

✅作者简介:大家好,我是 Meteors., 向往着更加简洁高效的代码写法与编程方式,持续分享Java技术内容。
🍎个人主页:Meteors.的博客

💞当前专栏:知识分享

✨特色专栏:知识分享
🥭本文内容:安卓进阶------OpenGL ES
📚 ** ps ** :阅读文章如果有问题或者疑惑,欢迎在评论区提问或指出。


目录

一、2D

(一)优点

[(二) 渲染核心概念](#(二) 渲染核心概念)

[1. 顶点(Vertices)](#1. 顶点(Vertices))

[2. 纹理坐标(Texture Coordinates)](#2. 纹理坐标(Texture Coordinates))

[3. 着色器(Shaders)](#3. 着色器(Shaders))

(1)顶点着色器

(2)片段着色器

[4. 程序(Program)](#4. 程序(Program))

[(三)在 Android 中的基本使用步骤](#(三)在 Android 中的基本使用步骤)

[1. 创建渲染表面(GLSurfaceView)](#1. 创建渲染表面(GLSurfaceView))

[2. 实现渲染器(GLSurfaceView.Renderer)](#2. 实现渲染器(GLSurfaceView.Renderer))

[(四)进阶 2D 技巧](#(四)进阶 2D 技巧)

二、3D

(一)介绍

[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)

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

  1. 纹理滤镜(Filtering) :当纹理被放大或缩小时,如何采样。GL_LINEAR(平滑)和 GL_NEAREST(像素化)。

  2. 纹理环绕(Wrapping) :当纹理坐标超出 [0, 1]时如何处理。GL_REPEAT(重复)和 GL_CLAMP_TO_EDGE(拉伸边缘)。

  3. 矩阵变换(Matrix Transformations) :在顶点着色器中使用模型(Model)、视图(View)、投影(Projection)矩阵来控制物体的位置、旋转、缩放以及相机视角。这是实现 2D 动画和复杂场景的基石。可以借助 android.opengl.Matrix类进行矩阵计算。

  4. 粒子系统(Particle Systems):用于实现火焰、烟雾、爆炸等效果,通过一次提交大量顶点并利用着色器进行高效计算。

  5. 帧缓冲区(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.frustumMMatrix.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_MVPMatrix

3. 深度测试

在 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)包含三个部分:

  • 环境光:模拟场景中的间接光,均匀地照亮所有物体。

  • 漫反射:模拟来自一个方向的光源,其强度与表面法线和光线方向的夹角有关。这是物体呈现颜色的主要部分。

  • 镜面反射:模拟物体表面的高光,产生亮斑,与观察视角有关。

计算光照需要在顶点或片段着色器中进行,需要提供法线、光源位置、相机位置等信息。

一个简单的漫反射光照的顶点着色器示例

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

用于实现透明效果(如玻璃、水)。需要开启混合并设置混合函数

java 复制代码
GLES20.glEnable(GLES20.GL_BLEND);
GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
// 注意:绘制透明物体时,通常需要从远到近排序,否则混合结果可能不正确。

(三)在 Android 中的实现步骤

框架与 2D 完全相同(GLSurfaceView+ Renderer),主要区别在于 onDrawFrame中的逻辑。

  1. 数据:顶点数据从 2D 矩形变为 3D 模型(如立方体、球体)。一个立方体至少需要 36 个顶点(6个面 * 2个三角形 * 3个顶点)。

  2. 着色器:顶点着色器必须包含 MVP 矩阵变换。通常会加入光照计算。

  3. 绘制:清屏时必须同时清空颜色和深度缓冲区。

  4. 矩阵管理 :需要在 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 角色动起来。

  • 着色器特效:如法线贴图(增加表面细节)、凹凸贴图、卡通渲染等。

相关推荐
椰羊sqrt3 小时前
CVE-2025-4334 深度分析:WordPress wp-registration 插件权限提升漏洞
android·开发语言·okhttp·网络安全
2501_916008893 小时前
金融类 App 加密加固方法,多工具组合的工程化实践(金融级别/IPA 加固/无源码落地/Ipa Guard + 流水线)
android·ios·金融·小程序·uni-app·iphone·webview
sun0077004 小时前
Android设备推送traceroute命令
android
来来走走4 小时前
Android开发(Kotlin) 高阶函数、内联函数
android·开发语言·kotlin
2501_915921434 小时前
Fastlane 结合 开心上架(Appuploader)命令行版本实现跨平台上传发布 iOS App 免 Mac 自动化上架实战全解析
android·macos·ios·小程序·uni-app·自动化·iphone
雨白5 小时前
重识 Java IO、NIO 与 OkIO
android·java
啦啦9117146 小时前
Niagara Launcher 全新Android桌面启动器!给手机换个门面!
android·智能手机
游戏开发爱好者86 小时前
iOS 上架要求全解析,App Store 审核标准、开发者准备事项与开心上架(Appuploader)跨平台免 Mac 实战指南
android·macos·ios·小程序·uni-app·iphone·webview
xrkhy6 小时前
canal1.1.8+mysql8.0+jdk17+redis的使用
android·redis·adb