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