《Android 美颜类相机开发汇总》第三章 Android OpenGLES 给相机添加滤镜

滤镜介绍

目前市面上的滤镜有很多,但整体归类也就几样,都是在fragment shader中进行处理。目前滤镜最常用的就是 lut滤镜以及调整RGB曲线的滤镜了。其他的类型变更大同小异。

动态滤镜的构建

为了实现动态下载的滤镜,我们接下来实现一套滤镜的json参数,主要包括滤镜类型、滤镜名称、vertex shader、fragment shader 文件、统一变量列表、与统一变量绑定的纹理图片、默认滤镜强度、是否带纹理宽高偏移量、音乐路径、音乐是否循环播放等参数。
json 以及各个字段的介绍如下:

{
    "filterList": [{
        "type": "filter",   // 表明滤镜类型,目前filter是只普通滤镜,后续还会加入其它类型的滤镜
        "name": "amaro",    // 滤镜名称
        "vertexShader": "", // vertex shader 文件名
        "fragmentShader": "fragment.glsl",  // fragment shader 文件名
        "uniformList":["blowoutTexture", "overlayTexture", "mapTexture"],   // 统一变量
        "uniformData": {  // 与统一变量绑定的纹理图片
            "blowoutTexture": "blowout.png",
            "overlayTexture": "overlay.png",
            "mapTexture": "map.png"
        },
        "strength": 1.0,        // 默认滤镜强度 0.0 ~ 1.0之间
        "texelOffset": 0,       // 是否需要支持宽高偏移值,即需要传递 1.0f/width, 1.0f/height到shader中
        "audioPath": "",        // 音乐路径
        "audioLooping": 1       // 是否循环播放音乐
    }]
}

有了json 之后,我们需要解码得到滤镜参数对象,解码如下:

/**
     * 解码滤镜数据
     * @param folderPath
     * @return
     */
    public static DynamicColor decodeFilterData(String folderPath)
            throws IOException, JSONException {

        File file = new File(folderPath, "json");
        String filterJson = FileUtils.convertToString(new FileInputStream(file));

        JSONObject jsonObject = new JSONObject(filterJson);
        DynamicColor dynamicColor = new DynamicColor();
        dynamicColor.unzipPath = folderPath;
        if (dynamicColor.filterList == null) {
            dynamicColor.filterList = new ArrayList<>();
        }

        JSONArray filterList = jsonObject.getJSONArray("filterList");
        for (int filterIndex = 0; filterIndex < filterList.length(); filterIndex++) {
            DynamicColorData filterData = new DynamicColorData();
            JSONObject jsonData = filterList.getJSONObject(filterIndex);
            String type = jsonData.getString("type");
            // TODO 目前滤镜只做普通的filter,其他复杂的滤镜类型后续在做处理
            if ("filter".equals(type)) {
                filterData.name = jsonData.getString("name");
                filterData.vertexShader = jsonData.getString("vertexShader");
                filterData.fragmentShader = jsonData.getString("fragmentShader");
                // 获取统一变量字段
                JSONArray uniformList = jsonData.getJSONArray("uniformList");
                for (int uniformIndex = 0; uniformIndex < uniformList.length(); uniformIndex++) {
                    String uniform = uniformList.getString(uniformIndex);
                    filterData.uniformList.add(uniform);
                }

                // 获取统一变量字段绑定的图片资源
                JSONObject uniformData = jsonData.getJSONObject("uniformData");
                if (uniformData != null) {
                    Iterator<String> dataIterator = uniformData.keys();
                    while (dataIterator.hasNext()) {
                        String key = dataIterator.next();
                        String value = uniformData.getString(key);
                        filterData.uniformDataList.add(new DynamicColorData.UniformData(key, value));
                    }
                }
                filterData.strength = (float) jsonData.getDouble("strength");
                filterData.texelOffset = (jsonData.getInt("texelOffset") == 1);
                filterData.audioPath = jsonData.getString("audioPath");
                filterData.audioLooping = (jsonData.getInt("audioLooping") == 1);
            }
            dynamicColor.filterList.add(filterData);
        }

        return dynamicColor;
    }

滤镜的实现

在解码得到滤镜参数之后,我们接下来实现动态滤镜渲染过程。为了方便构建滤镜,我们创建一个滤镜资源加载器,代码如下:

/**
 * 滤镜资源加载器
 */
public class DynamicColorLoader {

    private static final String TAG = "DynamicColorLoader";

    // 滤镜所在的文件夹
    private String mFolderPath;
    // 动态滤镜数据
    private DynamicColorData mColorData;
    // 资源索引加载器
    private ResourceDataCodec mResourceCodec;
    // 动态滤镜
    private final WeakReference<DynamicColorBaseFilter> mWeakFilter;
    // 统一变量列表
    private HashMap<String, Integer> mUniformHandleList = new HashMap<>();
    // 纹理列表
    private int[] mTextureList;

    // 句柄
    private int mTexelWidthOffsetHandle = OpenGLUtils.GL_NOT_INIT;
    private int mTexelHeightOffsetHandle = OpenGLUtils.GL_NOT_INIT;
    private int mStrengthHandle = OpenGLUtils.GL_NOT_INIT;
    private float mStrength = 1.0f;
    private float mTexelWidthOffset = 1.0f;
    private float mTexelHeightOffset = 1.0f;

    public DynamicColorLoader(DynamicColorBaseFilter filter, DynamicColorData colorData, String folderPath) {
        mWeakFilter = new WeakReference<>(filter);
        mFolderPath = folderPath.startsWith("file://") ? folderPath.substring("file://".length()) : folderPath;
        mColorData = colorData;
        mStrength = (colorData == null) ? 1.0f : colorData.strength;
        Pair pair = ResourceCodec.getResourceFile(mFolderPath);
        if (pair != null) {
            mResourceCodec = new ResourceDataCodec(mFolderPath + "/" + (String) pair.first, mFolderPath + "/" + pair.second);
        }
        if (mResourceCodec != null) {
            try {
                mResourceCodec.init();
            } catch (IOException e) {
                Log.e(TAG, "DynamicColorLoader: ", e);
                mResourceCodec = null;
            }
        }
        if (!TextUtils.isEmpty(mColorData.audioPath)) {
            if (mWeakFilter.get() != null) {
                mWeakFilter.get().setAudioPath(Uri.parse(mFolderPath + "/" + mColorData.audioPath));
                mWeakFilter.get().setLooping(mColorData.audioLooping);
            }
        }
        loadColorTexture();
    }

    /**
     * 加载纹理
     */
    private void loadColorTexture() {
        if (mColorData.uniformDataList == null || mColorData.uniformDataList.size() <= 0) {
            return;
        }
        mTextureList = new int[mColorData.uniformDataList.size()];
        for (int dataIndex = 0; dataIndex < mColorData.uniformDataList.size(); dataIndex++) {
            Bitmap bitmap = null;
            if (mResourceCodec != null) {
                bitmap = mResourceCodec.loadBitmap(mColorData.uniformDataList.get(dataIndex).value);
            }
            if (bitmap == null) {
                bitmap = BitmapUtils.getBitmapFromFile(mFolderPath + "/" + String.format(mColorData.uniformDataList.get(dataIndex).value));
            }
            if (bitmap != null) {
                mTextureList[dataIndex] = OpenGLUtils.createTexture(bitmap);
                bitmap.recycle();
            } else {
                mTextureList[dataIndex] = OpenGLUtils.GL_NOT_TEXTURE;
            }
        }
    }


    /**
     * 绑定统一变量句柄
     * @param programHandle
     */
    public void onBindUniformHandle(int programHandle) {
        if (programHandle == OpenGLUtils.GL_NOT_INIT || mColorData == null) {
            return;
        }
        mStrengthHandle = GLES30.glGetUniformLocation(programHandle, "strength");
        if (mColorData.texelOffset) {
            mTexelWidthOffsetHandle = GLES30.glGetUniformLocation(programHandle, "texelWidthOffset");
            mTexelHeightOffsetHandle = GLES30.glGetUniformLocation(programHandle, "texelHeightOffset");
        } else {
            mTexelWidthOffsetHandle = OpenGLUtils.GL_NOT_INIT;
            mTexelHeightOffsetHandle = OpenGLUtils.GL_NOT_INIT;
        }
        for (int uniformIndex = 0; uniformIndex < mColorData.uniformList.size(); uniformIndex++) {
            String uniformString = mColorData.uniformList.get(uniformIndex);
            int handle = GLES30.glGetUniformLocation(programHandle, uniformString);
            mUniformHandleList.put(uniformString, handle);
        }
    }

    /**
     * 输入纹理大小
     * @param width
     * @param height
     */
    public void onInputSizeChange(int width, int height) {
        mTexelWidthOffset = 1.0f / width;
        mTexelHeightOffset = 1.0f / height;
    }

    /**
     * 绑定滤镜纹理,只需要绑定一次就行,不用重复绑定,减少开销
     */
    public void onDrawFrameBegin() {
        if (mStrengthHandle != OpenGLUtils.GL_NOT_INIT) {
            GLES30.glUniform1f(mStrengthHandle, mStrength);
        }
        if (mTexelWidthOffsetHandle != OpenGLUtils.GL_NOT_INIT) {
            GLES30.glUniform1f(mTexelWidthOffsetHandle, mTexelWidthOffset);
        }
        if (mTexelHeightOffsetHandle != OpenGLUtils.GL_NOT_INIT) {
            GLES30.glUniform1f(mTexelHeightOffsetHandle, mTexelHeightOffset);
        }

        if (mTextureList == null || mColorData == null) {
            return;
        }
        // 逐个绑定纹理
        for (int dataIndex = 0; dataIndex < mColorData.uniformDataList.size(); dataIndex++) {
            for (int uniformIndex = 0; uniformIndex < mUniformHandleList.size(); uniformIndex++) {
                // 如果统一变量存在,则直接绑定纹理
                Integer handle = mUniformHandleList.get(mColorData.uniformDataList.get(dataIndex).uniform);
                if (handle != null && mTextureList[dataIndex] != OpenGLUtils.GL_NOT_TEXTURE) {
                    OpenGLUtils.bindTexture(handle, mTextureList[dataIndex], dataIndex + 1);
                }
            }
        }
    }

    /**
     * 释放资源
     */
    public void release() {
        if (mTextureList != null && mTextureList.length > 0) {
            GLES30.glDeleteTextures(mTextureList.length, mTextureList, 0);
            mTextureList = null;
        }
        if (mWeakFilter.get() != null) {
            mWeakFilter.clear();
        }
    }

    /**
     * 设置强度
     * @param strength
     */
    public void setStrength(float strength) {
        mStrength = strength;
    }

}

然后我们构建一个DynamicColorFilter的基类,方便后续添加其他类型的滤镜,代码如下:

public class DynamicColorBaseFilter extends GLImageAudioFilter {

    // 颜色滤镜参数
    protected DynamicColorData mDynamicColorData;
    protected DynamicColorLoader mDynamicColorLoader;

    public DynamicColorBaseFilter(Context context, DynamicColorData dynamicColorData, String unzipPath) {
        super(context, (dynamicColorData == null || TextUtils.isEmpty(dynamicColorData.vertexShader)) ? VERTEX_SHADER
                        : getShaderString(context, unzipPath, dynamicColorData.vertexShader),
                (dynamicColorData == null || TextUtils.isEmpty(dynamicColorData.fragmentShader)) ? FRAGMENT_SHADER_2D
                        : getShaderString(context, unzipPath, dynamicColorData.fragmentShader));
        mDynamicColorData = dynamicColorData;
        mDynamicColorLoader = new DynamicColorLoader(this, mDynamicColorData, unzipPath);
        mDynamicColorLoader.onBindUniformHandle(mProgramHandle);
    }

    @Override
    public void onInputSizeChanged(int width, int height) {
        super.onInputSizeChanged(width, height);
        if (mDynamicColorLoader != null) {
            mDynamicColorLoader.onInputSizeChange(width, height);
        }
    }

    @Override
    public void onDrawFrameBegin() {
        super.onDrawFrameBegin();
        if (mDynamicColorLoader != null) {
            mDynamicColorLoader.onDrawFrameBegin();
        }
    }

    @Override
    public void release() {
        super.release();
        if (mDynamicColorLoader != null) {
            mDynamicColorLoader.release();
        }
    }

    /**
     * 设置强度,调节滤镜的轻重程度
     * @param strength
     */
    public void setStrength(float strength) {
        if (mDynamicColorLoader != null) {
            mDynamicColorLoader.setStrength(strength);
        }
    }

    /**
     * 根据解压路径和shader名称读取shader的字符串内容
     * @param unzipPath
     * @param shaderName
     * @return
     */
    protected static String getShaderString(Context context, String unzipPath, String shaderName) {
        if (TextUtils.isEmpty(unzipPath) || TextUtils.isEmpty(shaderName)) {
            throw new IllegalArgumentException("shader is empty!");
        }
        String path = unzipPath + "/" + shaderName;
        if (path.startsWith("assets://")) {
            return OpenGLUtils.getShaderFromAssets(context, path.substring("assets://".length()));
        } else if (path.startsWith("file://")) {
            return OpenGLUtils.getShaderFromFile(path.substring("file://".length()));
        }
        return OpenGLUtils.getShaderFromFile(path);
    }

}

接下来我们构建动态滤镜组,因为动态滤镜有可能有多个滤镜组合而成。代码如下:

public class GLImageDynamicColorFilter extends GLImageGroupFilter {

    public GLImageDynamicColorFilter(Context context, DynamicColor dynamicColor) {
        super(context);
        // 判断数据是否存在
        if (dynamicColor == null || dynamicColor.filterList == null
                || TextUtils.isEmpty(dynamicColor.unzipPath)) {
            return;
        }
        // 添加滤镜
        for (int i = 0; i < dynamicColor.filterList.size(); i++) {
            mFilters.add(new DynamicColorFilter(context, dynamicColor.filterList.get(i), dynamicColor.unzipPath));
        }
    }

    /**
     * 设置滤镜强度
     * @param strength
     */
    public void setStrength(float strength) {
        for (int i = 0; i < mFilters.size(); i++) {
            if (mFilters.get(i) != null && mFilters.get(i) instanceof DynamicColorBaseFilter) {
                ((DynamicColorBaseFilter) mFilters.get(i)).setStrength(strength);
            }
        }
    }
}

总结

基本的动态滤镜实现起来比较简单,总的来说就是简单的json参数、shader、统一变量和纹理绑定需要做成动态构建的过程而已。
效果如下:


动态滤镜效果

该效果是通过解压asset目录下的压缩包资源来实现的。你只需要提供包含shader 、纹理资源、以及json的压缩包即可更改滤镜。

详细实现过程,可参考本人的开源项目:
CainCamera

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容