Skip to content

Instantly share code, notes, and snippets.

@TheDreamsWind
Last active September 1, 2023 17:41
Show Gist options
  • Save TheDreamsWind/5049f1db908580b594f642f3f73836b5 to your computer and use it in GitHub Desktop.
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
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);
}
}
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
}
}
}
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