MultiFilter to merge filters (#559)
* Ensure copied filters have right size * Create MultiFilter base implementation * Working implementation with replaceAll hack * Implement OneParameterFilter and TwoParameterFilter in MultiFilter * Add docs * Add tests * Fix rotation issues * Fix tests and cleanup * Small improvementspull/565/head
parent
83307c527e
commit
d333348ce6
@ -0,0 +1,213 @@ |
|||||||
|
package com.otaliastudios.cameraview.filter; |
||||||
|
|
||||||
|
|
||||||
|
import android.opengl.GLES20; |
||||||
|
|
||||||
|
import androidx.annotation.NonNull; |
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4; |
||||||
|
import androidx.test.filters.SmallTest; |
||||||
|
|
||||||
|
import com.otaliastudios.cameraview.BaseEglTest; |
||||||
|
import com.otaliastudios.cameraview.filters.AutoFixFilter; |
||||||
|
import com.otaliastudios.cameraview.filters.BrightnessFilter; |
||||||
|
import com.otaliastudios.cameraview.filters.DuotoneFilter; |
||||||
|
import com.otaliastudios.cameraview.filters.VignetteFilter; |
||||||
|
import com.otaliastudios.cameraview.internal.GlUtils; |
||||||
|
import com.otaliastudios.cameraview.internal.egl.EglViewport; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.runner.RunWith; |
||||||
|
import org.mockito.invocation.InvocationOnMock; |
||||||
|
import org.mockito.stubbing.Answer; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals; |
||||||
|
import static org.junit.Assert.assertFalse; |
||||||
|
import static org.junit.Assert.assertNotNull; |
||||||
|
import static org.junit.Assert.assertNull; |
||||||
|
import static org.junit.Assert.assertTrue; |
||||||
|
import static org.mockito.ArgumentMatchers.any; |
||||||
|
import static org.mockito.ArgumentMatchers.anyInt; |
||||||
|
import static org.mockito.Mockito.atLeastOnce; |
||||||
|
import static org.mockito.Mockito.doAnswer; |
||||||
|
import static org.mockito.Mockito.never; |
||||||
|
import static org.mockito.Mockito.spy; |
||||||
|
import static org.mockito.Mockito.times; |
||||||
|
import static org.mockito.Mockito.verify; |
||||||
|
import static org.mockito.Mockito.when; |
||||||
|
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4.class) |
||||||
|
@SmallTest |
||||||
|
public class MultiFilterTest extends BaseEglTest { |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testConstructor1() { |
||||||
|
MultiFilter multiFilter = new MultiFilter( |
||||||
|
new DuotoneFilter(), |
||||||
|
new AutoFixFilter() |
||||||
|
); |
||||||
|
assertEquals(2, multiFilter.filters.size()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testConstructor2() { |
||||||
|
List<Filter> filters = new ArrayList<>(); |
||||||
|
filters.add(new DuotoneFilter()); |
||||||
|
filters.add(new AutoFixFilter()); |
||||||
|
MultiFilter multiFilter = new MultiFilter(filters); |
||||||
|
assertEquals(2, multiFilter.filters.size()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testAddFilter() { |
||||||
|
MultiFilter multiFilter = new MultiFilter(); |
||||||
|
assertEquals(0, multiFilter.filters.size()); |
||||||
|
multiFilter.addFilter(new DuotoneFilter()); |
||||||
|
assertEquals(1, multiFilter.filters.size()); |
||||||
|
multiFilter.addFilter(new AutoFixFilter()); |
||||||
|
assertEquals(2, multiFilter.filters.size()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testAddFilter_multi() { |
||||||
|
MultiFilter multiFilter = new MultiFilter(new DuotoneFilter()); |
||||||
|
assertEquals(1, multiFilter.filters.size()); |
||||||
|
MultiFilter other = new MultiFilter( |
||||||
|
new AutoFixFilter(), |
||||||
|
new BrightnessFilter(), |
||||||
|
new VignetteFilter()); |
||||||
|
multiFilter.addFilter(other); |
||||||
|
assertEquals(4, multiFilter.filters.size()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testSetSize() { |
||||||
|
DuotoneFilter filter = new DuotoneFilter(); |
||||||
|
MultiFilter multiFilter = new MultiFilter(filter); |
||||||
|
MultiFilter.State state = multiFilter.states.get(filter); |
||||||
|
assertNotNull(state); |
||||||
|
assertNull(state.size); |
||||||
|
multiFilter.setSize(WIDTH, HEIGHT); |
||||||
|
assertNotNull(state.size); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testCopy() { |
||||||
|
DuotoneFilter filter = spy(new DuotoneFilter()); |
||||||
|
MultiFilter multiFilter = new MultiFilter(filter); |
||||||
|
MultiFilter multiFilterCopy = (MultiFilter) multiFilter.copy(); |
||||||
|
assertEquals(1, multiFilterCopy.filters.size()); |
||||||
|
verify(filter, times(1)).onCopy(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testParameter1() { |
||||||
|
DuotoneFilter filter = new DuotoneFilter(); |
||||||
|
MultiFilter multiFilter = new MultiFilter(filter); |
||||||
|
float desired = 0.21582F; // whatever
|
||||||
|
multiFilter.setParameter1(desired); |
||||||
|
assertEquals(desired, multiFilter.getParameter1(), 0.001F); |
||||||
|
assertEquals(desired, filter.getParameter1(), 0.001F); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testParameter2() { |
||||||
|
DuotoneFilter filter = new DuotoneFilter(); |
||||||
|
MultiFilter multiFilter = new MultiFilter(filter); |
||||||
|
float desired = 0.21582F; // whatever
|
||||||
|
multiFilter.setParameter2(desired); |
||||||
|
assertEquals(desired, multiFilter.getParameter2(), 0.001F); |
||||||
|
assertEquals(desired, filter.getParameter2(), 0.001F); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testOnCreate_isLazy() { |
||||||
|
DuotoneFilter filter = spy(new DuotoneFilter()); |
||||||
|
MultiFilter multiFilter = new MultiFilter(filter); |
||||||
|
|
||||||
|
int program = GlUtils.createProgram(multiFilter.getVertexShader(), |
||||||
|
multiFilter.getFragmentShader()); |
||||||
|
multiFilter.onCreate(program); |
||||||
|
verify(filter, never()).onCreate(anyInt()); |
||||||
|
|
||||||
|
multiFilter.onDestroy(); |
||||||
|
GLES20.glDeleteProgram(program); |
||||||
|
verify(filter, never()).onDestroy(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testDraw_simple() { |
||||||
|
DuotoneFilter filter = spy(new DuotoneFilter()); |
||||||
|
MultiFilter multiFilter = new MultiFilter(filter); |
||||||
|
multiFilter.setSize(WIDTH, HEIGHT); |
||||||
|
EglViewport viewport = new EglViewport(multiFilter); |
||||||
|
int texture = viewport.createTexture(); |
||||||
|
float[] matrix = new float[16]; |
||||||
|
viewport.drawFrame(texture, matrix); |
||||||
|
viewport.release(); |
||||||
|
|
||||||
|
// The child should have experienced the whole lifecycle.
|
||||||
|
verify(filter, atLeastOnce()).getVertexShader(); |
||||||
|
verify(filter, atLeastOnce()).getFragmentShader(); |
||||||
|
verify(filter, atLeastOnce()).setSize(anyInt(), anyInt()); |
||||||
|
verify(filter, times(1)).onCreate(anyInt()); |
||||||
|
verify(filter, times(1)).draw(matrix); |
||||||
|
verify(filter, times(1)).onDestroy(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testDraw_multi() { |
||||||
|
// Want to test that when filter1 is drawn, the current framebuffer is a
|
||||||
|
// non-default one. When filter2 is drawn, the current framebuffer is 0.
|
||||||
|
final DuotoneFilter filter1 = spy(new DuotoneFilter()); |
||||||
|
final DuotoneFilter filter2 = spy(new DuotoneFilter()); |
||||||
|
final MultiFilter multiFilter = new MultiFilter(filter1, filter2); |
||||||
|
multiFilter.setSize(WIDTH, HEIGHT); |
||||||
|
float[] matrix = new float[16]; |
||||||
|
final int[] result = new int[1]; |
||||||
|
|
||||||
|
doAnswer(new Answer() { |
||||||
|
@Override |
||||||
|
public Object answer(InvocationOnMock invocation) { |
||||||
|
MultiFilter.State state = multiFilter.states.get(filter1); |
||||||
|
assertNotNull(state); |
||||||
|
assertTrue(state.isCreated); |
||||||
|
assertTrue(state.isFramebufferCreated); |
||||||
|
|
||||||
|
GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, result, 0); |
||||||
|
assertTrue(result[0] != 0); |
||||||
|
return null; |
||||||
|
} |
||||||
|
}).when(filter1).draw(matrix); |
||||||
|
|
||||||
|
// Note: second filter is drawn with the identity matrix!
|
||||||
|
doAnswer(new Answer() { |
||||||
|
@Override |
||||||
|
public Object answer(InvocationOnMock invocation) { |
||||||
|
// The last filter has no FBO / texture.
|
||||||
|
MultiFilter.State state = multiFilter.states.get(filter2); |
||||||
|
assertNotNull(state); |
||||||
|
assertTrue(state.isCreated); |
||||||
|
assertFalse(state.isFramebufferCreated); |
||||||
|
|
||||||
|
GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, result, 0); |
||||||
|
assertEquals(0, result[0]); |
||||||
|
return null; |
||||||
|
|
||||||
|
} |
||||||
|
}).when(filter2).draw(any(float[].class)); |
||||||
|
|
||||||
|
EglViewport viewport = new EglViewport(multiFilter); |
||||||
|
int texture = viewport.createTexture(); |
||||||
|
viewport.drawFrame(texture, matrix); |
||||||
|
viewport.release(); |
||||||
|
|
||||||
|
// Verify that both are drawn.
|
||||||
|
verify(filter1, times(1)).draw(matrix); |
||||||
|
verify(filter2, times(1)).draw(any(float[].class)); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,325 @@ |
|||||||
|
package com.otaliastudios.cameraview.filter; |
||||||
|
|
||||||
|
import android.opengl.GLES11Ext; |
||||||
|
import android.opengl.GLES20; |
||||||
|
|
||||||
|
import androidx.annotation.NonNull; |
||||||
|
import androidx.annotation.VisibleForTesting; |
||||||
|
|
||||||
|
import com.otaliastudios.cameraview.internal.GlUtils; |
||||||
|
import com.otaliastudios.cameraview.size.Size; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Collection; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* A {@link MultiFilter} is a special {@link Filter} that can group one or more filters together. |
||||||
|
* When this happens, filters are applied in sequence: |
||||||
|
* - the first filter reads from input frames |
||||||
|
* - the second filters reads the output of the first |
||||||
|
* And so on, until the last filter which will read from the previous and write to the real output. |
||||||
|
* |
||||||
|
* New filters can be added at any time through {@link #addFilter(Filter)}, but currently they |
||||||
|
* can not be removed because we can not easily ensure that they would be correctly released. |
||||||
|
* |
||||||
|
* The {@link MultiFilter} does also implement {@link OneParameterFilter} and {@link TwoParameterFilter}, |
||||||
|
* dispatching all the parameter calls to child filters, assuming they support it. |
||||||
|
* |
||||||
|
* There are some important technical caveats when using {@link MultiFilter}: |
||||||
|
* - each child filter requires the allocation of a GL framebuffer. Using a large number of filters |
||||||
|
* will likely cause memory issues (e.g. https://stackoverflow.com/q/6354208/4288782).
|
||||||
|
* - some of the children need to write into {@link GLES20#GL_TEXTURE_2D} instead of |
||||||
|
* {@link GLES11Ext#GL_TEXTURE_EXTERNAL_OES}! To achieve this, we replace samplerExternalOES |
||||||
|
* with sampler2D in your fragment shader code. This might cause issues for some shaders. |
||||||
|
*/ |
||||||
|
@SuppressWarnings("unused") |
||||||
|
public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilter { |
||||||
|
|
||||||
|
@VisibleForTesting |
||||||
|
static class State { |
||||||
|
@VisibleForTesting boolean isCreated = false; |
||||||
|
@VisibleForTesting boolean isFramebufferCreated = false; |
||||||
|
@VisibleForTesting Size size = null; |
||||||
|
|
||||||
|
private int programHandle = -1; |
||||||
|
private int framebufferId = -1; |
||||||
|
private int textureId = -1; |
||||||
|
} |
||||||
|
|
||||||
|
@VisibleForTesting final List<Filter> filters = new ArrayList<>(); |
||||||
|
@VisibleForTesting final Map<Filter, State> states = new HashMap<>(); |
||||||
|
private final Object lock = new Object(); |
||||||
|
private Size size = null; |
||||||
|
private float parameter1 = 0F; |
||||||
|
private float parameter2 = 0F; |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a new group with the given filters. |
||||||
|
* @param filters children |
||||||
|
*/ |
||||||
|
@SuppressWarnings("WeakerAccess") |
||||||
|
public MultiFilter(@NonNull Filter... filters) { |
||||||
|
this(Arrays.asList(filters)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a new group with the given filters. |
||||||
|
* @param filters children |
||||||
|
*/ |
||||||
|
@SuppressWarnings("WeakerAccess") |
||||||
|
public MultiFilter(@NonNull Collection<Filter> filters) { |
||||||
|
for (Filter filter : filters) { |
||||||
|
addFilter(filter); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Adds a new filter. It will be used in the next frame. |
||||||
|
* If the filter is a {@link MultiFilter}, we'll use its children instead. |
||||||
|
* |
||||||
|
* @param filter a new filter |
||||||
|
*/ |
||||||
|
@SuppressWarnings("WeakerAccess") |
||||||
|
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()); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// We don't offer a removeFilter method since that would cause issues
|
||||||
|
// with cleanup. Cleanup must happen on the GL thread so we'd have to wait
|
||||||
|
// for new rendering call (which might not even happen).
|
||||||
|
|
||||||
|
private void maybeCreate(@NonNull Filter filter, boolean isFirst) { |
||||||
|
State state = states.get(filter); |
||||||
|
//noinspection ConstantConditions
|
||||||
|
if (!state.isCreated) { |
||||||
|
state.isCreated = true; |
||||||
|
String shader = filter.getFragmentShader(); |
||||||
|
if (!isFirst) { |
||||||
|
// The first shader actually reads from a OES texture, but the others
|
||||||
|
// will read from the 2d framebuffer texture. This is a dirty hack.
|
||||||
|
shader = shader.replace("samplerExternalOES ", "sampler2D "); |
||||||
|
} |
||||||
|
state.programHandle = GlUtils.createProgram(filter.getVertexShader(), shader); |
||||||
|
filter.onCreate(state.programHandle); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void maybeDestroy(@NonNull Filter filter) { |
||||||
|
State state = states.get(filter); |
||||||
|
//noinspection ConstantConditions
|
||||||
|
if (state.isCreated) { |
||||||
|
state.isCreated = false; |
||||||
|
filter.onDestroy(); |
||||||
|
GLES20.glDeleteProgram(state.programHandle); |
||||||
|
state.programHandle = -1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void maybeCreateFramebuffer(@NonNull Filter filter, boolean isLast) { |
||||||
|
if (isLast) return; |
||||||
|
State state = states.get(filter); |
||||||
|
//noinspection ConstantConditions
|
||||||
|
if (!state.isFramebufferCreated) { |
||||||
|
state.isFramebufferCreated = true; |
||||||
|
|
||||||
|
int[] framebufferArray = new int[1]; |
||||||
|
int[] textureArray = new int[1]; |
||||||
|
GLES20.glGenFramebuffers(1, framebufferArray, 0); |
||||||
|
GLES20.glGenTextures(1, textureArray, 0); |
||||||
|
state.framebufferId = framebufferArray[0]; |
||||||
|
state.textureId = textureArray[0]; |
||||||
|
|
||||||
|
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, state.textureId); |
||||||
|
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, state.size.getWidth(), state.size.getHeight(), 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); |
||||||
|
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); |
||||||
|
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); |
||||||
|
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, state.framebufferId); |
||||||
|
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, |
||||||
|
GLES20.GL_COLOR_ATTACHMENT0, |
||||||
|
GLES20.GL_TEXTURE_2D, |
||||||
|
state.textureId, |
||||||
|
0); |
||||||
|
|
||||||
|
int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER); |
||||||
|
if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) { |
||||||
|
throw new RuntimeException("Invalid framebuffer generation. Error:" + status); |
||||||
|
} |
||||||
|
|
||||||
|
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); |
||||||
|
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void maybeDestroyFramebuffer(@NonNull Filter filter) { |
||||||
|
State state = states.get(filter); |
||||||
|
//noinspection ConstantConditions
|
||||||
|
if (state.isFramebufferCreated) { |
||||||
|
state.isFramebufferCreated = false; |
||||||
|
GLES20.glDeleteFramebuffers(1, new int[]{state.framebufferId}, 0); |
||||||
|
state.framebufferId = -1; |
||||||
|
GLES20.glDeleteTextures(1, new int[]{state.textureId}, 0); |
||||||
|
state.textureId = -1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void maybeSetSize(@NonNull Filter filter) { |
||||||
|
State state = states.get(filter); |
||||||
|
//noinspection ConstantConditions
|
||||||
|
if (size != null && !size.equals(state.size)) { |
||||||
|
state.size = size; |
||||||
|
filter.setSize(size.getWidth(), size.getHeight()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public String getVertexShader() { |
||||||
|
// Whatever, we won't be using this.
|
||||||
|
return new NoFilter().getVertexShader(); |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public String getFragmentShader() { |
||||||
|
// Whatever, we won't be using this.
|
||||||
|
return new NoFilter().getFragmentShader(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onCreate(int programHandle) { |
||||||
|
// We'll create children during the draw() op, since some of them
|
||||||
|
// might have been added after this onCreate() is called.
|
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onDestroy() { |
||||||
|
synchronized (lock) { |
||||||
|
for (Filter filter : filters) { |
||||||
|
maybeDestroyFramebuffer(filter); |
||||||
|
maybeDestroy(filter); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void setSize(int width, int height) { |
||||||
|
size = new Size(width, height); |
||||||
|
synchronized (lock) { |
||||||
|
for (Filter filter : filters) { |
||||||
|
maybeSetSize(filter); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void draw(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); |
||||||
|
maybeCreate(filter, isFirst); |
||||||
|
maybeCreateFramebuffer(filter, 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) { |
||||||
|
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, state.framebufferId); |
||||||
|
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(transformMatrix); |
||||||
|
} else { |
||||||
|
filter.draw(GlUtils.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) { |
||||||
|
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, state.textureId); |
||||||
|
} else { |
||||||
|
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); |
||||||
|
} |
||||||
|
|
||||||
|
GLES20.glUseProgram(0); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public Filter copy() { |
||||||
|
synchronized (lock) { |
||||||
|
MultiFilter copy = new MultiFilter(); |
||||||
|
for (Filter filter : filters) { |
||||||
|
copy.addFilter(filter.copy()); |
||||||
|
} |
||||||
|
return copy; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void setParameter1(float parameter1) { |
||||||
|
this.parameter1 = parameter1; |
||||||
|
synchronized (lock) { |
||||||
|
for (Filter filter : filters) { |
||||||
|
if (filter instanceof OneParameterFilter) { |
||||||
|
((OneParameterFilter) filter).setParameter1(parameter1); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void setParameter2(float parameter2) { |
||||||
|
this.parameter2 = parameter2; |
||||||
|
synchronized (lock) { |
||||||
|
for (Filter filter : filters) { |
||||||
|
if (filter instanceof TwoParameterFilter) { |
||||||
|
((TwoParameterFilter) filter).setParameter2(parameter2); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public float getParameter1() { |
||||||
|
return parameter1; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public float getParameter2() { |
||||||
|
return parameter2; |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue