Last active
September 1, 2023 17:41
-
-
Save TheDreamsWind/5049f1db908580b594f642f3f73836b5 to your computer and use it in GitHub Desktop.
[SO-a/53732051/5690248] GLSurfaceView.Renderer with functionality of subtracting colors via Android ColorFilters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package the.dreams.wind.blendingfilter; | |
import android.content.Context; | |
import android.graphics.Bitmap; | |
import android.opengl.GLES20; | |
import android.opengl.GLException; | |
import android.opengl.GLSurfaceView; | |
import android.opengl.GLUtils; | |
import android.support.annotation.NonNull; | |
import android.view.WindowManager; | |
import java.lang.ref.WeakReference; | |
import java.nio.ByteBuffer; | |
import java.nio.ByteOrder; | |
import java.nio.FloatBuffer; | |
import java.util.Objects; | |
import javax.microedition.khronos.egl.EGLConfig; | |
import javax.microedition.khronos.opengles.GL10; | |
class BlendingFilterRenderer implements GLSurfaceView.Renderer { | |
@NonNull | |
private final Bitmap mBitmap; | |
@NonNull | |
private final WeakReference<GLSurfaceView> mHostViewReference; | |
@NonNull | |
private final float[] mColorFilter; | |
@NonNull | |
private final BlendingFilterUtil.Callback mCallback; | |
private boolean mFinished = false; | |
// ========================================== // | |
// Lifecycle | |
// ========================================== // | |
BlendingFilterRenderer(@NonNull GLSurfaceView hostView, @NonNull Bitmap bitmap, | |
@NonNull float[] colorFilter, | |
@NonNull BlendingFilterUtil.Callback callback) | |
throws IllegalArgumentException { | |
if (colorFilter.length != 4 * 5) { | |
throw new IllegalArgumentException("Color filter should be a 4 x 5 matrix"); | |
} | |
mBitmap = bitmap; | |
mHostViewReference = new WeakReference<>(hostView); | |
mColorFilter = colorFilter; | |
mCallback = callback; | |
} | |
// ========================================== // | |
// GLSurfaceView.Renderer | |
// ========================================== // | |
@Override | |
public void onSurfaceCreated(GL10 gl, EGLConfig config) { | |
GLES20.glEnable(GLES20.GL_BLEND); | |
GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ZERO); | |
} | |
@Override | |
public void onSurfaceChanged(GL10 gl, int width, int height) { | |
GLES20.glViewport(0, 0, width, height); | |
final int program = loadProgram(); | |
GLES20.glUseProgram(program); | |
initVertices(program); | |
attachTexture(program); | |
attachColorFilter(program); | |
} | |
@Override | |
public void onDrawFrame(GL10 gl) { | |
if (mFinished) { | |
return; | |
} | |
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); | |
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); | |
postResult(); | |
} | |
// ========================================== // | |
// Private | |
// ========================================== // | |
private int loadShader(int type, String shaderCode) throws GLException { | |
int reference = GLES20.glCreateShader(type); | |
GLES20.glShaderSource(reference, shaderCode); | |
GLES20.glCompileShader(reference); | |
int[] compileStatus = new int[1]; | |
GLES20.glGetShaderiv(reference, GLES20.GL_COMPILE_STATUS, compileStatus, 0); | |
if (compileStatus[0] != GLES20.GL_TRUE) { | |
GLES20.glDeleteShader(reference); | |
final String message = GLES20.glGetShaderInfoLog(reference); | |
throw new GLException(compileStatus[0], message); | |
} | |
return reference; | |
} | |
private int loadProgram() { | |
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, "precision mediump float;" + | |
"struct ColorFilter {" + | |
" mat4 factor;" + | |
" vec4 shift;" + | |
"};" + | |
"uniform sampler2D uSampler;" + | |
"uniform ColorFilter uColorFilter;" + | |
"varying vec2 vTextureCoord;" + | |
"void main() {" + | |
" vec4 originalColor = texture2D(uSampler, vTextureCoord);" + | |
" originalColor.rgb *= originalColor.a;" + | |
" vec4 filteredColor = (originalColor * uColorFilter.factor) + uColorFilter.shift;" + | |
" filteredColor.rgb *= filteredColor.a;" + | |
" gl_FragColor = vec4(originalColor.rgb - filteredColor.rgb, originalColor.a);" + | |
"}"); | |
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, "attribute vec2 aPosition;" + | |
"attribute vec2 aTextureCoord;" + | |
"varying vec2 vTextureCoord;" + | |
"void main() {" + | |
" gl_Position = vec4(aPosition.x, aPosition.y, 0.0, 1.0);" + | |
" vTextureCoord = aTextureCoord;"+ | |
"}"); | |
int programReference = GLES20.glCreateProgram(); | |
GLES20.glAttachShader(programReference, vertexShader); | |
GLES20.glAttachShader(programReference, fragmentShader); | |
GLES20.glLinkProgram(programReference); | |
return programReference; | |
} | |
private FloatBuffer convertToBuffer(float[] array) { | |
final ByteBuffer buffer = ByteBuffer.allocateDirect(array.length * PrimitiveSizes.FLOAT); | |
FloatBuffer output = buffer.order(ByteOrder.nativeOrder()).asFloatBuffer(); | |
output.put(array); | |
output.position(0); | |
return output; | |
} | |
private void initVertices(int programReference) { | |
// for GL the texture coordinates are flipped vertically, but it doesn't matter for the | |
// purpose of this utility, as we should return the image back to the android coordinate | |
// system in the end | |
createVerticesBuffer(new float[] { | |
//NDCS coords //UV map | |
-1, 1, 0, 1, | |
-1, -1, 0, 0, | |
1, 1, 1, 1, | |
1, -1, 1, 0 | |
}); | |
final int stride = 4 * PrimitiveSizes.FLOAT; | |
enableVertexAttribute(programReference, "aPosition", 2, stride, 0); | |
enableVertexAttribute(programReference, "aTextureCoord", 2, stride, | |
2 * PrimitiveSizes.FLOAT); | |
} | |
private void attachTexture(int programReference) { | |
final int[] textures = new int[1]; | |
GLES20.glGenTextures(1, textures, 0); | |
final int textureId = textures[0]; | |
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); | |
GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1); | |
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, | |
GLES20.GL_NEAREST); | |
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, | |
GLES20.GL_NEAREST); | |
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT); | |
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT); | |
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0); | |
GLES20.glActiveTexture(GLES20.GL_TEXTURE0); | |
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); | |
final int samplerLocation = GLES20.glGetUniformLocation(programReference, "uSampler"); | |
GLES20.glUniform1i(samplerLocation, 0); | |
} | |
private void attachColorFilter(int program) { | |
final float[] colorFilterFactor = new float[4 * 4]; | |
final float[] colorFilterShift = new float[4]; | |
for (int i = 0; i < mColorFilter.length; i++) { | |
final float value = mColorFilter[i]; | |
final int calculateIndex = i + 1; | |
if (calculateIndex % 5 == 0) { | |
colorFilterShift[calculateIndex / 5 - 1] = value / 255; | |
} else { | |
colorFilterFactor[i - calculateIndex / 5] = value; | |
} | |
} | |
final int colorFactorLocation = GLES20.glGetUniformLocation(program, | |
"uColorFilter.factor"); | |
GLES20.glUniformMatrix4fv( | |
colorFactorLocation, 1, false, colorFilterFactor, 0 | |
); | |
final int colorShiftLocation = GLES20.glGetUniformLocation(program, | |
"uColorFilter.shift"); | |
GLES20.glUniform4fv( | |
colorShiftLocation, 1, colorFilterShift, 0 | |
); | |
} | |
private void createVerticesBuffer(float[] verticesData) { | |
final int verticesBuffer = createGlBuffer(); | |
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, verticesBuffer); | |
GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, verticesData.length * PrimitiveSizes.FLOAT, | |
convertToBuffer(verticesData), GLES20.GL_STREAM_DRAW); | |
} | |
private int createGlBuffer() { | |
int buffers[] = new int[1]; | |
GLES20.glGenBuffers(1, buffers, 0); | |
return buffers[0]; | |
} | |
@SuppressWarnings("SameParameterValue") | |
private void enableVertexAttribute(int program, String attributeName, int size, int stride, | |
int offset) { | |
final int attributeLocation = GLES20.glGetAttribLocation(program, attributeName); | |
GLES20.glVertexAttribPointer(attributeLocation, size, GLES20.GL_FLOAT, false, | |
stride, offset); | |
GLES20.glEnableVertexAttribArray(attributeLocation); | |
} | |
private Bitmap retrieveBitmapFromGl(int width, int height) { | |
final ByteBuffer pixelBuffer = ByteBuffer.allocateDirect(width * height * | |
PrimitiveSizes.FLOAT); | |
pixelBuffer.order(ByteOrder.LITTLE_ENDIAN); | |
GLES20.glReadPixels(0,0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, | |
pixelBuffer); | |
final Bitmap image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); | |
image.copyPixelsFromBuffer(pixelBuffer); | |
return image; | |
} | |
private GLException getGlError() { | |
int errorValue = GLES20.glGetError(); | |
switch (errorValue) { | |
case GLES20.GL_NO_ERROR: | |
return null; | |
default: | |
return new GLException(errorValue); | |
} | |
} | |
private void postResult() { | |
final GLSurfaceView hostView = mHostViewReference.get(); | |
if (hostView == null) { | |
return; | |
} | |
GLException glError = getGlError(); | |
if (glError != null) { | |
hostView.post(() -> { | |
mCallback.onFailure(glError); | |
removeHostView(hostView); | |
}); | |
} else { | |
final Bitmap result = retrieveBitmapFromGl(mBitmap.getWidth(), mBitmap.getHeight()); | |
hostView.post(() -> { | |
mCallback.onSuccess(result); | |
removeHostView(hostView); | |
}); | |
} | |
mFinished = true; | |
} | |
private void removeHostView(@NonNull GLSurfaceView hostView) { | |
if (hostView.getParent() == null) { | |
return; | |
} | |
final WindowManager windowManager = (WindowManager) hostView.getContext() | |
.getSystemService(Context.WINDOW_SERVICE); | |
Objects.requireNonNull(windowManager).removeView(hostView); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package the.dreams.wind.blendingfilter; | |
import android.app.Activity; | |
import android.app.ActivityManager; | |
import android.content.Context; | |
import android.content.pm.ConfigurationInfo; | |
import android.graphics.Bitmap; | |
import android.graphics.PixelFormat; | |
import android.opengl.GLSurfaceView; | |
import android.support.annotation.NonNull; | |
import android.support.annotation.Nullable; | |
import android.view.WindowManager; | |
import java.util.Objects; | |
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; | |
final class BlendingFilterUtil { | |
interface Callback { | |
void onSuccess(@NonNull Bitmap blendedImage); | |
void onFailure(@Nullable Exception error); | |
} | |
// ========================================== // | |
// Actions | |
// ========================================== // | |
static void subtractMatrixColorFilter(@NonNull Bitmap image, @Nullable float[] filterValues, | |
@NonNull Activity activityContext, @NonNull Callback callback) { | |
// If no filter is provided, there is no reason to proceed, just return original image | |
if (filterValues == null) { | |
callback.onSuccess(image); | |
return; | |
} | |
final Context appContext = activityContext.getApplicationContext(); | |
if (!isGl2Supported(appContext)) { | |
callback.onFailure(new UnsupportedOperationException( | |
"The devices doesn't support GLES 2 configuration" | |
)); | |
return; | |
} | |
GLSurfaceView hostView = new GLSurfaceView(activityContext); | |
hostView.setEGLContextClientVersion(2); | |
// Make GLView background transparent | |
hostView.setEGLConfigChooser(8, 8, 8, 8, 0, 0); | |
// And set renderer | |
hostView.setRenderer(new BlendingFilterRenderer(hostView, image, filterValues, callback)); | |
// We don't need continuous rendering, so set render mode to DIRTY | |
hostView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); | |
attachGlView(hostView, image.getWidth(), image.getHeight()); | |
} | |
// ========================================== // | |
// Private | |
// ========================================== // | |
private static boolean isGl2Supported(@NonNull Context context) { | |
int supportedGlVersion = getVersionFromActivityManager(context); | |
return supportedGlVersion >= 0x00020000; | |
} | |
private static void attachGlView(GLSurfaceView view, int width, int height) { | |
// View should be of bitmap size | |
final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams( | |
width, height, TYPE_APPLICATION, 0, PixelFormat.OPAQUE); | |
view.setLayoutParams(layoutParams); | |
final WindowManager windowManager = (WindowManager) view.getContext() | |
.getSystemService(Context.WINDOW_SERVICE); | |
Objects.requireNonNull(windowManager).addView(view, layoutParams); | |
} | |
private static int getVersionFromActivityManager(Context context) { | |
final ActivityManager activityManager = | |
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); | |
ConfigurationInfo configInfo = | |
Objects.requireNonNull(activityManager).getDeviceConfigurationInfo(); | |
if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) { | |
return configInfo.reqGlEsVersion; | |
} else { | |
return 1 << 16; // Lack of property means OpenGL ES version 1 | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package the.dreams.wind.blendingfilter; | |
class PrimitiveSizes { | |
@SuppressWarnings("WeakerAccess") | |
static final int BYTE = 1; | |
static final int FLOAT = 4 * BYTE; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment