programing

Android에서 ColorMatrixFilter를 사용하여 블렌드 모드를 빼시겠습니까?

firstcheck 2021. 1. 16. 09:54
반응형

Android에서 ColorMatrixFilter를 사용하여 블렌드 모드를 빼시겠습니까?


다음 ColorMatrixFilter가 있습니다. 하지만 직접 사용하는 대신 Subtract-Blend 모드의 마스크로 사용하고 싶습니다. 이를 달성하려면 어떻게해야합니까?

ColorMatrix :

colorMatrix[
        0.393, 0.7689999, 0.18899999, 0, 0,
        0.349, 0.6859999, 0.16799999, 0, 0,
        0.272, 0.5339999, 0.13099999, 0, 0,
        0,     0,         0,          1, 0
    ];

짧은 이야기

Android에서는 기본적으로 빼기 블렌딩이 없습니다. 그러나 OpenGL을 사용하여 원하는 색상 혼합을 얻을 수 있습니다. 다음 과 같이 사용할 수있는 요점은 다음 과 같습니다.

BlendingFilterUtil.subtractMatrixColorFilter(bitmap, new float[]{
   0.393f, 0.7689999f, 0.18899999f, 0, 0,
   0.349f, 0.6859999f, 0.16799999f, 0, 0,
   0.272f, 0.5339999f, 0.13099999f, 0, 0,
   0,      0,          0,           1, 0
}, activity, callback);

이론

솔직히 말해서이 질문은 다소 혼란스러워 보입니다. 이를 분류하기 위해 Android의 색상 혼합색상 필터링 이라는 두 가지 고유 한 기능 집합을 정의 해 보겠습니다 .

색상 혼합

색상 혼합은 디자이너와 그래픽 작업을하는 사람들 사이에서 꽤 잘 알려져 있습니다. 제목에서 알 수 있듯이 채널 값 (Red, Green, Blue 및 Alpha라고 함)과 혼합 기능을 사용하여 두 가지 색상을 혼합합니다. 이러한 기능을 블렌드 모드라고합니다. 이 모드 중 하나는 빼기 라고 합니다. 빼기 혼합 모드는 다음 공식을 사용하여 출력 색상을 얻습니다.

블렌드 모드 빼기

여기서 (COUT)를이 결과 색상이다 Cdst는 "현재"색이며 CSRC는 원래 색을 변경하기 위해 사용되는 색상 값이다. 채널 차이가 음수이면 0 값이 적용됩니다. 짐작할 수 있듯이 Subtract 블렌딩의 결과는 채널이 0에 가까워지기 때문에 원본 이미지보다 어두워지는 경향이 있습니다. 나는에서 예를 찾아 이 페이지 빼기 효과를 보여 아주 명확 :

목적지

목적지

출처

출처

출력 빼기

산출

색상 필터링

Android의 경우 색상 필터링은 색상 혼합에 비해 일종의 수퍼 세트입니다. 전체 목록은 ColorFilter하위 클래스 설명을 참조하십시오 . 문서에서 볼 수 있듯이 다음 세 가지 구현을 사용할 수 있습니다 ColorFilter.

  • PorterDuffColorFilter 본질적으로 위에서 논의한 블렌드 모드입니다.
  • LightingColorFilter매우 간단합니다. 두 개의 매개 변수로 구성되며, 그중 하나는 요인으로 사용되고 다른 하나는 빨간색, 녹색 및 파란색 채널에 대한 추가로 사용됩니다. 알파 채널은 그대로 유지됩니다. 따라서 일부 이미지를 더 밝게 만들 수 있습니다 (또는 요소가 0과 1 사이이거나 더하기가 음수이면 더 어둡게).
  • ColorMatrixColorFilter더 멋진 것입니다. 이 필터는 ColorMatrix. a ColorMatrixColorFiltera 와 비슷하지만 LightingColorFilter원래 색상에 대해 약간의 수학을 수행하고 사용되는 매개 변수를 구성하지만 훨씬 더 강력합니다. ColorMatrix실제로 작동하는 방법에 대해 자세히 알아 보려면 설명서를 참조하십시오 .

    비트 맵의 ​​색상 및 알파 구성 요소를 변환하기위한 4x5 매트릭스입니다. 행렬은 단일 배열로 전달 될 수 있으며 다음과 같이 처리됩니다.

    [ a, b, c, d, e,
      f, g, h, i, j,
      k, l, m, n, o,
      p, q, r, s, t ]

    색상 [R, G, B, A]에 적용하면 결과 색상은 다음과 같이 계산됩니다.

    R’ = a*R + b*G + c*B + d*A + e;
    G’ = f*R + g*G + h*B + i*A + j; 
    B’ = k*R + l*G + m*B + n*A + o;
    A’ = p*R + q*G + r*B + s*A + t;

다음은 OP의 게시물에 지정된 필터를 사용한 샘플 이미지의 모습입니다.

필터링 된 이미지

목표

이제 실제 목표를 정의해야하는 지점에 도달했습니다. 나는 그의 질문에서 OP가 (이 매트릭스를 활용하는 다른 방법이 없기 때문에) 정확히 말하고 있다고 생각ColorMatrixColorFilter 합니다. 위의 설명에서 알 수 있듯이 Subtract Blend Mode는 두 가지 색상을 사용하고 Color Matrix 컬러 필터는 색상과 해당 색상을 변경하는 매트릭스를 사용합니다. 이들은 두 가지 다른 함수이며 다른 유형의 인수를 사용합니다. 그들이 어떻게 결합 될 수 있는지 생각할 수있는 유일한 방법은 원래 색상 ( Cdst ) 을 가져 ColorMatrix와서 먼저 적용 하고 ( 필터 함수) 원래 색상에서이 연산의 결과를 빼는 것이므로 다음 공식으로 끝납니다.

필터 공식 빼기

문제

위의 작업은 그다지 어렵지 않습니다. a ColorMatrixColorFilter를 사용한 다음 PorterDuffColorFilter필터링 된 결과를 소스 이미지로 사용하여 빼기 모드와 함께 사용할 수 있습니다 . 그러나 PorterDuff.Mode참조를 자세히 살펴보면 Android에는 기능에 빼기 혼합 모드가 없다는 것을 알 수 있습니다. Android OS는 캔버스 그리기를 위해 Google의 Skia 라이브러리를 사용 하며 어떤 이유로 든 Subtract 모드가 부족 하므로 다른 방법으로 빼기를해야합니다. Open GL에서 이러한 것은 비교적 간단합니다. 주된 과제는 Open GL 환경을 설정하여 필요한 방식으로 필요한 것을 그릴 수 있도록하는 것입니다.


해결책

저는 우리가 모든 노력을 스스로하도록 만들고 싶지 않습니다. Android는 이미 GLSurfaceViewOpen GL 컨텍스트를 설정하고 필요한 모든 기능을 제공하지만이 뷰를 View 계층 구조에 추가 할 때까지 작동하지 않으므로 내 계획은를 인스턴스화하여 GLSurfaceView애플리케이션 창에 연결하는 것입니다. 효과를 적용하고 멋진 작업을 모두 수행 할 비트 맵을 제공합니다. OpenGL 자체에 대한 자세한 내용은 질문과 직접 ​​관련이 없기 때문에 자세히 설명하지 않겠습니다.하지만 명확히해야 할 사항이 있으면 의견을 남겨주세요.

첨가 GLSurfaceView

먼저 GLSurfaceView목표 매개 변수에 필요한 모든 인스턴스를 만들고 설정해 보겠습니다 .

GLSurfaceView hostView = new GLSurfaceView(activityContext);
hostView.setEGLContextClientVersion(2);
hostView.setEGLConfigChooser(8, 8, 8, 8, 0, 0);

그런 다음 뷰 계층 구조에이 뷰를 추가하여 드로잉주기를 실행해야합니다.

// 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);

이 GL 뷰를 루트 창에 추가 했으므로 앱의 모든 활동에서 호출 할 수 있습니다. 레이아웃 widthheight매개 변수는 처리하려는 비트 맵 width일치해야 height합니다.

렌더러 추가

GLSurfaceView그 자체로는 아무것도 그리지 않습니다. 이 작업은 Renderer학급에서 수행해야합니다 . 몇 개의 필드가있는 클래스를 정의 해 보겠습니다.

class BlendingFilterRenderer implements GLSurfaceView.Renderer {
    private final Bitmap mBitmap;
    private final WeakReference<GLSurfaceView> mHostViewReference;
    private final float[] mColorFilter;
    private final BlendingFilterUtil.Callback mCallback;
    private boolean mFinished = false;

    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) {}

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {}

    @Override
    public void onDrawFrame(GL10 gl) {}
}

렌더러는 Bitmap변경 될 것임을 유지해야합니다 . 실제 ColorMatrix인스턴스 대신 일반 float[]자바 배열을 사용할 것입니다. 결국 Android 기능을 사용하여이 효과를 적용하지 않고이 클래스가 필요하지 않기 때문입니다. 또한 GLSurfaceView작업이 완료되면 응용 프로그램 창에서 제거 할 수 있도록에 대한 참조를 유지해야합니다 . 마지막으로 중요한 것은 콜백입니다. 의 모든 그리기 GLSurfaceView는 별도의 스레드에서 발생하므로이 작업을 동 기적으로 수행 할 수 없으며 결과를 반환하기 위해 콜백이 필요합니다. 다음과 같이 콜백 인터페이스를 정의했습니다.

interface Callback {
    void onSuccess(@NonNull Bitmap blendedImage);
    void onFailure(@Nullable Exception error);
}

따라서 성공적인 결과 또는 선택적 오류를 반환합니다. mFinished결과를 게시 할 때 추가 작업을 방지하기 위해 맨 끝에 플래그가 필요합니다. 렌더러가 정의 된 후 GLSurfaceView설정으로 돌아가서 렌더러 인스턴스를 설정합니다. RENDERMODE_WHEN_DIRTY초당 60 회 그리기를 방지 하려면 렌더링 모드를 설정하는 것이 좋습니다 .

hostView.setRenderer(new BlendingFilterRenderer(hostView, image, filterValues, callback));
hostView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

메쉬 그리기

아직 OpenGL 표면에 비트 맵을 그릴 수 없습니다. 먼저 텍스처의 표면이되는 메시를 그려야합니다. 이를 위해서는 GPU에서 실행되는 작은 프로그램, 메시 형태와 위치를 정의하는 프로그램 (정점 셰이더), 출력 색상을 결정하는 프로그램 (조각 셰이더) 등 셰이더를 정의해야합니다. 두 셰이더가 모두 컴파일 될 때 프로그램에 연결되어야합니다. 글쎄, 충분한 이론. 먼저 렌더러 클래스에서 다음 메서드를 정의하고 셰이더 프로그램을 만드는 데 사용합니다.

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;
}

이 메서드의 첫 번째 속성은 셰이더 유형 (Vertex 또는 Fragment)을 정의하고 두 번째 속성은 실제 코드를 정의합니다. Vertex 쉐이더는 다음과 같습니다.

attribute vec2 aPosition;
void main() {
  gl_Position = vec4(aPosition.x, aPosition.y, 0.0, 1.0);
}

aPosition속성은 정규화 된 좌표계 ​​(x 및 y 좌표는 -1에서 1까지 임)에서 x 및 y 좌표를 취하여 전역 gl_Position변수에 전달합니다 .

그리고 여기 우리의 조각 쉐이더 :

precision mediump float;
void main() {
  gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}

OpenGL 버전 2에서는 float 정밀도를 명시 적으로 지정해야합니다. 그렇지 않으면이 프로그램이 컴파일되지 않습니다. 이 셰이더는 gl_FragColor출력 색상을 정의하는 전역 변수에도 씁니다 (실제 마술이 일어나는 곳입니다). 이제이 셰이더를 컴파일하고 프로그램에 연결해야합니다.

private int loadProgram() {
    int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, "precision mediump float;" +
            "void main() {" +
            "  gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);" +
            "}");
    int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, "attribute vec2 aPosition;" +
            "void main() {" +
            "  gl_Position = vec4(aPosition.x, aPosition.y, 0.0, 1.0);" +
            "}");
    int programReference = GLES20.glCreateProgram();
    GLES20.glAttachShader(programReference, vertexShader);
    GLES20.glAttachShader(programReference, fragmentShader);
    GLES20.glLinkProgram(programReference);
    return programReference;
}

