【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. 坐标适配:处理纹理坐标系与屏幕坐标系的差异,保证图像方向正确。
相关推荐
·云扬·1 天前
MySQL Binlog落盘机制深度解析:性能与安全性的平衡艺术
android·mysql·adb
独自破碎E1 天前
【BISHI9】田忌赛马
android·java·开发语言
代码s贝多芬的音符1 天前
android 两个人脸对比 mlkit
android
darkb1rd1 天前
五、PHP类型转换与类型安全
android·安全·php
gjxDaniel1 天前
Kotlin编程语言入门与常见问题
android·开发语言·kotlin
csj501 天前
安卓基础之《(22)—高级控件(4)碎片Fragment》
android
yunjingtianhe1 天前
EL隐裂检测仪的优势—精准捕捉细微隐裂、微小断栅等隐蔽性极强的隐患
数码相机
峥嵘life1 天前
Android16 【CTS】CtsMediaCodecTestCases等一些列Media测试存在Failed项
android·linux·学习
PHOSKEY1 天前
3D工业相机如何“读透”每一个字符?快速识别、高精度3D测量
数码相机·3d
stevenzqzq1 天前
Compose 中的状态可变性体系
android·compose