【Android 美颜相机】第十二天:GPUImageFilterGroup 源码解析

GPUImageFilterGroup 源码解析

GPUImageFilterGroup 是安卓 GPUImage 开源库中用于管理多滤镜串联执行的核心类,它继承自基础滤镜类 GPUImageFilter,能够将多个独立滤镜组合成一个滤镜组,按顺序逐次应用滤镜效果,实现复杂的图像/视频处理能力。

本文将逐行解析该类的代码结构、注释及核心实现逻辑。

版权声明与包声明

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.
 */

// 声明类所属的包:GPUImage 滤镜模块
package jp.co.cyberagent.android.gpuimage.filter;

这部分是标准的开源项目版权与包声明,指定了代码的开源协议(Apache 2.0)和所属的模块路径,保证代码的合规性和工程结构的清晰性。

导入依赖类

java 复制代码
import android.annotation.SuppressLint;  // 用于抑制 Android Lint 静态检查警告
import android.opengl.GLES20;  // OpenGL ES 2.0 核心 API,用于操作纹理、帧缓冲等

import java.nio.ByteBuffer;  // 字节缓冲区,用于构建 OpenGL 所需的浮点缓冲
import java.nio.ByteOrder;   // 字节序工具,保证缓冲区字节序与本地系统一致
import java.nio.FloatBuffer; // 浮点缓冲区,存储顶点/纹理坐标数据

import java.util.ArrayList;  // 动态数组,存储滤镜列表
import java.util.List;       // 集合接口,定义滤镜列表规范

import jp.co.cyberagent.android.gpuimage.util.Rotation;  // 旋转枚举(正常、90度、180度等)
import jp.co.cyberagent.android.gpuimage.util.TextureRotationUtil;  // 纹理坐标旋转/翻转工具类

// 导入 GPUImageRenderer 中的立方体顶点常量(用于纹理映射)
import static jp.co.cyberagent.android.gpuimage.GPUImageRenderer.CUBE;
// 导入 TextureRotationUtil 中的无旋转纹理坐标常量
import static jp.co.cyberagent.android.gpuimage.util.TextureRotationUtil.TEXTURE_NO_ROTATION;

导入的类分为三类:

  1. Android 系统类:处理 OpenGL 调用、Lint 警告抑制;
  2. Java 基础类:缓冲区操作、集合存储;
  3. GPUImage 库内部工具类:旋转枚举、纹理坐标工具、常量定义。

类定义与核心成员变量

java 复制代码
/**
 * Resembles a filter that consists of multiple filters applied after each
 * other.  // 类注释:表示由多个滤镜按顺序应用组成的复合滤镜
 */
public class GPUImageFilterGroup extends GPUImageFilter {  // 继承基础滤镜类 GPUImageFilter

    // 原始滤镜列表(可包含子滤镜组 GPUImageFilterGroup)
    private List<GPUImageFilter> filters;
    // 扁平化后的滤镜列表(递归展开所有子滤镜组,仅保留基础滤镜)
    private List<GPUImageFilter> mergedFilters;
    // 帧缓冲数组:每个中间滤镜的输出会渲染到对应的帧缓冲
    private int[] frameBuffers;
    // 帧缓冲关联的纹理数组:帧缓冲的内容会被存储到该纹理,供下一个滤镜使用
    private int[] frameBufferTextures;

    // 立方体顶点缓冲:存储图像绘制的顶点坐标(固定为矩形,对应屏幕/纹理区域)
    private final FloatBuffer glCubeBuffer;
    // 无旋转的纹理坐标缓冲:存储原始纹理坐标(与顶点坐标映射)
    private final FloatBuffer glTextureBuffer;
    // 翻转后的纹理坐标缓冲:处理纹理上下翻转的场景(适配 OpenGL 纹理坐标系)
    private final FloatBuffer glTextureFlipBuffer;

核心成员变量的设计思路:

  • filters 保留原始的滤镜层级结构(支持嵌套滤镜组);
  • mergedFilters 扁平化列表简化绘制逻辑(无需递归处理子组);
  • frameBuffers/frameBufferTextures 是 OpenGL 中间渲染载体,实现"前一个滤镜输出 → 后一个滤镜输入"的串联;
  • 三个 FloatBuffer 是 OpenGL 绘制的基础数据,分别存储顶点坐标和不同场景的纹理坐标。

构造方法

无参构造

java 复制代码
    /**
     * Instantiates a new GPUImageFilterGroup with no filters.
     *  // 构造方法注释:创建一个空的滤镜组
     */
    public GPUImageFilterGroup() {
        this(null);  // 调用带 List 参数的构造方法,传入 null
    }

4带滤镜列表的构造

java 复制代码
    /**
     * Instantiates a new GPUImageFilterGroup with the given filters.
     *
     * @param filters the filters which represent this filter  // 参数:组成该滤镜组的初始滤镜列表
     */
    public GPUImageFilterGroup(List<GPUImageFilter> filters) {
        // 初始化原始滤镜列表
        this.filters = filters;
        // 若传入的列表为 null,初始化空列表;否则扁平化滤镜列表
        if (this.filters == null) {
            this.filters = new ArrayList<>();
        } else {
            updateMergedFilters();  // 扁平化滤镜列表(展开子组)
        }

        // 初始化立方体顶点缓冲:
        // 1. 分配缓冲区(CUBE 是浮点数组,每个浮点数占4字节)
        // 2. 设置字节序为本地系统序(避免跨平台字节序问题)
        // 3. 写入 CUBE 数据并重置指针到起始位置
        glCubeBuffer = ByteBuffer.allocateDirect(CUBE.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        glCubeBuffer.put(CUBE).position(0);

        // 初始化无旋转纹理坐标缓冲:逻辑同顶点缓冲
        glTextureBuffer = ByteBuffer.allocateDirect(TEXTURE_NO_ROTATION.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        glTextureBuffer.put(TEXTURE_NO_ROTATION).position(0);

        // 生成"正常旋转、垂直翻转"的纹理坐标(适配 OpenGL 纹理 Y 轴向下的特性)
        float[] flipTexture = TextureRotationUtil.getRotation(Rotation.NORMAL, false, true);
        // 初始化翻转后的纹理坐标缓冲:逻辑同前
        glTextureFlipBuffer = ByteBuffer.allocateDirect(flipTexture.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        glTextureFlipBuffer.put(flipTexture).position(0);
    }

构造方法核心逻辑:

  1. 初始化滤镜列表(处理 null 情况)并扁平化;
  2. 构建 OpenGL 绘制所需的顶点/纹理坐标缓冲:
    • OpenGL 不直接使用 Java 数组,需转换为 NIO 缓冲(堆外内存,避免 JVM 垃圾回收影响);
    • 纹理翻转缓冲是为了适配 OpenGL 纹理坐标系(Y 轴向下)与安卓屏幕坐标系(Y 轴向上)的差异。

添加滤镜方法

java 复制代码
    /**
     * 向滤镜组添加单个滤镜
     * @param aFilter 待添加的滤镜(null 则忽略)
     */
    public void addFilter(GPUImageFilter aFilter) {
        if (aFilter == null) {  // 空值校验,避免空指针
            return;
        }
        filters.add(aFilter);  // 添加到原始滤镜列表
        updateMergedFilters(); // 重新扁平化滤镜列表(适配新添加的滤镜)
    }

该方法保证了添加滤镜后,mergedFilters 始终与 filters 同步,避免扁平化列表过期。

生命周期方法:初始化与销毁

初始化方法(重写父类)

java 复制代码
    /*
     * (non-Javadoc)
     * @see jp.co.cyberagent.android.gpuimage.filter.GPUImageFilter#onInit()
     * 重写父类的初始化方法:初始化滤镜组中所有滤镜
     */
    @Override
    public void onInit() {
        super.onInit();  // 调用父类初始化(初始化基础滤镜的 OpenGL 程序等)
        // 遍历所有原始滤镜,执行各自的初始化逻辑
        for (GPUImageFilter filter : filters) {
            filter.ifNeedInit();  // 按需初始化(避免重复初始化)
        }
    }

初始化逻辑:先执行父类的基础初始化,再遍历初始化所有子滤镜,保证每个滤镜的 OpenGL 程序、着色器都被正确初始化。

销毁方法(重写父类)

java 复制代码
    /*
     * (non-Javadoc)
     * @see jp.co.cyberagent.android.gpuimage.filter.GPUImageFilter#onDestroy()
     * 重写父类的销毁方法:释放滤镜组所有资源
     */
    @Override
    public void onDestroy() {
        destroyFramebuffers();  // 销毁帧缓冲和关联纹理(释放 OpenGL 资源)
        // 遍历销毁所有子滤镜的资源
        for (GPUImageFilter filter : filters) {
            filter.destroy();
        }
        super.onDestroy();  // 调用父类销毁方法(释放基础滤镜资源)
    }

销毁逻辑遵循"先释放自定义资源,再释放父类资源"的原则,避免内存泄漏。

帧缓冲销毁工具方法

java 复制代码
    /**
     * 销毁帧缓冲和关联的纹理:释放 OpenGL 资源
     */
    private void destroyFramebuffers() {
        // 销毁纹理:若纹理数组非空,调用 OpenGL API 删除纹理
        if (frameBufferTextures != null) {
            GLES20.glDeleteTextures(frameBufferTextures.length, frameBufferTextures, 0);
            frameBufferTextures = null;  // 置空,帮助 GC 回收
        }
        // 销毁帧缓冲:若帧缓冲数组非空,调用 OpenGL API 删除帧缓冲
        if (frameBuffers != null) {
            GLES20.glDeleteFramebuffers(frameBuffers.length, frameBuffers, 0);
            frameBuffers = null;  // 置空,帮助 GC 回收
        }
    }

OpenGL 资源(纹理、帧缓冲)不会被 JVM 自动回收,必须通过 glDeleteTextures/glDeleteFramebuffers 手动释放,否则会导致显存泄漏。

输出尺寸变化处理(重写父类)

java 复制代码
    /*
     * (non-Javadoc)
     * @see
     * jp.co.cyberagent.android.gpuimage.filter.GPUImageFilter#onOutputSizeChanged(int,
     * int)
     * 重写父类方法:当输出尺寸(屏幕/纹理大小)变化时,更新滤镜组的渲染配置
     */
    @Override
    public void onOutputSizeChanged(final int width, final int height) {
        super.onOutputSizeChanged(width, height);  // 调用父类尺寸更新逻辑
        // 销毁旧的帧缓冲(尺寸变化后,旧帧缓冲不再适配)
        if (frameBuffers != null) {
            destroyFramebuffers();
        }

        // 第一步:更新所有原始滤镜的输出尺寸
        int size = filters.size();
        for (int i = 0; i < size; i++) {
            filters.get(i).onOutputSizeChanged(width, height);
        }

        // 第二步:为扁平化后的滤镜列表创建帧缓冲(仅为前 n-1 个滤镜创建,最后一个直接渲染到屏幕)
        if (mergedFilters != null && mergedFilters.size() > 0) {
            size = mergedFilters.size();
            // 帧缓冲数量 = 滤镜数量 - 1(最后一个滤镜无需中间帧缓冲)
            frameBuffers = new int[size - 1];
            frameBufferTextures = new int[size - 1];

            // 遍历创建每个中间帧缓冲和纹理
            for (int i = 0; i < size - 1; i++) {
                // 1. 生成帧缓冲 ID 并存储到数组
                GLES20.glGenFramebuffers(1, frameBuffers, i);
                // 2. 生成纹理 ID 并存储到数组
                GLES20.glGenTextures(1, frameBufferTextures, i);
                
                // 3. 绑定纹理,配置纹理参数
                GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, frameBufferTextures[i]);
                // 创建 2D 纹理:宽高为输出尺寸,格式 RGBA,无初始数据
                GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0,
                        GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
                // 设置纹理放大过滤:线性过滤(模糊效果,避免锯齿)
                GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
                        GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
                // 设置纹理缩小过滤:线性过滤
                GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
                        GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
                // 设置纹理 S 轴(水平)环绕模式:边缘夹紧(避免纹理重复)
                GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
                        GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
                // 设置纹理 T 轴(垂直)环绕模式:边缘夹紧
                GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
                        GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);

                // 4. 绑定帧缓冲,将纹理关联到帧缓冲的颜色附件
                GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffers[i]);
                GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
                        GLES20.GL_TEXTURE_2D, frameBufferTextures[i], 0);

                // 5. 解绑纹理和帧缓冲(避免后续操作污染)
                GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
                GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
            }
        }
    }

