1. 前言
这段时间,在使用 natario1/CameraView 来实现带滤镜的预览
、拍照
、录像
功能。
由于CameraView
封装的比较到位,在项目前期,的确为我们节省了不少时间。
但随着项目持续深入,对于CameraView
的使用进入深水区,逐渐出现满足不了我们需求的情况。
Github
中的issues
中,有些BUG
作者一直没有修复。
那要怎么办呢 ? 项目迫切地需要实现相关功能,只能自己硬着头皮去看它的源码,去解决这些问题。
上篇文章,我们对拍照的流程有了大致的了解,这篇文章,我们来看下滤镜相关的类,为后面带滤镜拍照的源码解析做下铺垫。
以下源码解析基于CameraView 2.7.2
implementation("com.otaliastudios:cameraview:2.7.2")
为了在博客上更好的展示,本文贴出的代码进行了部分精简
2. 如何设置滤镜
在CameraView
中,通过setFilter(Filter filter)
来设置滤镜。
//初始化亮度滤镜
val brightnessFilter = BrightnessFilter()
//设置亮度值
brightnessFilter.setBrightness(1.5F)
//设置滤镜
cameraView.setFilter(brightnessFilter)
3. Filter
Filter
是一个接口,定义了获取顶点着色器
、获取片元着色器
、当初始化时
、当销毁时
、当绘制时
、设置尺寸
、拷贝滤镜
public interface Filter {
/**
* 获取顶点着色器
*/
String getVertexShader();
/**
* 获取片元着色器
*/
String getFragmentShader();
/**
* 初始化时调用
*/
void onCreate(int programHandle);
/**
* 销毁时调用
*
*/
void onDestroy();
/**
* 当绘制的时候
*/
void draw(long timestampUs, float[] transformMatrix);
/**
* 设置尺寸
*/
void setSize(int width, int height);
/**
* 复制滤镜
*/
Filter copy();
}
4. BaseFilter
BaseFilter
是一个抽象类,实现了Filter
接口,BaseFilter
实现了默认的顶点着色器和片元着色器,在onCreate
的时候,创建了具体执行OpenGL API
的GlTextureProgram
,copy
的时候,会根据OneParameterFilter
和TwoParameterFilter
接口,复制Filter
。
public abstract class BaseFilter implements Filter {
//...省略了具体代码...
}
接下来来看BaseFilter
的具体代码
4.1 默认的顶点着色器和片元着色器
实现了默认的顶点着色器和片元着色器
protected final static String DEFAULT_VERTEX_POSITION_NAME = "aPosition";
protected final static String DEFAULT_VERTEX_TEXTURE_COORDINATE_NAME = "aTextureCoord";
protected final static String DEFAULT_VERTEX_MVP_MATRIX_NAME = "uMVPMatrix";
protected final static String DEFAULT_VERTEX_TRANSFORM_MATRIX_NAME = "uTexMatrix";
protected final static String DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME = "vTextureCoord";
private static String createDefaultVertexShader(
@NonNull String vertexPositionName,
@NonNull String vertexTextureCoordinateName,
@NonNull String vertexModelViewProjectionMatrixName,
@NonNull String vertexTransformMatrixName,
@NonNull String fragmentTextureCoordinateName) {
return "uniform mat4 "+vertexModelViewProjectionMatrixName+";\n"
+ "uniform mat4 "+vertexTransformMatrixName+";\n"
+ "attribute vec4 "+vertexPositionName+";\n"
+ "attribute vec4 "+vertexTextureCoordinateName+";\n"
+ "varying vec2 "+fragmentTextureCoordinateName+";\n"
+ "void main() {\n"
+ " gl_Position = " +vertexModelViewProjectionMatrixName+" * "
+ vertexPositionName+";\n"
+ " "+fragmentTextureCoordinateName+" = ("+vertexTransformMatrixName+" * "
+ vertexTextureCoordinateName+").xy;\n"
+ "}\n";
}
private static String createDefaultFragmentShader(
@NonNull String fragmentTextureCoordinateName) {
return "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "varying vec2 "+fragmentTextureCoordinateName+";\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "void main() {\n"
+ " gl_FragColor = texture2D(sTexture, "+fragmentTextureCoordinateName+");\n"
+ "}\n";
}
4.2 创建GlTextureProgram
GlTextureProgram
是对OpenGL
纹理绘制的具体实现,这里传入了顶点着色器和片元着色器
等,创建了GlTextureProgram
@Override
public void onCreate(int programHandle) {
program = new GlTextureProgram(programHandle,
vertexPositionName,
vertexModelViewProjectionMatrixName,
vertexTextureCoordinateName,
vertexTransformMatrixName);
programDrawable = new GlRect();
}
4.3 设置尺寸并绘制
在合适的机会设置尺寸并绘制,绘制里面有三个方法onPreDraw
、onDraw
、onPostDraw
,内部都是调用的GlTextureProgram
对应的onPreDraw
、onDraw
、onPostDraw
,而GlTextureProgram
里面,我们现在只需要知道是OpenGL API
具体的方法就行了。
@Override
public void setSize(int width, int height) {
size = new Size(width, height);
}
@Override
public void draw(long timestampUs, @NonNull float[] transformMatrix) {
onPreDraw(timestampUs, transformMatrix);
onDraw(timestampUs);
onPostDraw(timestampUs);
}
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
program.setTextureTransform(transformMatrix);
program.onPreDraw(programDrawable, programDrawable.getModelMatrix());
}
protected void onDraw(long timestampUs) {
program.onDraw(programDrawable);
}
protected void onPostDraw(long timestampUs) {
program.onPostDraw(programDrawable);
}
4.4 拷贝滤镜
copy
方法,内部调用了getClass().newInstance()
来反射得到一个新的BaseFilter
,并赋值了Size
,如果实现了OneParameterFilter
或TwoParameterFilter
接口,还会给设置相关的参数。
比如亮度滤镜的亮度值,就需要实现
OneParameterFilter
或TwoParameterFilter
接口,从而使设置的亮度值,赋值到新的BaseFilter
中
@NonNull
@Override
public final BaseFilter copy() {
BaseFilter copy = onCopy();
if (size != null) {
copy.setSize(size.getWidth(), size.getHeight());
}
if (this instanceof OneParameterFilter) {
((OneParameterFilter) copy).setParameter1(((OneParameterFilter) this).getParameter1());
}
if (this instanceof TwoParameterFilter) {
((TwoParameterFilter) copy).setParameter2(((TwoParameterFilter) this).getParameter2());
}
return copy;
}
@NonNull
protected BaseFilter onCopy() {
try {
return getClass().newInstance();
} catch (IllegalAccessException e) {
throw new RuntimeException("Filters should have a public no-arguments constructor.", e);
} catch (InstantiationException e) {
throw new RuntimeException("Filters should have a public no-arguments constructor.", e);
}
}
那么我们就会有疑问了,copy
方法在什么情况下会使用呢 ?
根据源码,可以看到在带滤镜拍照相关的SnapshotGlPictureRecorder
类中,会用到copy
方法。
protected void onRendererFilterChanged(@NonNull Filter filter) {
mTextureDrawer.setFilter(filter.copy());
}
就是预览和拍照用的BaseFilter
其实不是同一个Fitler
,而是会先copy
一份,再去拍照。
因为为了预览流畅,预览和拍照其实用的不是同一个Surface
(后面会讲),原来的Fitler
已经被预览使用了,所以需要Copy
一份,再给拍照使用。
5. 预置的滤镜
CameraView
预置了一些常见的滤镜,可以直接拿来使用。
5.1 预设的滤镜大全
预设的滤镜有以下这些
5.2 亮度滤镜
比如BrightnessFilter
是调节亮度的滤镜,其代码如下
可以看到,里面传入了相关的GLSL
代码,并在onPreDraw
设置了亮度值。
public class BrightnessFilter extends BaseFilter implements OneParameterFilter {
private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "uniform float brightness;\n"
+ "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n"
+ "void main() {\n"
+ " vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+");\n"
+ " gl_FragColor = brightness * color;\n"
+ "}\n";
private float brightness = 2.0f; // 1.0F...2.0F
private int brightnessLocation = -1;
public BrightnessFilter() { }
/**
* Sets the brightness adjustment.
* 1.0: normal brightness.
* 2.0: high brightness.
*
* @param brightness brightness.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public void setBrightness(float brightness) {
if (brightness < 1.0f) brightness = 1.0f;
if (brightness > 2.0f) brightness = 2.0f;
this.brightness = brightness;
}
/**
* Returns the current brightness.
*
* @see #setBrightness(float)
* @return brightness
*/
@SuppressWarnings({"unused", "WeakerAccess"})
public float getBrightness() {
return brightness;
}
@Override
public void setParameter1(float value) {
// parameter is 0...1, brightness is 1...2.
setBrightness(value + 1);
}
@Override
public float getParameter1() {
// parameter is 0...1, brightness is 1...2.
return getBrightness() - 1F;
}
@NonNull
@Override
public String getFragmentShader() {
return FRAGMENT_SHADER;
}
@Override
public void onCreate(int programHandle) {
super.onCreate(programHandle);
brightnessLocation = GLES20.glGetUniformLocation(programHandle, "brightness");
Egloo.checkGlProgramLocation(brightnessLocation, "brightness");
}
@Override
public void onDestroy() {
super.onDestroy();
brightnessLocation = -1;
}
@Override
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix);
GLES20.glUniform1f(brightnessLocation, brightness);
Egloo.checkGlError("glUniform1f");
}
}
6. MultiFilter
单个滤镜的调用直接调用某个滤镜就可以了,但如果是多个滤镜进行叠加,那么就需要用到MultiFilter
,通过addFilter()
来叠加多个滤镜。
public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilter {
//...省略了具体代码...
}
6.1 添加滤镜
将添加的滤镜存储在filters
列表中
final List<Filter> filters = new ArrayList<>();
public void addFilter(@NonNull Filter filter) {
if (filter instanceof MultiFilter) {
MultiFilter multiFilter = (MultiFilter) filter;
for (Filter multiChild : multiFilter.filters) {
addFilter(multiChild);
}
return;
}
synchronized (lock) {
if (!filters.contains(filter)) {
filters.add(filter);
states.put(filter, new State());
}
}
}
6.2 绘制滤镜
遍历filters
列表,并调用一系列OpenGL
的方法,逐个绘制滤镜,上一个滤镜绘制好后,下一个滤镜在上一个滤镜的基础上再绘制,从而最终达到滤镜叠加的效果。
@Override
public void draw(long timestampUs, @NonNull float[] transformMatrix) {
synchronized (lock) {
for (int i = 0; i < filters.size(); i++) {
boolean isFirst = i == 0;
boolean isLast = i == filters.size() - 1;
Filter filter = filters.get(i);
State state = states.get(filter);
maybeSetSize(filter);
maybeCreateProgram(filter, isFirst, isLast);
maybeCreateFramebuffer(filter, isFirst, isLast);
//noinspection ConstantConditions
GLES20.glUseProgram(state.programHandle);
// Define the output framebuffer.
// Each filter outputs into its own framebuffer object, except the
// last filter, which outputs into the default framebuffer.
if (!isLast) {
state.outputFramebuffer.bind();
GLES20.glClearColor(0, 0, 0, 0);
} else {
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
}
// Perform the actual drawing.
// The first filter should apply all the transformations. Then,
// since they are applied, we should use a no-op matrix.
if (isFirst) {
filter.draw(timestampUs, transformMatrix);
} else {
filter.draw(timestampUs, Egloo.IDENTITY_MATRIX);
}
// Set the input for the next cycle:
// It is the framebuffer texture from this cycle. If this is the last
// filter, reset this value just to cleanup.
if (!isLast) {
state.outputTexture.bind();
} else {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
}
GLES20.glUseProgram(0);
}
}
}
7. 其他
7.1 CameraView源码解析系列
Android 相机库CameraView源码解析 (一) : 预览-CSDN博客
Android 相机库CameraView源码解析 (二) : 拍照-CSDN博客
Android 相机库CameraView源码解析 (三) : 滤镜相关类说明-CSDN博客
Android 相机库CameraView源码解析 (四) : 带滤镜拍照-CSDN博客
Android 相机库CameraView源码解析 (五) : 保存滤镜效果-CSDN博客