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博客