
GPUImageTwoInputFilter 代码全解析
代码整体概述
GPUImageTwoInputFilter 是基于 Android 平台 OpenGL ES 2.0 实现的双输入纹理滤镜基类 ,继承自 GPUImageFilter,核心作用是支持两个纹理(如两张图片)的混合渲染,是 GPUImage 安卓库中实现多纹理滤镜(如混合、叠加、遮罩等效果)的基础类。
以下是逐行注释的完整代码,并分段解析核心逻辑与使用方式。
逐行注释 + 分段解析
1. 版权与许可证声明(1-15行)
java
/*
* Copyright (C) 2018 CyberAgent, Inc. // 版权归属:CyberAgent 公司
*
* Licensed under the Apache License, Version 2.0 (the "License"); // 遵循 Apache 2.0 开源许可证
* you may not use this file except in compliance with the License. // 使用代码需遵守许可证条款
* You may obtain a copy of the License at // 许可证获取地址
*
* http://www.apache.org/licenses/LICENSE-2.0 // 许可证官方地址
*
* Unless required by applicable law or agreed to in writing, software // 除非法律要求或书面约定,否则
* distributed under the License is distributed on an "AS IS" BASIS, // 软件按"原样"分发
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // 不附带明示/默示的担保
* See the License for the specific language governing permissions and // 许可证定义了权限和限制的具体规则
* limitations under the License. // 限制条款参考许可证
*/
解析:开源代码标准的版权与许可证声明,明确代码的使用范围和约束,遵循 Apache 2.0 协议(允许商用、修改,需保留版权声明)。
2. 包导入(17-25行)
java
// 声明当前类所属包:GPUImage 滤镜核心包
package jp.co.cyberagent.android.gpuimage.filter;
import android.graphics.Bitmap; // 安卓位图类,用于承载第二张输入图片
import android.opengl.GLES20; // OpenGL ES 2.0 核心 API,用于纹理/着色器操作
import java.nio.ByteBuffer; // 字节缓冲区,用于存储纹理坐标数据
import java.nio.ByteOrder; // 字节序工具,保证跨平台数据一致性
import java.nio.FloatBuffer; // 浮点缓冲区,辅助处理纹理坐标
import jp.co.cyberagent.android.gpuimage.util.OpenGlUtils; // OpenGL 工具类(加载纹理、销毁纹理等)
import jp.co.cyberagent.android.gpuimage.util.Rotation; // 旋转枚举(正常、90度、180度、270度)
import jp.co.cyberagent.android.gpuimage.util.TextureRotationUtil; // 纹理坐标旋转/翻转工具类
解析:导入实现双输入滤镜所需的核心依赖:
- 安卓基础:
Bitmap用于承载第二张输入图片; - OpenGL 核心:
GLES20是安卓操作 OpenGL ES 2.0 的核心类; - 缓冲区:
ByteBuffer/FloatBuffer用于存储 OpenGL 所需的纹理坐标数据(OpenGL 不直接使用 Java 数组); - 工具类:
OpenGlUtils封装纹理加载/销毁,Rotation/TextureRotationUtil处理纹理旋转/翻转。
3. 类定义 + 顶点着色器常量(27-45行)
java
// 双输入滤镜基类,继承自 GPUImageFilter(单输入滤镜基类)
public class GPUImageTwoInputFilter extends GPUImageFilter {
// 顶点着色器源码:负责将顶点坐标、两个纹理坐标传递给片段着色器
private static final String VERTEX_SHADER = "attribute vec4 position;\n" + // 顶点位置属性(由外部传入)
"attribute vec4 inputTextureCoordinate;\n" + // 第一个纹理的坐标属性
"attribute vec4 inputTextureCoordinate2;\n" + // 第二个纹理的坐标属性
" \n" +
"varying vec2 textureCoordinate;\n" + // 传递给片段着色器的第一个纹理坐标(易变变量)
"varying vec2 textureCoordinate2;\n" + // 传递给片段着色器的第二个纹理坐标
" \n" +
"void main()\n" + // 顶点着色器主函数
"{\n" +
" gl_Position = position;\n" + // 设置顶点最终位置(OpenGL 裁剪空间坐标)
" textureCoordinate = inputTextureCoordinate.xy;\n" + // 传递第一个纹理坐标(仅取xy分量)
" textureCoordinate2 = inputTextureCoordinate2.xy;\n" + // 传递第二个纹理坐标
"}";
解析:
- 类继承:
GPUImageTwoInputFilter扩展了单输入的GPUImageFilter,核心是新增第二个纹理的处理逻辑; - 顶点着色器:OpenGL 渲染分为"顶点着色器"和"片段着色器",此处顶点着色器的作用是:
- 接收 3 个属性(顶点位置、纹理1坐标、纹理2坐标);
- 将两个纹理坐标通过
varying变量传递给片段着色器; - 设置顶点最终显示位置(
gl_Position)。
4. 成员变量定义(47-52行)
java
// 第二个纹理坐标的属性句柄(关联着色器中的 inputTextureCoordinate2)
private int filterSecondTextureCoordinateAttribute;
// 第二个纹理的统一变量句柄(关联着色器中的 inputImageTexture2)
private int filterInputTextureUniform2;
// 第二个纹理的 ID(默认值 NO_TEXTURE 表示无纹理)
private int filterSourceTexture2 = OpenGlUtils.NO_TEXTURE;
// 第二个纹理的坐标缓冲区(存储旋转/翻转后的纹理坐标)
private ByteBuffer texture2CoordinatesBuffer;
// 第二个输入图片的 Bitmap 实例
private Bitmap bitmap;
解析:核心成员变量用于管理第二个纹理的关键资源:
- 句柄(Attribute/Uniform):OpenGL 中通过"句柄"关联 Java 代码与着色器中的变量;
- 纹理 ID:每个纹理在 OpenGL 中有唯一 ID,
filterSourceTexture2是第二个纹理的标识; - 坐标缓冲区:纹理坐标需要转换为 ByteBuffer 才能被 OpenGL 识别,存储旋转后的坐标数据;
- Bitmap:承载第二个输入图片的内存对象。
5. 构造方法(54-60行)
java
// 构造方法1:仅传入片段着色器,顶点着色器使用默认的 VERTEX_SHADER
public GPUImageTwoInputFilter(String fragmentShader) {
this(VERTEX_SHADER, fragmentShader);
}
// 构造方法2:自定义顶点着色器 + 片段着色器,初始化时设置默认旋转(正常方向,不翻转)
public GPUImageTwoInputFilter(String vertexShader, String fragmentShader) {
super(vertexShader, fragmentShader); // 调用父类构造方法,初始化着色器程序
setRotation(Rotation.NORMAL, false, false); // 设置第二个纹理的默认旋转/翻转
}
解析:
- 构造方法重载:支持两种初始化方式,适配"默认顶点着色器"和"自定义顶点着色器"场景;
- 旋转初始化:默认设置第二个纹理为"正常方向、不水平/垂直翻转",保证纹理显示方向正确。
6. 初始化方法(onInit)(62-70行)
java
// 重写父类 onInit:着色器程序初始化时调用
@Override
public void onInit() {
super.onInit(); // 执行父类初始化(创建着色器程序、获取基础句柄等)
// 获取第二个纹理坐标属性的句柄(关联着色器中的 inputTextureCoordinate2)
filterSecondTextureCoordinateAttribute = GLES20.glGetAttribLocation(getProgram(), "inputTextureCoordinate2");
// 获取第二个纹理统一变量的句柄(关联着色器中的 inputImageTexture2)
filterInputTextureUniform2 = GLES20.glGetUniformLocation(getProgram(), "inputImageTexture2");
// 启用第二个纹理坐标的属性数组(OpenGL 必须启用才能传递数据)
GLES20.glEnableVertexAttribArray(filterSecondTextureCoordinateAttribute);
}
关键说明:
onInit时机:当 OpenGL 上下文创建、着色器程序编译链接完成后调用;- 核心操作:
- 获取着色器中第二个纹理相关变量的句柄(后续通过句柄传递数据);
- 启用纹理坐标属性数组(OpenGL 中 Attribute 变量需手动启用才能接收数据);
- 备注:片段着色器中第二个纹理必须命名为
inputImageTexture2,否则句柄获取失败。
7. 初始化完成回调(onInitialized)(72-77行)
java
// 重写父类 onInitialized:初始化完成后调用
@Override
public void onInitialized() {
super.onInitialized(); // 执行父类初始化完成逻辑
// 如果 Bitmap 不为空且未被回收,立即设置第二张纹理
if (bitmap != null && !bitmap.isRecycled()) {
setBitmap(bitmap);
}
}
解析:
- 执行时机:
onInit完成后调用,确保 OpenGL 环境完全就绪; - 逻辑:如果提前设置了 Bitmap(初始化前),此时立即将 Bitmap 加载为 OpenGL 纹理(避免初始化未完成导致纹理加载失败)。
8. Bitmap 设置方法(setBitmap)(79-100行)
java
// 设置第二个输入图片的 Bitmap
public void setBitmap(final Bitmap bitmap) {
// 校验:Bitmap 为空或已回收则直接返回
if (bitmap != null && bitmap.isRecycled()) {
return;
}
this.bitmap = bitmap; // 保存 Bitmap 引用
if (this.bitmap == null) {
return;
}
// 提交到 OpenGL 绘制线程执行(OpenGL 操作必须在 GL 线程)
runOnDraw(new Runnable() {
public void run() {
// 如果第二个纹理未创建(NO_TEXTURE),则加载纹理
if (filterSourceTexture2 == OpenGlUtils.NO_TEXTURE) {
// 二次校验 Bitmap 有效性
if (bitmap == null || bitmap.isRecycled()) {
return;
}
// 激活 GL_TEXTURE3 纹理单元(OpenGL 有多个纹理单元,此处选第3个)
GLES20.glActiveTexture(GLES20.GL_TEXTURE3);
// 加载 Bitmap 为 OpenGL 纹理,返回纹理 ID 并赋值给 filterSourceTexture2
filterSourceTexture2 = OpenGlUtils.loadTexture(bitmap, OpenGlUtils.NO_TEXTURE, false);
}
}
});
}
核心解析:
- 线程安全:
runOnDraw保证 OpenGL 操作在 GL 绘制线程执行(Android 中 OpenGL 上下文绑定到特定线程,跨线程操作会崩溃); - 纹理单元:OpenGL 通过"纹理单元"管理多个纹理(如 GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE3),此处使用 GL_TEXTURE3 避免与主纹理冲突;
- 纹理加载:
OpenGlUtils.loadTexture封装了"Bitmap → OpenGL 纹理"的转换逻辑,包括:创建纹理、绑定纹理、设置纹理参数、将 Bitmap 像素数据传入纹理。
9. Bitmap 辅助方法(getBitmap/recycleBitmap)(102-113行)
java
// 获取当前设置的第二个 Bitmap
public Bitmap getBitmap() {
return bitmap;
}
// 回收 Bitmap(释放内存,避免内存泄漏)
public void recycleBitmap() {
// 校验:Bitmap 不为空且未被回收时执行回收
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle(); // 回收 Bitmap 内存
bitmap = null; // 置空引用,帮助 GC 回收
}
}
解析:
getBitmap:提供外部获取当前第二个输入图片的入口;recycleBitmap:手动回收 Bitmap 内存,避免安卓中 Bitmap 占用过多内存导致 OOM(尤其图片较大时)。
10. 销毁方法(onDestroy)(115-122行)
java
// 重写父类 onDestroy:滤镜销毁时调用,释放资源
public void onDestroy() {
super.onDestroy(); // 执行父类销毁逻辑(释放着色器程序、主纹理等)
// 删除第二个纹理(传入纹理ID数组,索引从0开始)
GLES20.glDeleteTextures(1, new int[]{
filterSourceTexture2
}, 0);
// 重置纹理ID为 NO_TEXTURE,表示已销毁
filterSourceTexture2 = OpenGlUtils.NO_TEXTURE;
}
解析:
- 执行时机:滤镜不再使用时(如页面销毁、滤镜切换)调用;
- 核心操作:调用
glDeleteTextures释放 OpenGL 纹理资源(必须手动释放,否则会导致显存泄漏)。
11. 绘制前预处理(onDrawArraysPre)(124-132行)
java
// 重写父类 onDrawArraysPre:绘制前的预处理操作
@Override
protected void onDrawArraysPre() {
// 启用第二个纹理坐标属性数组(确保数据能传递到着色器)
GLES20.glEnableVertexAttribArray(filterSecondTextureCoordinateAttribute);
// 激活 GL_TEXTURE3 纹理单元(与 setBitmap 中一致)
GLES20.glActiveTexture(GLES20.GL_TEXTURE3);
// 绑定第二个纹理到 GL_TEXTURE_2D 目标(将纹理ID与纹理单元关联)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, filterSourceTexture2);
// 将纹理单元 3 赋值给片段着色器的 inputImageTexture2 变量
GLES20.glUniform1i(filterInputTextureUniform2, 3);
// 重置第二个纹理坐标缓冲区的指针到起始位置
texture2CoordinatesBuffer.position(0);
// 将纹理坐标缓冲区数据传递给着色器的 inputTextureCoordinate2 属性
GLES20.glVertexAttribPointer(filterSecondTextureCoordinateAttribute, 2, GLES20.GL_FLOAT, false, 0, texture2CoordinatesBuffer);
}
核心解析:
- 执行时机:每次绘制帧之前调用;
- 核心操作(OpenGL 多纹理渲染关键步骤):
- 激活纹理单元(GL_TEXTURE3)→ 绑定纹理(filterSourceTexture2)→ 将纹理单元编号(3)传递给片段着色器的 uniform 变量,完成"纹理 → 着色器"的关联;
- 将纹理坐标缓冲区的数据传递给着色器的 attribute 变量,让着色器知道如何采样第二个纹理的像素。
12. 旋转/翻转设置(setRotation)(134-145行)
java
// 设置第二个纹理的旋转/翻转,更新纹理坐标缓冲区
public void setRotation(final Rotation rotation, final boolean flipHorizontal, final boolean flipVertical) {
// 获取旋转/翻转后的纹理坐标数组(TextureRotationUtil 封装了坐标转换逻辑)
float[] buffer = TextureRotationUtil.getRotation(rotation, flipHorizontal, flipVertical);
// 创建字节缓冲区:容量32(8个浮点值 × 4字节/浮点),使用本地字节序(保证跨平台)
ByteBuffer bBuffer = ByteBuffer.allocateDirect(32).order(ByteOrder.nativeOrder());
// 转换为浮点缓冲区,用于存储坐标数据
FloatBuffer fBuffer = bBuffer.asFloatBuffer();
// 将旋转后的坐标数组放入浮点缓冲区
fBuffer.put(buffer);
// 翻转缓冲区指针到起始位置(准备读取)
fBuffer.flip();
// 保存更新后的纹理坐标缓冲区
texture2CoordinatesBuffer = bBuffer;
}
}
解析:
- 纹理坐标旋转:OpenGL 纹理坐标的原点在左下角,而安卓 Bitmap 原点在左上角,且图片旋转后需要调整坐标才能正确显示;
- 缓冲区创建:
allocateDirect创建直接缓冲区(不在 JVM 堆中),避免数据拷贝,提升 OpenGL 效率; - 坐标数组长度:纹理坐标是 4 个顶点 × 2 个分量(xy)= 8 个浮点值,因此缓冲区容量为 8×4=32 字节。
顶点着色器详解
**GPUImageTwoInputFilter(双输入纹理滤镜基类)**专属的顶点着色器(Vertex Shader),基于OpenGL ES 2.0的GLSL语言编写,是双纹理混合渲染的「顶点处理核心」。
它的核心作用是接收CPU传递的顶点位置、两个纹理的坐标数据,完成顶点最终渲染位置的设置,并将两个纹理坐标传递给片段着色器,是衔接CPU端顶点数据与GPU端片段着色器的关键桥梁。
顶点着色器的执行规则是:为绘制图形的每个顶点执行一次 (比如渲染图片的矩形会有4个顶点,该代码会执行4次),执行后通过「光栅化阶段」对纹理坐标做插值处理 ,最终让片段着色器能为图像的每个像素拿到对应的纹理坐标,实现精准的双纹理像素采样。
逐行注释
先附上全量逐行注释代码,再对每行的关键字、变量、设计逻辑做详细说明:
c
// 声明顶点位置属性:接收CPU端传递的顶点裁剪空间坐标
// attribute:GLSL属性变量,仅顶点着色器可用,由CPU逐顶点传值,不可修改
// vec4:四维向量,存储(x,y,z,w),OpenGL ES中顶点位置统一用vec4表示
attribute vec4 position;
// 声明第一个纹理的坐标属性:接收CPU端传递的、与当前顶点绑定的第一张纹理(基础图)UV坐标
attribute vec4 inputTextureCoordinate;
// 声明第二个纹理的坐标属性:接收CPU端传递的、与当前顶点绑定的第二张纹理(叠加图)UV坐标
// 【双输入滤镜核心差异】:比单输入滤镜多了这一个纹理坐标属性
attribute vec4 inputTextureCoordinate2;
// 声明易变变量:用于将第一个纹理坐标传递给片段着色器
// varying:易变变量,顶点着色器赋值后,经光栅化插值传递给片段着色器
// vec2:二维向量,存储纹理UV坐标(仅需x/y分量,范围0~1)
varying vec2 textureCoordinate;
// 声明易变变量:用于将第二个纹理坐标传递给片段着色器
varying vec2 textureCoordinate2;
// 顶点着色器主函数:所有顶点处理逻辑的入口,无参数无返回值
void main()
{
// 设置顶点最终渲染位置:将CPU传递的顶点位置赋值给OpenGL内置变量
// gl_Position:OpenGL ES内置输出变量,必须赋值,代表顶点在裁剪空间的最终位置
gl_Position = position;
// 提取第一个纹理坐标的x/y分量,赋值给易变变量传递给片段着色器
// .xy:GLSL向量分量提取语法,取vec4的前两个分量(x=U坐标,y=V坐标)
textureCoordinate = inputTextureCoordinate.xy;
// 提取第二个纹理坐标的x/y分量,赋值给对应易变变量
textureCoordinate2 = inputTextureCoordinate2.xy;
}
关键行设计细节补充
- 为什么用
vec4存储纹理坐标,却只取xy分量?
OpenGL ES中,attribute属性变量的类型设计通常与顶点位置(vec4)保持一致 ,目的是统一CPU端的数据传递格式 ,减少数据类型转换的开销。纹理坐标本质是二维(UV),因此仅需提取x/y分量,剩余的z/w分量会被忽略(默认值为0.0/1.0,不影响使用)。 - 为什么没有复杂的计算逻辑?
这段顶点着色器是通用型基础实现 ,仅完成「数据接收-传递」的核心功能,不做坐标变换(如旋转、翻转)。双纹理的坐标变换(如图片旋转90度、水平翻转)会在CPU端 完成(通过TextureRotationUtil计算变换后的坐标数组),再传递给该着色器,保证着色器的执行效率。
模块1:变量声明区(第1-8行)
负责声明所有需要的输入变量(attribute)和跨阶段传递变量(varying),是数据的「入口」和「传递通道」,变量命名完全遵循GPUImage框架的规范,保证与Java端代码的无缝衔接。
| 变量类型 | 变量名 | 核心作用 |
|---|---|---|
| attribute | position | 接收CPU传递的顶点裁剪空间坐标,决定顶点在屏幕中的最终位置 |
| attribute | inputTextureCoordinate | 接收第一张纹理(基础图)的顶点绑定坐标,是单/双输入滤镜的通用纹理坐标 |
| attribute | inputTextureCoordinate2 | 接收第二张纹理(叠加图)的顶点绑定坐标,双输入滤镜的专属核心变量 |
| varying | textureCoordinate | 将第一张纹理坐标传递给片段着色器,经光栅化插值后覆盖整个图像区域 |
| varying | textureCoordinate2 | 将第二张纹理坐标传递给片段着色器,与第一张坐标一一对应,实现双纹理像素对齐 |
模块2:主函数执行区(第10-16行)
顶点着色器的逻辑执行主体,仅做2件核心事,无任何多余计算,保证移动端GPU的执行效率:
- 设置顶点最终位置 :将CPU传递的
position赋值给OpenGL内置变量gl_Position,这是顶点着色器的必做操作,否则顶点无法在屏幕上渲染; - 传递纹理坐标 :提取两个纹理坐标的
xy分量,赋值给对应的varying变量,为片段着色器的双纹理采样做准备。
核心GLSL&OpenGL概念说明
理解这段代码的关键是掌握5个核心概念,这些是OpenGL ES着色器开发的基础,也是GPUImage框架的底层实现原理:
1. attribute(属性变量)
- 「作用范围」:仅顶点着色器可用,片段着色器无法访问;
- 「数据来源」:由CPU端(Java代码)通过OpenGL API逐顶点传递(如GPUImage中通过
glVertexAttribPointer传递顶点位置、纹理坐标); - 「核心特点」:每个顶点的
attribute值可以不同(比如4个顶点有4个不同的纹理坐标)。
2. varying(易变变量)
- 「作用范围」:顶点着色器赋值,片段着色器读取,是两个着色器之间的唯一数据传递通道;
- 「核心特点」:顶点着色器仅为4个顶点赋值
varying变量,光栅化阶段会对其做线性插值 ,最终让图像的每个像素 都能拿到对应的、平滑过渡的varying值(比如两个顶点之间的像素,纹理坐标会在顶点坐标之间平滑插值); - 「设计意义」:让片段着色器能为每个像素精准获取纹理坐标,实现像素级的纹理采样。
3. gl_Position(OpenGL内置输出变量)
- 「核心作用」:存储顶点在裁剪空间 的最终坐标,是OpenGL渲染管线的核心变量,必须在顶点着色器中赋值,否则顶点无法被光栅化;
- 「坐标类型」:
vec4(x,y,z,w),GPUImage中渲染图片时,z=0.0(2D渲染,无深度)、w=1.0(标准齐次坐标)。
4. 纹理坐标(UV坐标)
- 「表示形式」:
vec2(U,V),范围通常为0~1,左上角为(0,0),右下角为(1,1)(OpenGL ES纹理坐标系规范); - 「顶点绑定」:图形的每个顶点会绑定一个纹理坐标,比如矩形的4个顶点会绑定纹理的4个角坐标,保证纹理能精准映射到图形上;
- 「插值特性」:经光栅化插值后,图像的每个像素都会有一个唯一的纹理坐标,通过该坐标可从纹理中采样到对应的像素颜色。
5. 顶点着色器的执行规则
- 「执行次数」:绘制图形的每个顶点执行一次(如渲染图片的矩形有4个顶点,执行4次;三角形带绘制会按顶点数量依次执行);
- 「无像素处理」:顶点着色器不处理任何像素,仅处理顶点的位置和纹理坐标;
- 「轻量高效」:设计原则是「极简计算」,避免复杂逻辑,否则会影响整个渲染管线的效率(移动端GPU对顶点着色器的计算资源分配较少)。
着色器在双纹理渲染中的完整工作流程
这段代码不是孤立执行的,它是GPUImage双输入滤镜渲染链路的一环,结合CPU端代码和片段着色器,完整工作流程如下(以渲染一张图片+叠加一张图片为例):
步骤1:CPU端准备数据(Java层)
- 准备顶点位置数组:如矩形的4个顶点裁剪空间坐标,传递给OpenGL;
- 准备两个纹理坐标数组 :分别为第一张图(基础图)、第二张图(叠加图)的4个顶点绑定纹理坐标,若需要旋转/翻转,会先通过
TextureRotationUtil计算变换后的坐标数组; - 将顶点位置、两个纹理坐标 通过OpenGL API(
glVertexAttribPointer)传递给顶点着色器的3个attribute变量。
步骤2:顶点着色器逐顶点执行(GPU层)
对矩形的4个顶点依次执行这段代码,完成2件事:
- 为每个顶点设置最终渲染位置(赋值
gl_Position); - 为每个顶点提取两个纹理坐标的
xy分量,赋值给对应的varying变量。
步骤3:光栅化阶段插值处理(GPU层)
OpenGL将4个顶点光栅化为图像的所有像素 ,并对顶点着色器输出的两个varying纹理坐标做线性插值 ,最终让每个像素 都能拿到对应的、唯一的两个纹理坐标(textureCoordinate、textureCoordinate2)。
步骤4:片段着色器逐像素执行(GPU层)
- 片段着色器接收光栅化插值后的两个纹理坐标;
- 通过
texture2D函数,根据两个纹理坐标分别从第一张纹理、第二张纹理中采样出对应的像素颜色; - 执行双纹理混合逻辑(如颜色相加、Alpha混合、滤镜叠加等,由片段着色器的代码定义);
- 将混合后的颜色赋值给
gl_FragColor,作为当前像素的最终渲染颜色。
步骤5:屏幕渲染(GPU层)
所有像素的颜色计算完成后,GPU将最终的图像帧渲染到屏幕的指定区域(如GLSurfaceView/TextureView),完成双纹理混合渲染。
与单输入滤镜顶点着色器的核心区别
对比GPUImage单输入滤镜(GPUImageFilter)的顶点着色器,这段双输入版本的唯一区别就是多了一套纹理坐标的处理,其余逻辑完全一致,具体对比如下:
| 对比项 | 单输入滤镜顶点着色器 | 双输入滤镜顶点着色器(本文代码) |
|---|---|---|
| 纹理坐标attribute | 仅inputTextureCoordinate |
新增inputTextureCoordinate2 |
| 纹理坐标varying | 仅textureCoordinate |
新增textureCoordinate2 |
| 顶点位置处理 | 一致(赋值gl_Position) |
一致 |
| 核心执行逻辑 | 一致 | 一致 |
| 适用场景 | 单纹理渲染(如单图滤镜) | 双纹理渲染(如图片叠加、混合) |
这种设计遵循**「开闭原则」**:在单输入滤镜的基础上,仅新增必要的变量和传递逻辑,不修改原有核心逻辑,保证框架的扩展性和兼容性。
实际使用示例
场景:实现"两张图片叠加"的双输入滤镜
步骤1:自定义双输入片段着色器(叠加效果)
glsl
// 片段着色器:将两张图片的像素叠加(简单的颜色相加)
precision highp float;
uniform sampler2D inputImageTexture; // 第一个纹理(主纹理)
uniform sampler2D inputImageTexture2; // 第二个纹理(叠加纹理)
varying vec2 textureCoordinate; // 第一个纹理坐标
varying vec2 textureCoordinate2; // 第二个纹理坐标
void main() {
// 采样第一个纹理的像素颜色
vec4 color1 = texture2D(inputImageTexture, textureCoordinate);
// 采样第二个纹理的像素颜色
vec4 color2 = texture2D(inputImageTexture2, textureCoordinate2);
// 叠加:颜色相加(可根据需求修改混合算法,如 alpha 混合)
gl_FragColor = color1 + color2;
}
步骤2:在安卓代码中使用 GPUImageTwoInputFilter
java
// 1. 初始化 GPUImage 实例(关联 SurfaceView/TextureView 用于显示)
GPUImage gpuImage = new GPUImage(context);
gpuImage.setGLSurfaceView((GLSurfaceView) findViewById(R.id.glsurfaceview));
// 2. 编写叠加效果的片段着色器字符串
String overlayFragmentShader = "precision highp float;\n" +
"uniform sampler2D inputImageTexture;\n" +
"uniform sampler2D inputImageTexture2;\n" +
"varying vec2 textureCoordinate;\n" +
"varying vec2 textureCoordinate2;\n" +
"void main() {\n" +
" vec4 color1 = texture2D(inputImageTexture, textureCoordinate);\n" +
" vec4 color2 = texture2D(inputImageTexture2, textureCoordinate2);\n" +
" gl_FragColor = color1 + color2;\n" +
"}";
// 3. 创建双输入滤镜实例
GPUImageTwoInputFilter twoInputFilter = new GPUImageTwoInputFilter(overlayFragmentShader);
// 4. 设置第二个输入图片(Bitmap)
Bitmap secondBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.overlay_image);
twoInputFilter.setBitmap(secondBitmap);
// 5. 设置滤镜并显示主图片
gpuImage.setFilter(twoInputFilter);
gpuImage.setImage(BitmapFactory.decodeResource(getResources(), R.drawable.main_image));
// 6. 销毁资源(如 Activity 销毁时)
@Override
protected void onDestroy() {
super.onDestroy();
twoInputFilter.recycleBitmap(); // 回收第二个 Bitmap
twoInputFilter.onDestroy(); // 释放 OpenGL 纹理资源
gpuImage.deleteImage();
}
关键注意事项
- 纹理单元一致性:
setBitmap中激活的是GL_TEXTURE3,onDrawArraysPre中必须保持一致,否则纹理采样失败; - 片段着色器命名:第二个纹理的 uniform 变量必须命名为
inputImageTexture2(与代码中glGetUniformLocation的参数一致); - 内存管理:使用完
Bitmap后必须调用recycleBitmap,避免内存泄漏; - 线程安全:所有 OpenGL 相关操作(如
setBitmap)必须通过runOnDraw执行,避免跨线程操作。
总结
GPUImageTwoInputFilter 的核心价值是封装了双输入纹理的通用逻辑,开发者无需关注 OpenGL 多纹理的底层细节(如纹理单元激活、坐标传递、纹理加载),只需:
- 编写自定义片段着色器(定义两张图片的混合规则);
- 设置第二个输入 Bitmap;
- 集成到 GPUImage 渲染流程中。