이제이 프로그램은 정점을 취할 준비가되었습니다. 전달하기 위해 다음 도우미 메서드를 사용합니다.

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);
}

모든 표면을 덮기 위해 메쉬가 필요하므로 GLSurfaceSize정규화 된 장치 좌표계 (NDCS)에서는 매우 간단합니다. 전체 표면 좌표는 x 및 y 좌표 모두에 대해 -1에서 1까지의 범위로 참조 될 수 있습니다. 여기에 좌표가 있습니다.

new float[] {
  -1, 1,
  -1, -1,
  1,  1,
  1,  -1,
}

불행히도 OpenGL에는 삼각형, 선 및 점의 세 가지 유형의 기본 요소 만 존재하므로 상자를 그리는 것만이 불가능합니다. 두 개의 직각 삼각형은 전체 표면을 덮는 직사각형을 만드는 데 충분합니다. 먼저 정점을 배열 버퍼에로드하여 셰이더에서 액세스 할 수 있도록하겠습니다.

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) {
    final float[] verticesData = new float[] {
            -1, 1,
            -1, -1,
            1,  1,
            1,  -1,
    }
    int buffers[] = new int[1];
    GLES20.glGenBuffers(1, buffers, 0);
    GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, buffers[0]);
    GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, verticesData.length * 4, convertToBuffer(verticesData), GLES20.GL_STREAM_DRAW);
    enableVertexAttribute(programReference, "aPosition", 2, 0, 0);
}

Renderer 인터페이스 함수에 모든 것을 합쳐 보겠습니다.

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {}

@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);
}

@Override
public void onDrawFrame(GL10 gl) {
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
}

지금 프로그램을 실행하면 검은 색 대신 흰색 표면이 보일 것입니다. 우리는 이제 거의 중간 지점에 있습니다.

비트 맵 그리기

이제 셰이더 프로그램에 전달하고 메시 (삼각형) 위에 그려야합니다. 텍스처 (이 경우 비트 맵) 자체와는 별도로 텍스처 좌표를 전달해야 텍스처가 표면에 걸쳐 보간 될 수 있습니다. 다음은 새로운 버텍스 쉐이더입니다.

attribute vec2 aPosition;
attribute vec2 aTextureCoord;
varying vec2 vTextureCoord;
void main() {
  gl_Position = vec4(aPosition.x, aPosition.y, 0.0, 1.0);
  vTextureCoord = aTextureCoord;
}

좋은 소식은이 셰이더는 더 이상 변경되지 않습니다. 버텍스 쉐이더는 이제 마지막 단계입니다. 조각 셰이더를 살펴 보겠습니다.

precision mediump float;
uniform sampler2D uSampler;
varying vec2 vTextureCoord;
void main() {
  gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
  gl_FragColor = texture2D(uSampler, vTextureCoord);
}

그래서 여기서 무슨 일이 일어나고 있습니까? 대략적으로 말하면 텍스처 좌표를 정점 ( aTextureCoord속성에)으로 전달한 후 정점 셰이더가 이러한 좌표를 vTextureCoord다양한 유형 의 특수 변수로 전달하여 정점 사이의 좌표를 보간하고 내삽 된 값을 프래그먼트 셰이더에 전달합니다. 프래그먼트 셰이더는 uSampleruniform 매개 변수 를 통해 텍스처를 가져 와서 현재 픽셀에 필요한 색상을 가져옵니다.texture2D버텍스 셰이더에서 전달 된 함수 및 텍스처 좌표. 버텍스 위치와는 별도로 텍스처 좌표를 전달해야합니다. 텍스처 좌표는 x 및 y에 대해 0.0에서 1.0까지 다양하며 시작 부분 (0.0, 0.0)은 왼쪽 하단 모서리입니다. 0,0이 항상 왼쪽 상단에있는 Android 좌표계에 익숙해지는 사람들에게는 드물게 들릴 수 있습니다. 운 좋게도 우리는 그것에 대해 너무 신경 쓸 필요가 없습니다. OpenGL에서 텍스처를 수직으로 뒤집어서 결국에는 이미지를 올바르게 배치 할 수 있습니다. initVertices다음과 같이 보이도록 변경하십시오 .

