用GL_TRIANGLE_STRIP画矩形:OpenGL ES顶点复用技巧与纹理映射实战

📅 发布时间:2026/7/9 7:13:20 👁️ 浏览次数:
用GL_TRIANGLE_STRIP画矩形:OpenGL ES顶点复用技巧与纹理映射实战
用GL_TRIANGLE_STRIP画矩形OpenGL ES顶点复用技巧与纹理映射实战在移动端图形开发中性能优化是个永恒的话题。当你在开发一个2D游戏界面或者实现一个实时视频滤镜时渲染效率直接决定了用户体验的流畅度。很多开发者习惯用GL_TRIANGLES模式绘制矩形——六个顶点两个三角形逻辑清晰但数据冗余。其实OpenGL ES提供了更高效的绘制模式比如GL_TRIANGLE_STRIP三角形条带它可以用四个顶点就画出一个完整的矩形将顶点数据量减少三分之一。这不仅仅是节省几个字节内存那么简单在顶点着色器执行、数据总线传输、GPU缓存利用等多个环节都能带来可观的性能提升。今天我们就深入探讨这个技巧并结合纹理映射的实战细节让你彻底掌握如何在保证视觉效果的同时榨干硬件的每一分性能。1. 理解三角形条带为何四个顶点就够了要理解GL_TRIANGLE_STRIP的魔力得先看看传统的GL_TRIANGLES是怎么画矩形的。一个矩形由两个三角形组成通常需要定义六个顶点坐标。即使两个三角形共享两个顶点在顶点数组中我们通常也得重复定义它们因为GL_TRIANGLES模式每三个顶点独立构成一个三角形。// GL_TRIANGLES 方式绘制矩形需要6个顶点 GLfloat vertices[] { // 第一个三角形 -1.0f, -1.0f, // 左下 1.0f, -1.0f, // 右下 -1.0f, 1.0f, // 左上 // 第二个三角形 1.0f, -1.0f, // 右下重复 -1.0f, 1.0f, // 左上重复 1.0f, 1.0f // 右上 };而GL_TRIANGLE_STRIP采用了一种巧妙的顶点复用机制。它从第三个顶点开始每个新顶点与前两个顶点一起构成一个新的三角形。对于矩形这种由两个三角形组成的凸多边形顶点顺序安排得当四个点就足够了。// GL_TRIANGLE_STRIP 方式绘制矩形只需要4个顶点 GLfloat vertices[] { -1.0f, -1.0f, // v0: 左下 1.0f, -1.0f, // v1: 右下 -1.0f, 1.0f, // v2: 左上 1.0f, 1.0f // v3: 右上 };绘制过程是这样的顶点v0、v1、v2构成第一个三角形左下、右下、左上顶点v1、v2、v3构成第二个三角形右下、左上、右上注意顶点顺序至关重要。在GL_TRIANGLE_STRIP中三角形的缠绕顺序用于确定正面会交替变化。第一个三角形是v0→v1→v2逆时针第二个三角形则是v2→v1→v3顺时针。如果开启了背面剔除GL_CULL_FACE需要确保所有三角形朝向一致或者直接关闭背面剔除。这种复用机制在绘制连续表面时优势明显。想象一下绘制一个网格地形或者一个复杂的UI面板顶点复用率越高传输到GPU的数据就越少顶点着色器的执行次数也相应减少。对于移动设备有限的带宽和算力这种优化往往能带来帧率的显著提升。2. 坐标系对齐顶点、纹理与屏幕的映射关系用四个顶点画出矩形只是第一步要让纹理正确贴上去还得理清三个坐标系的关系顶点坐标系、纹理坐标系以及最终显示的屏幕坐标系。很多开发者在这里栽跟头特别是处理图像上下颠倒的问题。2.1 三大坐标系详解顶点坐标系归一化设备坐标这是OpenGL ES的核心坐标系范围是[-1, 1]的立方体空间。原点在中心X轴向右为正Y轴向上为正。我们定义的顶点坐标都在这个空间内。// 覆盖整个视口的矩形顶点坐标 GLfloat vertexVertices[] { -1.0f, -1.0f, // 左下 1.0f, -1.0f, // 右下 -1.0f, 1.0f, // 左上 1.0f, 1.0f // 右上 };纹理坐标系纹理坐标定义了如何从纹理图像中采样范围是[0, 1]。原点在左下角这是OpenGL ES的标准约定。X轴向右Y轴向上。// 对应整个纹理的纹理坐标 GLfloat textureVertices[] { 0.0f, 0.0f, // 纹理左下角 1.0f, 0.0f, // 纹理右下角 0.0f, 1.0f, // 纹理左上角 1.0f, 1.0f // 纹理右上角 };屏幕坐标系视口坐标这是最终像素显示的位置由glViewport函数定义。原点通常在左下角OpenGL标准但在Android等平台上SurfaceView或TextureView的坐标系原点可能在左上角这需要特别注意。2.2 图像翻转问题与解决方案为什么按照上面的坐标定义图像经常是倒着的根源在于图像数据的存储顺序和OpenGL纹理坐标系的不匹配。大多数图像格式如PNG、JPEG的数据是从左上角开始存储的而OpenGL纹理采样默认从左下角开始。有几种常见的解决方案方案一翻转纹理坐标的Y轴这是最直接的方法在传递纹理坐标时就把Y值翻转。// 翻转纹理坐标Y值 GLfloat textureVerticesFlipped[] { 0.0f, 1.0f, // 原左上角 - 现左下角 1.0f, 1.0f, // 原右上角 - 现右下角 0.0f, 0.0f, // 原左下角 - 现左上角 1.0f, 0.0f // 原右下角 - 现右上角 };方案二在着色器中翻转如果不想修改CPU端的坐标数据可以在片元着色器中处理// 顶点着色器 attribute vec2 aTexCoord; varying vec2 vTexCoord; void main() { // ... 其他计算 vTexCoord aTexCoord; } // 片元着色器 varying vec2 vTexCoord; uniform sampler2D uTexture; void main() { // 在采样前翻转Y坐标 vec2 flippedCoord vec2(vTexCoord.x, 1.0 - vTexCoord.y); gl_FragColor texture2D(uTexture, flippedCoord); }方案三预处理图像数据加载图像时直接翻转一劳永逸。很多图像加载库都支持这个功能// 使用stb_image示例 #define STB_IMAGE_IMPLEMENTATION #include stb_image.h // 加载时垂直翻转 stbi_set_flip_vertically_on_load(true); int width, height, channels; unsigned char *data stbi_load(image.jpg, width, height, channels, 0);在实际项目中我通常推荐方案一因为它不增加着色器的计算负担也不影响图像加载性能。特别是对于视频流处理在CPU端一次性处理好坐标映射比在片元着色器中对每一帧都进行翻转计算要高效得多。3. 完整渲染管线配置从数据到屏幕理解了基本原理后我们来看一个完整的渲染流程。这里以Android平台上的OpenGL ES 2.0为例展示如何用GL_TRIANGLE_STRIP绘制带纹理的矩形。3.1 顶点与纹理数据准备首先定义顶点和纹理坐标数组。注意顺序要匹配GL_TRIANGLE_STRIP的要求。// Java代码示例 public class RectangleRenderer { // 顶点坐标NDC空间 private static final float[] VERTEX_COORDS { -1.0f, -1.0f, // 左下 1.0f, -1.0f, // 右下 -1.0f, 1.0f, // 左上 1.0f, 1.0f // 右上 }; // 纹理坐标考虑Y轴翻转 private static final float[] TEXTURE_COORDS { 0.0f, 1.0f, // 对应图像左上角 1.0f, 1.0f, // 对应图像右上角 0.0f, 0.0f, // 对应图像左下角 1.0f, 0.0f // 对应图像右下角 }; // 将Java数组转换为Native需要的Buffer private FloatBuffer vertexBuffer; private FloatBuffer textureBuffer; public void initBuffers() { ByteBuffer bb ByteBuffer.allocateDirect(VERTEX_COORDS.length * 4); bb.order(ByteOrder.nativeOrder()); vertexBuffer bb.asFloatBuffer(); vertexBuffer.put(VERTEX_COORDS); vertexBuffer.position(0); ByteBuffer tb ByteBuffer.allocateDirect(TEXTURE_COORDS.length * 4); tb.order(ByteOrder.nativeOrder()); textureBuffer tb.asFloatBuffer(); textureBuffer.put(TEXTURE_COORDS); textureBuffer.position(0); } }3.2 GLSL着色器编写顶点着色器相对简单主要工作是传递坐标和纹理坐标// vertex_shader.glsl attribute vec4 aPosition; attribute vec2 aTexCoord; varying vec2 vTexCoord; void main() { gl_Position aPosition; vTexCoord aTexCoord; }片元着色器负责纹理采样。这里展示一个基础版本// fragment_shader.glsl precision mediump float; varying vec2 vTexCoord; uniform sampler2D uTexture; void main() { gl_FragColor texture2D(uTexture, vTexCoord); }如果需要在片元着色器中处理Y轴翻转可以这样修改// fragment_shader_flip.glsl precision mediump float; varying vec2 vTexCoord; uniform sampler2D uTexture; void main() { // 在着色器内部翻转Y坐标 vec2 flippedCoord vec2(vTexCoord.x, 1.0 - vTexCoord.y); gl_FragColor texture2D(uTexture, flippedCoord); }3.3 程序链接与属性绑定有了着色器源码下一步是编译、链接并获取属性位置public class RectangleRenderer { private int mProgram; private int mPositionHandle; private int mTexCoordHandle; private int mTextureHandle; public void compileShaders() { // 编译顶点着色器 int vertexShader loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode); // 编译片元着色器 int fragmentShader loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode); // 创建程序并链接 mProgram GLES20.glCreateProgram(); GLES20.glAttachShader(mProgram, vertexShader); GLES20.glAttachShader(mProgram, fragmentShader); GLES20.glLinkProgram(mProgram); // 检查链接状态 int[] linkStatus new int[1]; GLES20.glGetProgramiv(mProgram, GLES20.GL_LINK_STATUS, linkStatus, 0); if (linkStatus[0] ! GLES20.GL_TRUE) { String error GLES20.glGetProgramInfoLog(mProgram); GLES20.glDeleteProgram(mProgram); throw new RuntimeException(Program link failed: error); } // 获取属性位置 mPositionHandle GLES20.glGetAttribLocation(mProgram, aPosition); mTexCoordHandle GLES20.glGetAttribLocation(mProgram, aTexCoord); mTextureHandle GLES20.glGetUniformLocation(mProgram, uTexture); } private int loadShader(int type, String shaderCode) { int shader GLES20.glCreateShader(type); GLES20.glShaderSource(shader, shaderCode); GLES20.glCompileShader(shader); int[] compiled new int[1]; GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); if (compiled[0] 0) { String error GLES20.glGetShaderInfoLog(shader); GLES20.glDeleteShader(shader); throw new RuntimeException(Shader compilation failed: error); } return shader; } }3.4 纹理创建与绑定纹理处理是OpenGL ES中的关键环节。正确的纹理参数设置能避免很多渲染问题。public class RectangleRenderer { private int mTextureId; public void setupTexture(Bitmap bitmap) { // 生成纹理ID int[] textures new int[1]; GLES20.glGenTextures(1, textures, 0); mTextureId textures[0]; // 绑定到纹理单元 GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId); // 设置纹理参数 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); // 加载位图数据到纹理 GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0); // 解绑纹理可选但好习惯 GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); } }这里有几个关键参数需要注意参数可选值作用GL_TEXTURE_MIN_FILTERGL_NEAREST,GL_LINEAR等纹理缩小时的采样方式GL_TEXTURE_MAG_FILTERGL_NEAREST,GL_LINEAR纹理放大时的采样方式GL_TEXTURE_WRAP_SGL_CLAMP_TO_EDGE,GL_REPEAT等纹理在S轴水平的环绕方式GL_TEXTURE_WRAP_TGL_CLAMP_TO_EDGE,GL_REPEAT等纹理在T轴垂直的环绕方式对于大多数2D渲染GL_LINEAR过滤配合GL_CLAMP_TO_EDGE环绕是不错的选择。GL_LINEAR会进行双线性插值让纹理在缩放时更平滑GL_CLAMP_TO_EDGE能防止纹理在边缘处产生不希望的重复。3.5 绘制调用一切准备就绪后绘制调用就很简单了public class RectangleRenderer { public void drawFrame() { // 清屏 GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); // 使用程序 GLES20.glUseProgram(mProgram); // 启用并设置顶点属性 GLES20.glEnableVertexAttribArray(mPositionHandle); GLES20.glVertexAttribPointer(mPositionHandle, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer); // 启用并设置纹理属性 GLES20.glEnableVertexAttribArray(mTexCoordHandle); GLES20.glVertexAttribPointer(mTexCoordHandle, 2, GLES20.GL_FLOAT, false, 0, textureBuffer); // 激活纹理单元并绑定纹理 GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId); GLES20.glUniform1i(mTextureHandle, 0); // 0对应GL_TEXTURE0 // 绘制调用 - 关键在这里 GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); // 禁用属性好习惯 GLES20.glDisableVertexAttribArray(mPositionHandle); GLES20.glDisableVertexAttribArray(mTexCoordHandle); } }注意glDrawArrays的调用第一个参数是GL_TRIANGLE_STRIP第二个参数是起始索引0第三个参数是顶点数量4。这就是用四个顶点绘制两个三角形的核心调用。4. 性能优化与实战技巧掌握了基础绘制后我们来看看如何在实际项目中优化性能。顶点复用只是开始真正的性能提升来自对渲染管线的深入理解。4.1 顶点缓冲区对象VBO的使用上面的示例使用了客户端顶点数组每次绘制时数据都需要从CPU内存传到GPU。对于静态或很少变化的几何体使用VBO能显著提升性能。public class RectangleRendererVBO { private int[] vboIds new int[2]; // 0: 顶点VBO, 1: 纹理VBO public void setupVBO() { // 生成VBO GLES20.glGenBuffers(2, vboIds, 0); // 绑定顶点VBO并上传数据 GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboIds[0]); ByteBuffer bb ByteBuffer.allocateDirect(VERTEX_COORDS.length * 4); bb.order(ByteOrder.nativeOrder()); FloatBuffer vertexBuffer bb.asFloatBuffer(); vertexBuffer.put(VERTEX_COORDS); vertexBuffer.position(0); GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, VERTEX_COORDS.length * 4, vertexBuffer, GLES20.GL_STATIC_DRAW); // 绑定纹理坐标VBO并上传数据 GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboIds[1]); ByteBuffer tb ByteBuffer.allocateDirect(TEXTURE_COORDS.length * 4); tb.order(ByteOrder.nativeOrder()); FloatBuffer texBuffer tb.asFloatBuffer(); texBuffer.put(TEXTURE_COORDS); texBuffer.position(0); GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, TEXTURE_COORDS.length * 4, texBuffer, GLES20.GL_STATIC_DRAW); // 解绑 GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); } public void drawWithVBO() { // ... 清屏、使用程序等 // 使用VBO设置顶点属性 GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboIds[0]); GLES20.glVertexAttribPointer(mPositionHandle, 2, GLES20.GL_FLOAT, false, 0, 0); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboIds[1]); GLES20.glVertexAttribPointer(mTexCoordHandle, 2, GLES20.GL_FLOAT, false, 0, 0); // 绘制 GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); // 解绑VBO GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); } }VBO将顶点数据存储在GPU内存中避免了每次绘制时的数据传输。对于复杂的场景这种优化效果非常明显。4.2 纹理图集与批处理在2D游戏或UI渲染中经常需要绘制多个带纹理的矩形。如果每个矩形都单独调用绘制命令会产生大量的OpenGL状态切换和绘制调用开销。纹理图集和批处理是解决这个问题的经典方案。纹理图集是将多个小纹理打包到一张大纹理中。比如一个游戏界面的所有图标、按钮都可以放在一张1024x1024的纹理中。// 假设图集包含4个图标每个256x256 // 图标0的纹理坐标位于图集左上角 float icon0TexCoords[] { 0.0f, 0.75f, // 左下 (0/1024, 256/1024) 0.25f, 0.75f, // 右下 (256/1024, 256/1024) 0.0f, 1.0f, // 左上 (0/1024, 0/1024) 0.25f, 1.0f // 右上 (256/1024, 0/1024) }; // 图标1的纹理坐标位于图集右上角 float icon1TexCoords[] { 0.75f, 0.75f, 1.0f, 0.75f, 0.75f, 1.0f, 1.0f, 1.0f };批处理是将多个矩形的顶点数据合并到一个大的顶点数组中然后一次性绘制。结合GL_TRIANGLE_STRIP我们可以用更少的绘制调用渲染大量矩形。public class BatchRenderer { // 批处理10个矩形每个矩形4个顶点每个顶点2个位置坐标2个纹理坐标 private static final int BATCH_SIZE 10; private static final int VERTICES_PER_RECT 4; private static final int FLOATS_PER_VERTEX 4; // x, y, s, t private FloatBuffer batchBuffer; private float[] batchData new float[BATCH_SIZE * VERTICES_PER_RECT * FLOATS_PER_VERTEX]; public void addRectToBatch(float x, float y, float width, float height, float s1, float t1, float s2, float t2) { // 计算矩形的四个顶点位置纹理坐标 // 左下 batchData[currentIndex] x; batchData[currentIndex] y; batchData[currentIndex] s1; batchData[currentIndex] t2; // 右下 batchData[currentIndex] x width; batchData[currentIndex] y; batchData[currentIndex] s2; batchData[currentIndex] t2; // 左上 batchData[currentIndex] x; batchData[currentIndex] y height; batchData[currentIndex] s1; batchData[currentIndex] t1; // 右上 batchData[currentIndex] x width; batchData[currentIndex] y height; batchData[currentIndex] s2; batchData[currentIndex] t1; } public void drawBatch() { if (currentIndex 0) return; // 上传批处理数据到VBO batchBuffer.put(batchData, 0, currentIndex); batchBuffer.position(0); // 设置交错属性位置和纹理坐标交错存储 int stride FLOATS_PER_VERTEX * 4; // 字节数 GLES20.glVertexAttribPointer(mPositionHandle, 2, GLES20.GL_FLOAT, false, stride, batchBuffer); GLES20.glVertexAttribPointer(mTexCoordHandle, 2, GLES20.GL_FLOAT, false, stride, batchBuffer.position(2)); // 一次性绘制所有矩形 GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, currentIndex / FLOATS_PER_VERTEX); } }提示在批处理中如果矩形之间没有共享顶点使用GL_TRIANGLE_STRIP需要插入退化三角形degenerate triangles来连接不连续的条带。或者对于完全独立的矩形使用GL_TRIANGLES配合索引缓冲区IBO/EBO可能更合适。4.3 着色器优化技巧即使是简单的纹理渲染着色器也有优化空间。以下是一些实用技巧避免不必要的计算如果顶点位置不需要变换直接在CPU端计算好不要传到着色器里做矩阵乘法。使用适当的精度限定符在片元着色器中对颜色计算使用mediump通常就足够了比highp更快。precision mediump float; // 大多数情况够用 // precision highp float; // 需要高精度时再用减少纹理采样次数如果多个片元着色器需要同一纹理确保只绑定一次。使用discard谨慎片元着色器中的discard指令会严重影响GPU的早期深度测试优化尽量避免使用。4.4 调试与性能分析优化离不开测量。OpenGL ES提供了一些性能查询工具// 检查OpenGL错误 public static void checkGlError(String op) { int error; while ((error GLES20.glGetError()) ! GLES20.GL_NO_ERROR) { Log.e(OpenGL, op : glError error); } } // 在关键操作后调用 checkGlError(glDrawArrays);对于更深入的性能分析可以使用Android GPU Inspector或厂商提供的工具如Mali Graphics Debugger、Adreno Profiler。这些工具可以显示每一帧的绘制调用次数、纹理上传量、着色器执行时间等关键指标。在实际项目中我遇到过这样一个案例一个2D游戏界面有50多个UI元素最初每个元素单独绘制帧率只有30fps。通过实施纹理图集和批处理将绘制调用从50减少到2-3次帧率提升到稳定的60fps。GL_TRIANGLE_STRIP的顶点复用在这个优化中起到了关键作用因为每个UI元素只需要4个顶点而不是6个整个批处理的顶点数据量减少了三分之一。5. 进阶应用视频滤镜与特效掌握了基础矩形绘制后我们可以扩展到更复杂的应用比如实时视频滤镜。视频的每一帧本质上就是一个纹理我们需要在矩形上应用各种片元着色器特效。5.1 视频纹理的特殊处理视频纹理通常使用GL_TEXTURE_EXTERNAL_OES扩展而不是标准的GL_TEXTURE_2D。这需要一些特殊的处理// 顶点着色器不变 #version 100 attribute vec4 aPosition; attribute vec2 aTexCoord; varying vec2 vTexCoord; void main() { gl_Position aPosition; vTexCoord aTexCoord; } // 片元着色器需要声明扩展 #version 100 #extension GL_OES_EGL_image_external : require precision mediump float; varying vec2 vTexCoord; uniform samplerExternalOES uTexture; void main() { vec4 color texture2D(uTexture, vTexCoord); // 应用滤镜效果 gl_FragColor color; }在Java代码中创建和绑定纹理的步骤也有所不同public class VideoRenderer { private int createOesTexture() { int[] textures new int[1]; GLES20.glGenTextures(1, textures, 0); int textureId textures[0]; GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); return textureId; } }5.2 常见滤镜实现基于矩形渲染我们可以实现各种滤镜效果。以下是一些示例灰度滤镜void main() { vec4 color texture2D(uTexture, vTexCoord); float gray dot(color.rgb, vec3(0.299, 0.587, 0.114)); gl_FragColor vec4(gray, gray, gray, color.a); }反色滤镜void main() { vec4 color texture2D(uTexture, vTexCoord); gl_FragColor vec4(1.0 - color.r, 1.0 - color.g, 1.0 - color.b, color.a); }边缘检测Sobel算子uniform float uWidth; uniform float uHeight; void main() { vec2 texelSize vec2(1.0 / uWidth, 1.0 / uHeight); // Sobel算子卷积核 float kernelX[9] float[](-1.0, 0.0, 1.0, -2.0, 0.0, 2.0, -1.0, 0.0, 1.0); float kernelY[9] float[](-1.0, -2.0, -1.0, 0.0, 0.0, 0.0, 1.0, 2.0, 1.0); float gradientX 0.0; float gradientY 0.0; int index 0; for (int y -1; y 1; y) { for (int x -1; x 1; x) { vec2 offset vec2(float(x) * texelSize.x, float(y) * texelSize.y); vec4 sample texture2D(uTexture, vTexCoord offset); float luminance dot(sample.rgb, vec3(0.299, 0.587, 0.114)); gradientX luminance * kernelX[index]; gradientY luminance * kernelY[index]; index; } } float edge sqrt(gradientX * gradientX gradientY * gradientY); gl_FragColor vec4(edge, edge, edge, 1.0); }5.3 多遍渲染与帧缓冲区对象FBO复杂的特效往往需要多遍渲染。比如模糊效果通常需要先水平模糊再垂直模糊。这时就需要使用帧缓冲区对象FBO作为离屏渲染目标。public class BlurEffect { private int[] fboIds new int[1]; private int[] textureIds new int[2]; // 用于乒乓缓冲 public void setupFBO(int width, int height) { // 生成FBO GLES20.glGenFramebuffers(1, fboIds, 0); // 生成两个纹理用于乒乓缓冲 GLES20.glGenTextures(2, textureIds, 0); for (int i 0; i 2; i) { GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIds[i]); GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null); // 设置纹理参数... } // 绑定第一个纹理到FBO的颜色附件 GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboIds[0]); GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, textureIds[0], 0); // 检查FBO完整性 int status GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER); if (status ! GLES20.GL_FRAMEBUFFER_COMPLETE) { throw new RuntimeException(Framebuffer not complete: status); } GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); } public void applyBlur() { // 第一遍水平模糊到FBO GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboIds[0]); GLES20.glViewport(0, 0, width, height); // 使用水平模糊着色器绘制到textureIds[0] drawWithShader(horizontalBlurShader, inputTexture); // 第二遍垂直模糊回屏幕 GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); GLES20.glViewport(0, 0, screenWidth, screenHeight); // 使用垂直模糊着色器以textureIds[0]为输入 drawWithShader(verticalBlurShader, textureIds[0]); } }在多遍渲染中GL_TRIANGLE_STRIP的高效性更加明显。每一遍渲染都需要绘制全屏矩形顶点复用减少了数据传递开销对于需要实时处理视频帧的应用这种优化是必不可少的。5.4 性能考量与移动端适配在移动设备上实现实时视频滤镜时性能是首要考虑因素。以下是一些经验之谈分辨率适配不需要总是以原始分辨率处理。对于预览640x360或720p通常就足够了比1080p或4K节省大量计算。着色器复杂度移动GPU的算力有限复杂的片元着色器特别是包含大量纹理采样或循环的很容易成为瓶颈。尽量简化算法或者使用查找表LUT等技术。发热与功耗长时间运行复杂的图形处理会导致设备发热和电池快速消耗。考虑动态调整效果质量或者在不需要时完全关闭某些效果。多线程渲染Android上可以使用GLSurfaceView的RENDERMODE_WHEN_DIRTY模式只在有更新时渲染减少不必要的GPU负载。从实际项目经验来看一个中等复杂度的视频滤镜如美颜或风格化滤镜在主流手机上以720p分辨率处理配合GL_TRIANGLE_STRIP的优化绘制通常可以稳定在30fps以上。如果效果过于复杂导致帧率下降可以考虑分帧处理或降低渲染分辨率。