OpenGL Android課程五:介紹混合(Blending)

奏響曲發表於2019-02-19

翻譯文

原文標題:Android Lesson Five: An Introduction to Blending 原文連結:www.learnopengles.com/android-les…


介紹混合(Blending)

這節課,我們來學習混合(blending)在OpenGL中的
基本使用。我們來看看如何開啟或關閉混合,怎樣設定
不同的混合模式,以及不同的混合模式如何模仿顯示生
活中的效果。在後面的課程中,我們還將介紹如何使用
alpha通道,如何使用深度緩衝區在同一個場景中渲染
半透明和不透明的物體,以及什麼時候按深度排序物件,
以及為什麼。

我們還將研究如何監聽觸控事件,然後基於此更改渲染
狀態。
display

基本混合

前提條件

本系列每個課程構建都是以前一個課程為基礎。然而,對於這節課,如果您理解了OpenGL Android課程一:入門就足夠了。儘管程式碼基本上是前一課的,照明和紋理部分已在本課中移除,因此我們僅關注混合。

混合(Blending)

混合是將一種顏色與另一種顏色組合以獲得第三種顏色的行為。我們在現實世界任何時候都能看到混合:當光穿過玻璃時,當它從表面反射時,當光源本身疊加在背景上時,例如我們在晚上看到一盞明亮的路燈周圍的耀斑。

OpenGL有不同的混合模式,我們能使用它模擬這種效果。在OpenGL中,混合發生在渲染過程的後期:一旦片段著色器計算出片段的最終輸出顏色並且它即將被寫入幀緩衝區,就會發生這種情況。通常情況下,這片段會覆蓋之前所有內容,但如果啟用了混合,那麼該片段將與之前的片段混合。

預設情況下,當glBlendEquation()設定為預設值GL_FUNC_ADD時OpenGL的預設混合方程式為:

// 輸出 = (源因子 * 源片段) + (目標因子 * 目標片段)
output = (source factor * source fragment) + (destination factor * destination fragment)
複製程式碼

OpenGL ES 2 中還有另外兩種模式GL_FUNC_SUBTRACTGL_FUNC_REVERSE_SUBTRACT。 這些可能在以後的教程中介紹,然而,當我嘗試呼叫此函式時,我在Nexus S上遇到了 UnsupportedOperationException,因此Android實現可能實際上不支援此功能。 這不是世界末日,因為你可以用GL_FUNC_ADD做很多事情。

使用函式glBlendFunc()設定源因子和目標因子。下面將給出幾個常見混合因子的概述;更多資訊以及不同可能的因素的列舉,請參閱Khronos線上手冊

擷取(Clamping)

OpenGL預期的輸入被限制在[0,1]的範圍內,並且輸入也被限制在[0,1]。這在實踐中意味著當您進行混合時,顏色可以在色調中移動。 如果繼續想幀緩衝區新增紅色(RGB = 1,0,0),最終顏色會是紅色。如果想新增一點兒綠色,您要新增(RGB = 1,0.1,0)到緩衝區,即使您開始帶紅色的色調,最後也會得到黃色! 開啟混合時,您可以在本課程的Demo中看到此效果:不同顏色的重疊的顏色變得過飽和。

不同型別的混合以及它們有怎樣不同的效果

相加混合(Additive blending)

rgb
RGB顏色相加模型; 來源:Wikipedia

相加混合是當我們新增不同顏色在一起的混合,這就是我們的視覺與光一起工作的模式,這就是我們如何在我們的顯示器上感知數百萬種不同的顏色——它們實際上只是將三種不同的原色混合在一起。

這種混合在3D混合中很有用,例如在粒子效果中,它們似乎發出光線和覆蓋物,例如燈光周圍的光暈,或光劍周圍的發光效果。

相加混合能通過呼叫glBlendFunc(GL_ONE, GL_ONE)指定, 混合的結果等式輸出=(1 * 源片段) + (1 * 目標片段),運算後:輸出=源片段 + 目標片段

相乘混合(Multiplicative blending)

rg
光照貼圖的一個例子

相乘混合(也稱為調製)是另一種有用的混合模式,它表示光在通過過濾器時的行為方式,或從被點燃的物體反射並進入我們的眼睛。一個紅色的物體看上去是紅色是因為白光照射到這個物體上,藍光和綠光被吸收,只有紅光反射回我們的眼睛。在上面的例子中,我們能看到一些紅色和綠色,但是很少會有一點藍色。

當多紋理不可用時,乘法混合用於在遊戲中實現光照貼圖。紋理與光照貼圖相乘,以填充在明亮和陰影的區域。

相乘混合能通過呼叫glBlendFunc(GL_DST_COLOR, GL_ZERO)指定, 其混合的結果等式輸出=(目標片段 * 源片段)+ (0 * 目標片段),寫作:輸出=目標片段 * 源片段

插值混合(Interpolative blending)

textures
一個兩個紋理一起插值的案例

插值混合結合了乘法和加法,以提供插值效果。與新增和調製本身不同,此混合模式也可是依賴繪製順序的。因此在某些情況下,如果您先畫出最遠的半透明物體,然後繪製更近的物體,結果才會是正確。即使排序也不是完美,因為三角形可能重疊並相交,但產生的偽像可能是可接受的。

插值通常是將相鄰的表面混合在一起,以及做有色玻璃或淡入淡出的效果。上面這個圖片顯示了兩個紋理(紋理來自公共領域紋理)使用插值混合在一起。