核心逻辑:

  1. 尺寸变化时先销毁旧帧缓冲(尺寸不匹配会导致渲染异常);
  2. 更新所有子滤镜的输出尺寸,保证每个滤镜适配新尺寸;
  3. 为扁平化后的前 n-1 个滤镜创建帧缓冲+纹理:
    • 每个中间滤镜的输出会渲染到对应的帧缓冲纹理;
    • 最后一个滤镜直接渲染到屏幕(无需中间帧缓冲);
  4. 配置纹理参数(线性过滤、边缘夹紧),保证渲染效果和纹理坐标的正确性。

核心绘制逻辑(重写父类)

java 复制代码
    /*
     * (non-Javadoc)
     * @see jp.co.cyberagent.android.gpuimage.filter.GPUImageFilter#onDraw(int,
     * java.nio.FloatBuffer, java.nio.FloatBuffer)
     * 重写父类绘制方法:按顺序执行滤镜组中所有滤镜的绘制
     */
    @SuppressLint("WrongCall")  // 抑制 Lint 对 filter.onDraw 调用的警告(逻辑上合法)
    @Override
    public void onDraw(final int textureId, final FloatBuffer cubeBuffer,
                       final FloatBuffer textureBuffer) {
        runPendingOnDrawTasks();  // 执行绘制前的待处理任务(如参数更新)
        // 校验初始化状态:未初始化/帧缓冲为空则直接返回
        if (!isInitialized() || frameBuffers == null || frameBufferTextures == null) {
            return;
        }
        
        if (mergedFilters != null) {
            int size = mergedFilters.size();
            int previousTexture = textureId;  // 初始输入纹理(原始图像纹理 ID)
            // 遍历扁平化后的所有滤镜
            for (int i = 0; i < size; i++) {
                GPUImageFilter filter = mergedFilters.get(i);
                boolean isNotLast = i < size - 1;  // 是否为最后一个滤镜
                
                // 若不是最后一个滤镜:绑定到对应的帧缓冲(输出到纹理)
                if (isNotLast) {
                    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffers[i]);
                    GLES20.glClearColor(0, 0, 0, 0);  // 清空帧缓冲(透明黑色背景)
                }

                // 分场景调用滤镜的绘制方法:
                if (i == 0) {
                    // 第一个滤镜:使用原始顶点/纹理缓冲(输入为原始图像)
                    filter.onDraw(previousTexture, cubeBuffer, textureBuffer);
                } else if (i == size - 1) {
                    // 最后一个滤镜:使用预定义的顶点缓冲 + 翻转/正常纹理缓冲(适配偶数个滤镜的翻转)
                    filter.onDraw(previousTexture, glCubeBuffer, (size % 2 == 0) ? glTextureFlipBuffer : glTextureBuffer);
                } else {
                    // 中间滤镜:使用预定义的顶点/纹理缓冲(输入为前一个帧缓冲的纹理)
                    filter.onDraw(previousTexture, glCubeBuffer, glTextureBuffer);
                }

                // 若不是最后一个滤镜:解绑帧缓冲,更新输入纹理为当前帧缓冲的纹理
                if (isNotLast) {
                    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
                    previousTexture = frameBufferTextures[i];  // 下一个滤镜的输入 = 当前帧缓冲纹理
                }
            }
        }
    }

绘制逻辑是滤镜组的核心,实现了"串联渲染":

  1. 初始输入为原始图像的纹理 ID;
  2. 前 n-1 个滤镜:渲染到对应的帧缓冲纹理,输出纹理作为下一个滤镜的输入;
  3. 最后一个滤镜:直接渲染到屏幕(默认帧缓冲 0);
  4. 纹理坐标翻转处理:偶数个滤镜时,纹理会被翻转,需通过 glTextureFlipBuffer 修正,保证图像方向正确。

获取器方法

java 复制代码
    /**
     * Gets the filters.  // 获取原始滤镜列表(包含子组)
     *
     * @return the filters
     */
    public List<GPUImageFilter> getFilters() {
        return filters;
    }

    /**
     * 获取扁平化后的滤镜列表(无嵌套子组)
     * @return 扁平化滤镜列表
     */
    public List<GPUImageFilter> getMergedFilters() {
        return mergedFilters;
    }

提供对外访问滤镜列表的接口,方便上层代码查看/修改滤镜组配置。

滤镜列表扁平化方法

java 复制代码
    /**
     * 扁平化滤镜列表:递归展开所有子滤镜组,将嵌套结构转为一维列表
     */
    public void updateMergedFilters() {
        if (filters == null) {  // 原始列表为空则直接返回
            return;
        }

        // 初始化/清空扁平化列表
        if (mergedFilters == null) {
            mergedFilters = new ArrayList<>();
        } else {
            mergedFilters.clear();
        }

        List<GPUImageFilter> filters;  // 临时变量,存储子组展开后的滤镜
        // 遍历原始滤镜列表
        for (GPUImageFilter filter : this.filters) {
            // 若当前滤镜是子滤镜组:递归展开
            if (filter instanceof GPUImageFilterGroup) {
                ((GPUImageFilterGroup) filter).updateMergedFilters();  // 先扁平化子组
                filters = ((GPUImageFilterGroup) filter).getMergedFilters();  // 获取子组的扁平化列表
                if (filters == null || filters.isEmpty())  // 子组为空则跳过
                    continue;
                mergedFilters.addAll(filters);  // 将子组的滤镜添加到当前列表
                continue;
            }
            // 基础滤镜:直接添加到扁平化列表
            mergedFilters.add(filter);
        }
    }

该方法解决了"嵌套滤镜组"的问题:

  • 若原始列表中包含 GPUImageFilterGroup 类型的子组,会递归展开子组的 mergedFilters
  • 最终 mergedFilters 中仅保留基础滤镜(非组类型),简化绘制时的遍历逻辑(无需递归)。

总结

GPUImageFilterGroup 的核心设计思路是**"扁平化管理 + 帧缓冲串联渲染"**:

  1. 扁平化:将嵌套的滤镜组转为一维列表,避免绘制时递归处理;
  2. 帧缓冲串联:通过中间帧缓冲实现"前一个滤镜输出 → 后一个滤镜输入",完成多滤镜的顺序应用;
  3. 生命周期管理:严格遵循 OpenGL 资源的创建/销毁逻辑,避免内存/显存泄漏;
  4. 坐标适配:处理纹理坐标系与屏幕坐标系的差异,保证图像方向正确。
相关推荐
_李小白2 小时前
【Android GLSurfaceView源码学习】第三天:GLSurfaceView的Surface、GLES与EGLSurface的关联
android·学习
技术摆渡人2 小时前
专题三:【Android 架构】全栈性能优化与架构演进全书
android·性能优化·架构
花卷HJ2 小时前
Android 10+ 使用 WifiNetworkSpecifier 连接指定 WiFi(完整封装 + 实战)
android
前端世界2 小时前
鸿蒙系统中时间与日期的国际化实践:一次把不同文化显示问题讲清楚
android·华为·harmonyos
木卫四科技2 小时前
【Claude Agent - 入门篇】:从原生 SDK 到自主智能体
android
2501_915918412 小时前
Mac 抓包软件有哪些?Charles、mitmproxy、Wireshark和Sniffmaster哪个更合适
android·ios·小程序·https·uni-app·iphone·webview
2501_915106322 小时前
iOS 抓包绕过 SSL 证书认证, HTTPS 暴力抓包、数据流分析
android·ios·小程序·https·uni-app·iphone·ssl
2501_9159214310 小时前
iOS App 电耗管理 通过系统电池记录、Xcode Instruments 与克魔(KeyMob)组合使用
android·ios·小程序·https·uni-app·iphone·webview
June bug12 小时前
【配环境】安卓项目开发环境
android