【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 渲染流程中。
相关推荐
wy3136228212 小时前
android——Android Studio 路径迁移指南(释放 C 盘空间)
android·ide·android studio
L1624762 小时前
基于 Xenon 实现 MySQL 高可用集群(完整配置教程,含监控告警 + 定时备份)
android·mysql·adb
2501_916008892 小时前
无需钥匙串快速创建 iOS 开发 / 发布证书 P12 CSR
android·ios·小程序·https·uni-app·iphone·webview
学海无涯书山有路2 小时前
Android ViewBinding 新手详解(Java 版)—— 结合 ViewModel+LiveData 实战
android·java·开发语言
独自破碎E2 小时前
【快手手撕】合并区间
android·java
海雅达手持终端PDA3 小时前
海雅达 Model 10X 工业平板赋能2026新能源汽车全链条数字化升级方案
android·物联网·5g·汽车·能源·制造·平板
angushine3 小时前
鲲鹏ARM服务MySQL镜像方式部署主从集群
android·mysql·adb
雨季6663 小时前
构建 OpenHarmony 简易密码强度指示器:用字符串长度实现直观反馈
android·开发语言·javascript
MengFly_3 小时前
Compose: Android整合yolo模型完成图像识别
android·yolo