diff --git a/README.md b/README.md index c433d6da..26ef8f02 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ api 'com.otaliastudios:cameraview:2.0.0' - Fast & reliable - Gestures support [[docs]](https://natario1.github.io/CameraView/docs/gestures.html) +- Real-time filters [[docs]](https://natario1.github.io/CameraView/docs/filters.html) - Camera1 or Camera2 powered engine [[docs]](https://natario1.github.io/CameraView/docs/previews.html) - Frame processing support [[docs]](https://natario1.github.io/CameraView/docs/frame-processing.html) - Watermarks & animated overlays [[docs]](https://natario1.github.io/CameraView/docs/watermarks-and-overlays.html) @@ -101,6 +102,7 @@ motivation boost to push the library forward. app:cameraAutoFocusResetDelay="@integer/autofocus_delay" app:cameraAutoFocusMarker="@string/cameraview_default_autofocus_marker" app:cameraUseDeviceOrientation="true|false" + app:cameraFilter="@string/real_time_filter" app:cameraExperimental="false|true"> diff --git a/cameraview/build.gradle b/cameraview/build.gradle index 923f85b7..2b9f9bd1 100644 --- a/cameraview/build.gradle +++ b/cameraview/build.gradle @@ -245,6 +245,9 @@ task mergedCoverageReport(type: JacocoReport) { // TODO these below could be testable ALSO outside of the integration tests classFilter.add('**/com/otaliastudios/cameraview/video/encoding/**.*') } + // We don't test OpenGL filters. + classFilter.add('**/com/otaliastudios/cameraview/filters/**.*') + classDirectories = fileTree(dir: classDir, excludes: classFilter); reports.html.enabled = true diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java index 86b63c22..1c0d7ecd 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java @@ -22,6 +22,9 @@ import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Flash; import com.otaliastudios.cameraview.controls.Preview; import com.otaliastudios.cameraview.engine.CameraEngine; +import com.otaliastudios.cameraview.filter.Filter; +import com.otaliastudios.cameraview.filter.Filters; +import com.otaliastudios.cameraview.filter.NoFilter; import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.FrameProcessor; import com.otaliastudios.cameraview.gesture.Gesture; @@ -65,7 +68,7 @@ public class CameraViewTest extends BaseTest { private CameraView cameraView; private MockCameraEngine mockController; - private CameraPreview mockPreview; + private MockCameraPreview mockPreview; private boolean hasPermissions; @Before @@ -825,4 +828,30 @@ public class CameraViewTest extends BaseTest { //endregion // TODO: test permissions + + //region Filter + + @Test(expected = RuntimeException.class) + public void testSetFilter_notExperimental() { + cameraView.setExperimental(false); + cameraView.setFilter(Filters.AUTO_FIX.newInstance()); + } + + @Test + public void testSetFilter_notExperimental_noFilter() { + cameraView.setExperimental(false); + cameraView.setFilter(Filters.NONE.newInstance()); + // no exception thrown + } + + @Test + public void testSetFilter() { + cameraView.setExperimental(true); + Filter filter = Filters.AUTO_FIX.newInstance(); + cameraView.setFilter(filter); + verify(mockPreview, times(1)).setFilter(filter); + assertEquals(filter, cameraView.getFilter()); + //noinspection ResultOfMethodCallIgnored + verify(mockPreview, times(1)).getCurrentFilter(); + } } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java index 86172788..a5b268c7 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java @@ -65,7 +65,6 @@ public abstract class CameraIntegrationTest extends BaseTest { private final static CameraLogger LOG = CameraLogger.create(CameraIntegrationTest.class.getSimpleName()); private final static long DELAY = 8000; - private final static long VIDEO_DELAY = 16000; @Rule public ActivityTestRule rule = new ActivityTestRule<>(TestActivity.class); @@ -183,7 +182,20 @@ public abstract class CameraIntegrationTest extends BaseTest { })); // Wait for onVideoTaken and check. - VideoResult result = video.await(VIDEO_DELAY); + VideoResult result = video.await(DELAY); + + // It seems that when running all the tests together, the device can go in some + // power saving mode which makes the CPU extremely slow. This is especially problematic + // with video snapshots where we do lots of processing. The videoEnd callback can return + // long after the actual stop() call, so if we're still processing, let's wait more. + if (expectSuccess && camera.isTakingVideo()) { + while (camera.isTakingVideo()) { + video.listen(); + result = video.await(DELAY); + } + } + + // Now we should be OK. if (expectSuccess) { assertEquals("Should call onVideoRecordingEnd", 0, onVideoRecordingEnd.getCount()); assertNotNull("Should end video", result); diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/FilterParserTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/FilterParserTest.java new file mode 100644 index 00000000..3a964b40 --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/FilterParserTest.java @@ -0,0 +1,52 @@ +package com.otaliastudios.cameraview.filter; + + +import android.content.res.TypedArray; + +import androidx.annotation.NonNull; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.otaliastudios.cameraview.BaseTest; +import com.otaliastudios.cameraview.R; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static junit.framework.TestCase.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class FilterParserTest extends BaseTest { + + @Test + public void testFallback() { + TypedArray array = mock(TypedArray.class); + when(array.hasValue(R.styleable.CameraView_cameraFilter)).thenReturn(false); + when(array.getString(R.styleable.CameraView_cameraFilter)).thenReturn(null); + FilterParser parser = new FilterParser(array); + assertNotNull(parser.getFilter()); + assertTrue(parser.getFilter() instanceof NoFilter); + } + @Test + public void testConstructor() { + TypedArray array = mock(TypedArray.class); + when(array.hasValue(R.styleable.CameraView_cameraFilter)).thenReturn(true); + when(array.getString(R.styleable.CameraView_cameraFilter)).thenReturn(MyFilter.class.getName()); + FilterParser parser = new FilterParser(array); + assertNotNull(parser.getFilter()); + assertTrue(parser.getFilter() instanceof MyFilter); + } + + public static class MyFilter extends BaseFilter { + @NonNull + @Override + public String getFragmentShader() { + return createDefaultFragmentShader(); + } + } +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/CameraPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/CameraPreviewTest.java index c1838bb3..2142eae9 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/CameraPreviewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/CameraPreviewTest.java @@ -23,16 +23,16 @@ import static org.junit.Assert.*; import static org.mockito.Mockito.*; -public abstract class CameraPreviewTest extends BaseTest { +public abstract class CameraPreviewTest extends BaseTest { private final static long DELAY = 4000; - protected abstract CameraPreview createPreview(Context context, ViewGroup parent); + protected abstract T createPreview(Context context, ViewGroup parent); @Rule public ActivityTestRule rule = new ActivityTestRule<>(TestActivity.class); - protected CameraPreview preview; + protected T preview; @SuppressWarnings("WeakerAccess") protected Size surfaceSize; private CameraPreview.SurfaceCallback callback; diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/GlCameraPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/GlCameraPreviewTest.java index ff8a085e..968ca5db 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/GlCameraPreviewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/GlCameraPreviewTest.java @@ -7,24 +7,37 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import android.view.ViewGroup; +import com.otaliastudios.cameraview.filter.Filter; +import com.otaliastudios.cameraview.filter.Filters; + +import org.junit.Test; import org.junit.runner.RunWith; +import static org.junit.Assert.assertEquals; + @RunWith(AndroidJUnit4.class) @SmallTest -public class GlCameraPreviewTest extends CameraPreviewTest { +public class GlCameraPreviewTest extends CameraPreviewTest { @Override - protected CameraPreview createPreview(Context context, ViewGroup parent) { + protected GlCameraPreview createPreview(Context context, ViewGroup parent) { return new GlCameraPreview(context, parent); } @Override protected float getCropScaleY() { - return 1F / ((GlCameraPreview) preview).mCropScaleY; + return 1F / preview.mCropScaleY; } @Override protected float getCropScaleX() { - return 1F / ((GlCameraPreview) preview).mCropScaleX; + return 1F / preview.mCropScaleX; + } + + @Test + public void testSetFilter() { + Filter filter = Filters.BLACK_AND_WHITE.newInstance(); + preview.setFilter(filter); + assertEquals(filter, preview.getCurrentFilter()); } } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/MockCameraPreview.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/MockCameraPreview.java index d1e5d647..bec20e0e 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/MockCameraPreview.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/MockCameraPreview.java @@ -7,15 +7,17 @@ import androidx.annotation.NonNull; import android.view.View; import android.view.ViewGroup; +import com.otaliastudios.cameraview.filter.Filter; import com.otaliastudios.cameraview.preview.CameraPreview; -public class MockCameraPreview extends CameraPreview { +public class MockCameraPreview extends FilterCameraPreview { public MockCameraPreview(Context context, ViewGroup parent) { super(context, parent); } private View rootView; + private Filter filter; @Override public boolean supportsCropping() { @@ -47,4 +49,15 @@ public class MockCameraPreview extends CameraPreview { public View getRootView() { return rootView; } + + @Override + public void setFilter(@NonNull Filter filter) { + this.filter = filter; + } + + @NonNull + @Override + public Filter getCurrentFilter() { + return filter; + } } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreviewTest.java index 8b92e042..fcf9b282 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreviewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreviewTest.java @@ -11,10 +11,10 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SmallTest -public class SurfaceCameraPreviewTest extends CameraPreviewTest { +public class SurfaceCameraPreviewTest extends CameraPreviewTest { @Override - protected CameraPreview createPreview(Context context, ViewGroup parent) { + protected SurfaceCameraPreview createPreview(Context context, ViewGroup parent) { return new SurfaceCameraPreview(context, parent); } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/TextureCameraPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/TextureCameraPreviewTest.java index e5678e43..0efbfeb5 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/TextureCameraPreviewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/TextureCameraPreviewTest.java @@ -11,10 +11,10 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SmallTest -public class TextureCameraPreviewTest extends CameraPreviewTest { +public class TextureCameraPreviewTest extends CameraPreviewTest { @Override - protected CameraPreview createPreview(Context context, ViewGroup parent) { + protected TextureCameraPreview createPreview(Context context, ViewGroup parent) { return new TextureCameraPreview(context, parent); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index b76543a6..bf7fb76e 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -47,8 +47,10 @@ import com.otaliastudios.cameraview.engine.Camera1Engine; import com.otaliastudios.cameraview.engine.Camera2Engine; import com.otaliastudios.cameraview.engine.CameraEngine; import com.otaliastudios.cameraview.engine.offset.Reference; -import com.otaliastudios.cameraview.filters.Filter; -import com.otaliastudios.cameraview.filters.Filters; +import com.otaliastudios.cameraview.filter.Filter; +import com.otaliastudios.cameraview.filter.FilterParser; +import com.otaliastudios.cameraview.filter.Filters; +import com.otaliastudios.cameraview.filter.NoFilter; import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.FrameProcessor; import com.otaliastudios.cameraview.gesture.Gesture; @@ -68,6 +70,7 @@ import com.otaliastudios.cameraview.markers.MarkerLayout; import com.otaliastudios.cameraview.markers.MarkerParser; import com.otaliastudios.cameraview.overlay.OverlayLayout; import com.otaliastudios.cameraview.preview.CameraPreview; +import com.otaliastudios.cameraview.preview.FilterCameraPreview; import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.preview.SurfaceCameraPreview; import com.otaliastudios.cameraview.preview.TextureCameraPreview; @@ -109,6 +112,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { private HashMap mGestureMap = new HashMap<>(4); private Preview mPreview; private Engine mEngine; + private Filter mPendingFilter; // Components @VisibleForTesting CameraCallbacks mCameraCallbacks; @@ -177,6 +181,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { SizeSelectorParser sizeSelectors = new SizeSelectorParser(a); GestureParser gestures = new GestureParser(a); MarkerParser markers = new MarkerParser(a); + FilterParser filters = new FilterParser(a); a.recycle(); @@ -234,6 +239,9 @@ public class CameraView extends FrameLayout implements LifecycleObserver { // Apply markers setAutoFocusMarker(markers.getAutoFocusMarker()); + // Apply filters + setFilter(filters.getFilter()); + if (!isInEditMode()) { mOrientationHelper = new OrientationHelper(context, mCameraCallbacks); } @@ -261,6 +269,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver { mCameraPreview = instantiatePreview(mPreview, getContext(), this); LOG.w("doInstantiateEngine:", "instantiated. preview:", mCameraPreview.getClass().getSimpleName()); mCameraEngine.setPreview(mCameraPreview); + if (mPendingFilter != null) { + setFilter(mPendingFilter); + mPendingFilter = null; + } } @@ -2132,18 +2144,58 @@ public class CameraView extends FrameLayout implements LifecycleObserver { //endregion - //region Effects + //region Filters - public void setFilter(@NonNull Filters filter) { - setFilter(filter.newInstance()); + /** + * Applies a real-time filter to the camera preview, if it supports it. + * The only preview type that does so is currently {@link Preview#GL_SURFACE}. + * + * The filter will be applied to any picture snapshot taken with + * {@link #takePictureSnapshot()} and any video snapshot taken with + * {@link #takeVideoSnapshot(File)}. + * + * Use {@link NoFilter} to clear the existing filter, + * and take a look at the {@link Filters} class for commonly used filters. + * + * This method will throw an exception if the current preview does not support real-time filters. + * Make sure you use {@link Preview#GL_SURFACE} (the default). + * + * @see Filters + * @param filter a new filter + */ + public void setFilter(@NonNull Filter filter) { + if (mCameraPreview == null) { + mPendingFilter = filter; + } else if (!(filter instanceof NoFilter) && !mExperimental) { + throw new RuntimeException("Filters are an experimental features and need the experimental flag set."); + } else if (mCameraPreview instanceof FilterCameraPreview) { + ((FilterCameraPreview) mCameraPreview).setFilter(filter); + } else { + throw new RuntimeException("Filters are only supported by the GL_SURFACE preview. Current:" + mPreview); + } } - public void setFilter(@NonNull Filter filter) { - if (mCameraPreview instanceof GlCameraPreview) { - ((GlCameraPreview) mCameraPreview).setShaderEffect(filter); + /** + * Returns the current real-time filter applied to the camera preview. + * + * This method will throw an exception if the current preview does not support real-time filters. + * Make sure you use {@link Preview#GL_SURFACE} (the default). + * + * @see #setFilter(Filter) + * @return the current filter + */ + @NonNull + public Filter getFilter() { + if (!mExperimental) { + throw new RuntimeException("Filters are an experimental features and need the experimental flag set."); + } else if (mCameraPreview == null) { + return mPendingFilter; + } else if (mCameraPreview instanceof FilterCameraPreview) { + return ((FilterCameraPreview) mCameraPreview).getCurrentFilter(); } else { - LOG.w("setFilter", "setFilter is supported only for GLSurfaceView"); + throw new RuntimeException("Filters are only supported by the GL_SURFACE preview. Current:" + mPreview); } + } //endregion diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/controls/Preview.java b/cameraview/src/main/java/com/otaliastudios/cameraview/controls/Preview.java index 9e8b9aee..71c7abf3 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/controls/Preview.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/controls/Preview.java @@ -29,7 +29,8 @@ public enum Preview implements Control { /** * Preview engine based on {@link android.opengl.GLSurfaceView}. * This is the best engine available. Supports video snapshots, - * and picture snapshots while taking videos. + * supports picture snapshots while taking videos, supports + * watermarks and overlays, supports real-time filters. */ GL_SURFACE(2); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java new file mode 100644 index 00000000..e85d2a9d --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java @@ -0,0 +1,226 @@ +package com.otaliastudios.cameraview.filter; + +import android.opengl.GLES20; + +import androidx.annotation.NonNull; + +import com.otaliastudios.cameraview.CameraLogger; +import com.otaliastudios.cameraview.internal.GlUtils; +import com.otaliastudios.cameraview.size.Size; + +import java.nio.FloatBuffer; + +/** + * A base implementation of {@link Filter} that just leaves the fragment shader to subclasses. + * See {@link NoFilter} for a non-abstract implementation. + * + * This class offers a default vertex shader implementation which in most cases is not required + * to be changed. Most effects can be rendered by simply changing the fragment shader, thus + * by overriding {@link #getFragmentShader()}. + * + * All {@link BaseFilter}s should have a no-op public constructor. + * This class will try to automatically implement {@link #copy()} thanks to this. + * If your filter implements public parameters, please implement {@link OneParameterFilter} + * and {@link TwoParameterFilter} to handle them and have them passed automatically to copies. + * + * NOTE - This class expects variable to have a certain name: + * - {@link #vertexPositionName} + * - {@link #vertexTransformMatrixName} + * - {@link #vertexModelViewProjectionMatrixName} + * - {@link #vertexTextureCoordinateName} + * - {@link #fragmentTextureCoordinateName} + * You can either change these variables, for example in your constructor, or change your + * vertex and fragment shader code to use them. + * + * NOTE - the {@link android.graphics.SurfaceTexture} restrictions apply: + * We only support the {@link android.opengl.GLES11Ext#GL_TEXTURE_EXTERNAL_OES} texture target + * and it must be specified in the fragment shader as a samplerExternalOES texture. + * You also have to explicitly require the extension: see {@link #createDefaultFragmentShader(String)}. + * + */ +public abstract class BaseFilter implements Filter { + + private final static String TAG = BaseFilter.class.getSimpleName(); + private final static CameraLogger LOG = CameraLogger.create(TAG); + + private final static String DEFAULT_VERTEX_POSITION_NAME = "aPosition"; + private final static String DEFAULT_VERTEX_TEXTURE_COORDINATE_NAME = "aTextureCoord"; + private final static String DEFAULT_VERTEX_MVP_MATRIX_NAME = "uMVPMatrix"; + private final static String DEFAULT_VERTEX_TRANSFORM_MATRIX_NAME = "uTexMatrix"; + private final static String DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME = "vTextureCoord"; + + @NonNull + 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" + + " vTextureCoord = ("+vertexTransformMatrixName+" * "+vertexTextureCoordinateName+").xy;\n" + + "}\n"; + } + + @NonNull + 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"; + } + + // When the model/view/projection matrix is identity, this will exactly cover the viewport. + private FloatBuffer vertexPosition = GlUtils.floatBuffer(new float[]{ + -1.0f, -1.0f, // 0 bottom left + 1.0f, -1.0f, // 1 bottom right + -1.0f, 1.0f, // 2 top left + 1.0f, 1.0f, // 3 top right + }); + + private FloatBuffer textureCoordinates = GlUtils.floatBuffer(new float[]{ + 0.0f, 0.0f, // 0 bottom left + 1.0f, 0.0f, // 1 bottom right + 0.0f, 1.0f, // 2 top left + 1.0f, 1.0f // 3 top right + }); + + private int vertexModelViewProjectionMatrixLocation = -1; + private int vertexTranformMatrixLocation = -1; + private int vertexPositionLocation = -1; + private int vertexTextureCoordinateLocation = -1; + private Size outputSize; + + @SuppressWarnings("WeakerAccess") + protected String vertexPositionName = DEFAULT_VERTEX_POSITION_NAME; + @SuppressWarnings("WeakerAccess") + protected String vertexTextureCoordinateName = DEFAULT_VERTEX_TEXTURE_COORDINATE_NAME; + @SuppressWarnings("WeakerAccess") + protected String vertexModelViewProjectionMatrixName = DEFAULT_VERTEX_MVP_MATRIX_NAME; + @SuppressWarnings("WeakerAccess") + protected String vertexTransformMatrixName = DEFAULT_VERTEX_TRANSFORM_MATRIX_NAME; + @SuppressWarnings({"unused", "WeakerAccess"}) + protected String fragmentTextureCoordinateName = DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME; + + @SuppressWarnings("WeakerAccess") + @NonNull + protected String createDefaultVertexShader() { + return createDefaultVertexShader(vertexPositionName, + vertexTextureCoordinateName, + vertexModelViewProjectionMatrixName, + vertexTransformMatrixName, + fragmentTextureCoordinateName); + } + + @SuppressWarnings("WeakerAccess") + @NonNull + protected String createDefaultFragmentShader() { + return createDefaultFragmentShader(fragmentTextureCoordinateName); + } + + @Override + public void onCreate(int programHandle) { + vertexPositionLocation = GLES20.glGetAttribLocation(programHandle, vertexPositionName); + GlUtils.checkLocation(vertexPositionLocation, vertexPositionName); + vertexTextureCoordinateLocation = GLES20.glGetAttribLocation(programHandle, vertexTextureCoordinateName); + GlUtils.checkLocation(vertexTextureCoordinateLocation, vertexTextureCoordinateName); + vertexModelViewProjectionMatrixLocation = GLES20.glGetUniformLocation(programHandle, vertexModelViewProjectionMatrixName); + GlUtils.checkLocation(vertexModelViewProjectionMatrixLocation, vertexModelViewProjectionMatrixName); + vertexTranformMatrixLocation = GLES20.glGetUniformLocation(programHandle, vertexTransformMatrixName); + GlUtils.checkLocation(vertexTranformMatrixLocation, vertexTransformMatrixName); + } + + @Override + public void onDestroy() { + vertexPositionLocation = -1; + vertexTextureCoordinateLocation = -1; + vertexModelViewProjectionMatrixLocation = -1; + vertexTranformMatrixLocation = -1; + } + + @NonNull + @Override + public String getVertexShader() { + return createDefaultVertexShader(); + } + + @Override + public void setSize(int width, int height) { + outputSize = new Size(width, height); + } + + @Override + public void draw(float[] transformMatrix) { + onPreDraw(transformMatrix); + onDraw(); + onPostDraw(); + } + + protected void onPreDraw(float[] transformMatrix) { + // Copy the model / view / projection matrix over. + GLES20.glUniformMatrix4fv(vertexModelViewProjectionMatrixLocation, 1, false, GlUtils.IDENTITY_MATRIX, 0); + GlUtils.checkError("glUniformMatrix4fv"); + + // Copy the texture transformation matrix over. + GLES20.glUniformMatrix4fv(vertexTranformMatrixLocation, 1, false, transformMatrix, 0); + GlUtils.checkError("glUniformMatrix4fv"); + + // Enable the "aPosition" vertex attribute. + // Connect vertexBuffer to "aPosition". + GLES20.glEnableVertexAttribArray(vertexPositionLocation); + GlUtils.checkError("glEnableVertexAttribArray: " + vertexPositionLocation); + GLES20.glVertexAttribPointer(vertexPositionLocation, 2, GLES20.GL_FLOAT, false, 8, vertexPosition); + GlUtils.checkError("glVertexAttribPointer"); + + // Enable the "aTextureCoord" vertex attribute. + // Connect texBuffer to "aTextureCoord". + GLES20.glEnableVertexAttribArray(vertexTextureCoordinateLocation); + GlUtils.checkError("glEnableVertexAttribArray"); + GLES20.glVertexAttribPointer(vertexTextureCoordinateLocation, 2, GLES20.GL_FLOAT, false, 8, textureCoordinates); + GlUtils.checkError("glVertexAttribPointer"); + } + + @SuppressWarnings("WeakerAccess") + protected void onDraw() { + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + GlUtils.checkError("glDrawArrays"); + } + + @SuppressWarnings("WeakerAccess") + protected void onPostDraw() { + GLES20.glDisableVertexAttribArray(vertexPositionLocation); + GLES20.glDisableVertexAttribArray(vertexTextureCoordinateLocation); + } + + @NonNull + @Override + public final BaseFilter copy() { + BaseFilter copy = onCopy(); + copy.setSize(outputSize.getWidth(), outputSize.getHeight()); + if (this instanceof OneParameterFilter) { + ((OneParameterFilter) copy).setParameter1(((OneParameterFilter) this).getParameter1()); + } + if (this instanceof TwoParameterFilter) { + ((TwoParameterFilter) copy).setParameter2(((TwoParameterFilter) this).getParameter2()); + } + return copy; + } + + @SuppressWarnings("WeakerAccess") + protected BaseFilter onCopy() { + try { + return getClass().newInstance(); + } catch (IllegalAccessException e) { + throw new RuntimeException("Filters should have a public no-op constructor.", e); + } catch (InstantiationException e) { + throw new RuntimeException("Filters should have a public no-op constructor.", e); + } + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/Filter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/Filter.java new file mode 100644 index 00000000..0a20b710 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/Filter.java @@ -0,0 +1,91 @@ +package com.otaliastudios.cameraview.filter; + +import com.otaliastudios.cameraview.CameraView; + +import androidx.annotation.NonNull; + +import java.io.File; + + +/** + * A Filter is a real time filter that operates onto the camera preview, plus any + * snapshot media taken with {@link CameraView#takePictureSnapshot()} and + * {@link CameraView#takeVideoSnapshot(File)}. + * + * You can apply filters to the camera engine using {@link CameraView#setFilter(Filter)}. + * The default filter is called {@link NoFilter} and can be used to restore the normal preview. + * A lof of other filters are collected in the {@link Filters} class. + * + * Advanced users can create custom filters using GLES. + * It is recommended to extend {@link BaseFilter} instead of this class. + * + * All {@link Filter}s should have a no-op public constructor. + * This ensures that you can pass the filter class to XML attribute {@code app:cameraFilter}, + * and also helps {@link BaseFilter} automatically make a copy of the filter. + * + * Parameterized filters can implement {@link OneParameterFilter} and {@link TwoParameterFilter} + * to receive parameters in the 0F-1F range. This helps in making filter copies and also let us + * map the filter parameter to gestures. + */ +public interface Filter { + + /** + * Returns a String containing the vertex shader. + * Together with {@link #getFragmentShader()}, this will be used to + * create the OpenGL program. + * + * @return vertex shader + */ + @NonNull + String getVertexShader(); + + /** + * Returns a String containing the fragment shader. + * Together with {@link #getVertexShader()}, this will be used to + * create the OpenGL program. + * + * @return fragment shader + */ + @NonNull + String getFragmentShader(); + + /** + * The filter program was just created. We pass in a handle to the OpenGL + * program that was created, so you can fetch pointers. + * + * @param programHandle handle + */ + void onCreate(int programHandle); + + /** + * The filter program is about to be destroyed. + * + */ + void onDestroy(); + + /** + * Called to render the actual texture. The given transformation matrix + * should be applied. + * + * @param transformMatrix matrix + */ + void draw(float[] transformMatrix); + + /** + * Called anytime the output size changes. + * + * @param width width + * @param height height + */ + void setSize(int width, int height); + + /** + * Clones this filter creating a new instance of it. + * If it has any important parameters, these should be passed + * to the new instance. + * + * @return a clone + */ + @NonNull + Filter copy(); +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/FilterParser.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/FilterParser.java new file mode 100644 index 00000000..1f407acd --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/FilterParser.java @@ -0,0 +1,32 @@ +package com.otaliastudios.cameraview.filter; + +import android.content.res.TypedArray; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.otaliastudios.cameraview.R; + +/** + * Parses filters from XML attributes. + */ +public class FilterParser { + + private Filter filter = null; + + public FilterParser(@NonNull TypedArray array) { + String filterName = array.getString(R.styleable.CameraView_cameraFilter); + try { + //noinspection ConstantConditions + Class filterClass = Class.forName(filterName); + filter = (Filter) filterClass.newInstance(); + } catch (Exception ignore) { + filter = new NoFilter(); + } + } + + @NonNull + public Filter getFilter() { + return filter; + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/Filters.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/Filters.java new file mode 100644 index 00000000..a0c279db --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/Filters.java @@ -0,0 +1,121 @@ +package com.otaliastudios.cameraview.filter; + +import androidx.annotation.NonNull; + +import com.otaliastudios.cameraview.filters.AutoFixFilter; +import com.otaliastudios.cameraview.filters.BlackAndWhiteFilter; +import com.otaliastudios.cameraview.filters.BrightnessFilter; +import com.otaliastudios.cameraview.filters.ContrastFilter; +import com.otaliastudios.cameraview.filters.CrossProcessFilter; +import com.otaliastudios.cameraview.filters.DocumentaryFilter; +import com.otaliastudios.cameraview.filters.DuotoneFilter; +import com.otaliastudios.cameraview.filters.FillLightFilter; +import com.otaliastudios.cameraview.filters.GammaFilter; +import com.otaliastudios.cameraview.filters.GrainFilter; +import com.otaliastudios.cameraview.filters.GrayscaleFilter; +import com.otaliastudios.cameraview.filters.HueFilter; +import com.otaliastudios.cameraview.filters.InvertColorsFilter; +import com.otaliastudios.cameraview.filters.LomoishFilter; +import com.otaliastudios.cameraview.filters.PosterizeFilter; +import com.otaliastudios.cameraview.filters.SaturationFilter; +import com.otaliastudios.cameraview.filters.SepiaFilter; +import com.otaliastudios.cameraview.filters.SharpnessFilter; +import com.otaliastudios.cameraview.filters.TemperatureFilter; +import com.otaliastudios.cameraview.filters.TintFilter; +import com.otaliastudios.cameraview.filters.VignetteFilter; + +/** + * Contains commonly used {@link Filter}s. + * + * You can use {@link #newInstance()} to create a new instance and + * pass it to {@link com.otaliastudios.cameraview.CameraView#setFilter(Filter)}. + */ +public enum Filters { + + /** @see NoFilter */ + NONE(NoFilter.class), + + /** @see AutoFixFilter */ + AUTO_FIX(AutoFixFilter.class), + + /** @see BlackAndWhiteFilter */ + BLACK_AND_WHITE(BlackAndWhiteFilter.class), + + /** @see BrightnessFilter */ + BRIGHTNESS(BrightnessFilter.class), + + /** @see ContrastFilter */ + CONTRAST(ContrastFilter.class), + + /** @see CrossProcessFilter */ + CROSS_PROCESS(CrossProcessFilter.class), + + /** @see DocumentaryFilter */ + DOCUMENTARY(DocumentaryFilter.class), + + /** @see DuotoneFilter */ + DUOTONE(DuotoneFilter.class), + + /** @see FillLightFilter */ + FILL_LIGHT(FillLightFilter.class), + + /** @see GammaFilter */ + GAMMA(GammaFilter.class), + + /** @see GrainFilter */ + GRAIN(GrainFilter.class), + + /** @see GrayscaleFilter */ + GRAYSCALE(GrayscaleFilter.class), + + /** @see HueFilter */ + HUE(HueFilter.class), + + /** @see InvertColorsFilter */ + INVERT_COLORS(InvertColorsFilter.class), + + /** @see LomoishFilter */ + LOMOISH(LomoishFilter.class), + + /** @see PosterizeFilter */ + POSTERIZE(PosterizeFilter.class), + + /** @see SaturationFilter */ + SATURATION(SaturationFilter.class), + + /** @see SepiaFilter */ + SEPIA(SepiaFilter.class), + + /** @see SharpnessFilter */ + SHARPNESS(SharpnessFilter.class), + + /** @see TemperatureFilter */ + TEMPERATURE(TemperatureFilter.class), + + /** @see TintFilter */ + TINT(TintFilter.class), + + /** @see VignetteFilter */ + VIGNETTE(VignetteFilter.class); + + private Class filterClass; + + Filters(@NonNull Class filterClass) { + this.filterClass = filterClass; + } + + /** + * Returns a new instance of the given filter. + * @return a new instance + */ + @NonNull + public Filter newInstance() { + try { + return filterClass.newInstance(); + } catch (IllegalAccessException e) { + return new NoFilter(); + } catch (InstantiationException e) { + return new NoFilter(); + } + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/NoFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/NoFilter.java new file mode 100644 index 00000000..c801da28 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/NoFilter.java @@ -0,0 +1,14 @@ +package com.otaliastudios.cameraview.filter; + +import androidx.annotation.NonNull; + +import com.otaliastudios.cameraview.filter.BaseFilter; + +public class NoFilter extends BaseFilter { + + @NonNull + @Override + public String getFragmentShader() { + return createDefaultFragmentShader(); + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/OneParameterFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/OneParameterFilter.java new file mode 100644 index 00000000..b3f0b760 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/OneParameterFilter.java @@ -0,0 +1,30 @@ +package com.otaliastudios.cameraview.filter; + +/** + * A special {@link Filter} that accepts a float parameter. + * + * The parameters will always be between 0F and 1F, so subclasses should + * map this range to their internal range if needed. + * + * A standardized range is useful for different applications. For example: + * - Filter parameters can be easily mapped to gestures since the range is fixed + * - {@link BaseFilter} can use this setters and getters to make a filter copy + */ +public interface OneParameterFilter extends Filter { + + /** + * Sets the parameter. + * The value should always be between 0 and 1. + * + * @param value parameter + */ + void setParameter1(float value); + + /** + * Returns the parameter. + * The returned value should always be between 0 and 1. + * + * @return parameter + */ + float getParameter1(); +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/TwoParameterFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/TwoParameterFilter.java new file mode 100644 index 00000000..8c944ff8 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/TwoParameterFilter.java @@ -0,0 +1,31 @@ +package com.otaliastudios.cameraview.filter; + +/** + * A special {@link Filter} that accepts two floats parameters. + * This is done by extending {@link OneParameterFilter}. + * + * The parameters will always be between 0F and 1F, so subclasses should + * map this range to their internal range if needed. + * + * A standardized range is useful for different applications. For example: + * - Filter parameters can be easily mapped to gestures since the range is fixed + * - {@link BaseFilter} can use this setters and getters to make a filter copy + */ +public interface TwoParameterFilter extends OneParameterFilter { + + /** + * Sets the second parameter. + * The value should always be between 0 and 1. + * + * @param value parameter + */ + void setParameter2(float value); + + /** + * Returns the second parameter. + * The returned value should always be between 0 and 1. + * + * @return parameter + */ + float getParameter2(); +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/AutoFixFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/AutoFixFilter.java index 92549229..a05669cb 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/AutoFixFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/AutoFixFilter.java @@ -1,96 +1,124 @@ package com.otaliastudios.cameraview.filters; +import android.opengl.GLES20; + import androidx.annotation.NonNull; +import com.otaliastudios.cameraview.filter.BaseFilter; +import com.otaliastudios.cameraview.filter.OneParameterFilter; +import com.otaliastudios.cameraview.internal.GlUtils; + /** - * Attempts to auto-fix the preview based on histogram equalization. + * Attempts to auto-fix the frames based on histogram equalization. */ -public class AutoFixFilter extends Filter { +public class AutoFixFilter extends BaseFilter implements OneParameterFilter { + + private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "uniform samplerExternalOES tex_sampler_0;\n" + + "uniform samplerExternalOES tex_sampler_1;\n" + + "uniform samplerExternalOES tex_sampler_2;\n" + + "uniform float scale;\n" + + "float shift_scale;\n" + + "float hist_offset;\n" + + "float hist_scale;\n" + + "float density_offset;\n" + + "float density_scale;\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " shift_scale = " + (1.0f / 256f) + ";\n" + + " hist_offset = " + (0.5f / 766f) + ";\n" + + " hist_scale = " + (765f / 766f) + ";\n" + + " density_offset = " + (0.5f / 1024f) + ";\n" + + " density_scale = " + (1023f / 1024f) + ";\n" + + " const vec3 weights = vec3(0.33333, 0.33333, 0.33333);\n" + + " vec4 color = texture2D(tex_sampler_0, vTextureCoord);\n" + + " float energy = dot(color.rgb, weights);\n" + + " float mask_value = energy - 0.5;\n" + + " float alpha;\n" + + " if (mask_value > 0.0) {\n" + + " alpha = (pow(2.0 * mask_value, 1.5) - 1.0) * scale + 1.0;\n" + + " } else { \n" + + " alpha = (pow(2.0 * mask_value, 2.0) - 1.0) * scale + 1.0;\n" + + " }\n" + + " float index = energy * hist_scale + hist_offset;\n" + + " vec4 temp = texture2D(tex_sampler_1, vec2(index, 0.5));\n" + + " float value = temp.g + temp.r * shift_scale;\n" + + " index = value * density_scale + density_offset;\n" + + " temp = texture2D(tex_sampler_2, vec2(index, 0.5));\n" + + " value = temp.g + temp.r * shift_scale;\n" + + " float dst_energy = energy * alpha + value * (1.0 - alpha);\n" + + " float max_energy = energy / max(color.r, max(color.g, color.b));\n" + + " if (dst_energy > max_energy) {\n" + + " dst_energy = max_energy;\n" + + " }\n" + + " if (energy == 0.0) {\n" + + " gl_FragColor = color;\n" + + " } else {\n" + + " gl_FragColor = vec4(color.rgb * dst_energy / energy, color.a);\n" + + " }\n" + + "}\n"; private float scale = 1.0f; + private int scaleLocation = -1; + + public AutoFixFilter() { } /** - * Initialize Effect + * A parameter between 0 and 1. Zero means no adjustment, while 1 indicates + * the maximum amount of adjustment. + * + * @param scale scale */ - public AutoFixFilter() { + public void setScale(float scale) { + if (scale < 0.0f) scale = 0.0f; + if (scale > 1.0f) scale = 1.0f; + this.scale = scale; } + /** + * Returns the current scale. + * + * @see #setScale(float) + * @return current scale + */ public float getScale() { return scale; } - /** - * @param scale Float, between 0 and 1. Zero means no adjustment, while 1 - * indicates the maximum amount of adjustment. - */ - public void setScale(float scale) { - if (scale < 0.0f) - scale = 0.0f; - else if (scale > 1.0f) - scale = 1.0f; + @Override + public void setParameter1(float value) { + setScale(value); + } - this.scale = scale; + @Override + public float getParameter1() { + return getScale(); } @NonNull @Override public String getFragmentShader() { + return FRAGMENT_SHADER; + } - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - + "uniform samplerExternalOES tex_sampler_0;\n" - + "uniform samplerExternalOES tex_sampler_1;\n" - + "uniform samplerExternalOES tex_sampler_2;\n" - + " float scale;\n" + " float shift_scale;\n" - + " float hist_offset;\n" + " float hist_scale;\n" - + " float density_offset;\n" + " float density_scale;\n" - + "varying vec2 vTextureCoord;\n" + "void main() {\n" - + " shift_scale = " - + (1.0f / 256f) - + ";\n" - + " hist_offset = " - + (0.5f / 766f) - + ";\n" - + " hist_scale = " - + (765f / 766f) - + ";\n" - + " density_offset = " - + (0.5f / 1024f) - + ";\n" - + " density_scale = " - + (1023f / 1024f) - + ";\n" - + " scale = " - + scale - + ";\n" - + " const vec3 weights = vec3(0.33333, 0.33333, 0.33333);\n" - + " vec4 color = texture2D(tex_sampler_0, vTextureCoord);\n" - + " float energy = dot(color.rgb, weights);\n" - + " float mask_value = energy - 0.5;\n" - + " float alpha;\n" - + " if (mask_value > 0.0) {\n" - + " alpha = (pow(2.0 * mask_value, 1.5) - 1.0) * scale + 1.0;\n" - + " } else { \n" - + " alpha = (pow(2.0 * mask_value, 2.0) - 1.0) * scale + 1.0;\n" - + " }\n" - + " float index = energy * hist_scale + hist_offset;\n" - + " vec4 temp = texture2D(tex_sampler_1, vec2(index, 0.5));\n" - + " float value = temp.g + temp.r * shift_scale;\n" - + " index = value * density_scale + density_offset;\n" - + " temp = texture2D(tex_sampler_2, vec2(index, 0.5));\n" - + " value = temp.g + temp.r * shift_scale;\n" - + " float dst_energy = energy * alpha + value * (1.0 - alpha);\n" - + " float max_energy = energy / max(color.r, max(color.g, color.b));\n" - + " if (dst_energy > max_energy) {\n" - + " dst_energy = max_energy;\n" - + " }\n" - + " if (energy == 0.0) {\n" - + " gl_FragColor = color;\n" - + " } else {\n" - + " gl_FragColor = vec4(color.rgb * dst_energy / energy, color.a);\n" - + " }\n" + "}\n"; + @Override + public void onCreate(int programHandle) { + super.onCreate(programHandle); + scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale"); + GlUtils.checkLocation(scaleLocation, "scale"); + } - return shader; + @Override + public void onDestroy() { + super.onDestroy(); + scaleLocation = -1; + } + @Override + protected void onPreDraw(float[] transformMatrix) { + super.onPreDraw(transformMatrix); + GLES20.glUniform1f(scaleLocation, scale); + GlUtils.checkError("glUniform1f"); } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/BlackAndWhiteFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/BlackAndWhiteFilter.java index c7316ea0..4395e713 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/BlackAndWhiteFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/BlackAndWhiteFilter.java @@ -2,34 +2,29 @@ package com.otaliastudios.cameraview.filters; import androidx.annotation.NonNull; +import com.otaliastudios.cameraview.filter.BaseFilter; + /** - * Converts the preview into black and white colors + * Converts the frames into black and white colors. */ -public class BlackAndWhiteFilter extends Filter { +public class BlackAndWhiteFilter extends BaseFilter { + private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform samplerExternalOES sTexture;\n" + "void main() {\n" + + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " float colorR = (color.r + color.g + color.b) / 3.0;\n" + + " float colorG = (color.r + color.g + color.b) / 3.0;\n" + + " float colorB = (color.r + color.g + color.b) / 3.0;\n" + + " gl_FragColor = vec4(colorR, colorG, colorB, color.a);\n" + + "}\n"; - /** - * Initialize effect - */ - public BlackAndWhiteFilter() { - } + public BlackAndWhiteFilter() { } @NonNull @Override public String getFragmentShader() { - - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - + "varying vec2 vTextureCoord;\n" - + "uniform samplerExternalOES sTexture;\n" + "void main() {\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" - + " float colorR = (color.r + color.g + color.b) / 3.0;\n" - + " float colorG = (color.r + color.g + color.b) / 3.0;\n" - + " float colorB = (color.r + color.g + color.b) / 3.0;\n" - + " gl_FragColor = vec4(colorR, colorG, colorB, color.a);\n" - + "}\n"; - - return shader; - + return FRAGMENT_SHADER; } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/BrightnessFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/BrightnessFilter.java index f2718b8c..5aa8e416 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/BrightnessFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/BrightnessFilter.java @@ -1,56 +1,94 @@ package com.otaliastudios.cameraview.filters; +import android.opengl.GLES20; + import androidx.annotation.NonNull; +import com.otaliastudios.cameraview.filter.BaseFilter; +import com.otaliastudios.cameraview.filter.OneParameterFilter; +import com.otaliastudios.cameraview.internal.GlUtils; + /** - * Adjusts the brightness of the preview. + * Adjusts the brightness of the frames. */ -public class BrightnessFilter extends Filter { - private float brightnessValue = 2.0f; +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 vTextureCoord;\n" + + "void main() {\n" + + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " gl_FragColor = brightness * color;\n" + + "}\n"; + + private float brightness = 2.0f; // 1.0F...2.0F + private int brightnessLocation = -1; + + + public BrightnessFilter() { } /** - * Initialize Effect + * Sets the brightness adjustment. + * 1.0: normal brightness. + * 2.0: high brightness. + * + * @param brightness brightness. */ - public BrightnessFilter() { + @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; } /** - * setBrightnessValue + * Returns the current brightness. * - * @param brightnessvalue Range should be between 0.0- 1.0 with 0.0 being normal. + * @see #setBrightness(float) + * @return brightness */ - public void setBrightnessValue(float brightnessvalue) { - if (brightnessvalue < 0.0f) - brightnessvalue = 0.0f; - else if (brightnessvalue > 1.0f) - brightnessvalue = 1.0f; + @SuppressWarnings({"unused", "WeakerAccess"}) + public float getBrightness() { + return brightness; + } - //since the shader excepts a range of 1.0 - 2.0 - // will add the 1.0 to every value - this.brightnessValue = 1.0f + brightnessvalue; + @Override + public void setParameter1(float value) { + // parameter is 0...1, brightness is 1...2. + setBrightness(value + 1); } - public float getBrightnessValue() { - //since the shader excepts a range of 1.0 - 2.0 - //to keep it between 0.0f - 1.0f range, will subtract the 1.0 to every value - return brightnessValue - 1.0f; + @Override + public float getParameter1() { + // parameter is 0...1, brightness is 1...2. + return getBrightness() - 1F; } @NonNull @Override public String getFragmentShader() { + return FRAGMENT_SHADER; + } - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - + "uniform samplerExternalOES sTexture;\n" - + "float brightness ;\n" + "varying vec2 vTextureCoord;\n" - + "void main() {\n" + " brightness =" + brightnessValue - + ";\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" - + " gl_FragColor = brightness * color;\n" + "}\n"; - - return shader; + @Override + public void onCreate(int programHandle) { + super.onCreate(programHandle); + brightnessLocation = GLES20.glGetUniformLocation(programHandle, "brightness"); + GlUtils.checkLocation(brightnessLocation, "brightness"); + } + @Override + public void onDestroy() { + super.onDestroy(); + brightnessLocation = -1; } + @Override + protected void onPreDraw(float[] transformMatrix) { + super.onPreDraw(transformMatrix); + GLES20.glUniform1f(brightnessLocation, brightness); + GlUtils.checkError("glUniform1f"); + } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/ContrastFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/ContrastFilter.java index 03034f46..cc2e601c 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/ContrastFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/ContrastFilter.java @@ -1,55 +1,96 @@ package com.otaliastudios.cameraview.filters; +import android.opengl.GLES20; + import androidx.annotation.NonNull; +import com.otaliastudios.cameraview.filter.BaseFilter; +import com.otaliastudios.cameraview.filter.OneParameterFilter; +import com.otaliastudios.cameraview.internal.GlUtils; + /** - * Adjusts the contrast of the preview. + * Adjusts the contrast. */ -public class ContrastFilter extends Filter { - private float contrast = 2.0f; +public class ContrastFilter 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 contrast;\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " color -= 0.5;\n" + + " color *= contrast;\n" + + " color += 0.5;\n" + + " gl_FragColor = color;\n" + + "}\n"; + + private float contrast = 2F; + private int contrastLocation = -1; + + public ContrastFilter() { } /** - * Initialize Effect + * Sets the current contrast adjustment. + * 1.0: no adjustment + * 2.0: increased contrast + * + * @param contrast contrast */ - public ContrastFilter() { + @SuppressWarnings("WeakerAccess") + public void setContrast(float contrast) { + if (contrast < 1.0f) contrast = 1.0f; + if (contrast > 2.0f) contrast = 2.0f; + this.contrast = contrast; } /** - * setContrast + * Returns the current contrast. * - * @param contrast Range should be between 0.0- 1.0 with 0.0 being normal. + * @see #setContrast(float) + * @return contrast */ - public void setContrast(float contrast) { - if (contrast < 0.0f) - contrast = 0.0f; - else if (contrast > 1.0f) - contrast = 1.0f; + @SuppressWarnings({"unused", "WeakerAccess"}) + public float getContrast() { + return contrast; + } - //since the shader excepts a range of 1.0 - 2.0 - //will add the 1.0 to every value - this.contrast = contrast + 1.0f; + @Override + public void setParameter1(float value) { + // parameter is 0...1, contrast is 1...2. + setContrast(value + 1); } - public float getContrast() { - //since the shader excepts a range of 1.0 - 2.0 - //to keep it between 0.0f - 1.0f range, will subtract the 1.0 to every value - return contrast - 1.0f; + @Override + public float getParameter1() { + // parameter is 0...1, contrast is 1...2. + return getContrast() - 1F; } @NonNull @Override public String getFragmentShader() { + return FRAGMENT_SHADER; + } - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - + "uniform samplerExternalOES sTexture;\n" - + " float contrast;\n" + "varying vec2 vTextureCoord;\n" - + "void main() {\n" + " contrast =" + contrast + ";\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" - + " color -= 0.5;\n" + " color *= contrast;\n" - + " color += 0.5;\n" + " gl_FragColor = color;\n" + "}\n"; - return shader; + @Override + public void onCreate(int programHandle) { + super.onCreate(programHandle); + contrastLocation = GLES20.glGetUniformLocation(programHandle, "contrast"); + GlUtils.checkLocation(contrastLocation, "contrast"); + } + @Override + public void onDestroy() { + super.onDestroy(); + contrastLocation = -1; } + @Override + protected void onPreDraw(float[] transformMatrix) { + super.onPreDraw(transformMatrix); + GLES20.glUniform1f(contrastLocation, contrast); + GlUtils.checkError("glUniform1f"); + } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/CrossProcessFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/CrossProcessFilter.java index 883a15bb..1ff8cca1 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/CrossProcessFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/CrossProcessFilter.java @@ -2,43 +2,53 @@ package com.otaliastudios.cameraview.filters; import androidx.annotation.NonNull; +import com.otaliastudios.cameraview.filter.BaseFilter; + /** - * Applies a cross process effect on preview, in which the red and green channels + * Applies a cross process effect, in which the red and green channels * are enhanced while the blue channel is restricted. */ -public class CrossProcessFilter extends Filter { - - /** - * Initialize Effect - */ - public CrossProcessFilter() { - } +public class CrossProcessFilter extends BaseFilter { + + private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "uniform samplerExternalOES sTexture;\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " vec3 ncolor = vec3(0.0, 0.0, 0.0);\n" + + " float value;\n" + + " if (color.r < 0.5) {\n" + + " value = color.r;\n" + + " } else {\n" + + " value = 1.0 - color.r;\n" + + " }\n" + + " float red = 4.0 * value * value * value;\n" + + " if (color.r < 0.5) {\n" + + " ncolor.r = red;\n" + + " } else {\n" + + " ncolor.r = 1.0 - red;\n" + + " }\n" + + " if (color.g < 0.5) {\n" + + " value = color.g;\n" + + " } else {\n" + + " value = 1.0 - color.g;\n" + + " }\n" + + " float green = 2.0 * value * value;\n" + + " if (color.g < 0.5) {\n" + + " ncolor.g = green;\n" + + " } else {\n" + + " ncolor.g = 1.0 - green;\n" + + " }\n" + + " ncolor.b = color.b * 0.5 + 0.25;\n" + + " gl_FragColor = vec4(ncolor.rgb, color.a);\n" + + "}\n"; + + public CrossProcessFilter() { } @NonNull @Override public String getFragmentShader() { - - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - + "uniform samplerExternalOES sTexture;\n" - + "varying vec2 vTextureCoord;\n" + "void main() {\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" - + " vec3 ncolor = vec3(0.0, 0.0, 0.0);\n" + " float value;\n" - + " if (color.r < 0.5) {\n" + " value = color.r;\n" - + " } else {\n" + " value = 1.0 - color.r;\n" + " }\n" - + " float red = 4.0 * value * value * value;\n" - + " if (color.r < 0.5) {\n" + " ncolor.r = red;\n" - + " } else {\n" + " ncolor.r = 1.0 - red;\n" + " }\n" - + " if (color.g < 0.5) {\n" + " value = color.g;\n" - + " } else {\n" + " value = 1.0 - color.g;\n" + " }\n" - + " float green = 2.0 * value * value;\n" - + " if (color.g < 0.5) {\n" + " ncolor.g = green;\n" - + " } else {\n" + " ncolor.g = 1.0 - green;\n" + " }\n" - + " ncolor.b = color.b * 0.5 + 0.25;\n" - + " gl_FragColor = vec4(ncolor.rgb, color.a);\n" + "}\n"; - - return shader; - + return FRAGMENT_SHADER; } - } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/CustomFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/CustomFilter.java deleted file mode 100644 index f1b53220..00000000 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/CustomFilter.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.otaliastudios.cameraview.filters; - -import androidx.annotation.NonNull; - - -/** - * This class is to implement any custom effect. - */ -public class CustomFilter extends Filter { - - /** - * Parameterized constructor with vertex and fragment shader as parameter - * - * @param vertexShader - * @param fragmentShader - */ - public CustomFilter(String vertexShader, String fragmentShader) { - this.mVertexShader = vertexShader; - this.mFragmentShader = fragmentShader; - } - - @NonNull - @Override - public String getFragmentShader() { - return mFragmentShader; - } -} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/DocumentaryFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/DocumentaryFilter.java index c81fcac4..c4a1532f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/DocumentaryFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/DocumentaryFilter.java @@ -1,96 +1,115 @@ package com.otaliastudios.cameraview.filters; +import android.opengl.GLES20; + import androidx.annotation.NonNull; -import java.util.Date; +import com.otaliastudios.cameraview.filter.BaseFilter; +import com.otaliastudios.cameraview.internal.GlUtils; + import java.util.Random; /** - * Applies black and white documentary style effect on preview. + * Applies black and white documentary style effect. */ -public class DocumentaryFilter extends Filter { - private Random mRandom; +public class DocumentaryFilter extends BaseFilter { + + private final static Random RANDOM = new Random(); + private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "uniform samplerExternalOES sTexture;\n" + + "vec2 seed;\n" + + "float stepsize;\n" + + "uniform float inv_max_dist;\n" + + "uniform vec2 scale;\n" + + "varying vec2 vTextureCoord;\n" + + "float rand(vec2 loc) {\n" + + " float theta1 = dot(loc, vec2(0.9898, 0.233));\n" + + " float theta2 = dot(loc, vec2(12.0, 78.0));\n" + + " float value = cos(theta1) * sin(theta2) + sin(theta1) * cos(theta2);\n" + + + // keep value of part1 in range: (2^-14 to 2^14). + " float temp = mod(197.0 * value, 1.0) + value;\n" + + " float part1 = mod(220.0 * temp, 1.0) + temp;\n" + + " float part2 = value * 0.5453;\n" + + " float part3 = cos(theta1 + theta2) * 0.43758;\n" + + " return fract(part1 + part2 + part3);\n" + + "}\n" + + "void main() {\n" + + " seed[0] = " + RANDOM.nextFloat() + ";\n" + + " seed[1] = " + RANDOM.nextFloat() + ";\n" + + " stepsize = " + 1.0f / 255.0f + ";\n" + + // black white + + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " float dither = rand(vTextureCoord + seed);\n" + + " vec3 xform = clamp(2.0 * color.rgb, 0.0, 1.0);\n" + + " vec3 temp = clamp(2.0 * (color.rgb + stepsize), 0.0, 1.0);\n" + + " vec3 new_color = clamp(xform + (temp - xform) * (dither - 0.5), 0.0, 1.0);\n" + // grayscale + + " float gray = dot(new_color, vec3(0.299, 0.587, 0.114));\n" + + " new_color = vec3(gray, gray, gray);\n" + // vignette + + " vec2 coord = vTextureCoord - vec2(0.5, 0.5);\n" + + " float dist = length(coord * scale);\n" + + " float lumen = 0.85 / (1.0 + exp((dist * inv_max_dist - 0.83) * 20.0)) + 0.15;\n" + + " gl_FragColor = vec4(new_color * lumen, color.a);\n" + + "}\n"; + + private int mWidth = 1; + private int mHeight = 1; + private int mScaleLocation = -1; + private int mMaxDistLocation = -1; - public DocumentaryFilter() { - mRandom = new Random(new Date().getTime()); + public DocumentaryFilter() { } + + @Override + public void setSize(int width, int height) { + super.setSize(width, height); + mWidth = width; + mHeight = height; } @NonNull @Override public String getFragmentShader() { - float scale[] = new float[2]; - if (mPreviewingViewWidth > mPreviewingViewHeight) { + return FRAGMENT_SHADER; + } + + @Override + public void onCreate(int programHandle) { + super.onCreate(programHandle); + mScaleLocation = GLES20.glGetUniformLocation(programHandle, "scale"); + GlUtils.checkLocation(mScaleLocation, "scale"); + mMaxDistLocation = GLES20.glGetUniformLocation(programHandle, "inv_max_dist"); + GlUtils.checkLocation(mMaxDistLocation, "inv_max_dist"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + mScaleLocation = -1; + mMaxDistLocation = -1; + } + + @Override + protected void onPreDraw(float[] transformMatrix) { + super.onPreDraw(transformMatrix); + float[] scale = new float[2]; + if (mWidth > mHeight) { scale[0] = 1f; - scale[1] = ((float) mPreviewingViewHeight) / mPreviewingViewWidth; + scale[1] = ((float) mHeight) / mWidth; } else { - scale[0] = ((float) mPreviewingViewWidth) / mPreviewingViewHeight; + scale[0] = ((float) mWidth) / mHeight; scale[1] = 1f; } - float max_dist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1] - * scale[1])) * 0.5f; - - float seed[] = {mRandom.nextFloat(), mRandom.nextFloat()}; - - String scaleString[] = new String[2]; - String seedString[] = new String[2]; - - scaleString[0] = "scale[0] = " + scale[0] + ";\n"; - scaleString[1] = "scale[1] = " + scale[1] + ";\n"; - - seedString[0] = "seed[0] = " + seed[0] + ";\n"; - seedString[1] = "seed[1] = " + seed[1] + ";\n"; - - String inv_max_distString = "inv_max_dist = " + 1.0f / max_dist + ";\n"; - String stepsizeString = "stepsize = " + 1.0f / 255.0f + ";\n"; - - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - + "uniform samplerExternalOES sTexture;\n" - + " vec2 seed;\n" - + " float stepsize;\n" - + " float inv_max_dist;\n" - + " vec2 scale;\n" - + "varying vec2 vTextureCoord;\n" - + "float rand(vec2 loc) {\n" - + " float theta1 = dot(loc, vec2(0.9898, 0.233));\n" - + " float theta2 = dot(loc, vec2(12.0, 78.0));\n" - + " float value = cos(theta1) * sin(theta2) + sin(theta1) * cos(theta2);\n" - + - // keep value of part1 in range: (2^-14 to 2^14). - " float temp = mod(197.0 * value, 1.0) + value;\n" - + " float part1 = mod(220.0 * temp, 1.0) + temp;\n" - + " float part2 = value * 0.5453;\n" - + " float part3 = cos(theta1 + theta2) * 0.43758;\n" - + " return fract(part1 + part2 + part3);\n" - + "}\n" - + "void main() {\n" - // Parameters that were created above - + scaleString[0] - + scaleString[1] - + seedString[0] - + seedString[1] - + inv_max_distString - + stepsizeString - - // black white - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" - + " float dither = rand(vTextureCoord + seed);\n" - + " vec3 xform = clamp(2.0 * color.rgb, 0.0, 1.0);\n" - + " vec3 temp = clamp(2.0 * (color.rgb + stepsize), 0.0, 1.0);\n" - + " vec3 new_color = clamp(xform + (temp - xform) * (dither - 0.5), 0.0, 1.0);\n" - + - // grayscale - " float gray = dot(new_color, vec3(0.299, 0.587, 0.114));\n" - + " new_color = vec3(gray, gray, gray);\n" - + - // vignette - " vec2 coord = vTextureCoord - vec2(0.5, 0.5);\n" - + " float dist = length(coord * scale);\n" - + " float lumen = 0.85 / (1.0 + exp((dist * inv_max_dist - 0.83) * 20.0)) + 0.15;\n" - + " gl_FragColor = vec4(new_color * lumen, color.a);\n" - + "}\n"; - - return shader; + GLES20.glUniform2fv(mScaleLocation, 1, scale, 0); + GlUtils.checkError("glUniform2fv"); + + float maxDist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1] * scale[1])) * 0.5f; + float invMaxDist = 1F / maxDist; + GLES20.glUniform1f(mMaxDistLocation, invMaxDist); + GlUtils.checkError("glUniform1f"); } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/DuotoneFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/DuotoneFilter.java index de34f7d5..5700c542 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/DuotoneFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/DuotoneFilter.java @@ -1,85 +1,158 @@ package com.otaliastudios.cameraview.filters; import android.graphics.Color; +import android.opengl.GLES20; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; +import com.otaliastudios.cameraview.filter.BaseFilter; +import com.otaliastudios.cameraview.filter.TwoParameterFilter; +import com.otaliastudios.cameraview.internal.GlUtils; + /** - * Representation of preview using only two color tones. + * Representation of input frames using only two color tones. */ -public class DuotoneFilter extends Filter { +public class DuotoneFilter extends BaseFilter implements TwoParameterFilter { + + private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "uniform samplerExternalOES sTexture;\n" + + "uniform vec3 first;\n" + + "uniform vec3 second;\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " float energy = (color.r + color.g + color.b) * 0.3333;\n" + + " vec3 new_color = (1.0 - energy) * first + energy * second;\n" + + " gl_FragColor = vec4(new_color.rgb, color.a);\n" + + "}\n"; + // Default values private int mFirstColor = Color.MAGENTA; private int mSecondColor = Color.YELLOW; + private int mFirstColorLocation = -1; + private int mSecondColorLocation = -1; + + public DuotoneFilter() { } /** - * Initialize effect + * Sets the two duotone ARGB colors. + * @param firstColor first + * @param secondColor second + */ + @SuppressWarnings({"unused"}) + public void setColors(@ColorInt int firstColor, @ColorInt int secondColor) { + setFirstColor(firstColor); + setSecondColor(secondColor); + } + + /** + * Sets the first of the duotone ARGB colors. + * Defaults to {@link Color#MAGENTA}. + * + * @param color first color */ - public DuotoneFilter() { + @SuppressWarnings("WeakerAccess") + public void setFirstColor(@ColorInt int color) { + mFirstColor = color; } /** - * setDuoToneColors + * Sets the second of the duotone ARGB colors. + * Defaults to {@link Color#YELLOW}. * - * @param firstColor Integer, representing an ARGB color with 8 bits per channel. - * May be created using Color class. - * @param secondColor Integer, representing an ARGB color with 8 bits per channel. - * May be created using Color class. + * @param color second color */ - public void setDuoToneColors(int firstColor, int secondColor) { - this.mFirstColor = firstColor; - this.mSecondColor = secondColor; + @SuppressWarnings("WeakerAccess") + public void setSecondColor(@ColorInt int color) { + mSecondColor = color; } + /** + * Returns the first color. + * + * @see #setFirstColor(int) + * @return first + */ + @SuppressWarnings({"unused", "WeakerAccess"}) + @ColorInt public int getFirstColor() { return mFirstColor; } + /** + * Returns the second color. + * + * @see #setSecondColor(int) + * @return second + */ + @SuppressWarnings({"unused", "WeakerAccess"}) + @ColorInt public int getSecondColor() { return mSecondColor; } + @Override + public void setParameter1(float value) { + // no easy way to transform 0...1 into a color. + setFirstColor((int) (value * Integer.MAX_VALUE)); + } + + @Override + public float getParameter1() { + return (float) getFirstColor() / Integer.MAX_VALUE; + } + + @Override + public void setParameter2(float value) { + // no easy way to transform 0...1 into a color. + setSecondColor((int) (value * Integer.MAX_VALUE)); + } + + @Override + public float getParameter2() { + return (float) getSecondColor() / Integer.MAX_VALUE; + } + @NonNull @Override public String getFragmentShader() { - float first[] = {Color.red(mFirstColor) / 255f, - Color.green(mFirstColor) / 255f, Color.blue(mFirstColor) / 255f}; - float second[] = {Color.red(mSecondColor) / 255f, + return FRAGMENT_SHADER; + } + + @Override + public void onCreate(int programHandle) { + super.onCreate(programHandle); + mFirstColorLocation = GLES20.glGetUniformLocation(programHandle, "first"); + GlUtils.checkLocation(mFirstColorLocation, "first"); + mSecondColorLocation = GLES20.glGetUniformLocation(programHandle, "second"); + GlUtils.checkLocation(mSecondColorLocation, "second"); + } + + @Override + protected void onPreDraw(float[] transformMatrix) { + super.onPreDraw(transformMatrix); + float[] first = new float[]{ + Color.red(mFirstColor) / 255f, + Color.green(mFirstColor) / 255f, + Color.blue(mFirstColor) / 255f + }; + float[] second = new float[]{ + Color.red(mSecondColor) / 255f, Color.green(mSecondColor) / 255f, - Color.blue(mSecondColor) / 255f}; - - String firstColorString[] = new String[3]; - String secondColorString[] = new String[3]; - - firstColorString[0] = "first[0] = " + first[0] + ";\n"; - firstColorString[1] = "first[1] = " + first[1] + ";\n"; - firstColorString[2] = "first[2] = " + first[2] + ";\n"; - - secondColorString[0] = "second[0] = " + second[0] + ";\n"; - secondColorString[1] = "second[1] = " + second[1] + ";\n"; - secondColorString[2] = "second[2] = " + second[2] + ";\n"; - - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - + "uniform samplerExternalOES sTexture;\n" - + " vec3 first;\n" - + " vec3 second;\n" - + "varying vec2 vTextureCoord;\n" - + "void main() {\n" - // Parameters that were created above - + firstColorString[0] - + firstColorString[1] - + firstColorString[2] - + secondColorString[0] - + secondColorString[1] - + secondColorString[2] - - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" - + " float energy = (color.r + color.g + color.b) * 0.3333;\n" - + " vec3 new_color = (1.0 - energy) * first + energy * second;\n" - + " gl_FragColor = vec4(new_color.rgb, color.a);\n" + "}\n"; - - return shader; + Color.blue(mSecondColor) / 255f + }; + GLES20.glUniform3fv(mFirstColorLocation, 1, first, 0); + GlUtils.checkError("glUniform3fv"); + GLES20.glUniform3fv(mSecondColorLocation, 1, second, 0); + GlUtils.checkError("glUniform3fv"); + } + @Override + public void onDestroy() { + super.onDestroy(); + mFirstColorLocation = -1; + mSecondColorLocation = -1; } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/FillLightFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/FillLightFilter.java index 8d807137..8ba50692 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/FillLightFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/FillLightFilter.java @@ -1,73 +1,111 @@ package com.otaliastudios.cameraview.filters; +import android.opengl.GLES20; + import androidx.annotation.NonNull; +import com.otaliastudios.cameraview.filter.BaseFilter; +import com.otaliastudios.cameraview.filter.OneParameterFilter; +import com.otaliastudios.cameraview.internal.GlUtils; + /** - * Applies back-light filling to the preview. + * Applies back-light filling to the frames. */ -public class FillLightFilter extends Filter { +public class FillLightFilter 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 mult;\n" + + "uniform float igamma;\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " const vec3 color_weights = vec3(0.25, 0.5, 0.25);\n" + + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " float lightmask = dot(color.rgb, color_weights);\n" + + " float backmask = (1.0 - lightmask);\n" + + " vec3 ones = vec3(1.0, 1.0, 1.0);\n" + + " vec3 diff = pow(mult * color.rgb, igamma * ones) - color.rgb;\n" + + " diff = min(diff, 1.0);\n" + + " vec3 new_color = min(color.rgb + diff * backmask, 1.0);\n" + + " gl_FragColor = vec4(new_color, color.a);\n" + + "}\n"; + private float strength = 0.5f; + private int multiplierLocation = -1; + private int gammaLocation = -1; - /** - * Initialize Effect - */ - public FillLightFilter() { - } + public FillLightFilter() { } /** - * setStrength + * Sets the current strength. + * 0.0: no change. + * 1.0: max strength. * - * @param strength Float, between 0.0 and 1.0 where 0.0 means no change. + * @param strength strength */ + @SuppressWarnings("WeakerAccess") public void setStrength(float strength) { - if (strength < 0.0f) - strength = 0f; - else if (strength > 1.0f) - strength = 1f; - + if (strength < 0.0f) strength = 0f; + if (strength > 1.0f) strength = 1f; this.strength = strength; } + /** + * Returns the current strength. + * + * @see #setStrength(float) + * @return strength + */ + @SuppressWarnings({"unused", "WeakerAccess"}) public float getStrength() { return strength; } + @Override + public void setParameter1(float value) { + setStrength(value); + } + + @Override + public float getParameter1() { + return getStrength(); + } + @NonNull @Override public String getFragmentShader() { - float fade_gamma = 0.3f; - float amt = 1.0f - strength; - float mult = 1.0f / (amt * 0.7f + 0.3f); - float faded = fade_gamma + (1.0f - fade_gamma) * mult; - float igamma = 1.0f / faded; - - String multString = "mult = " + mult + ";\n"; - String igammaString = "igamma = " + igamma + ";\n"; - - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - + "uniform samplerExternalOES sTexture;\n" - + " float mult;\n" - + " float igamma;\n" - + "varying vec2 vTextureCoord;\n" - + "void main()\n" - + "{\n" - // Parameters that were created above - + multString - + igammaString - - + " const vec3 color_weights = vec3(0.25, 0.5, 0.25);\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" - + " float lightmask = dot(color.rgb, color_weights);\n" - + " float backmask = (1.0 - lightmask);\n" - + " vec3 ones = vec3(1.0, 1.0, 1.0);\n" - + " vec3 diff = pow(mult * color.rgb, igamma * ones) - color.rgb;\n" - + " diff = min(diff, 1.0);\n" - + " vec3 new_color = min(color.rgb + diff * backmask, 1.0);\n" - + " gl_FragColor = vec4(new_color, color.a);\n" + "}\n"; - - return shader; + return FRAGMENT_SHADER; + } + + @Override + public void onCreate(int programHandle) { + super.onCreate(programHandle); + multiplierLocation = GLES20.glGetUniformLocation(programHandle, "mult"); + GlUtils.checkLocation(multiplierLocation, "mult"); + gammaLocation = GLES20.glGetUniformLocation(programHandle, "igamma"); + GlUtils.checkLocation(gammaLocation, "igamma"); + } + @Override + public void onDestroy() { + super.onDestroy(); + multiplierLocation = -1; + gammaLocation = -1; } + @Override + protected void onPreDraw(float[] transformMatrix) { + super.onPreDraw(transformMatrix); + float amount = 1.0f - strength; + float multiplier = 1.0f / (amount * 0.7f + 0.3f); + GLES20.glUniform1f(multiplierLocation, multiplier); + GlUtils.checkError("glUniform1f"); + + float fadeGamma = 0.3f; + float faded = fadeGamma + (1.0f - fadeGamma) * multiplier; + float gamma = 1.0f / faded; + GLES20.glUniform1f(gammaLocation, gamma); + GlUtils.checkError("glUniform1f"); + } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/Filter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/Filter.java deleted file mode 100644 index da11521e..00000000 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/Filter.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.otaliastudios.cameraview.filters; - -import androidx.annotation.NonNull; - -/** - * A Base abstract class that every effect must extend so that there is a common getShader method. - *

- * This class has a default Vertex Shader implementation which in most cases not required to touch. - * In Effects like sepia, B/W any many, only pixel color changes which can be managed by only fragment shader. - * If there is some other effect which requires vertex shader also change, you can override it. - *

- * If your provide your own vertex and fragment shader, - * please set the {@link #mPositionVariableName}, {@link #mTextureCoordinateVariableName}, - * {@link #mMVPMatrixVariableName}, {@link #mTextureMatrixVariableName} - * according to your shader code. - *

- * Please note that these shader applies on live preview as well as pictureSnapshot and videoSnapshot, - * we only support GLES11Ext.GL_TEXTURE_EXTERNAL_OES - * check EglViewport() - *

- * The default implementation of this class is NoEffect. - */ -public abstract class Filter { - - /** - * Vertex shader code written in Shader Language (C) and stored as String. - * This wil be used by GL to apply any effect. - */ - @NonNull - String mVertexShader = - "uniform mat4 uMVPMatrix;\n" + - "uniform mat4 uTexMatrix;\n" + - "attribute vec4 aPosition;\n" + - "attribute vec4 aTextureCoord;\n" + - "varying vec2 vTextureCoord;\n" + - "void main() {\n" + - " gl_Position = uMVPMatrix * aPosition;\n" + - " vTextureCoord = (uTexMatrix * aTextureCoord).xy;\n" + - "}\n"; - - - /** - * Fragment shader code written in Shader Language (C) and stored as String. - * This wil be used by GL to apply any effect. - */ - @NonNull - String mFragmentShader = - "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - + "varying vec2 vTextureCoord;\n" - + "uniform samplerExternalOES sTexture;\n" - + "void main() {\n" - + " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" - + "}\n"; - - /** - * Width and height of previewing GlSurfaceview. - * This will be used by a few effects. - */ - int mPreviewingViewWidth = 0; - int mPreviewingViewHeight = 0; - - public void setPreviewingViewSize(int width, int height) { - mPreviewingViewWidth = width; - mPreviewingViewHeight = height; - } - - /** - * Local variable name which were used in the shader code. - * These will be used by openGL program to render these vertex and fragment shader - */ - private String mPositionVariableName = "aPosition"; - private String mTextureCoordinateVariableName = "aTextureCoord"; - private String mMVPMatrixVariableName = "uMVPMatrix"; - private String mTextureMatrixVariableName = "uTexMatrix"; - - public String getPositionVariableName() { - return mPositionVariableName; - } - - public void setPositionVariableName(String positionVariableName) { - this.mPositionVariableName = positionVariableName; - } - - public String getTexttureCoordinateVariableName() { - return mTextureCoordinateVariableName; - } - - public void setTexttureCoordinateVariableName(String texttureCoordinateVariableName) { - this.mTextureCoordinateVariableName = texttureCoordinateVariableName; - } - - public String getMVPMatrixVariableName() { - return mMVPMatrixVariableName; - } - - public void setMVPMatrixVariableName(String mvpMatrixVariableName) { - this.mMVPMatrixVariableName = mvpMatrixVariableName; - } - - public String getTextureMatrixVariableName() { - return mTextureMatrixVariableName; - } - - public void setTextureMatrixVariableName(String textureMatrixVariableName) { - this.mTextureMatrixVariableName = textureMatrixVariableName; - } - - /** - * Get vertex Shader code - * - * @return complete shader code in C - */ - @NonNull - public String getVertexShader() { - return mVertexShader; - } - - - /** - * Get fragment Shader code - * - * @return complete shader code in C - */ - @NonNull - public abstract String getFragmentShader(); - -} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/Filters.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/Filters.java deleted file mode 100644 index 26bf6d82..00000000 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/Filters.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.otaliastudios.cameraview.filters; - -public enum Filters { - NO_FILTER, - - AUTO_FIX_FILTER, - BLACK_AND_WHITE_FILTER, - BRIGHTNESS_FILTER, - CONTRAST_FILTER, - CROSS_PROCESS_FILTER, - DOCUMENTARY_FILTER, - DUO_TONE_COLOR_FILTER, - FILL_LIGHT_FILTER, - GAMMA_FILTER, - GRAIN_FILTER, - GREY_SCALE_FILTER, - HUE_FILTER, - INVERT_COLOR_FILTER, - LAMOISH_FILTER, - POSTERIZE_FILTER, - SATURATION_FILTER, - SEPIA_FILTER, - SHARPNESS_FILTER, - TEMPERATURE_FILTER, - TINT_FILTER, - VIGNETTE_FILTER; - - public Filter newInstance() { - Filter shaderEffect; - switch (this) { - - case AUTO_FIX_FILTER: - shaderEffect = new AutoFixFilter(); - break; - - case BLACK_AND_WHITE_FILTER: - shaderEffect = new BlackAndWhiteFilter(); - break; - - case BRIGHTNESS_FILTER: - shaderEffect = new BrightnessFilter(); - break; - - case CONTRAST_FILTER: - shaderEffect = new ContrastFilter(); - break; - - case CROSS_PROCESS_FILTER: - shaderEffect = new CrossProcessFilter(); - break; - - case DOCUMENTARY_FILTER: - shaderEffect = new DocumentaryFilter(); - break; - - case DUO_TONE_COLOR_FILTER: - shaderEffect = new DuotoneFilter(); - break; - - case FILL_LIGHT_FILTER: - shaderEffect = new FillLightFilter(); - break; - - case GAMMA_FILTER: - shaderEffect = new GammaFilter(); - break; - - case GRAIN_FILTER: - shaderEffect = new GrainFilter(); - break; - - case GREY_SCALE_FILTER: - shaderEffect = new GreyScaleFilter(); - break; - - case HUE_FILTER: - shaderEffect = new HueFilter(); - break; - - case INVERT_COLOR_FILTER: - shaderEffect = new InvertColorsFilter(); - break; - - case LAMOISH_FILTER: - shaderEffect = new LamoishFilter(); - break; - - case POSTERIZE_FILTER: - shaderEffect = new PosterizeFilter(); - break; - - case SATURATION_FILTER: - shaderEffect = new SaturationFilter(); - break; - - case SEPIA_FILTER: - shaderEffect = new SepiaFilter(); - break; - - case SHARPNESS_FILTER: - shaderEffect = new SharpnessFilter(); - break; - - case TEMPERATURE_FILTER: - shaderEffect = new TemperatureFilter(); - break; - - case TINT_FILTER: - shaderEffect = new TintFilter(); - break; - - case VIGNETTE_FILTER: - shaderEffect = new VignetteFilter(); - break; - - - case NO_FILTER: - default: - shaderEffect = new NoFilter(); - } - - return shaderEffect; - } -} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GammaFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GammaFilter.java index aa17715a..55cc2e86 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GammaFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GammaFilter.java @@ -1,59 +1,90 @@ package com.otaliastudios.cameraview.filters; +import android.opengl.GLES20; + import androidx.annotation.NonNull; +import com.otaliastudios.cameraview.filter.BaseFilter; +import com.otaliastudios.cameraview.filter.OneParameterFilter; +import com.otaliastudios.cameraview.internal.GlUtils; + /** - * Apply Gamma Effect on preview being played + * Applies gamma correction to the frames. */ -public class GammaFilter extends Filter { - private float gammaValue = 2.0f; +public class GammaFilter extends BaseFilter implements OneParameterFilter { + + private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform samplerExternalOES sTexture;\n" + + "uniform float gamma;\n" + + "void main() {\n" + + " vec4 textureColor = texture2D(sTexture, vTextureCoord);\n" + + " gl_FragColor = vec4(pow(textureColor.rgb, vec3(gamma)), textureColor.w);\n" + + "}\n"; + + private float gamma = 2.0f; + private int gammaLocation = -1; + + public GammaFilter() { } /** - * Initialize Effect + * Sets the new gamma value in the 0.0 - 2.0 range. + * The 1.0 value means no correction will be applied. + * + * @param gamma gamma value */ - public GammaFilter() { + @SuppressWarnings("WeakerAccess") + public void setGamma(float gamma) { + if (gamma < 0.0f) gamma = 0.0f; + if (gamma > 2.0f) gamma = 2.0f; + this.gamma = gamma; } /** - * setGammaValue + * Returns the current gamma. * - * @param gammaValue Range should be between 0.0 - 1.0 with 0.5 being normal. + * @see #setGamma(float) + * @return gamma */ - public void setGammaValue(float gammaValue) { - if (gammaValue < 0.0f) - gammaValue = 0.0f; - else if (gammaValue > 1.0f) - gammaValue = 1.0f; + @SuppressWarnings("WeakerAccess") + public float getGamma() { + return gamma; + } - //since the shader excepts a range of 0.0 - 2.0 - //will multiply the 2.0 to every value - this.gammaValue = gammaValue * 2.0f; + @Override + public void setParameter1(float value) { + setGamma(value * 2F); } - public float getGammaValue() { - //since the shader excepts a range of 0.0 - 2.0 - //to keep it between 0.0f - 1.0f range, will divide it with 2.0 - return gammaValue / 2.0f; + @Override + public float getParameter1() { + return getGamma() / 2F; } @NonNull @Override public String getFragmentShader() { + return FRAGMENT_SHADER; + } - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - - + "varying vec2 vTextureCoord;\n" - + "uniform samplerExternalOES sTexture;\n" - + "float gamma=" + gammaValue + ";\n" - - + "void main() {\n" - - + "vec4 textureColor = texture2D(sTexture, vTextureCoord);\n" - + "gl_FragColor = vec4(pow(textureColor.rgb, vec3(gamma)), textureColor.w);\n" + @Override + public void onCreate(int programHandle) { + super.onCreate(programHandle); + gammaLocation = GLES20.glGetUniformLocation(programHandle, "gamma"); + GlUtils.checkLocation(gammaLocation, "gamma"); + } - + "}\n"; + @Override + public void onDestroy() { + super.onDestroy(); + gammaLocation = -1; + } - return shader; + @Override + protected void onPreDraw(float[] transformMatrix) { + super.onPreDraw(transformMatrix); + GLES20.glUniform1f(gammaLocation, gamma); + GlUtils.checkError("glUniform1f"); } } \ No newline at end of file diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GrainFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GrainFilter.java index e7292ac7..01ddd853 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GrainFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GrainFilter.java @@ -1,98 +1,144 @@ package com.otaliastudios.cameraview.filters; +import android.opengl.GLES20; + import androidx.annotation.NonNull; -import java.util.Date; +import com.otaliastudios.cameraview.filter.BaseFilter; +import com.otaliastudios.cameraview.filter.OneParameterFilter; +import com.otaliastudios.cameraview.internal.GlUtils; + import java.util.Random; /** - * Applies film grain effect to preview. + * Applies film grain effect to the frames. */ -public class GrainFilter extends Filter { +public class GrainFilter extends BaseFilter implements OneParameterFilter { + + private final static Random RANDOM = new Random(); + private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "vec2 seed;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform samplerExternalOES tex_sampler_0;\n" + + "uniform samplerExternalOES tex_sampler_1;\n" + + "uniform float scale;\n" + + "uniform float stepX;\n" + + "uniform float stepY;\n" + + "float rand(vec2 loc) {\n" + + " float theta1 = dot(loc, vec2(0.9898, 0.233));\n" + + " float theta2 = dot(loc, vec2(12.0, 78.0));\n" + + " float value = cos(theta1) * sin(theta2) + sin(theta1) * cos(theta2);\n" + // keep value of part1 in range: (2^-14 to 2^14). + + " float temp = mod(197.0 * value, 1.0) + value;\n" + + " float part1 = mod(220.0 * temp, 1.0) + temp;\n" + + " float part2 = value * 0.5453;\n" + + " float part3 = cos(theta1 + theta2) * 0.43758;\n" + + " float sum = (part1 + part2 + part3);\n" + + " return fract(sum)*scale;\n" + + "}\n" + + "void main() {\n" + + " seed[0] = " + RANDOM.nextFloat() + ";\n" + + " seed[1] = " + RANDOM.nextFloat() + ";\n" + + " float noise = texture2D(tex_sampler_1, vTextureCoord + vec2(-stepX, -stepY)).r * 0.224;\n" + + " noise += texture2D(tex_sampler_1, vTextureCoord + vec2(-stepX, stepY)).r * 0.224;\n" + + " noise += texture2D(tex_sampler_1, vTextureCoord + vec2(stepX, -stepY)).r * 0.224;\n" + + " noise += texture2D(tex_sampler_1, vTextureCoord + vec2(stepX, stepY)).r * 0.224;\n" + + " noise += 0.4448;\n" + + " noise *= scale;\n" + + " vec4 color = texture2D(tex_sampler_0, vTextureCoord);\n" + + " float energy = 0.33333 * color.r + 0.33333 * color.g + 0.33333 * color.b;\n" + + " float mask = (1.0 - sqrt(energy));\n" + + " float weight = 1.0 - 1.333 * mask * noise;\n" + + " gl_FragColor = vec4(color.rgb * weight, color.a);\n" + + " gl_FragColor = gl_FragColor+vec4(rand(vTextureCoord + seed), rand(vTextureCoord + seed),rand(vTextureCoord + seed),1);\n" + + "}\n"; + private float strength = 0.5f; - private Random mRandom; + private int width = 1; + private int height = 1; + private int strengthLocation = -1; + private int stepXLocation = -1; + private int stepYLocation = -1; - /** - * Initialize Effect - */ - public GrainFilter() { - mRandom = new Random(new Date().getTime()); + @SuppressWarnings("WeakerAccess") + public GrainFilter() { } + + @Override + public void setSize(int width, int height) { + super.setSize(width, height); + this.width = width; + this.height = height; } /** - * setDistortionStrength + * Sets the current distortion strength. + * 0.0: no distortion. + * 1.0: maximum distortion. * - * @param strength Float, between 0.0f and 1.0. Zero means no distortion, while 1 - * indicates the maximum amount of adjustment. + * @param strength strength */ - public void setDistortionStrength(float strength) { - if (strength < 0.0f) - strength = 0.0f; - else if (strength > 1.0f) - strength = 1.0f; + @SuppressWarnings("WeakerAccess") + public void setStrength(float strength) { + if (strength < 0.0f) strength = 0.0f; + if (strength > 1.0f) strength = 1.0f; this.strength = strength; } + /** + * Returns the current strength. + * + * @see #setStrength(float) + * @return strength + */ + @SuppressWarnings({"unused", "WeakerAccess"}) public float getStrength() { return strength; } + @Override + public void setParameter1(float value) { + setStrength(value); + } + + @Override + public float getParameter1() { + return getStrength(); + } + @NonNull @Override public String getFragmentShader() { - float seed[] = {mRandom.nextFloat(), mRandom.nextFloat()}; - String scaleString = "scale = " + strength + ";\n"; - String seedString[] = new String[2]; - seedString[0] = "seed[0] = " + seed[0] + ";\n"; - seedString[1] = "seed[1] = " + seed[1] + ";\n"; - String stepX = "stepX = " + 0.5f / mPreviewingViewWidth + ";\n"; - String stepY = "stepY = " + 0.5f / mPreviewingViewHeight + ";\n"; - - // locString[1] = "loc[1] = loc[1]+" + seedString[1] + ";\n"; - - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - + " vec2 seed;\n" - + "varying vec2 vTextureCoord;\n" - + "uniform samplerExternalOES tex_sampler_0;\n" - + "uniform samplerExternalOES tex_sampler_1;\n" - + "float scale;\n" - + " float stepX;\n" - + " float stepY;\n" - + "float rand(vec2 loc) {\n" - + " float theta1 = dot(loc, vec2(0.9898, 0.233));\n" - + " float theta2 = dot(loc, vec2(12.0, 78.0));\n" - + " float value = cos(theta1) * sin(theta2) + sin(theta1) * cos(theta2);\n" - + - // keep value of part1 in range: (2^-14 to 2^14). - " float temp = mod(197.0 * value, 1.0) + value;\n" - + " float part1 = mod(220.0 * temp, 1.0) + temp;\n" - + " float part2 = value * 0.5453;\n" - + " float part3 = cos(theta1 + theta2) * 0.43758;\n" - + " float sum = (part1 + part2 + part3);\n" - + " return fract(sum)*scale;\n" - + "}\n" - + "void main() {\n" - // Parameters that were created above - + seedString[0] - + seedString[1] - + scaleString - + stepX - + stepY - + " float noise = texture2D(tex_sampler_1, vTextureCoord + vec2(-stepX, -stepY)).r * 0.224;\n" - + " noise += texture2D(tex_sampler_1, vTextureCoord + vec2(-stepX, stepY)).r * 0.224;\n" - + " noise += texture2D(tex_sampler_1, vTextureCoord + vec2(stepX, -stepY)).r * 0.224;\n" - + " noise += texture2D(tex_sampler_1, vTextureCoord + vec2(stepX, stepY)).r * 0.224;\n" - + " noise += 0.4448;\n" - + " noise *= scale;\n" - + " vec4 color = texture2D(tex_sampler_0, vTextureCoord);\n" - + " float energy = 0.33333 * color.r + 0.33333 * color.g + 0.33333 * color.b;\n" - + " float mask = (1.0 - sqrt(energy));\n" - + " float weight = 1.0 - 1.333 * mask * noise;\n" - + " gl_FragColor = vec4(color.rgb * weight, color.a);\n" - + " gl_FragColor = gl_FragColor+vec4(rand(vTextureCoord + seed), rand(vTextureCoord + seed),rand(vTextureCoord + seed),1);\n" - + "}\n"; - return shader; + return FRAGMENT_SHADER; + } + + @Override + public void onCreate(int programHandle) { + super.onCreate(programHandle); + strengthLocation = GLES20.glGetUniformLocation(programHandle, "scale"); + GlUtils.checkLocation(strengthLocation, "scale"); + stepXLocation = GLES20.glGetUniformLocation(programHandle, "stepX"); + GlUtils.checkLocation(stepXLocation, "stepX"); + stepYLocation = GLES20.glGetUniformLocation(programHandle, "stepY"); + GlUtils.checkLocation(stepYLocation, "stepY"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + strengthLocation = -1; + stepXLocation = -1; + stepYLocation = -1; + } + @Override + protected void onPreDraw(float[] transformMatrix) { + super.onPreDraw(transformMatrix); + GLES20.glUniform1f(strengthLocation, strength); + GlUtils.checkError("glUniform1f"); + GLES20.glUniform1f(stepXLocation, 0.5f / width); + GlUtils.checkError("glUniform1f"); + GLES20.glUniform1f(stepYLocation, 0.5f / height); + GlUtils.checkError("glUniform1f"); } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GrayscaleFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GrayscaleFilter.java new file mode 100644 index 00000000..a4a49f3d --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GrayscaleFilter.java @@ -0,0 +1,29 @@ +package com.otaliastudios.cameraview.filters; + +import androidx.annotation.NonNull; + +import com.otaliastudios.cameraview.filter.BaseFilter; + +/** + * Converts frames to gray scale. + */ +public class GrayscaleFilter extends BaseFilter { + + private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "uniform samplerExternalOES sTexture;\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " float y = dot(color, vec4(0.299, 0.587, 0.114, 0));\n" + + " gl_FragColor = vec4(y, y, y, color.a);\n" + + "}\n"; + + public GrayscaleFilter() { } + + @NonNull + @Override + public String getFragmentShader() { + return FRAGMENT_SHADER; + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GreyScaleFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GreyScaleFilter.java deleted file mode 100644 index a5872562..00000000 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GreyScaleFilter.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.otaliastudios.cameraview.filters; - -import androidx.annotation.NonNull; - -/** - * Converts preview to GreyScale. - */ -public class GreyScaleFilter extends Filter { - /** - * Initialize Effect - */ - public GreyScaleFilter() { - } - - @NonNull - @Override - public String getFragmentShader() { - - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - + "uniform samplerExternalOES sTexture;\n" - + "varying vec2 vTextureCoord;\n" + "void main() {\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" - + " float y = dot(color, vec4(0.299, 0.587, 0.114, 0));\n" - + " gl_FragColor = vec4(y, y, y, color.a);\n" + "}\n"; - ; - - return shader; - - } -} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/HueFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/HueFilter.java index 7a74cd2f..a013d5b4 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/HueFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/HueFilter.java @@ -1,72 +1,106 @@ package com.otaliastudios.cameraview.filters; +import android.opengl.GLES20; + import androidx.annotation.NonNull; +import com.otaliastudios.cameraview.filter.BaseFilter; +import com.otaliastudios.cameraview.filter.OneParameterFilter; +import com.otaliastudios.cameraview.internal.GlUtils; + /** - * Apply Hue effect on the preview + * Applies a hue effect on the input frames. */ -public class HueFilter extends Filter { - float hueValue = 0.0f; +public class HueFilter extends BaseFilter implements OneParameterFilter { + + private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform samplerExternalOES sTexture;\n" + + "uniform float hue;\n" + + "void main() {\n" + + " vec4 kRGBToYPrime = vec4 (0.299, 0.587, 0.114, 0.0);\n" + + " vec4 kRGBToI = vec4 (0.595716, -0.274453, -0.321263, 0.0);\n" + + " vec4 kRGBToQ = vec4 (0.211456, -0.522591, 0.31135, 0.0);\n" + + " vec4 kYIQToR = vec4 (1.0, 0.9563, 0.6210, 0.0);\n" + + " vec4 kYIQToG = vec4 (1.0, -0.2721, -0.6474, 0.0);\n" + + " vec4 kYIQToB = vec4 (1.0, -1.1070, 1.7046, 0.0);\n" + + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " float YPrime = dot(color, kRGBToYPrime);\n" + + " float I = dot(color, kRGBToI);\n" + + " float Q = dot(color, kRGBToQ);\n" + + " float chroma = sqrt (I * I + Q * Q);\n" + + " Q = chroma * sin (hue);\n" + + " I = chroma * cos (hue);\n" + + " vec4 yIQ = vec4 (YPrime, I, Q, 0.0);\n" + + " color.r = dot (yIQ, kYIQToR);\n" + + " color.g = dot (yIQ, kYIQToG);\n" + + " color.b = dot (yIQ, kYIQToB);\n" + + " gl_FragColor = color;\n" + + "}\n"; + + private float hue = 0.0f; + private int hueLocation = -1; + + public HueFilter() { } /** - * Initialize Effect + * Sets the hue value in degrees. See the values chart: + * https://cloud.githubusercontent.com/assets/2201511/21810115/b99ac22a-d74a-11e6-9f6c-ef74d15c88c7.jpg + * + * @param hue hue degrees */ - public HueFilter() { + @SuppressWarnings({"unused", "WeakerAccess"}) + public void setHue(float hue) { + this.hue = hue % 360; } /** - * Hue value chart - https://cloud.githubusercontent.com/assets/2201511/21810115/b99ac22a-d74a-11e6-9f6c-ef74d15c88c7.jpg" + * Returns the current hue value. * - * @param hueDegrees Range of value should be between 0 to 360 degrees as described in the image above + * @see #setHue(float) + * @return hue */ - public void setHueDegreeValue(float hueDegrees) { - // manipulating input value so that we can map it on 360 degree circle - hueValue = ((hueDegrees - 45) / 45f + 0.5f) * -1; + @SuppressWarnings("WeakerAccess") + public float getHue() { + return hue; } - @NonNull @Override - public String getFragmentShader() { - - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - - + "varying vec2 vTextureCoord;\n" - + "uniform samplerExternalOES sTexture;\n" - + "float hue=" + hueValue + ";\n" - - + "void main() {\n" - - + "vec4 kRGBToYPrime = vec4 (0.299, 0.587, 0.114, 0.0);\n" - + "vec4 kRGBToI = vec4 (0.595716, -0.274453, -0.321263, 0.0);\n" - + "vec4 kRGBToQ = vec4 (0.211456, -0.522591, 0.31135, 0.0);\n" - - + "vec4 kYIQToR = vec4 (1.0, 0.9563, 0.6210, 0.0);\n" - + "vec4 kYIQToG = vec4 (1.0, -0.2721, -0.6474, 0.0);\n" - + "vec4 kYIQToB = vec4 (1.0, -1.1070, 1.7046, 0.0);\n" - - - + "vec4 color = texture2D(sTexture, vTextureCoord);\n" - - + "float YPrime = dot(color, kRGBToYPrime);\n" - + "float I = dot(color, kRGBToI);\n" - + "float Q = dot(color, kRGBToQ);\n" - - + "float chroma = sqrt (I * I + Q * Q);\n" - - + "Q = chroma * sin (hue);\n" + public void setParameter1(float value) { + setHue(value * 360F); + } - + "I = chroma * cos (hue);\n" + @Override + public float getParameter1() { + return getHue() / 360F; + } - + "vec4 yIQ = vec4 (YPrime, I, Q, 0.0);\n" + @NonNull + @Override + public String getFragmentShader() { + return FRAGMENT_SHADER; + } - + "color.r = dot (yIQ, kYIQToR);\n" - + "color.g = dot (yIQ, kYIQToG);\n" - + "color.b = dot (yIQ, kYIQToB);\n" - + "gl_FragColor = color;\n" + @Override + public void onCreate(int programHandle) { + super.onCreate(programHandle); + hueLocation = GLES20.glGetUniformLocation(programHandle, "hue"); + GlUtils.checkLocation(hueLocation, "hue"); + } - + "}\n"; + @Override + public void onDestroy() { + super.onDestroy(); + hueLocation = -1; + } - return shader; + @Override + protected void onPreDraw(float[] transformMatrix) { + super.onPreDraw(transformMatrix); + // map it on 360 degree circle + float shaderHue = ((hue - 45) / 45f + 0.5f) * -1; + GLES20.glUniform1f(hueLocation, shaderHue); + GlUtils.checkError("glUniform1f"); } } \ No newline at end of file diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/InvertColorsFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/InvertColorsFilter.java index 638819d8..a5490d88 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/InvertColorsFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/InvertColorsFilter.java @@ -2,32 +2,30 @@ package com.otaliastudios.cameraview.filters; import androidx.annotation.NonNull; +import com.otaliastudios.cameraview.filter.BaseFilter; + /** - * Inverts the preview colors. This can also be known as negative Effect. + * Inverts the input colors. This is also known as negative effect. */ -public class InvertColorsFilter extends Filter { - /** - * Initialize Effect - */ - public InvertColorsFilter() { - } +public class InvertColorsFilter extends BaseFilter { + + private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform samplerExternalOES sTexture;\n" + + "void main() {\n" + + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " float colorR = (1.0 - color.r) / 1.0;\n" + + " float colorG = (1.0 - color.g) / 1.0;\n" + + " float colorB = (1.0 - color.b) / 1.0;\n" + + " gl_FragColor = vec4(colorR, colorG, colorB, color.a);\n" + + "}\n"; + + public InvertColorsFilter() { } @NonNull @Override public String getFragmentShader() { - - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - + "varying vec2 vTextureCoord;\n" - + "uniform samplerExternalOES sTexture;\n" + "void main() {\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" - + " float colorR = (1.0 - color.r) / 1.0;\n" - + " float colorG = (1.0 - color.g) / 1.0;\n" - + " float colorB = (1.0 - color.b) / 1.0;\n" - + " gl_FragColor = vec4(colorR, colorG, colorB, color.a);\n" - + "}\n"; - - return shader; - + return FRAGMENT_SHADER; } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/LamoishFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/LamoishFilter.java deleted file mode 100644 index b4fce780..00000000 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/LamoishFilter.java +++ /dev/null @@ -1,143 +0,0 @@ -package com.otaliastudios.cameraview.filters; - -import androidx.annotation.NonNull; - -import java.util.Date; -import java.util.Random; - -/** - * Applies lomo-camera style effect to preview. - */ -public class LamoishFilter extends Filter { - private Random mRandom; - - /** - * Initialize Effect - */ - public LamoishFilter() { - mRandom = new Random(new Date().getTime()); - } - - @NonNull - @Override - public String getFragmentShader() { - float scale[] = new float[2]; - if (mPreviewingViewWidth > mPreviewingViewHeight) { - scale[0] = 1f; - scale[1] = ((float) mPreviewingViewHeight) / mPreviewingViewWidth; - } else { - scale[0] = ((float) mPreviewingViewWidth) / mPreviewingViewHeight; - scale[1] = 1f; - } - float max_dist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1] - * scale[1])) * 0.5f; - - float seed[] = {mRandom.nextFloat(), mRandom.nextFloat()}; - - String scaleString[] = new String[2]; - String seedString[] = new String[2]; - - scaleString[0] = "scale[0] = " + scale[0] + ";\n"; - scaleString[1] = "scale[1] = " + scale[1] + ";\n"; - - seedString[0] = "seed[0] = " + seed[0] + ";\n"; - seedString[1] = "seed[1] = " + seed[1] + ";\n"; - - String inv_max_distString = "inv_max_dist = " + 1.0f / max_dist + ";\n"; - String stepsizeString = "stepsize = " + 1.0f / 255.0f + ";\n"; - String stepsizeXString = "stepsizeX = " + 1.0f / mPreviewingViewWidth + ";\n"; - String stepsizeYString = "stepsizeY = " + 1.0f / mPreviewingViewHeight + ";\n"; - - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - + "uniform samplerExternalOES sTexture;\n" - + " vec2 seed;\n" - + " float stepsizeX;\n" - + " float stepsizeY;\n" - + " float stepsize;\n" - + " vec2 scale;\n" - + " float inv_max_dist;\n" - + "varying vec2 vTextureCoord;\n" - + "float rand(vec2 loc) {\n" - + " float theta1 = dot(loc, vec2(0.9898, 0.233));\n" - + " float theta2 = dot(loc, vec2(12.0, 78.0));\n" - + " float value = cos(theta1) * sin(theta2) + sin(theta1) * cos(theta2);\n" - + - // keep value of part1 in range: (2^-14 to 2^14). - " float temp = mod(197.0 * value, 1.0) + value;\n" - + " float part1 = mod(220.0 * temp, 1.0) + temp;\n" - + " float part2 = value * 0.5453;\n" - + " float part3 = cos(theta1 + theta2) * 0.43758;\n" - + " return fract(part1 + part2 + part3);\n" + "}\n" - + "void main() {\n" - // Parameters that were created above - + scaleString[0] - + scaleString[1] - + seedString[0] - + seedString[1] - + inv_max_distString - + stepsizeString - + stepsizeXString - + stepsizeYString - // sharpen - + " vec3 nbr_color = vec3(0.0, 0.0, 0.0);\n" - + " vec2 coord;\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" - + " coord.x = vTextureCoord.x - 0.5 * stepsizeX;\n" - + " coord.y = vTextureCoord.y - stepsizeY;\n" - + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" - + " coord.x = vTextureCoord.x - stepsizeX;\n" - + " coord.y = vTextureCoord.y + 0.5 * stepsizeY;\n" - + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" - + " coord.x = vTextureCoord.x + stepsizeX;\n" - + " coord.y = vTextureCoord.y - 0.5 * stepsizeY;\n" - + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" - + " coord.x = vTextureCoord.x + stepsizeX;\n" - + " coord.y = vTextureCoord.y + 0.5 * stepsizeY;\n" - + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" - + " vec3 s_color = vec3(color.rgb + 0.3 * nbr_color);\n" - + - // cross process - " vec3 c_color = vec3(0.0, 0.0, 0.0);\n" - + " float value;\n" - + " if (s_color.r < 0.5) {\n" - + " value = s_color.r;\n" - + " } else {\n" - + " value = 1.0 - s_color.r;\n" - + " }\n" - + " float red = 4.0 * value * value * value;\n" - + " if (s_color.r < 0.5) {\n" - + " c_color.r = red;\n" - + " } else {\n" - + " c_color.r = 1.0 - red;\n" - + " }\n" - + " if (s_color.g < 0.5) {\n" - + " value = s_color.g;\n" - + " } else {\n" - + " value = 1.0 - s_color.g;\n" - + " }\n" - + " float green = 2.0 * value * value;\n" - + " if (s_color.g < 0.5) {\n" - + " c_color.g = green;\n" - + " } else {\n" - + " c_color.g = 1.0 - green;\n" - + " }\n" - + " c_color.b = s_color.b * 0.5 + 0.25;\n" - + - // blackwhite - " float dither = rand(vTextureCoord + seed);\n" - + " vec3 xform = clamp((c_color.rgb - 0.15) * 1.53846, 0.0, 1.0);\n" - + " vec3 temp = clamp((color.rgb + stepsize - 0.15) * 1.53846, 0.0, 1.0);\n" - + " vec3 bw_color = clamp(xform + (temp - xform) * (dither - 0.5), 0.0, 1.0);\n" - + - // vignette - " coord = vTextureCoord - vec2(0.5, 0.5);\n" - + " float dist = length(coord * scale);\n" - + " float lumen = 0.85 / (1.0 + exp((dist * inv_max_dist - 0.73) * 20.0)) + 0.15;\n" - + " gl_FragColor = vec4(bw_color * lumen, color.a);\n" + "}\n"; - ; - - return shader; - - } -} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/LomoishFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/LomoishFilter.java new file mode 100644 index 00000000..aaaf88a0 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/LomoishFilter.java @@ -0,0 +1,165 @@ +package com.otaliastudios.cameraview.filters; + +import android.opengl.GLES20; + +import androidx.annotation.NonNull; + +import com.otaliastudios.cameraview.filter.BaseFilter; +import com.otaliastudios.cameraview.internal.GlUtils; + +import java.util.Date; +import java.util.Random; + +/** + * Applies a lomo-camera style effect to the input frames. + */ +public class LomoishFilter extends BaseFilter { + + private final static Random RANDOM = new Random(); + private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "uniform samplerExternalOES sTexture;\n" + + "uniform float stepsizeX;\n" + + "uniform float stepsizeY;\n" + + "uniform vec2 scale;\n" + + "uniform float inv_max_dist;\n" + + "vec2 seed;\n" + + "float stepsize;\n" + + "varying vec2 vTextureCoord;\n" + + "float rand(vec2 loc) {\n" + + " float theta1 = dot(loc, vec2(0.9898, 0.233));\n" + + " float theta2 = dot(loc, vec2(12.0, 78.0));\n" + + " float value = cos(theta1) * sin(theta2) + sin(theta1) * cos(theta2);\n" + // keep value of part1 in range: (2^-14 to 2^14). + + " float temp = mod(197.0 * value, 1.0) + value;\n" + + " float part1 = mod(220.0 * temp, 1.0) + temp;\n" + + " float part2 = value * 0.5453;\n" + + " float part3 = cos(theta1 + theta2) * 0.43758;\n" + + " return fract(part1 + part2 + part3);\n" + + "}\n" + + "void main() {\n" + + " seed[0] = " + RANDOM.nextFloat() + ";\n" + + " seed[1] = " + RANDOM.nextFloat() + ";\n" + + " stepsize = " + 1.0f / 255.0f + ";\n" + // sharpen + + " vec3 nbr_color = vec3(0.0, 0.0, 0.0);\n" + + " vec2 coord;\n" + + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " coord.x = vTextureCoord.x - 0.5 * stepsizeX;\n" + + " coord.y = vTextureCoord.y - stepsizeY;\n" + + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" + + " coord.x = vTextureCoord.x - stepsizeX;\n" + + " coord.y = vTextureCoord.y + 0.5 * stepsizeY;\n" + + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" + + " coord.x = vTextureCoord.x + stepsizeX;\n" + + " coord.y = vTextureCoord.y - 0.5 * stepsizeY;\n" + + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" + + " coord.x = vTextureCoord.x + stepsizeX;\n" + + " coord.y = vTextureCoord.y + 0.5 * stepsizeY;\n" + + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" + + " vec3 s_color = vec3(color.rgb + 0.3 * nbr_color);\n" + // cross process + + " vec3 c_color = vec3(0.0, 0.0, 0.0);\n" + + " float value;\n" + + " if (s_color.r < 0.5) {\n" + + " value = s_color.r;\n" + + " } else {\n" + + " value = 1.0 - s_color.r;\n" + + " }\n" + + " float red = 4.0 * value * value * value;\n" + + " if (s_color.r < 0.5) {\n" + + " c_color.r = red;\n" + + " } else {\n" + + " c_color.r = 1.0 - red;\n" + + " }\n" + + " if (s_color.g < 0.5) {\n" + + " value = s_color.g;\n" + + " } else {\n" + + " value = 1.0 - s_color.g;\n" + + " }\n" + + " float green = 2.0 * value * value;\n" + + " if (s_color.g < 0.5) {\n" + + " c_color.g = green;\n" + + " } else {\n" + + " c_color.g = 1.0 - green;\n" + + " }\n" + + " c_color.b = s_color.b * 0.5 + 0.25;\n" + // blackwhite + + " float dither = rand(vTextureCoord + seed);\n" + + " vec3 xform = clamp((c_color.rgb - 0.15) * 1.53846, 0.0, 1.0);\n" + + " vec3 temp = clamp((color.rgb + stepsize - 0.15) * 1.53846, 0.0, 1.0);\n" + + " vec3 bw_color = clamp(xform + (temp - xform) * (dither - 0.5), 0.0, 1.0);\n" + // vignette + + " coord = vTextureCoord - vec2(0.5, 0.5);\n" + + " float dist = length(coord * scale);\n" + + " float lumen = 0.85 / (1.0 + exp((dist * inv_max_dist - 0.73) * 20.0)) + 0.15;\n" + + " gl_FragColor = vec4(bw_color * lumen, color.a);\n" + + "}\n"; + + private int width = 1; + private int height = 1; + + private int scaleLocation = -1; + private int maxDistLocation = -1; + private int stepSizeXLocation = -1; + private int stepSizeYLocation = -1; + + public LomoishFilter() { } + + @Override + public void setSize(int width, int height) { + super.setSize(width, height); + this.width = width; + this.height = height; + } + + @NonNull + @Override + public String getFragmentShader() { + return FRAGMENT_SHADER; + } + + @Override + public void onCreate(int programHandle) { + super.onCreate(programHandle); + scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale"); + GlUtils.checkLocation(scaleLocation, "scale"); + maxDistLocation = GLES20.glGetUniformLocation(programHandle, "inv_max_dist"); + GlUtils.checkLocation(maxDistLocation, "inv_max_dist"); + stepSizeXLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeX"); + GlUtils.checkLocation(stepSizeXLocation, "stepsizeX"); + stepSizeYLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeY"); + GlUtils.checkLocation(stepSizeYLocation, "stepsizeY"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + scaleLocation = -1; + maxDistLocation = -1; + stepSizeXLocation = -1; + stepSizeYLocation = -1; + } + + @Override + protected void onPreDraw(float[] transformMatrix) { + super.onPreDraw(transformMatrix); + float[] scale = new float[2]; + if (width > height) { + scale[0] = 1f; + scale[1] = ((float) height) / width; + } else { + scale[0] = ((float) width) / height; + scale[1] = 1f; + } + float maxDist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1] * scale[1])) * 0.5f; + GLES20.glUniform2fv(scaleLocation, 1, scale, 0); + GlUtils.checkError("glUniform2fv"); + GLES20.glUniform1f(maxDistLocation, 1.0F / maxDist); + GlUtils.checkError("glUniform1f"); + GLES20.glUniform1f(stepSizeXLocation, 1.0F / width); + GlUtils.checkError("glUniform1f"); + GLES20.glUniform1f(stepSizeYLocation, 1.0F / height); + GlUtils.checkError("glUniform1f"); + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/NoFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/NoFilter.java deleted file mode 100644 index 5f76028d..00000000 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/NoFilter.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.otaliastudios.cameraview.filters; - -import androidx.annotation.NonNull; - -public class NoFilter extends Filter { - - @NonNull - @Override - public String getFragmentShader() { - return mFragmentShader; - } -} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/PosterizeFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/PosterizeFilter.java index df70697e..5ce1d386 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/PosterizeFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/PosterizeFilter.java @@ -2,31 +2,30 @@ package com.otaliastudios.cameraview.filters; import androidx.annotation.NonNull; +import com.otaliastudios.cameraview.filter.BaseFilter; + /** - * Applies Posterization effect to Preview. + * Applies a posterization effect to the input frames. */ -public class PosterizeFilter extends Filter { - /** - * Initialize Effect - */ - public PosterizeFilter() { - } +public class PosterizeFilter extends BaseFilter { + + private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "uniform samplerExternalOES sTexture;\n" + + "varying vec2 vTextureCoord;\n" + "void main() {\n" + + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " vec3 pcolor;\n" + + " pcolor.r = (color.r >= 0.5) ? 0.75 : 0.25;\n" + + " pcolor.g = (color.g >= 0.5) ? 0.75 : 0.25;\n" + + " pcolor.b = (color.b >= 0.5) ? 0.75 : 0.25;\n" + + " gl_FragColor = vec4(pcolor, color.a);\n" + + "}\n"; + + public PosterizeFilter() { } @NonNull @Override public String getFragmentShader() { - - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - + "uniform samplerExternalOES sTexture;\n" - + "varying vec2 vTextureCoord;\n" + "void main() {\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" - + " vec3 pcolor;\n" - + " pcolor.r = (color.r >= 0.5) ? 0.75 : 0.25;\n" - + " pcolor.g = (color.g >= 0.5) ? 0.75 : 0.25;\n" - + " pcolor.b = (color.b >= 0.5) ? 0.75 : 0.25;\n" - + " gl_FragColor = vec4(pcolor, color.a);\n" + "}\n"; - return shader; - + return FRAGMENT_SHADER; } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SaturationFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SaturationFilter.java index ddb02931..905addbd 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SaturationFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SaturationFilter.java @@ -1,102 +1,124 @@ package com.otaliastudios.cameraview.filters; +import android.opengl.GLES20; + import androidx.annotation.NonNull; +import com.otaliastudios.cameraview.filter.BaseFilter; +import com.otaliastudios.cameraview.filter.OneParameterFilter; +import com.otaliastudios.cameraview.internal.GlUtils; + /** - * Adjusts color saturation of preview. + * Adjusts color saturation. */ -public class SaturationFilter extends Filter { - private float scale = 1.0f; +public class SaturationFilter 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 scale;\n" + + "uniform vec3 exponents;\n" + + "float shift;\n" + + "vec3 weights;\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " weights[0] = " + 2f / 8f + ";\n" + + " weights[1] = " + 5f / 8f + ";\n" + + " weights[2] = " + 1f / 8f + ";\n" + + " shift = " + 1.0f / 255.0f + ";\n" + + " vec4 oldcolor = texture2D(sTexture, vTextureCoord);\n" + + " float kv = dot(oldcolor.rgb, weights) + shift;\n" + + " vec3 new_color = scale * oldcolor.rgb + (1.0 - scale) * kv;\n" + + " gl_FragColor = vec4(new_color, oldcolor.a);\n" + + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " float de = dot(color.rgb, weights);\n" + + " float inv_de = 1.0 / de;\n" + + " vec3 verynew_color = de * pow(color.rgb * inv_de, exponents);\n" + + " float max_color = max(max(max(verynew_color.r, verynew_color.g), verynew_color.b), 1.0);\n" + + " gl_FragColor = gl_FragColor+vec4(verynew_color / max_color, color.a);\n" + + "}\n"; + + private float scale = 1F; // -1...1 + private int scaleLocation = -1; + private int exponentsLocation = -1; + + public SaturationFilter() { } /** - * Initialize Effect + * Sets the saturation correction value: + * -1.0: fully desaturated, grayscale. + * 0.0: no change. + * +1.0: fully saturated. + * + * @param value new value */ - public SaturationFilter() { + @SuppressWarnings("WeakerAccess") + public void setSaturation(float value) { + if (value < -1F) value = -1F; + if (value > 1F) value = 1F; + scale = value; } /** - * @param value Float, between 0.0 and 1. 0 means no change, while 0.0 indicates - * full desaturated, i.e. grayscale. - * and 1.0 indicates full saturation + * Returns the current saturation. + * + * @see #setSaturation(float) + * @return saturation */ - public void setSaturationValue(float value) { - if (value < 0.0f) - value = 0.0f; - else if (value > 1.0f) - value = 1.0f; + @SuppressWarnings("WeakerAccess") + public float getSaturation() { + return scale; + } - //since the shader excepts a range of -1.0 to 1.0 - //will multiply it by 2.0f and subtract 1.0 to every value - this.scale = (2.0f * value) - 1.0f; + @Override + public void setParameter1(float value) { + setSaturation(2F * value - 1F); } - public float getSaturationValue() { - //since the shader excepts a range of -1.0 to 1.0 - //will add 1.0 to every value and divide it by 2.0f - return (scale + 1.0f) / 2.0f; + @Override + public float getParameter1() { + return (getSaturation() + 1F) / 2F; } @NonNull @Override public String getFragmentShader() { - float shift = 1.0f / 255.0f; - float weights[] = {2f / 8f, 5f / 8f, 1f / 8f}; - float exponents[] = new float[3]; - - String weightsString[] = new String[3]; - String exponentsString[] = new String[3]; - exponentsString[0] = ""; - exponentsString[1] = ""; - exponentsString[2] = ""; - String scaleString = ""; - - if (scale > 0.0f) { - exponents[0] = (0.9f * scale) + 1.0f; - exponents[1] = (2.1f * scale) + 1.0f; - exponents[2] = (2.7f * scale) + 1.0f; - exponentsString[0] = "exponents[0] = " + exponents[0] + ";\n"; - exponentsString[1] = "exponents[1] = " + exponents[1] + ";\n"; - exponentsString[2] = "exponents[2] = " + exponents[2] + ";\n"; - } else - scaleString = "scale = " + (1.0f + scale) + ";\n"; - - weightsString[0] = "weights[0] = " + weights[0] + ";\n"; - weightsString[1] = "weights[1] = " + weights[1] + ";\n"; - weightsString[2] = "weights[2] = " + weights[2] + ";\n"; - String shiftString = "shift = " + shift + ";\n"; - - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - + "uniform samplerExternalOES sTexture;\n" + " float scale;\n" - + " float shift;\n" + " vec3 weights;\n" + " vec3 exponents;\n" - + "varying vec2 vTextureCoord;\n" + "void main() {\n" - // Parameters that were created above - + weightsString[0] - + weightsString[1] - + weightsString[2] - + shiftString - + scaleString - + " vec4 oldcolor = texture2D(sTexture, vTextureCoord);\n" - + " float kv = dot(oldcolor.rgb, weights) + shift;\n" - + " vec3 new_color = scale * oldcolor.rgb + (1.0 - scale) * kv;\n" - + " gl_FragColor= vec4(new_color, oldcolor.a);\n" - // Parameters that were created above - + weightsString[0] - + weightsString[1] - + weightsString[2] - + exponentsString[0] - + exponentsString[1] - + exponentsString[2] - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" - + " float de = dot(color.rgb, weights);\n" - + " float inv_de = 1.0 / de;\n" - + " vec3 verynew_color = de * pow(color.rgb * inv_de, exponents);\n" - + " float max_color = max(max(max(verynew_color.r, verynew_color.g), verynew_color.b), 1.0);\n" - + " gl_FragColor = gl_FragColor+vec4(verynew_color / max_color, color.a);\n" - + "}\n"; + return FRAGMENT_SHADER; + } - return shader; + @Override + public void onCreate(int programHandle) { + super.onCreate(programHandle); + scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale"); + GlUtils.checkLocation(scaleLocation, "scale"); + exponentsLocation = GLES20.glGetUniformLocation(programHandle, "exponents"); + GlUtils.checkLocation(exponentsLocation, "exponents"); + } + @Override + public void onDestroy() { + super.onDestroy(); + scaleLocation = -1; + exponentsLocation = -1; } + @Override + protected void onPreDraw(float[] transformMatrix) { + super.onPreDraw(transformMatrix); + if (scale > 0.0f) { + GLES20.glUniform1f(scaleLocation, 0F); + GlUtils.checkError("glUniform1f"); + GLES20.glUniform3f(exponentsLocation, + (0.9f * scale) + 1.0f, + (2.1f * scale) + 1.0f, + (2.7f * scale) + 1.0f + ); + GlUtils.checkError("glUniform3f"); + } else { + GLES20.glUniform1f(scaleLocation, 1.0F + scale); + GlUtils.checkError("glUniform1f"); + GLES20.glUniform3f(exponentsLocation, 0F, 0F, 0F); + GlUtils.checkError("glUniform3f"); + } + } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SepiaFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SepiaFilter.java index c312bee2..7099bbea 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SepiaFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SepiaFilter.java @@ -2,47 +2,38 @@ package com.otaliastudios.cameraview.filters; import androidx.annotation.NonNull; +import com.otaliastudios.cameraview.filter.BaseFilter; + /** - * Converts preview to Sepia tone. + * Converts frames to sepia tone. */ -public class SepiaFilter extends Filter { - /** - * Initialize Effect - */ - public SepiaFilter() { - } +public class SepiaFilter extends BaseFilter { + + private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "uniform samplerExternalOES sTexture;\n" + + "mat3 matrix;\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " matrix[0][0]=" + 805.0f / 2048.0f + ";\n" + + " matrix[0][1]=" + 715.0f / 2048.0f + ";\n" + + " matrix[0][2]=" + 557.0f / 2048.0f + ";\n" + + " matrix[1][0]=" + 1575.0f / 2048.0f + ";\n" + + " matrix[1][1]=" + 1405.0f / 2048.0f + ";\n" + + " matrix[1][2]=" + 1097.0f / 2048.0f + ";\n" + + " matrix[2][0]=" + 387.0f / 2048.0f + ";\n" + + " matrix[2][1]=" + 344.0f / 2048.0f + ";\n" + + " matrix[2][2]=" + 268.0f / 2048.0f + ";\n" + + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " vec3 new_color = min(matrix * color.rgb, 1.0);\n" + + " gl_FragColor = vec4(new_color.rgb, color.a);\n" + + "}\n"; + + public SepiaFilter() { } @NonNull @Override public String getFragmentShader() { - float weights[] = {805.0f / 2048.0f, 715.0f / 2048.0f, - 557.0f / 2048.0f, 1575.0f / 2048.0f, 1405.0f / 2048.0f, - 1097.0f / 2048.0f, 387.0f / 2048.0f, 344.0f / 2048.0f, - 268.0f / 2048.0f}; - String matrixString[] = new String[9]; - - matrixString[0] = " matrix[0][0]=" + weights[0] + ";\n"; - matrixString[1] = " matrix[0][1]=" + weights[1] + ";\n"; - matrixString[2] = " matrix[0][2]=" + weights[2] + ";\n"; - matrixString[3] = " matrix[1][0]=" + weights[3] + ";\n"; - matrixString[4] = " matrix[1][1]=" + weights[4] + ";\n"; - matrixString[5] = " matrix[1][2]=" + weights[5] + ";\n"; - matrixString[6] = " matrix[2][0]=" + weights[6] + ";\n"; - matrixString[7] = " matrix[2][1]=" + weights[7] + ";\n"; - matrixString[8] = " matrix[2][2]=" + weights[8] + ";\n"; - - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - + "uniform samplerExternalOES sTexture;\n" + " mat3 matrix;\n" - + "varying vec2 vTextureCoord;\n" + "void main() {\n" - + matrixString[0] + matrixString[1] + matrixString[2] - + matrixString[3] + matrixString[4] + matrixString[5] - + matrixString[6] + matrixString[7] + matrixString[8] - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" - + " vec3 new_color = min(matrix * color.rgb, 1.0);\n" - + " gl_FragColor = vec4(new_color.rgb, color.a);\n" + "}\n"; - - return shader; - + return FRAGMENT_SHADER; } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SharpnessFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SharpnessFilter.java index 8984a12d..5e60543a 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SharpnessFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SharpnessFilter.java @@ -1,75 +1,128 @@ package com.otaliastudios.cameraview.filters; +import android.opengl.GLES20; + import androidx.annotation.NonNull; +import com.otaliastudios.cameraview.filter.BaseFilter; +import com.otaliastudios.cameraview.filter.OneParameterFilter; +import com.otaliastudios.cameraview.internal.GlUtils; + /** - * Sharpens the preview. + * Sharpens the input frames. */ -public class SharpnessFilter extends Filter { +public class SharpnessFilter 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 scale;\n" + + "uniform float stepsizeX;\n" + + "uniform float stepsizeY;\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " vec3 nbr_color = vec3(0.0, 0.0, 0.0);\n" + + " vec2 coord;\n" + + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " coord.x = vTextureCoord.x - 0.5 * stepsizeX;\n" + + " coord.y = vTextureCoord.y - stepsizeY;\n" + + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" + + " coord.x = vTextureCoord.x - stepsizeX;\n" + + " coord.y = vTextureCoord.y + 0.5 * stepsizeY;\n" + + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" + + " coord.x = vTextureCoord.x + stepsizeX;\n" + + " coord.y = vTextureCoord.y - 0.5 * stepsizeY;\n" + + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" + + " coord.x = vTextureCoord.x + stepsizeX;\n" + + " coord.y = vTextureCoord.y + 0.5 * stepsizeY;\n" + + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" + + " gl_FragColor = vec4(color.rgb - 2.0 * scale * nbr_color, color.a);\n" + + "}\n"; + private float scale = 0.5f; + private int width = 1; + private int height = 1; + private int scaleLocation = -1; + private int stepSizeXLocation = -1; + private int stepSizeYLocation = -1; + + public SharpnessFilter() { } + + @Override + public void setSize(int width, int height) { + super.setSize(width, height); + this.width = width; + this.height = height; + } /** - * Initialize Effect + * Sets the current sharpness value: + * 0.0: no change. + * 1.0: maximum sharpness. + * + * @param value new sharpness */ - public SharpnessFilter() { + @SuppressWarnings("WeakerAccess") + public void setSharpness(float value) { + if (value < 0.0f) value = 0.0f; + if (value > 1.0f) value = 1.0f; + this.scale = value; } /** - * @param value Float, between 0 and 1. 0 means no change. + * Returns the current sharpness. + * + * @see #setSharpness(float) + * @return sharpness */ - public void setSharpnessValue(float value) { - if (value < 0.0f) - value = 0.0f; - else if (value > 1.0f) - value = 1.0f; + @SuppressWarnings("WeakerAccess") + public float getSharpness() { + return scale; + } - this.scale = value; + @Override + public void setParameter1(float value) { + setSharpness(value); } - public float getSharpnessValue() { - return scale; + @Override + public float getParameter1() { + return getSharpness(); } @NonNull @Override public String getFragmentShader() { + return FRAGMENT_SHADER; + } - String stepsizeXString = "stepsizeX = " + 1.0f / mPreviewingViewWidth + ";\n"; - String stepsizeYString = "stepsizeY = " + 1.0f / mPreviewingViewHeight + ";\n"; - String scaleString = "scale = " + scale + ";\n"; - - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - + "uniform samplerExternalOES sTexture;\n" - + " float scale;\n" - + " float stepsizeX;\n" - + " float stepsizeY;\n" - + "varying vec2 vTextureCoord;\n" - + "void main() {\n" - // Parameters that were created above - + stepsizeXString - + stepsizeYString - + scaleString - + " vec3 nbr_color = vec3(0.0, 0.0, 0.0);\n" - + " vec2 coord;\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" - + " coord.x = vTextureCoord.x - 0.5 * stepsizeX;\n" - + " coord.y = vTextureCoord.y - stepsizeY;\n" - + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" - + " coord.x = vTextureCoord.x - stepsizeX;\n" - + " coord.y = vTextureCoord.y + 0.5 * stepsizeY;\n" - + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" - + " coord.x = vTextureCoord.x + stepsizeX;\n" - + " coord.y = vTextureCoord.y - 0.5 * stepsizeY;\n" - + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" - + " coord.x = vTextureCoord.x + stepsizeX;\n" - + " coord.y = vTextureCoord.y + 0.5 * stepsizeY;\n" - + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" - + " gl_FragColor = vec4(color.rgb - 2.0 * scale * nbr_color, color.a);\n" - + "}\n"; - - return shader; + @Override + public void onCreate(int programHandle) { + super.onCreate(programHandle); + scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale"); + GlUtils.checkLocation(scaleLocation, "scale"); + stepSizeXLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeX"); + GlUtils.checkLocation(stepSizeXLocation, "stepsizeX"); + stepSizeYLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeY"); + GlUtils.checkLocation(stepSizeYLocation, "stepsizeY"); + } + @Override + public void onDestroy() { + super.onDestroy(); + scaleLocation = -1; + stepSizeXLocation = -1; + stepSizeYLocation = -1; } + @Override + protected void onPreDraw(float[] transformMatrix) { + super.onPreDraw(transformMatrix); + GLES20.glUniform1f(scaleLocation, scale); + GlUtils.checkError("glUniform1f"); + GLES20.glUniform1f(stepSizeXLocation, 1.0F / width); + GlUtils.checkError("glUniform1f"); + GLES20.glUniform1f(stepSizeYLocation, 1.0F / height); + GlUtils.checkError("glUniform1f"); + } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/TemperatureFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/TemperatureFilter.java index fbcf5710..b9c7e6df 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/TemperatureFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/TemperatureFilter.java @@ -1,61 +1,102 @@ package com.otaliastudios.cameraview.filters; +import android.opengl.GLES20; + import androidx.annotation.NonNull; +import com.otaliastudios.cameraview.filter.BaseFilter; +import com.otaliastudios.cameraview.filter.OneParameterFilter; +import com.otaliastudios.cameraview.internal.GlUtils; + /** - * Adjusts color temperature of the preview. + * Adjusts color temperature. */ -public class TemperatureFilter extends Filter { - private float scale = 0f; +public class TemperatureFilter 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 scale;\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " vec3 new_color = color.rgb;\n" + + " new_color.r = color.r + color.r * ( 1.0 - color.r) * scale;\n" + + " new_color.b = color.b - color.b * ( 1.0 - color.b) * scale;\n" + + " if (scale > 0.0) { \n" + + " new_color.g = color.g + color.g * ( 1.0 - color.g) * scale * 0.25;\n" + + " }\n" + + " float max_value = max(new_color.r, max(new_color.g, new_color.b));\n" + + " if (max_value > 1.0) { \n" + + " new_color /= max_value;\n" + + " } \n" + + " gl_FragColor = vec4(new_color, color.a);\n" + + "}\n"; + + private float scale = 1F; // -1...1 + private int scaleLocation = -1; + + public TemperatureFilter() { } /** - * Initialize Effect + * Sets the new temperature value: + * -1.0: cool colors + * 0.0: no change + * 1.0: warm colors + * + * @param value new value */ - public TemperatureFilter() { + @SuppressWarnings("WeakerAccess") + public void setTemperature(float value) { + if (value < -1F) value = -1F; + if (value > 1F) value = 1F; + this.scale = value; } /** - * @param scale Float, between 0 and 1, with 0 indicating cool, and 1 - * indicating warm. A value of of 0.5 indicates no change. + * Returns the current temperature. + * + * @see #setTemperature(float) + * @return temperature */ - public void setTemperatureScale(float scale) { - if (scale < 0.0f) - scale = 0.0f; - else if (scale > 1.0f) - scale = 1.0f; - this.scale = scale; + @SuppressWarnings("WeakerAccess") + public float getTemperature() { + return scale; } - public float getTemperatureScale() { - return scale; + @Override + public void setParameter1(float value) { + setTemperature((2F * value - 1F)); + } + + @Override + public float getParameter1() { + return (getTemperature() + 1F) / 2F; } @NonNull @Override public String getFragmentShader() { + return FRAGMENT_SHADER; + } - String scaleString = "scale = " + (2.0f * scale - 1.0f) + ";\n"; - - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - + "uniform samplerExternalOES sTexture;\n" - + " float scale;\n" - + "varying vec2 vTextureCoord;\n" - + "void main() {\n" // Parameters that were created above - + scaleString - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" - + " vec3 new_color = color.rgb;\n" - + " new_color.r = color.r + color.r * ( 1.0 - color.r) * scale;\n" - + " new_color.b = color.b - color.b * ( 1.0 - color.b) * scale;\n" - + " if (scale > 0.0) { \n" - + " new_color.g = color.g + color.g * ( 1.0 - color.g) * scale * 0.25;\n" - + " }\n" - + " float max_value = max(new_color.r, max(new_color.g, new_color.b));\n" - + " if (max_value > 1.0) { \n" - + " new_color /= max_value;\n" + " } \n" - + " gl_FragColor = vec4(new_color, color.a);\n" + "}\n"; - - return shader; + @Override + public void onCreate(int programHandle) { + super.onCreate(programHandle); + scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale"); + GlUtils.checkLocation(scaleLocation, "scale"); + } + @Override + public void onDestroy() { + super.onDestroy(); + scaleLocation = -1; + } + + @Override + protected void onPreDraw(float[] transformMatrix) { + super.onPreDraw(transformMatrix); + GLES20.glUniform1f(scaleLocation, scale); + GlUtils.checkError("glUniform1f"); } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/TintFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/TintFilter.java index a7a9cd08..e5f31769 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/TintFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/TintFilter.java @@ -1,66 +1,101 @@ package com.otaliastudios.cameraview.filters; import android.graphics.Color; +import android.opengl.GLES20; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; +import com.otaliastudios.cameraview.filter.BaseFilter; +import com.otaliastudios.cameraview.filter.OneParameterFilter; +import com.otaliastudios.cameraview.internal.GlUtils; + /** - * Tints the preview with specified color.. + * Tints the frames with specified color. */ -public class TintFilter extends Filter { - private int mTint = 0xFFFF0000; +public class TintFilter 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 vec3 tint;\n" + + "vec3 color_ratio;\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " color_ratio[0] = " + 0.21f + ";\n" + + " color_ratio[1] = " + 0.71f + ";\n" + + " color_ratio[2] = " + 0.07f + ";\n" + + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " float avg_color = dot(color_ratio, color.rgb);\n" + + " vec3 new_color = min(0.8 * avg_color + 0.2 * tint, 1.0);\n" + + " gl_FragColor = vec4(new_color.rgb, color.a);\n" + "}\n"; + + private int tint = Color.RED; + private int tintLocation = -1; + + public TintFilter() { } /** - * Initialize Effect + * Sets the current tint. + * @param color current tint */ - public TintFilter() { + @SuppressWarnings("WeakerAccess") + public void setTint(@ColorInt int color) { + this.tint = color; } - public void setTintColor(int color) { - this.mTint = color; + /** + * Returns the current tint. + * + * @see #setTint(int) + * @return tint + */ + @SuppressWarnings("WeakerAccess") + @ColorInt + public int getTint() { + return tint; + } + + @Override + public void setParameter1(float value) { + // no easy way to transform 0...1 into a color. + setTint((int) (value * Integer.MAX_VALUE)); } - public int getTintColor() { - return mTint; + @Override + public float getParameter1() { + return (float) getTint() / Integer.MAX_VALUE; } @NonNull @Override public String getFragmentShader() { - float color_ratio[] = {0.21f, 0.71f, 0.07f}; - String color_ratioString[] = new String[3]; - color_ratioString[0] = "color_ratio[0] = " + color_ratio[0] + ";\n"; - color_ratioString[1] = "color_ratio[1] = " + color_ratio[1] + ";\n"; - color_ratioString[2] = "color_ratio[2] = " + color_ratio[2] + ";\n"; - - float tint_color[] = {Color.red(mTint) / 255f, - Color.green(mTint) / 255f, Color.blue(mTint) / 255f}; - - String tintString[] = new String[3]; - tintString[0] = "tint[0] = " + tint_color[0] + ";\n"; - tintString[1] = "tint[1] = " + tint_color[1] + ";\n"; - tintString[2] = "tint[2] = " + tint_color[2] + ";\n"; - - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - + "uniform samplerExternalOES sTexture;\n" - + " vec3 tint;\n" - + " vec3 color_ratio;\n" - + "varying vec2 vTextureCoord;\n" - + "void main() {\n" - // Parameters that were created above - + color_ratioString[0] - + color_ratioString[1] - + color_ratioString[2] - + tintString[0] - + tintString[1] - + tintString[2] - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" - + " float avg_color = dot(color_ratio, color.rgb);\n" - + " vec3 new_color = min(0.8 * avg_color + 0.2 * tint, 1.0);\n" - + " gl_FragColor = vec4(new_color.rgb, color.a);\n" + "}\n"; - return shader; + return FRAGMENT_SHADER; + } + + @Override + public void onCreate(int programHandle) { + super.onCreate(programHandle); + tintLocation = GLES20.glGetUniformLocation(programHandle, "tint"); + GlUtils.checkLocation(tintLocation, "tint"); + } + @Override + public void onDestroy() { + super.onDestroy(); + tintLocation = -1; + } + + @Override + protected void onPreDraw(float[] transformMatrix) { + super.onPreDraw(transformMatrix); + float[] channels = new float[]{ + Color.red(tint) / 255f, + Color.green(tint) / 255f, + Color.blue(tint) / 255f + }; + GLES20.glUniform3fv(tintLocation, 1, channels, 0); + GlUtils.checkError("glUniform3fv"); } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/VignetteFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/VignetteFilter.java index 3d515e48..b4b8af3b 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/VignetteFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/VignetteFilter.java @@ -1,99 +1,174 @@ package com.otaliastudios.cameraview.filters; +import android.opengl.GLES20; + import androidx.annotation.NonNull; +import com.otaliastudios.cameraview.filter.BaseFilter; +import com.otaliastudios.cameraview.filter.TwoParameterFilter; +import com.otaliastudios.cameraview.internal.GlUtils; + /** - * Applies lomo-camera style effect to your preview. + * Applies a vignette effect to input frames. */ -public class VignetteFilter extends Filter { - private float mScale = 0.85f; - private float mShade = 0.5f; +public class VignetteFilter extends BaseFilter implements TwoParameterFilter { + + private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "uniform samplerExternalOES sTexture;\n" + + "uniform float range;\n" + + "uniform float inv_max_dist;\n" + + "uniform float shade;\n" + + "uniform vec2 scale;\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " const float slope = 20.0;\n" + + " vec2 coord = vTextureCoord - vec2(0.5, 0.5);\n" + + " float dist = length(coord * scale);\n" + + " float lumen = shade / (1.0 + exp((dist * inv_max_dist - range) * slope)) + (1.0 - shade);\n" + + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " gl_FragColor = vec4(color.rgb * lumen, color.a);\n" + + "}\n"; + + private float mScale = 0.85f; // 0...1 + private float mShade = 0.5f; // 0...1 + private int mWidth = 1; + private int mHeight = 1; + + private int mRangeLocation = -1; + private int mMaxDistLocation = -1; + private int mShadeLocation = -1; + private int mScaleLocation = -1; + + public VignetteFilter() { } + + @Override + public void setSize(int width, int height) { + super.setSize(width, height); + mWidth = width; + mHeight = height; + } /** - * Initialize Effect + * Sets the vignette effect scale (0.0 - 1.0). + * @param scale new scale */ - public VignetteFilter() { + @SuppressWarnings("WeakerAccess") + public void setVignetteScale(float scale) { + if (scale < 0.0f) scale = 0.0f; + if (scale > 1.0f) scale = 1.0f; + mScale = scale; } /** - * setVignetteEffectScale + * Sets the vignette effect shade (0.0 - 1.0). + * @param shade new shade + */ + @SuppressWarnings("WeakerAccess") + public void setVignetteShade(float shade) { + if (shade < 0.0f) shade = 0.0f; + if (shade > 1.0f) shade = 1.0f; + this.mShade = shade; + } + + /** + * Gets the current vignette scale. * - * @param scale Float, between 0.0 and 1. 0 + * @see #setVignetteScale(float) + * @return scale */ - public void setVignetteEffectScale(float scale) { - if (scale < 0.0f) - scale = 0.0f; - else if (scale > 1.0f) - scale = 1.0f; - this.mScale = scale; + @SuppressWarnings("WeakerAccess") + public float getVignetteScale() { + return mScale; } /** - * setVignetteEffectShade + * Gets the current vignette shade. * - * @param shade Float, between 0.0 and 1. 0 + * @see #setVignetteShade(float) + * @return shade */ - public void setVignetteEffectShade(float shade) { - if (shade < 0.0f) - shade = 0.0f; - else if (shade > 1.0f) - shade = 1.0f; - this.mShade = shade; + @SuppressWarnings("WeakerAccess") + public float getVignetteShade() { + return mShade; + } + + + @Override + public void setParameter1(float value) { + setVignetteScale(value); + } + + @Override + public float getParameter1() { + return getVignetteScale(); + } + + @Override + public void setParameter2(float value) { + setVignetteShade(value); + } + + @Override + public float getParameter2() { + return getVignetteShade(); } @NonNull @Override public String getFragmentShader() { - float scale[] = new float[2]; - if (mPreviewingViewWidth > mPreviewingViewHeight) { + return FRAGMENT_SHADER; + } + + @Override + public void onCreate(int programHandle) { + super.onCreate(programHandle); + mRangeLocation = GLES20.glGetUniformLocation(programHandle, "range"); + GlUtils.checkLocation(mRangeLocation, "range"); + mMaxDistLocation = GLES20.glGetUniformLocation(programHandle, "inv_max_dist"); + GlUtils.checkLocation(mMaxDistLocation, "inv_max_dist"); + mShadeLocation = GLES20.glGetUniformLocation(programHandle, "shade"); + GlUtils.checkLocation(mShadeLocation, "shade"); + mScaleLocation = GLES20.glGetUniformLocation(programHandle, "scale"); + GlUtils.checkLocation(mScaleLocation, "scale"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + mRangeLocation = -1; + mMaxDistLocation = -1; + mShadeLocation = -1; + mScaleLocation = -1; + } + + @Override + protected void onPreDraw(float[] transformMatrix) { + super.onPreDraw(transformMatrix); + float[] scale = new float[2]; + if (mWidth > mHeight) { scale[0] = 1f; - scale[1] = ((float) mPreviewingViewHeight) / mPreviewingViewWidth; + scale[1] = ((float) mHeight) / mWidth; } else { - scale[0] = ((float) mPreviewingViewWidth) / mPreviewingViewHeight; + scale[0] = ((float) mWidth) / mHeight; scale[1] = 1f; } - float max_dist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1] - * scale[1])) * 0.5f; + GLES20.glUniform2fv(mScaleLocation, 1, scale, 0); + GlUtils.checkError("glUniform2fv"); - String scaleString[] = new String[2]; + float maxDist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1] * scale[1])) * 0.5f; + GLES20.glUniform1f(mMaxDistLocation, 1F / maxDist); + GlUtils.checkError("glUniform1f"); - scaleString[0] = "scale[0] = " + scale[0] + ";\n"; - scaleString[1] = "scale[1] = " + scale[1] + ";\n"; - String inv_max_distString = "inv_max_dist = " + 1.0f / max_dist + ";\n"; - String shadeString = "shade = " + mShade + ";\n"; + GLES20.glUniform1f(mShadeLocation, mShade); + GlUtils.checkError("glUniform1f"); - // The 'range' is between 1.3 to 0.6. When scale is zero then range is - // 1.3 + // The 'range' is between 1.3 to 0.6. When scale is zero then range is 1.3 // which means no vignette at all because the luminousity difference is // less than 1/256 and will cause nothing. - String rangeString = "range = " - + (1.30f - (float) Math.sqrt(mScale) * 0.7f) + ";\n"; - - String shader = "#extension GL_OES_EGL_image_external : require\n" - + "precision mediump float;\n" - + "uniform samplerExternalOES sTexture;\n" - + " float range;\n" - + " float inv_max_dist;\n" - + " float shade;\n" - + " vec2 scale;\n" - + "varying vec2 vTextureCoord;\n" - + "void main() {\n" - // Parameters that were created above - + scaleString[0] - + scaleString[1] - + inv_max_distString - + shadeString - + rangeString - + " const float slope = 20.0;\n" - + " vec2 coord = vTextureCoord - vec2(0.5, 0.5);\n" - + " float dist = length(coord * scale);\n" - + " float lumen = shade / (1.0 + exp((dist * inv_max_dist - range) * slope)) + (1.0 - shade);\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" - + " gl_FragColor = vec4(color.rgb * lumen, color.a);\n" - + "}\n"; - - return shader; - + float range = (1.30f - (float) Math.sqrt(mScale) * 0.7f); + GLES20.glUniform1f(mRangeLocation, range); + GlUtils.checkError("glUniform1f"); } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglElement.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/GlUtils.java similarity index 74% rename from cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglElement.java rename to cameraview/src/main/java/com/otaliastudios/cameraview/internal/GlUtils.java index 5be000fa..c5af965b 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglElement.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/GlUtils.java @@ -1,27 +1,29 @@ -package com.otaliastudios.cameraview.internal.egl; +package com.otaliastudios.cameraview.internal; import android.opengl.GLES20; import android.opengl.Matrix; +import androidx.annotation.NonNull; + import com.otaliastudios.cameraview.CameraLogger; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; -class EglElement { +public class GlUtils { - private final static CameraLogger LOG = CameraLogger.create(EglElement.class.getSimpleName()); + private final static String TAG = GlUtils.class.getSimpleName(); + private final static CameraLogger LOG = CameraLogger.create(TAG); // Identity matrix for general use. - protected static final float[] IDENTITY_MATRIX = new float[16]; + public static final float[] IDENTITY_MATRIX = new float[16]; static { Matrix.setIdentityM(IDENTITY_MATRIX, 0); } - // Check for GLES errors. - protected static void check(String opName) { + public static void checkError(@NonNull String opName) { int error = GLES20.glGetError(); if (error != GLES20.GL_NO_ERROR) { String message = LOG.e("Error during", opName, "glError 0x", Integer.toHexString(error)); @@ -29,18 +31,17 @@ class EglElement { } } - // Check for valid location. - protected static void checkLocation(int location, String label) { + public static void checkLocation(int location, @NonNull String name) { if (location < 0) { - String message = LOG.e("Unable to locate", label, "in program"); + String message = LOG.e("Unable to locate", name, "in program"); throw new RuntimeException(message); } } - // Compiles the given shader, returns a handle. - protected static int loadShader(int shaderType, String source) { + @SuppressWarnings("WeakerAccess") + public static int loadShader(int shaderType, @NonNull String source) { int shader = GLES20.glCreateShader(shaderType); - check("glCreateShader type=" + shaderType); + checkError("glCreateShader type=" + shaderType); GLES20.glShaderSource(shader, source); GLES20.glCompileShader(shader); int[] compiled = new int[1]; @@ -54,21 +55,21 @@ class EglElement { } // Creates a program with given vertex shader and pixel shader. - protected static int createProgram(String vertexSource, String fragmentSource) { + public static int createProgram(@NonNull String vertexSource, @NonNull String fragmentSource) { int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); if (vertexShader == 0) return 0; int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); if (pixelShader == 0) return 0; int program = GLES20.glCreateProgram(); - check("glCreateProgram"); + checkError("glCreateProgram"); if (program == 0) { LOG.e("Could not create program"); } GLES20.glAttachShader(program, vertexShader); - check("glAttachShader"); + checkError("glAttachShader"); GLES20.glAttachShader(program, pixelShader); - check("glAttachShader"); + checkError("glAttachShader"); GLES20.glLinkProgram(program); int[] linkStatus = new int[1]; GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); @@ -81,7 +82,7 @@ class EglElement { } // Allocates a direct float buffer, and populates it with the float array data. - protected static FloatBuffer floatBuffer(float[] coords) { + public static FloatBuffer floatBuffer(@NonNull float[] coords) { // Allocate a direct ByteBuffer, using 4 bytes per float, and copy coords into it. ByteBuffer bb = ByteBuffer.allocateDirect(coords.length * 4); bb.order(ByteOrder.nativeOrder()); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java index de2919fa..9ec4315a 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java @@ -25,6 +25,7 @@ import androidx.annotation.RequiresApi; import android.util.Log; import com.otaliastudios.cameraview.CameraLogger; +import com.otaliastudios.cameraview.internal.GlUtils; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; @@ -40,7 +41,7 @@ import java.nio.ByteOrder; * There can be multiple surfaces associated with a single context. */ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) -public class EglBaseSurface extends EglElement { +public class EglBaseSurface { protected static final String TAG = EglBaseSurface.class.getSimpleName(); private final static CameraLogger LOG = CameraLogger.create(TAG); @@ -187,7 +188,7 @@ public class EglBaseSurface extends EglElement { buf.order(ByteOrder.LITTLE_ENDIAN); GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf); - check("glReadPixels"); + GlUtils.checkError("glReadPixels"); buf.rewind(); BufferedOutputStream bos = null; @@ -229,7 +230,7 @@ public class EglBaseSurface extends EglElement { ByteBuffer buf = ByteBuffer.allocateDirect(width * height * 4); buf.order(ByteOrder.LITTLE_ENDIAN); GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf); - check("glReadPixels"); + GlUtils.checkError("glReadPixels"); buf.rewind(); ByteArrayOutputStream bos = new ByteArrayOutputStream(buf.array().length); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java index d1be7c14..c7d03a87 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java @@ -7,186 +7,89 @@ import android.opengl.GLES20; import androidx.annotation.NonNull; import com.otaliastudios.cameraview.CameraLogger; -import com.otaliastudios.cameraview.filters.Filter; -import com.otaliastudios.cameraview.filters.NoFilter; +import com.otaliastudios.cameraview.filter.Filter; +import com.otaliastudios.cameraview.filter.NoFilter; +import com.otaliastudios.cameraview.internal.GlUtils; -import java.nio.FloatBuffer; -/** - * This is a mix of 3 grafika classes, FullFrameRect, Texture2dProgram, Drawable2d. - */ -public class EglViewport extends EglElement { +public class EglViewport { private final static CameraLogger LOG = CameraLogger.create(EglViewport.class.getSimpleName()); - // Stuff from Drawable2d.FULL_RECTANGLE - // A full square, extending from -1 to +1 in both dimensions. - // When the model/view/projection matrix is identity, this will exactly cover the viewport. - private static final float[] FULL_RECTANGLE_COORDS = { - -1.0f, -1.0f, // 0 bottom left - 1.0f, -1.0f, // 1 bottom right - -1.0f, 1.0f, // 2 top left - 1.0f, 1.0f, // 3 top right - }; - - // Stuff from Drawable2d.FULL_RECTANGLE - // A full square, extending from -1 to +1 in both dimensions. - private static final float[] FULL_RECTANGLE_TEX_COORDS = { - 0.0f, 0.0f, // 0 bottom left - 1.0f, 0.0f, // 1 bottom right - 0.0f, 1.0f, // 2 top left - 1.0f, 1.0f // 3 top right - }; - - // Stuff from Drawable2d.FULL_RECTANGLE - private final static int VERTEX_COUNT = FULL_RECTANGLE_COORDS.length / 2; - private FloatBuffer mVertexCoordinatesArray = floatBuffer(FULL_RECTANGLE_COORDS); - private FloatBuffer mTextureCoordinatesArray = floatBuffer(FULL_RECTANGLE_TEX_COORDS); - - // Stuff from Texture2dProgram - private int mProgramHandle; + private int mProgramHandle = -1; private int mTextureTarget; private int mTextureUnit; - // Program attributes - private int muMVPMatrixLocation; - private int muTexMatrixLocation; - private int maPositionLocation; - private int maTextureCoordLocation; - - // private int muKernelLoc; // Used for filtering - // private int muTexOffsetLoc; // Used for filtering - // private int muColorAdjustLoc; // Used for filtering - - private Filter mShaderEffect; - - private boolean mIsShaderChanged = false; + private Filter mFilter; + private boolean mFilterChanged = false; public EglViewport() { - mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES; - mTextureUnit = GLES20.GL_TEXTURE0; - - //init the default shader effect - mShaderEffect = new NoFilter(); - initProgram(); + this(new NoFilter()); } - private void initProgram() { - release(); - mProgramHandle = createProgram(mShaderEffect.getVertexShader(), mShaderEffect.getFragmentShader()); - maPositionLocation = GLES20.glGetAttribLocation(mProgramHandle, mShaderEffect.getPositionVariableName()); - checkLocation(maPositionLocation, mShaderEffect.getPositionVariableName()); - maTextureCoordLocation = GLES20.glGetAttribLocation(mProgramHandle, mShaderEffect.getTexttureCoordinateVariableName()); - checkLocation(maTextureCoordLocation, mShaderEffect.getTexttureCoordinateVariableName()); - muMVPMatrixLocation = GLES20.glGetUniformLocation(mProgramHandle, mShaderEffect.getMVPMatrixVariableName()); - checkLocation(muMVPMatrixLocation, mShaderEffect.getMVPMatrixVariableName()); - muTexMatrixLocation = GLES20.glGetUniformLocation(mProgramHandle, mShaderEffect.getTextureMatrixVariableName()); - checkLocation(muTexMatrixLocation, mShaderEffect.getTextureMatrixVariableName()); + public EglViewport(@NonNull Filter filter) { + mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES; + mTextureUnit = GLES20.GL_TEXTURE0; + mFilter = filter; + createProgram(); } - public void release(boolean doEglCleanup) { - if (doEglCleanup) GLES20.glDeleteProgram(mProgramHandle); - mProgramHandle = -1; + private void createProgram() { + release(); // Release old program if present. + mProgramHandle = GlUtils.createProgram(mFilter.getVertexShader(), mFilter.getFragmentShader()); + mFilter.onCreate(mProgramHandle); } public void release() { - release(true); + if (mProgramHandle != -1) { + mFilter.onDestroy(); + GLES20.glDeleteProgram(mProgramHandle); + mProgramHandle = -1; + } } public int createTexture() { int[] textures = new int[1]; GLES20.glGenTextures(1, textures, 0); - check("glGenTextures"); + GlUtils.checkError("glGenTextures"); int texId = textures[0]; GLES20.glActiveTexture(mTextureUnit); GLES20.glBindTexture(mTextureTarget, texId); - check("glBindTexture " + texId); + GlUtils.checkError("glBindTexture " + texId); GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); - check("glTexParameter"); + GlUtils.checkError("glTexParameter"); return texId; } - public void changeShaderFilter(@NonNull Filter shaderEffect){ - this.mShaderEffect = shaderEffect; - mIsShaderChanged = true; + public void setFilter(@NonNull Filter filter){ + this.mFilter = filter; + mFilterChanged = true; } - - public void drawFrame(int textureId, float[] textureMatrix) { - drawFrame(textureId, textureMatrix, - mVertexCoordinatesArray, - mTextureCoordinatesArray); - } - - /** - * The issue with the CIRCLE shader is that if the textureMatrix has a scale value, - * it fails miserably, not taking the scale into account. - * So what we can do here is - * - * - read textureMatrix scaleX and scaleY values. This is pretty much impossible to do from the matrix itself - * without making risky assumptions over the order of operations. - * https://www.opengl.org/discussion_boards/showthread.php/159215-Is-it-possible-to-extract-rotation-translation-scale-given-a-matrix - * So we prefer passing scaleX and scaleY here to the draw function. - * - pass these values to the vertex shader - * - pass them to the fragment shader - * - in the fragment shader, take this scale value into account - */ - private void drawFrame(int textureId, float[] textureMatrix, - FloatBuffer vertexBuffer, - FloatBuffer texBuffer) { - - if (mIsShaderChanged){ - initProgram(); - mIsShaderChanged = false; + if (mFilterChanged) { + createProgram(); + mFilterChanged = false; } - check("draw start"); + GlUtils.checkError("draw start"); - // Select the program. + // Select the program and the active texture. GLES20.glUseProgram(mProgramHandle); - check("glUseProgram"); - - // Set the texture. + GlUtils.checkError("glUseProgram"); GLES20.glActiveTexture(mTextureUnit); GLES20.glBindTexture(mTextureTarget, textureId); - // Copy the model / view / projection matrix over. - GLES20.glUniformMatrix4fv(muMVPMatrixLocation, 1, false, IDENTITY_MATRIX, 0); - check("glUniformMatrix4fv"); - - // Copy the texture transformation matrix over. - GLES20.glUniformMatrix4fv(muTexMatrixLocation, 1, false, textureMatrix, 0); - check("glUniformMatrix4fv"); - - // Enable the "aPosition" vertex attribute. - // Connect vertexBuffer to "aPosition". - GLES20.glEnableVertexAttribArray(maPositionLocation); - check("glEnableVertexAttribArray"); - GLES20.glVertexAttribPointer(maPositionLocation, 2, GLES20.GL_FLOAT, false, 8, vertexBuffer); - check("glVertexAttribPointer"); - - // Enable the "aTextureCoord" vertex attribute. - // Connect texBuffer to "aTextureCoord". - GLES20.glEnableVertexAttribArray(maTextureCoordLocation); - check("glEnableVertexAttribArray"); - GLES20.glVertexAttribPointer(maTextureCoordLocation, 2, GLES20.GL_FLOAT, false, 8, texBuffer); - check("glVertexAttribPointer"); - - - // Draw the rect. - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, VERTEX_COUNT); - check("glDrawArrays"); + // Draw. + mFilter.draw(textureMatrix); - // Done -- disable vertex array, texture, and program. - GLES20.glDisableVertexAttribArray(maPositionLocation); - GLES20.glDisableVertexAttribArray(maTextureCoordLocation); + // Release. GLES20.glBindTexture(mTextureTarget, 0); GLES20.glUseProgram(0); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java index 9e88c955..a9939f0f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java @@ -26,7 +26,7 @@ import com.otaliastudios.cameraview.overlay.OverlayDrawer; import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.preview.RendererFrameCallback; import com.otaliastudios.cameraview.preview.RendererThread; -import com.otaliastudios.cameraview.filters.Filter; +import com.otaliastudios.cameraview.filter.Filter; import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.Size; @@ -97,15 +97,14 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { @RendererThread @Override - public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, final float scaleX, final float scaleY, Filter shaderEffect) { + public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, final float scaleX, final float scaleY) { mPreview.removeRendererFrameCallback(this); - SnapshotGlPictureRecorder.this.onRendererFrame(surfaceTexture, scaleX, scaleY, shaderEffect); + SnapshotGlPictureRecorder.this.onRendererFrame(surfaceTexture, scaleX, scaleY); } @Override - public void - onFilterChanged(@NonNull Filter filter) { - mViewport.changeShaderFilter(filter); + public void onFilterChanged(@NonNull Filter filter) { + mViewport.setFilter(filter.copy()); } }); } @@ -151,7 +150,7 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { */ @RendererThread @TargetApi(Build.VERSION_CODES.KITKAT) - private void onRendererFrame(final @NonNull SurfaceTexture surfaceTexture, final float scaleX, final float scaleY, @NonNull Filter filter) { + private void onRendererFrame(final @NonNull SurfaceTexture surfaceTexture, final float scaleX, final float scaleY) { // Get egl context from the RendererThread, which is the one in which we have created // the textureId and the overlayTextureId, managed by the GlSurfaceView. // Next operations can then be performed on different threads using this handle. diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/FilterCameraPreview.java b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/FilterCameraPreview.java new file mode 100644 index 00000000..a164aa86 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/FilterCameraPreview.java @@ -0,0 +1,39 @@ +package com.otaliastudios.cameraview.preview; + + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; + +import com.otaliastudios.cameraview.filter.Filter; + + +/** + * A preview that support GL filters defined through the {@link Filter} interface. + * + * The preview has the responsibility of calling {@link Filter#setSize(int, int)} + * whenever the preview size changes and as soon as the filter is applied. + */ +public abstract class FilterCameraPreview extends CameraPreview { + + @SuppressWarnings("WeakerAccess") + public FilterCameraPreview(@NonNull Context context, @NonNull ViewGroup parent) { + super(context, parent); + } + + /** + * Sets a new filter. + * @param filter new filter + */ + public abstract void setFilter(@NonNull Filter filter); + + /** + * Returns the currently used filter. + * @return currently used filter + */ + @SuppressWarnings("unused") + @NonNull + public abstract Filter getCurrentFilter(); +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java index 28142c89..d65e23ef 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java @@ -15,8 +15,8 @@ import android.view.ViewGroup; import com.otaliastudios.cameraview.R; import com.otaliastudios.cameraview.internal.egl.EglViewport; import com.otaliastudios.cameraview.internal.utils.Op; -import com.otaliastudios.cameraview.filters.Filter; -import com.otaliastudios.cameraview.filters.NoFilter; +import com.otaliastudios.cameraview.filter.Filter; +import com.otaliastudios.cameraview.filter.NoFilter; import com.otaliastudios.cameraview.size.AspectRatio; import java.util.Collections; @@ -58,7 +58,7 @@ import javax.microedition.khronos.opengles.GL10; * Callbacks are guaranteed to be called on the renderer thread, which means that we can fetch * the GL context that was created and is managed by the {@link GLSurfaceView}. */ -public class GlCameraPreview extends CameraPreview { +public class GlCameraPreview extends FilterCameraPreview { private boolean mDispatched; private final float[] mTransformMatrix = new float[16]; @@ -69,8 +69,7 @@ public class GlCameraPreview extends CameraPreview { private EglCore mEglCore; private EglWindowSurface mWindow; private EglViewport mViewport; - private Pool mFramePool = new Pool<>(100, new Pool.Factory() { + private Pool mFramePool = new Pool<>(Integer.MAX_VALUE, new Pool.Factory() { @Override public Frame create() { return new Frame(); @@ -137,7 +137,7 @@ public class TextureMediaEncoder extends VideoMediaEncoder { if (event.equals(FILTER_EVENT)) { Filter filter = (Filter) data; - mViewport.changeShaderFilter(filter); + mViewport.setFilter(filter); } else if (event.equals(FRAME_EVENT)) { Frame frame = (Frame) data; if (frame == null) { @@ -224,7 +224,7 @@ public class TextureMediaEncoder extends VideoMediaEncoder { mWindow = null; } if (mViewport != null) { - mViewport.release(true); + mViewport.release(); mViewport = null; } if (mEglCore != null) { diff --git a/cameraview/src/main/res/values/attrs.xml b/cameraview/src/main/res/values/attrs.xml index 083105fa..83eb4dfc 100644 --- a/cameraview/src/main/res/values/attrs.xml +++ b/cameraview/src/main/res/values/attrs.xml @@ -131,6 +131,8 @@ + + diff --git a/cameraview/src/main/res/values/strings.xml b/cameraview/src/main/res/values/strings.xml index dd25e92b..297dd303 100644 --- a/cameraview/src/main/res/values/strings.xml +++ b/cameraview/src/main/res/values/strings.xml @@ -1,4 +1,27 @@ com.otaliastudios.cameraview.markers.DefaultAutoFocusMarker + + com.otaliastudios.cameraview.filter.NoFilter + com.otaliastudios.cameraview.filters.AutoFixFilter + com.otaliastudios.cameraview.filters.BlackAndWhiteFilter + com.otaliastudios.cameraview.filters.BrightnessFilter + com.otaliastudios.cameraview.filters.ContrastFilter + com.otaliastudios.cameraview.filters.CrossProcessFilter + com.otaliastudios.cameraview.filters.DocumentaryFilter + com.otaliastudios.cameraview.filters.DuotoneFilter + com.otaliastudios.cameraview.filters.FillLightFilter + com.otaliastudios.cameraview.filters.GammaFilter + com.otaliastudios.cameraview.filters.GrainFilter + com.otaliastudios.cameraview.filters.GrayscaleFilter + com.otaliastudios.cameraview.filters.HueFilter + com.otaliastudios.cameraview.filters.InvertColorsFilter + com.otaliastudios.cameraview.filters.LomoishFilter + com.otaliastudios.cameraview.filters.PosterizeFilter + com.otaliastudios.cameraview.filters.SaturationFilter + com.otaliastudios.cameraview.filters.SepiaFilter + com.otaliastudios.cameraview.filters.SharpnessFilter + com.otaliastudios.cameraview.filters.TemperatureFilter + com.otaliastudios.cameraview.filters.TintFilter + com.otaliastudios.cameraview.filters.VignetteFilter \ No newline at end of file diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java index 9e3880c9..672765a2 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java @@ -27,7 +27,8 @@ import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.controls.Preview; -import com.otaliastudios.cameraview.filters.Filters; +import com.otaliastudios.cameraview.filter.Filters; +import com.otaliastudios.cameraview.filters.BrightnessFilter; import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.FrameProcessor; @@ -36,8 +37,6 @@ import java.io.File; import java.util.Arrays; import java.util.List; -import static com.otaliastudios.cameraview.filters.Filters.*; - public class CameraActivity extends AppCompatActivity implements View.OnClickListener, OptionView.Callback { @@ -49,7 +48,8 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis private ViewGroup controlPanel; private long mCaptureTime; - private Filters mCurrentEffect = NO_FILTER; + private int mCurrentFilter = 0; + private final Filters[] mAllFilters = Filters.values(); @Override protected void onCreate(Bundle savedInstanceState) { @@ -321,84 +321,17 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis } } - private void changeCurrentFilter(){ + private void changeCurrentFilter() { if (camera.getPreview() != Preview.GL_SURFACE) { - message("Filters are supported only for GLSurfaceView", true); + message("Filters are supported only when preview is Preview.GL_SURFACE.", true); return; } - - switch (mCurrentEffect){ - case NO_FILTER: - mCurrentEffect = AUTO_FIX_FILTER; - break; - case AUTO_FIX_FILTER: - mCurrentEffect = BLACK_AND_WHITE_FILTER; - break; - case BLACK_AND_WHITE_FILTER: - mCurrentEffect = BRIGHTNESS_FILTER; - break; - case BRIGHTNESS_FILTER: - mCurrentEffect = CONTRAST_FILTER; - break; - case CONTRAST_FILTER: - mCurrentEffect = CROSS_PROCESS_FILTER; - break; - case CROSS_PROCESS_FILTER: - mCurrentEffect = DOCUMENTARY_FILTER; - break; - case DOCUMENTARY_FILTER: - mCurrentEffect = DUO_TONE_COLOR_FILTER; - break; - case DUO_TONE_COLOR_FILTER: - mCurrentEffect = FILL_LIGHT_FILTER; - break; - case FILL_LIGHT_FILTER: - mCurrentEffect = GAMMA_FILTER; - break; - case GAMMA_FILTER: - mCurrentEffect = GRAIN_FILTER; - break; - case GRAIN_FILTER: - mCurrentEffect = GREY_SCALE_FILTER; - break; - case GREY_SCALE_FILTER: - mCurrentEffect = HUE_FILTER; - break; - case HUE_FILTER: - mCurrentEffect = INVERT_COLOR_FILTER; - break; - case INVERT_COLOR_FILTER: - mCurrentEffect = LAMOISH_FILTER; - break; - case LAMOISH_FILTER: - mCurrentEffect = POSTERIZE_FILTER; - break; - case POSTERIZE_FILTER: - mCurrentEffect = SATURATION_FILTER; - break; - case SATURATION_FILTER: - mCurrentEffect = SEPIA_FILTER; - break; - case SEPIA_FILTER: - mCurrentEffect = SHARPNESS_FILTER; - break; - case SHARPNESS_FILTER: - mCurrentEffect = TEMPERATURE_FILTER; - break; - case TEMPERATURE_FILTER: - mCurrentEffect = TINT_FILTER; - break; - case TINT_FILTER: - mCurrentEffect = VIGNETTE_FILTER; - break; - - case VIGNETTE_FILTER: - default: - mCurrentEffect = NO_FILTER; - break; + if (mCurrentFilter < mAllFilters.length - 1) { + mCurrentFilter++; + } else { + mCurrentFilter = 0; } - - camera.setFilter(mCurrentEffect); + camera.setFilter(mAllFilters[mCurrentFilter].newInstance()); } @Override diff --git a/docs/_posts/2018-12-20-debugging.md b/docs/_posts/2018-12-20-debugging.md index 1ea6f1b3..17382d00 100644 --- a/docs/_posts/2018-12-20-debugging.md +++ b/docs/_posts/2018-12-20-debugging.md @@ -2,7 +2,7 @@ layout: page title: "Debugging" category: docs -order: 13 +order: 14 date: 2018-12-20 20:02:38 disqus: 1 --- diff --git a/docs/_posts/2018-12-20-error-handling.md b/docs/_posts/2018-12-20-error-handling.md index 3a3a1968..069e039e 100644 --- a/docs/_posts/2018-12-20-error-handling.md +++ b/docs/_posts/2018-12-20-error-handling.md @@ -2,7 +2,7 @@ layout: page title: "Error Handling" category: docs -order: 12 +order: 13 date: 2018-12-20 20:02:31 disqus: 1 --- diff --git a/docs/_posts/2018-12-20-more-features.md b/docs/_posts/2018-12-20-more-features.md index 45f1cd67..36cb9eed 100644 --- a/docs/_posts/2018-12-20-more-features.md +++ b/docs/_posts/2018-12-20-more-features.md @@ -4,7 +4,7 @@ title: "More features" subtitle: "Undocumented features & more" description: "Undocumented features & more" category: docs -order: 14 +order: 15 date: 2018-12-20 20:41:20 disqus: 1 --- diff --git a/docs/_posts/2018-12-20-previews.md b/docs/_posts/2018-12-20-previews.md index c346522d..32c9a532 100644 --- a/docs/_posts/2018-12-20-previews.md +++ b/docs/_posts/2018-12-20-previews.md @@ -39,7 +39,7 @@ to use all the features available. However, experienced user might prefer a diff |-------|---------|----| |`Preview.SURFACE`|A `SurfaceView`|Can be good for battery, but will not work well with dynamic layout changes and similar things. No support for video snapshots.| |`Preview.TEXTURE`|A `TextureView`|Better. Requires hardware acceleration. No support for video snapshots.| -|`Preview.GL_SURFACE`|A `GLSurfaceView`|Recommended. Supports video snapshots. Might support GL real time filters in the future.| +|`Preview.GL_SURFACE`|A `GLSurfaceView`|Recommended. Supports video snapshots. Supports [overlays](watermarks-and-overlays.html). Supports [real-time filters](filters.html).| The GL surface, as an extra benefit, has a much more efficient way of capturing picture snapshots, that avoids OOM errors, rotating the image on the fly, reading EXIF, and other horrible things belonging to v1. diff --git a/docs/_posts/2018-12-20-runtime-permissions.md b/docs/_posts/2018-12-20-runtime-permissions.md index 84882504..cd3d7934 100644 --- a/docs/_posts/2018-12-20-runtime-permissions.md +++ b/docs/_posts/2018-12-20-runtime-permissions.md @@ -4,7 +4,7 @@ title: "Runtime Permissions" subtitle: "Permissions and Manifest setup" description: "Permissions and Manifest setup" category: docs -order: 11 +order: 12 date: 2018-12-20 20:03:03 disqus: 1 --- diff --git a/docs/_posts/2019-08-06-filters.md b/docs/_posts/2019-08-06-filters.md new file mode 100644 index 00000000..1d8c6a0c --- /dev/null +++ b/docs/_posts/2019-08-06-filters.md @@ -0,0 +1,102 @@ +--- +layout: page +title: "Real-time Filters" +subtitle: "Apply filters to preview and snapshots" +description: "Apply filters to preview and snapshots" +category: docs +order: 11 +date: 2019-08-06 17:10:17 +disqus: 1 +--- + +Starting from version `2.1.0`, CameraView experimentally supports real-time filters that can modify +the camera frames before they are shown and recorded. Just like [overlays](watermarks-and-overlays.html), +these filters are applied to the preview and to any [picture or video snapshots](capturing-media.html). + +Conditions: + +- you must set the experimental flag: `app:cameraExperimental="true"` +- you must use `Preview.GL_SURFACE` as a preview + +### Simple usage + +```xml + +``` + +Real-time filters are applied at creation time, through the `app:cameraFilter` XML attribute, +or anytime during the camera lifecycle using `cameraView.setFilter()`. + +We offers a reasonable amount of filters through the `Filters` class, for example: + +```java +cameraView.setFilter(Filters.BLACK_AND_WHITE.newInstance()); +cameraView.setFilter(Filters.VIGNETTE.newInstance()); +cameraView.setFilter(Filters.SEPIA.newInstance()); +``` + +All of the filters stored in the `Filters` class have an XML string resource that you can use +to quickly setup the camera view. For example, `Filters.BLACK_AND_WHITE` can be set by using +`app:cameraFilter="@string/cameraview_filter_black_and_white`. + +The default filter is called `NoFilter` (`Filters.NONE`) and can be used to clear up any other +filter that was previously set and return back to normal. + +|Filter class|Filters value|XML resource value| +|------------|-------------|---------------------| +|`NoFilter`|`Filters.NONE`|`@string/cameraview_filter_none`| +|`AutoFixFilter`|`Filters.AUTO_FIX`|`@string/cameraview_filter_autofix`| +|`BlackAndWhiteFilter`|`Filters.BLACK_AND_WHITE`|`@string/cameraview_filter_black_and_white`| +|`BrightnessFilter`|`Filters.BRIGHTNESS`|`@string/cameraview_filter_brightness`| +|`ContrastFilter`|`Filters.CONTRAST`|`@string/cameraview_filter_contrast`| +|`CrossProcessFilter`|`Filters.CROSS_PROCESS`|`@string/cameraview_filter_cross_process`| +|`DocumentaryFilter`|`Filters.DOCUMENTARY`|`@string/cameraview_filter_documentary`| +|`DuotoneFilter`|`Filters.DUOTONE`|`@string/cameraview_filter_duotone`| +|`FillLightFilter`|`Filters.FILL_LIGHT`|`@string/cameraview_filter_fill_light`| +|`GammaFilter`|`Filters.GAMMA`|`@string/cameraview_filter_gamma`| +|`GrainFilter`|`Filters.GRAIN`|`@string/cameraview_filter_grain`| +|`GrayscaleFilter`|`Filters.GRAYSCALE`|`@string/cameraview_filter_grayscale`| +|`HueFilter`|`Filters.HUE`|`@string/cameraview_filter_hue`| +|`InvertColorsFilter`|`Filters.INVERT_COLORS`|`@string/cameraview_filter_invert_colors`| +|`LomoishFilter`|`Filters.LOMOISH`|`@string/cameraview_filter_lomoish`| +|`PosterizeFilter`|`Filters.POSTERIZE`|`@string/cameraview_filter_posterize`| +|`SaturationFilter`|`Filters.SATURATION`|`@string/cameraview_filter_saturation`| +|`SepiaFilter`|`Filters.SEPIA`|`@string/cameraview_filter_sepia`| +|`SharpnessFilter`|`Filters.SHARPNESS`|`@string/cameraview_filter_sharpness`| +|`TemperatureFilter`|`Filters.TEMPERATURE`|`@string/cameraview_filter_temperature`| +|`TintFilter`|`Filters.TINT`|`@string/cameraview_filter_tint`| +|`VignetteFilter`|`Filters.VIGNETTE`|`@string/cameraview_filter_vignette`| + +Most of these filters accept input parameters to tune them. For example, `DuotoneFilter` will +accept two colors to apply the duotone effect. + +```java +duotoneFilter.setFirstColor(Color.RED); +duotoneFilter.setSecondColor(Color.GREEN); +``` + +You can change these values by acting on the filter object, before or after passing it to `CameraView`. +Whenever something is changed, the updated values will be visible immediately in the next frame. + +### Advanced usage + +Advanced users with OpenGL experience can create their own filters by implementing the `Filter` interface +and passing in a fragment shader and a vertex shader that will be used for drawing. + +We recommend: + +- Subclassing `BaseFilter` instead of implementing `Filter`, since that takes care of most of the work +- If accepting parameters, implementing `OneParameterFilter` or `TwoParameterFilter` as well + +Most of all, the best way of learning is by looking at the current filters implementations in the +`com.otaliastudios.cameraview.filters` package. + +### Related APIs + +|Method|Description| +|------|-----------| +|`setFilter(Filter)`|Sets a new real-time filter.| +|`getFilter()`|Returns the current real-time filter.| \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 4081cf1d..6621266e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,6 +14,7 @@ addressing most of the common issues and needs, and still leaving you with flexi - Fast & reliable - Gestures support [[docs]](docs/gestures.html) +- Real-time filters [[docs]](docs/filters.html) - Camera1 or Camera2 powered engine [[docs]](docs/previews.html) - Frame processing support [[docs]](docs/frame-processing.html) - Watermarks & animated overlays [[docs]](docs/watermarks-and-overlays.html)