【GLSL教程】(五)卡通著色

vampirem發表於2013-09-17
引言
卡通著色可能是最簡單的非真真實模式shader。它使用很少的顏色,通常是幾種色調(tone),因此不同色調之間是突變的效果。下圖顯示的就是我們試圖達到的效果:

茶壺上的色調是通過角度的餘弦值選擇的,這個角度是指光線和麵的法線之間的夾角角度。如果法線和光的夾角比較小,我們使用較亮的色調,隨著夾角變大,逐步使用更暗的色調。換句話說,角度餘弦值將決定色調的強度。
在本教程中,我們先介紹逐頂點計算色調強度(intensity)的方法,之後把這個計算移到片斷shader中,此外還將介紹如何訪問OpenGL中光源的方向。

卡通著色——版本1
這個版本使用逐頂點計算色調強度的方法,之後片斷shader使用頂點色調強度的插值來決定片斷選擇那個色調。因此頂點shader必須宣告一個易變變數儲存強度值,片斷shader中也需要宣告一個同名的易變變數,用來接收經過插值的強度值。
在頂點shader中,光線方向可以定義為一個區域性變數或者常量,不過定義為一個一致變數將可以獲得更大的靈活性,因為這樣就可以在OpenGL程式中任意設定了。所以我們在shader中這樣定義光的方向:
  1. uniform vec3 lightDir;  
uniform vec3 lightDir;
現在起我們假設光的方向定義在世界空間之中。
頂點shader通過屬性變數gl_Normal來訪問在OpenGL程式中指定的法線,這些法線在OpenGL程式中通過glNormal函式定義,因此位於模型空間。
如果在OpenGL程式中沒有對模型進行旋轉或縮放等操作,那麼傳給頂點shader的位於世界空間的gl_Normal正好等於模型空間中定義的法線。另外法線只包含方向,所以不受移動變換的影響。
由於法線和光線方向定義在相同的空間中,頂點shader可以直接進行餘弦計算,兩個方向分別為lightDir和gl_Normal。計算餘弦的公式如下:
cos(lightDir,normal) = lightDir . normal / (|lightDir| * |normal|)
公式中的“.”表示內積,亦稱為點積。如果lightDir和gl_Normal已經經過了歸一化:
| normal | = 1
| lightDir | = 1
那麼計算餘弦的公式可以簡化為:
cos(lightDir,normal) = lightDir . normal
因為lightDir是由OpenGL程式提供的,所以我們可以假定它傳到shader之前已經歸一化了。只有光線方向改變時,才需要重新計算歸一化。此外OpenGL程式傳過來的法線也應該是經過歸一化的。
我們將定義一個名為intensity的變數儲存餘弦值,這個值可以直接使用GLSL提供的dot函式計算。
  1. intensity = dot(lightDir, gl_Normal);  
intensity = dot(lightDir, gl_Normal);
最後頂點要做的就是變換頂點座標。頂點shader的完整程式碼如下:
  1. uniform vec3 lightDir;  
  2. varying float intensity;  
  3.   
  4. void main()  
  5. {  
  6.     intensity = dot(lightDir,gl_Normal);  
  7.     gl_Position = ftransform();  
  8. }  
uniform vec3 lightDir;
varying float intensity;

void main()
{
    intensity = dot(lightDir,gl_Normal);
    gl_Position = ftransform();
}
如果想使用OpenGL中的變數作為光的方向,那麼可以用gl_LightSource[0].position代替一致變數lightDir,程式碼如下:
  1. varying float intensity;  
  2.   
  3. void main()  
  4. {  
  5.     vec3 lightDir = normalize(vec3(gl_LightSource[0].position));  
  6.     intensity = dot(lightDir,gl_Normal);  
  7.   
  8.     gl_Position = ftransform();  
  9. }  
varying float intensity;

void main()
{
    vec3 lightDir = normalize(vec3(gl_LightSource[0].position));
    intensity = dot(lightDir,gl_Normal);

    gl_Position = ftransform();
}
現在在片斷shader中唯一要做的就是根據intensity定義片斷的顏色。前面已經提到,變數intensity在兩個shader中都定義為易變變數,所以它將會在頂點shader中寫入,然後再片斷shader中讀出。片斷shader中的顏色可以用如下方式計算:
  1. vec4 color;  
  2.   
  3. if (intensity > 0.95)  
  4.     color = vec4(1.0,0.5,0.5,1.0);  
  5. else if (intensity > 0.5)  
  6.     color = vec4(0.6,0.3,0.3,1.0);  
  7. else if (intensity > 0.25)  
  8.     color = vec4(0.4,0.2,0.2,1.0);  
  9. else  
  10.     color = vec4(0.2,0.1,0.1,1.0);  
vec4 color;

if (intensity > 0.95)
    color = vec4(1.0,0.5,0.5,1.0);
else if (intensity > 0.5)
    color = vec4(0.6,0.3,0.3,1.0);
else if (intensity > 0.25)
    color = vec4(0.4,0.2,0.2,1.0);
else
    color = vec4(0.2,0.1,0.1,1.0);
可以看到,餘弦大於0.95時使用最亮的顏色,小於0.25時使用最暗的顏色。得到這個顏色後只需要再將其寫入gl_FragColor即可,片斷shader的完整程式碼如下:
  1. varying float intensity;  
  2.   
  3. void main()  
  4. {  
  5.     vec4 color;  
  6.     if (intensity > 0.95)  
  7.   
  8.         color = vec4(1.0,0.5,0.5,1.0);  
  9.     else if (intensity > 0.5)  
  10.         color = vec4(0.6,0.3,0.3,1.0);  
  11.     else if (intensity > 0.25)  
  12.         color = vec4(0.4,0.2,0.2,1.0);  
  13.     else  
  14.         color = vec4(0.2,0.1,0.1,1.0);  
  15.     gl_FragColor = color;  
  16. }  
varying float intensity;

void main()
{
    vec4 color;
    if (intensity > 0.95)

        color = vec4(1.0,0.5,0.5,1.0);
    else if (intensity > 0.5)
        color = vec4(0.6,0.3,0.3,1.0);
    else if (intensity > 0.25)
        color = vec4(0.4,0.2,0.2,1.0);
    else
        color = vec4(0.2,0.1,0.1,1.0);
    gl_FragColor = color;
}
下圖顯示出本節的最終效果,看起來不是很好。主要原因是因為我們對intensity進行插值,插值的結果與用片斷法線算出的intensity有區別,下一節我們將展示如何更好的實現卡通著色效果。


卡通著色——版本2
本節中我們要實現逐片斷的卡通著色效果。為了達到這個目的,我們需要訪問每個片斷的法線。頂點shader中需要將頂點的法線寫入一個易變變數,這樣在片斷shader中就可以得到經過插值的法線。
頂點shader比上一版變得更簡單了,因為顏色強度的計算移到片斷shader中進行了。一致變數lightDir也要移到片斷shader中,下面就是新的頂點shader程式碼:
  1. varying vec3 normal;  
  2.   
  3. void main()  
  4. {  
  5.     normal = gl_Normal;  
  6.     gl_Position = ftransform();  
  7. }  
varying vec3 normal;

void main()
{
    normal = gl_Normal;
    gl_Position = ftransform();
}
在片斷shader中,我們需要宣告一致變數lightDir,還需要一個易變變數接收插值後的法線。片斷shader的程式碼如下:
  1. uniform vec3 lightDir;  
  2. varying vec3 normal;  
  3.   
  4. void main()  
  5. {  
  6.     float intensity;  
  7.     vec4 color;  
  8.     intensity = dot(lightDir,normal);  
  9.   
  10.     if (intensity > 0.95)  
  11.         color = vec4(1.0,0.5,0.5,1.0);  
  12.     else if (intensity > 0.5)  
  13.         color = vec4(0.6,0.3,0.3,1.0);  
  14.     else if (intensity > 0.25)  
  15.         color = vec4(0.4,0.2,0.2,1.0);  
  16.     else  
  17.         color = vec4(0.2,0.1,0.1,1.0);  
  18.     gl_FragColor = color;  
  19. }  
uniform vec3 lightDir;
varying vec3 normal;

void main()
{
    float intensity;
    vec4 color;
    intensity = dot(lightDir,normal);

    if (intensity > 0.95)
        color = vec4(1.0,0.5,0.5,1.0);
    else if (intensity > 0.5)
        color = vec4(0.6,0.3,0.3,1.0);
    else if (intensity > 0.25)
        color = vec4(0.4,0.2,0.2,1.0);
    else
        color = vec4(0.2,0.1,0.1,1.0);
    gl_FragColor = color;
}
下圖就是渲染結果:

令人吃驚的是新的渲染結果居然和前一節的一模一樣,這是為什麼呢?
讓我們仔細看看這兩個版本的區別。在第一版中,我們在頂點shader中計算出一個intensity值,然後在片斷shader中使用這個值的插值結果。在第二個版中,我們先對法線插值,然後在片斷shader中計算點積。插值和點積都是線性運算,所以兩者運算的順序並不影響結果。
真正的問題在於片斷shader中對插值後的法線進行點積運算的時候,儘管這時法線的方向是對的,但是它並沒有歸一化。
我們說法線方向是對的,因為我們假定傳入頂點shader的法線是經過歸一化的,對法線插值可以得到一個方向正確的向量。但是,這個向量的長度在大部分情況下都是錯的,因為對歸一化法線進行插值時,只有在所有法線的方向一致時才會得到一個單位長度的向量。(關於法線插值的問題,後面的教程會專門解釋)
綜上所述,在片斷shader中,我們接收到的是一個方向正確長度錯誤的法線,為了修正這個問題,我們必須將這個法線歸一化。下面是正確實現的程式碼:
  1. uniform vec3 lightDir;  
  2. varying vec3 normal;  
  3.   
  4. void main()  
  5. {  
  6.     float intensity;  
  7.     vec4 color;  
  8.     intensity = dot(lightDir,normalize(normal));  
  9.   
  10.     if (intensity > 0.95)  
  11.         color = vec4(1.0,0.5,0.5,1.0);  
  12.     else if (intensity > 0.5)  
  13.         color = vec4(0.6,0.3,0.3,1.0);  
  14.     else if (intensity > 0.25)  
  15.         color = vec4(0.4,0.2,0.2,1.0);  
  16.     else  
  17.         color = vec4(0.2,0.1,0.1,1.0);  
  18.     gl_FragColor = color;  
  19. }  
uniform vec3 lightDir;
varying vec3 normal;

void main()
{
    float intensity;
    vec4 color;
    intensity = dot(lightDir,normalize(normal));

    if (intensity > 0.95)
        color = vec4(1.0,0.5,0.5,1.0);
    else if (intensity > 0.5)
        color = vec4(0.6,0.3,0.3,1.0);
    else if (intensity > 0.25)
        color = vec4(0.4,0.2,0.2,1.0);
    else
        color = vec4(0.2,0.1,0.1,1.0);
    gl_FragColor = color;
}
下圖就是新版本卡通著色的效果,看起來漂亮多了,雖然並不完美。圖中物體還有些鋸齒(aliasing)問題,不過這超出了本教程討論的範圍。

下一節我們將在OpenGL程式中設定shader中的光線方向。

卡通著色——版本3
結束關於卡通著色的內容之前,還有一件事需要解決:使用OpenGL中的光來代替變數lightDir。我們需要在OpenGL程式中定義一個光源,然後在我們的shader中使用這個光源的方向資料。注意:不需要用glEnable開啟這個光源,因為我們使用了shader。
我們假設OpenGL程式中定義的1號光源(GL_LIGHT0)是方向光。GLSL已經宣告瞭一個C語言形式的結構體,描述光源屬性。這些結構體組成一個陣列,儲存所有光源的資訊。
  1. struct gl_LightSourceParameters  
  2. {  
  3.     vec4 ambient;  
  4.     vec4 diffuse;  
  5.     vec4 specular;  
  6.     vec4 position;  
  7.     ...  
  8. };  
  9.   
  10. uniform gl_LightSourceParameters gl_LightSource[gl_MaxLights];  
struct gl_LightSourceParameters
{
    vec4 ambient;
    vec4 diffuse;
    vec4 specular;
    vec4 position;
    ...
};

uniform gl_LightSourceParameters gl_LightSource[gl_MaxLights];
這意味著我們可以在shader中訪問光源的方向(使用結構體中的position域),這裡依然假定OpenGL程式對光源方向進行了歸一化。
OpenGL標準中規定,當一個光源的位置確定後,將自動轉換到視點空間(eye space)的座標系中,例如攝像機座標系。如果模型檢視矩陣的左上3×3子陣是正交的(如果使用gluLookAt並且不使用縮放變換就可以滿足這點),便能保證光線方向向量在自動變換到視點空間之後保持歸一化。
我們必須將法線也變換到視點空間,然後計算其與光線的點積。只有在相同空間,計算兩個向量的點積得到餘弦值才有意義。
為了將法線變換到視點空間,我們必須使用預先定義的mat3型的一致變數gl_NormalMatrix。這個矩陣是模型檢視矩陣的左上3×3子陣的逆矩陣的轉置矩陣(關於這個問題,後面的教程會專門解釋)。需要對每個法線進行這個變換,現在頂點shader變為如下形式:
  1. varying vec3 normal;  
  2.   
  3. void main()  
  4. {  
  5.     normal = gl_NormalMatrix * gl_Normal;  
  6.     gl_Position = ftransform();  
  7. }  
varying vec3 normal;

void main()
{
    normal = gl_NormalMatrix * gl_Normal;
    gl_Position = ftransform();
}
在片斷shader中,我們必須訪問光線方向來計算intensity值:
  1. varying vec3 normal;  
  2.   
  3. void main()  
  4. {  
  5.     float intensity;  
  6.     vec4 color;  
  7.     vec3 n = normalize(normal);  
  8.     intensity = dot(vec3(gl_LightSource[0].position),n);  
  9.   
  10.     if (intensity > 0.95)  
  11.         color = vec4(1.0,0.5,0.5,1.0);  
  12.     else if (intensity > 0.5)  
  13.         color = vec4(0.6,0.3,0.3,1.0);  
  14.     else if (intensity > 0.25)  
  15.         color = vec4(0.4,0.2,0.2,1.0);  
  16.     else  
  17.         color = vec4(0.2,0.1,0.1,1.0);  
  18.     gl_FragColor = color;  
  19. }  
varying vec3 normal;

void main()
{
    float intensity;
    vec4 color;
    vec3 n = normalize(normal);
    intensity = dot(vec3(gl_LightSource[0].position),n);

    if (intensity > 0.95)
        color = vec4(1.0,0.5,0.5,1.0);
    else if (intensity > 0.5)
        color = vec4(0.6,0.3,0.3,1.0);
    else if (intensity > 0.25)
        color = vec4(0.4,0.2,0.2,1.0);
    else
        color = vec4(0.2,0.1,0.1,1.0);
    gl_FragColor = color;
}
本小節內容的Shader Desinger工程下載地址:
http://lighthouse3d.com/wptest/wp-content/uploads/2011/03/toonf2.zip
基於GLEW的原始碼下載地址:
http://lighthouse3d.com/wptest/wp-content/uploads/2011/03/toonglut_2.0.zip

相關文章