private void initVertices(int programReference) {
    final float[] verticesData = new float[] {
           //NDCS coords   //UV map
            -1, 1,          0, 1,
            -1, -1,         0, 0,
            1,  1,          1, 1,
            1,  -1,         1, 0
    }
    int buffers[] = new int[1];
    GLES20.glGenBuffers(1, buffers, 0);
    GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, buffers[0]);
    GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, verticesData.length * 4, convertToBuffer(verticesData), GLES20.GL_STREAM_DRAW);
    final int stride = 4 * 4;
    enableVertexAttribute(programReference, "aPosition", 2, stride, 0);
    enableVertexAttribute(programReference, "aTextureCoord", 2, stride, 2 * 4);
}

이제 실제 비트 맵을 조각 셰이더에 전달해 보겠습니다. 우리를위한 방법은 다음과 같습니다.

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);
}

메서드에서이 메서드를 호출하는 것을 잊지 마십시오 onSurfaceChanged.

@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);
}

컬러 필터 적용

이제 우리는 모두 컬러 필터를 적용 할 준비가되었습니다. 다시 셰이더부터 시작하겠습니다. 정점 셰이더의 경우 변경 사항이 없으며 조각 버퍼 만 색상 계산에 관심이 있습니다. 컬러 필터는 4x5 행렬이고 문제는 OpenGL이 행 또는 열에 최대 4 개의 행렬 만 있다는 것입니다. 그것을 이해하기 위해 우리는 4x4 행렬과 4x 벡터로 구성되는 새로운 구조를 정의 할 것입니다. 컬러 필터를 통과 한 후에는 색상 변환 및 블렌딩을 수행하는 데 필요한 모든 것이 있습니다. 이미 공식을 알고 있으므로 더 이상 설명하지 않겠습니다. 여기에 거의 마지막 조각 셰이더가 있습니다.

precision mediump float;
struct ColorFilter {
  mat4 factor;
  vec4 shift;
};
uniform sampler2D uSampler;
uniform ColorFilter uColorFilter;
varying vec2 vTextureCoord;
void main() {
  gl_FragColor = texture2D(uSampler, vTextureCoord);
  vec4 originalColor = texture2D(uSampler, vTextureCoord);
  vec4 filteredColor = (originalColor * uColorFilter.factor) + uColorFilter.shift;
  gl_FragColor = originalColor - filteredColor;
}

색상 필터를 셰이더에 전달하는 방법은 다음과 같습니다.

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);
}

또한 메서드에서이 메서드를 호출해야합니다 onSurfaceChanged.

@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);
}

알파 채널 블렌딩

처음에이 매개 변수를 설정할 때 : hostView.setEGLConfigChooser(8, 8, 8, 8, 0, 0);실제로 OpenGL 컨텍스트에서 알파 채널 용 버퍼를 추가했습니다. 그렇지 않으면 항상 출력 이미지에 대한 배경을 얻을 수 있습니다 (일부 픽셀에 대해 png 이미지가 다른 알파 채널을 갖는 경향이 있다는 점을 고려하면 정확하지 않음). 나쁜 소식은 알파 블렌딩 메커니즘을 깨고 일부 코너 케이스의 경우 예상치 못한 색상을 얻을 수 있다는 것입니다. 좋은 소식-우리는 쉽게 고칠 수 있습니다. 먼저 조각 셰이더에 알파 블렌딩을 적용해야합니다.

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 = originalColor - filteredColor
  gl_FragColor = vec4(originalColor.rgb - filteredColor.rgb, originalColor.a);
}

또한 블렌드 함수를 다음과 같이 설정하는 것이 좋습니다. 따라서 출력은 현재 색상 버퍼에있는 항목의 영향을받지 않으며 동작이 Android의 ImageView. 그러나 우리는 명확한 색상을 위해 색상을 설정하지 않았으며 아무것도 변경하지 않는 것 같습니다.

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    GLES20.glEnable(GLES20.GL_BLEND);
    GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ZERO);
}

결과 게시

거의 해냈습니다. 남은 것은 결과를 호출자 측에 반환하는 것입니다. 먼저에서 비트 맵을 가져 GLSurfaceView옵니다. 다른 stackoverflow 답변 에서 빌린 훌륭한 솔루션이 하나 있습니다 .

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() {
    if (mFinished) {
        return;
    }
    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);
}

그리고 onDrawFrame메소드 에서 이것을 호출하십시오 .

@Override
public void onDrawFrame(GL10 gl) {
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
    postResult();
}

결과

이제 방금 만든 유틸리티를 사용해 보겠습니다. 0 필터부터 시작하여 어떤 채널에서도 원본 이미지에 영향을주지 않습니다.

암호

BlendingFilterUtil.subtractMatrixColorFilter(bitmap, new float[]{
    0,      0,      0,      0,      0,
    0,      0,      0,      0,      0,
    0,      0,      0,      0,      0,
    0,      0,      0,      0,      0
}, activity, callback);

산출

원본 이미지

원본 이미지는 왼쪽에 있고 필터 차감 된 이미지는 오른쪽에 있습니다. 예상대로 동일합니다. 이제 더 흥미로운 작업을 수행해 보겠습니다. 예를 들어 빨간색 및 녹색 채널을 완전히 제거합니다.

암호

BlendingFilterUtil.subtractMatrixColorFilter(bitmap, new float[]{
    1,      0,      0,      0,      0,
    0,      1,      0,      0,      0,
    0,      0,      0,      0,      0,
    0,      0,      0,      1,      0
}, activity, callback);

산출

파란색 이미지

이제 출력에는 파란색 채널 만 있고 나머지 두 개는 완전히 뺍니다. 그의 질문에 주어진 필터 OP를 시도해 봅시다.

암호

BlendingFilterUtil.subtractMatrixColorFilter(bitmap, new float[]{
   0.393f, 0.7689999f, 0.18899999f, 0, 0,
   0.349f, 0.6859999f, 0.16799999f, 0, 0,
   0.272f, 0.5339999f, 0.13099999f, 0, 0,
   0,      0,          0,           1, 0
}, activity, callback);

산출

빼낸 fiter 이미지

요점

어떤 단계에서든 어려움을 겪고 있다면 위에서 설명한 유틸리티의 전체 코드로 요점 을 참조하십시오 .


이 긴 게시물에 너무 지루하지 않았기를 바랍니다. 나는 그것이 어떻게 작동하는지 간략하게 설명하려고 노력했다. 그래서 아마도 무언가가 너무 모호 할 것이다. 문제가 있거나 일관성이없는 경우 알려주세요.


저는 컴퓨터 그래픽 전문가는 아니지만 블렌딩하려는 이미지의 모든 픽셀을 반복하고, 각 픽셀의 중심을 맞추고, colorMatrix매트릭스가 접촉하는 주변 픽셀을 사용하여 평균을 계산하고 싶다고 가정합니다. 이 평균을 픽셀에 적용합니다. 분명히 당신은 어떻게 든 가장자리 픽셀을 처리해야 할 것입니다.

예 : 픽셀 값이 다음과 같은 5x4 이미지가 있다고 가정합니다.

    1     2    3    4    5
 1 1000 1000 1000 1000 1000
 2 1000 1000 1000 1000 1000
 3 1000 1000 1000 1000 1000
 4 1000 1000 1000 1000 1000

(1) 위치에서 (3,3)픽셀 (i,j)(i,j)가져 와서 변환 행렬을 적용합니다. 즉, 이미지 픽셀 과 행렬 위치를 하면됩니다.

     1     2    3    4    5
 1  393  769  189    0    0
 2  349  686  168    0    0
 3  272  534  131    0    0
 4    0    0    0 1000    0

(2) 이제이 변환의 평균을 취합니다. 즉, 모든 숫자를 더하고 20으로 나누면 224.5 또는 약 225가됩니다. 따라서 새로 변환 된 이미지는 다음과 같습니다.

    1     2    3    4    5
 1 1000 1000 1000 1000 1000
 2 1000 1000 1000 1000 1000
 3 1000 1000  225 1000 1000
 4 1000 1000 1000 1000 1000

전체 빼기 블렌드를 얻으려면 모든 픽셀에 대해 이렇게하십시오.

편집 : 실제로 위의 내용이 가우시안 블러 일 수 있다고 생각합니다.

참조 URL : https://stackoverflow.com/questions/33358030/subtract-blend-mode-using-colormatrixfilter-in-android

반응형