插值混合能通過呼叫glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)指定, 其混合結果等式輸出 = (源alpha * 源片段) + ((1 - 源alpha) * 目標片段)。這是一個例子:

想象一下,我們正在繪製一個只有25%不透明的綠色(0,1,0),當前螢幕上的物體時紅色(1,0,0)。

輸出 = (源因子 * 源片段) + (目標因子 * 目標片段)
輸出 = (源alpha * 源片段) + ((1 - 源alpha) * 目標片段)

輸出 = (0.25 * (0, 1, 0)) + (0.72 * (1, 0, 0))
輸出 = (0, 0.25, 0) + (0.75, 0, 0)
輸出 = (0.75, 0.25, 0)
複製程式碼

注意,我們不需要對目標alpha做任何涉及,因為這個幀緩衝區本身不需要alpha通道,這為我們提供了更多的顏色通道位。

使用混合

在我們的課程中,我們的Demo將使用相加混合將立方體顯示為光的發射器。發光的東西不需要其他光源照亮,因此這個Demo中沒有燈光。我也刪除了紋理,雖然它可以很好地使用。本課程的著色器程式很簡單;我們只需要一個可傳遞顏色的著色器。

頂點著色器

uniform mat4 u_MVPMatrix;
attribute vec4 a_Position;
attribute vec4 a_Color;

varying vec4 v_Color;

void main()
{
    v_Color = a_Color;
    gl_Position = u_MVPMatrix * a_Position;
}
複製程式碼

片段著色器

precision mediump float;
varying vec4 v_Color;

void main()
{
    gl_FragColor = v_Color;
}
複製程式碼

開啟混合

開啟混合就像是做一些方法呼叫那麼簡單:

// 關閉剔除去掉背面
GLES20.glDisable(GLES20.GL_CULL_FACE);
// 關閉深度測試
GLES20.glDisable(GLES20.GL_DEPTH_TEST);

// 啟動混合
GLES20.glEnable(GLES20.GL_BLEND);
GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE);
複製程式碼

我們關閉背面剔除,是因為如果立方體是半透明的,那麼現在我們能看到立方體的背面。我們需要繪製它們,否則可能看起來會很奇怪。出於同樣的原因我們關閉了深度測試。

學習觸控事件並進行操作

你將注意到,當您執行Demo時,可以通過點選螢幕來開啟和關閉混合。

現實觸控事件,您首先需要建立您的GLSurfaceView自定義view。在這個view中,建立一個預設構造用來呼叫父類,建立一個新的方法來接收特定的渲染器替換常用介面,並覆寫onTouchEvent()。我們傳入一個具體的渲染器類,因為我們將要在onTouchEvent()方法中呼叫這個類的特定方法。

在Android中,OpenGL渲染器在獨立的執行緒中完成,因此我們還將看看如何安全的從正在監聽觸控事件的主執行緒排程到單獨的渲染器執行緒。

public class LessonFiveGLSurfaceView extends GLSurfaceView {

    private LessonFiveRenderer mRenderer;

    public LessonFiveGLSurfaceView(Context context) {
        super(context);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (
                event == null
                || event.getAction() != MotionEvent.ACTION_DOWN
                || mRenderer == null) {
            return super.onTouchEvent(event);
        }
        // 確保我們在OpenGL執行緒上呼叫switchMode()
        // queueEvent() 是GLSurfaceView的一個方法,它將為我們做到這點
        queueEvent(new Runnable() {
            @Override
            public void run() {
                mRenderer.switchMode();
            }
        });
        return true;
    }

    public void setRenderer(LessonFiveRenderer renderer) {
        mRenderer = renderer;
        super.setRenderer(renderer);
    }
}
複製程式碼

LessonFiveRenderer中實現switchMode()

public void switchMode() {
    mBlending = !mBlending;

    if (mBlending) {
        // 關閉剔除去掉背面
        GLES20.glDisable(GLES20.GL_CULL_FACE);
        // 關閉深度測試
        GLES20.glDisable(GLES20.GL_DEPTH_TEST);

        // 啟動混合
        GLES20.glEnable(GLES20.GL_BLEND);
        GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE);
    } else {
        GLES20.glEnable(GLES20.GL_CULL_FACE);
        GLES20.glEnable(GLES20.GL_DEPTH_TEST);
        GLES20.glDisable(GLES20.GL_BLEND);
    }
}
複製程式碼

仔細看LessonFiveGLSurfaceView::onTouchEvent(),主要記住觸控事件都是在UI主執行緒中 ,而GLSurfaceView在一個單獨的執行緒中建立OpenGL ES上下文,這意味著我們的渲染器的回撥也在一個單獨的執行緒中執行。這是一個需要記住的重點,因為我們不能再其他執行緒呼叫OpenGL並希望其工作。

辛運的是,編寫GLSurfaceView的人也想到了這點,並提供了一個queueEvent()方法,這使得你可以呼叫OpenGL執行緒上的東西。因此,當我們想通過點選螢幕開啟和關閉混合時,我們確保通過在UI執行緒中使用queueEvent()來正確呼叫OpenGL執行緒中的內容。

進一步練習

這個Demo目前僅使用相加混合,嘗試改變其為插值混合並重新新增燈光和紋理。如果您只在黑色背景上繪製兩個半透明紋理,繪製順序是否重要?什麼時候重要?

教程目錄

打包教材

可以在Github下載本課程原始碼:下載專案
本課的編譯版本也可以再Android市場下:google play 下載apk
“我”也編譯了個apk,方便大家下載:github download

相關文章