【Android 美颜相机】第十六天:GPUImageTwoInputFilter 解析

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 渲染分为"顶点着色器"和"片段着色器",此处顶点着色器的作用是:
    1. 接收 3 个属性(顶点位置、纹理1坐标、纹理2坐标);
    2. 将两个纹理坐标通过 varying 变量传递给片段着色器;
    3. 设置顶点最终显示位置(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 上下文创建、着色器程序编译链接完成后调用;
  • 核心操作:
    1. 获取着色器中第二个纹理相关变量的句柄(后续通过句柄传递数据);
    2. 启用纹理坐标属性数组(OpenGL 中 Attribute 变量需手动启用才能接收数据);
    3. 备注:片段着色器中第二个纹理必须命名为 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 多纹理渲染关键步骤):
    1. 激活纹理单元(GL_TEXTURE3)→ 绑定纹理(filterSourceTexture2)→ 将纹理单元编号(3)传递给片段着色器的 uniform 变量,完成"纹理 → 着色器"的关联;
    2. 将纹理坐标缓冲区的数据传递给着色器的 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;
}

关键行设计细节补充

  1. 为什么用vec4存储纹理坐标,却只取xy分量?
    OpenGL ES中,attribute属性变量的类型设计通常与顶点位置(vec4)保持一致 ,目的是统一CPU端的数据传递格式 ,减少数据类型转换的开销。纹理坐标本质是二维(UV),因此仅需提取x/y分量,剩余的z/w分量会被忽略(默认值为0.0/1.0,不影响使用)。
  2. 为什么没有复杂的计算逻辑?
    这段顶点着色器是通用型基础实现 ,仅完成「数据接收-传递」的核心功能,不做坐标变换(如旋转、翻转)。双纹理的坐标变换(如图片旋转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的执行效率:

  1. 设置顶点最终位置 :将CPU传递的position赋值给OpenGL内置变量gl_Position,这是顶点着色器的必做操作,否则顶点无法在屏幕上渲染;
  2. 传递纹理坐标 :提取两个纹理坐标的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层)

  1. 准备顶点位置数组:如矩形的4个顶点裁剪空间坐标,传递给OpenGL;
  2. 准备两个纹理坐标数组 :分别为第一张图(基础图)、第二张图(叠加图)的4个顶点绑定纹理坐标,若需要旋转/翻转,会先通过TextureRotationUtil计算变换后的坐标数组;
  3. 顶点位置、两个纹理坐标 通过OpenGL API(glVertexAttribPointer)传递给顶点着色器的3个attribute变量。

步骤2:顶点着色器逐顶点执行(GPU层)

对矩形的4个顶点依次执行这段代码,完成2件事:

  1. 为每个顶点设置最终渲染位置(赋值gl_Position);
  2. 为每个顶点提取两个纹理坐标的xy分量,赋值给对应的varying变量。

步骤3:光栅化阶段插值处理(GPU层)

OpenGL将4个顶点光栅化为图像的所有像素 ,并对顶点着色器输出的两个varying纹理坐标做线性插值 ,最终让每个像素 都能拿到对应的、唯一的两个纹理坐标(textureCoordinatetextureCoordinate2)。

步骤4:片段着色器逐像素执行(GPU层)

  1. 片段着色器接收光栅化插值后的两个纹理坐标
  2. 通过texture2D函数,根据两个纹理坐标分别从第一张纹理、第二张纹理中采样出对应的像素颜色;
  3. 执行双纹理混合逻辑(如颜色相加、Alpha混合、滤镜叠加等,由片段着色器的代码定义);
  4. 将混合后的颜色赋值给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();
}

关键注意事项

  1. 纹理单元一致性:setBitmap 中激活的是 GL_TEXTURE3onDrawArraysPre 中必须保持一致,否则纹理采样失败;
  2. 片段着色器命名:第二个纹理的 uniform 变量必须命名为 inputImageTexture2(与代码中 glGetUniformLocation 的参数一致);
  3. 内存管理:使用完 Bitmap 后必须调用 recycleBitmap,避免内存泄漏;
  4. 线程安全:所有 OpenGL 相关操作(如 setBitmap)必须通过 runOnDraw 执行,避免跨线程操作。

总结

GPUImageTwoInputFilter 的核心价值是封装了双输入纹理的通用逻辑,开发者无需关注 OpenGL 多纹理的底层细节(如纹理单元激活、坐标传递、纹理加载),只需:

  1. 编写自定义片段着色器(定义两张图片的混合规则);
  2. 设置第二个输入 Bitmap;
  3. 集成到 GPUImage 渲染流程中。
相关推荐
AFinalStone1 小时前
Android 7系统异常问题排查(四)Framework层(上)—System Server Watchdog
android·系统异常
宝杰X72 小时前
Android Room3 多平台数据库
android·数据库
huibin1478523695 小时前
热缓解学习记录
android·学习
小田的博客8 小时前
SAP MM 供应商银行主数据更新报错!message R1228!
android·java·服务器
一笑的小酒馆9 小时前
Android智能猫砂盆视频加载慢卡顿问题分析
android
只会cv的小前端11 小时前
七巧低代码服务端脚本使用方法
android·低代码·rxjava
11 小时前
Android 自定义 View 实战:从零打造工业级全向摇杆控件(OmniJoystickView)
android·kotlin·自定义view·摇杆控件
雨白11 小时前
深入理解 Kotlin 协程 (十一):以逸待劳,探秘 select 多路复用与并发安全策略
android·kotlin
hunterandroid14 小时前
[Android 从零到一] Android 深度链接与 App Links:从 URI Scheme 到可验证的应用跳转
android
我命由我1234516 小时前
Jetpack Compose - Material Design 断点范围、WindowSizeClass、针对不同屏幕尺寸创建预览、四类导航栏
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime