Android OpenGLES how to add filters to the camera

Filter introduction

At present, there are many filters on the market, but the overall classification is just a few, which are processed in the fragment shader. At present, the most commonly used filters are LUT filter and filter for adjusting RGB curve. Other types of changes are similar.

Construction of dynamic filter

In order to realize the dynamically downloaded filter, we next implement a set of JSON parameters of the filter, mainly including filter type, filter name, vertex shader, fragment shader file, unified variable list, texture pictures bound to unified variables, default filter strength, whether to bring texture width and height offset, music path, whether to play music circularly, etc.

JSON and various fields are described as follows:

{
 "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  // 是否循环播放音乐
 }]
}

After having JSON, we need to decode the filter parameter object as follows:

/**
  * 解码滤镜数据
  * @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;
 }

Implementation of filter

After decoding the filter parameters, we then implement the dynamic filter rendering process. To facilitate the construction of filters, we create a filter resource loader with the following code:

/**
 * 滤镜资源加载器
 */
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;
 }
}

Then we build a base class of dynamiccolorfilter to facilitate the subsequent addition of other types of filters. The code is as follows:

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,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);
 }

}

Next, we build a dynamic filter group, because the dynamic filter may be composed of multiple filters. The code is as follows:

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);
   }
  }
 }
}

summary

The basic dynamic filter is relatively simple to implement. In general, it is just a dynamic construction process for simple JSON parameters, shaders, unified variables and texture binding.

The effects are as follows:

This effect is achieved by decompressing the compressed package resources in the asset directory. You only need to provide a compressed package containing shaders, texture resources, and JSON to change the filter.

For the detailed implementation process, please refer to my open source project: caincamera

Well, the above is the whole content of this article. I hope the content of this article has a certain reference value for your study or work. Thank you for your support.

The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>