【GLSL教程】(五)卡通著色
引言
卡通著色可能是最簡單的非真真實模式shader。它使用很少的顏色,通常是幾種色調(tone),因此不同色調之間是突變的效果。下圖顯示的就是我們試圖達到的效果:
茶壺上的色調是通過角度的餘弦值選擇的,這個角度是指光線和麵的法線之間的夾角角度。如果法線和光的夾角比較小,我們使用較亮的色調,隨著夾角變大,逐步使用更暗的色調。換句話說,角度餘弦值將決定色調的強度。
在本教程中,我們先介紹逐頂點計算色調強度(intensity)的方法,之後把這個計算移到片斷shader中,此外還將介紹如何訪問OpenGL中光源的方向。
卡通著色——版本1
這個版本使用逐頂點計算色調強度的方法,之後片斷shader使用頂點色調強度的插值來決定片斷選擇那個色調。因此頂點shader必須宣告一個易變變數儲存強度值,片斷shader中也需要宣告一個同名的易變變數,用來接收經過插值的強度值。
在頂點shader中,光線方向可以定義為一個區域性變數或者常量,不過定義為一個一致變數將可以獲得更大的靈活性,因為這樣就可以在OpenGL程式中任意設定了。所以我們在shader中這樣定義光的方向:
頂點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函式計算。
卡通著色——版本2
本節中我們要實現逐片斷的卡通著色效果。為了達到這個目的,我們需要訪問每個片斷的法線。頂點shader中需要將頂點的法線寫入一個易變變數,這樣在片斷shader中就可以得到經過插值的法線。
頂點shader比上一版變得更簡單了,因為顏色強度的計算移到片斷shader中進行了。一致變數lightDir也要移到片斷shader中,下面就是新的頂點shader程式碼:
令人吃驚的是新的渲染結果居然和前一節的一模一樣,這是為什麼呢?
讓我們仔細看看這兩個版本的區別。在第一版中,我們在頂點shader中計算出一個intensity值,然後在片斷shader中使用這個值的插值結果。在第二個版中,我們先對法線插值,然後在片斷shader中計算點積。插值和點積都是線性運算,所以兩者運算的順序並不影響結果。
真正的問題在於片斷shader中對插值後的法線進行點積運算的時候,儘管這時法線的方向是對的,但是它並沒有歸一化。
我們說法線方向是對的,因為我們假定傳入頂點shader的法線是經過歸一化的,對法線插值可以得到一個方向正確的向量。但是,這個向量的長度在大部分情況下都是錯的,因為對歸一化法線進行插值時,只有在所有法線的方向一致時才會得到一個單位長度的向量。(關於法線插值的問題,後面的教程會專門解釋)
綜上所述,在片斷shader中,我們接收到的是一個方向正確長度錯誤的法線,為了修正這個問題,我們必須將這個法線歸一化。下面是正確實現的程式碼:
下一節我們將在OpenGL程式中設定shader中的光線方向。
卡通著色——版本3
結束關於卡通著色的內容之前,還有一件事需要解決:使用OpenGL中的光來代替變數lightDir。我們需要在OpenGL程式中定義一個光源,然後在我們的shader中使用這個光源的方向資料。注意:不需要用glEnable開啟這個光源,因為我們使用了shader。
我們假設OpenGL程式中定義的1號光源(GL_LIGHT0)是方向光。GLSL已經宣告瞭一個C語言形式的結構體,描述光源屬性。這些結構體組成一個陣列,儲存所有光源的資訊。
OpenGL標準中規定,當一個光源的位置確定後,將自動轉換到視點空間(eye space)的座標系中,例如攝像機座標系。如果模型檢視矩陣的左上3×3子陣是正交的(如果使用gluLookAt並且不使用縮放變換就可以滿足這點),便能保證光線方向向量在自動變換到視點空間之後保持歸一化。
我們必須將法線也變換到視點空間,然後計算其與光線的點積。只有在相同空間,計算兩個向量的點積得到餘弦值才有意義。
為了將法線變換到視點空間,我們必須使用預先定義的mat3型的一致變數gl_NormalMatrix。這個矩陣是模型檢視矩陣的左上3×3子陣的逆矩陣的轉置矩陣(關於這個問題,後面的教程會專門解釋)。需要對每個法線進行這個變換,現在頂點shader變為如下形式:
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
卡通著色可能是最簡單的非真真實模式shader。它使用很少的顏色,通常是幾種色調(tone),因此不同色調之間是突變的效果。下圖顯示的就是我們試圖達到的效果:
茶壺上的色調是通過角度的餘弦值選擇的,這個角度是指光線和麵的法線之間的夾角角度。如果法線和光的夾角比較小,我們使用較亮的色調,隨著夾角變大,逐步使用更暗的色調。換句話說,角度餘弦值將決定色調的強度。
在本教程中,我們先介紹逐頂點計算色調強度(intensity)的方法,之後把這個計算移到片斷shader中,此外還將介紹如何訪問OpenGL中光源的方向。
卡通著色——版本1
這個版本使用逐頂點計算色調強度的方法,之後片斷shader使用頂點色調強度的插值來決定片斷選擇那個色調。因此頂點shader必須宣告一個易變變數儲存強度值,片斷shader中也需要宣告一個同名的易變變數,用來接收經過插值的強度值。
在頂點shader中,光線方向可以定義為一個區域性變數或者常量,不過定義為一個一致變數將可以獲得更大的靈活性,因為這樣就可以在OpenGL程式中任意設定了。所以我們在shader中這樣定義光的方向:
- 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函式計算。
- intensity = dot(lightDir, gl_Normal);
intensity = dot(lightDir, gl_Normal);
最後頂點要做的就是變換頂點座標。頂點shader的完整程式碼如下:- uniform vec3 lightDir;
- varying float intensity;
- void main()
- {
- intensity = dot(lightDir,gl_Normal);
- gl_Position = ftransform();
- }
uniform vec3 lightDir;
varying float intensity;
void main()
{
intensity = dot(lightDir,gl_Normal);
gl_Position = ftransform();
}
如果想使用OpenGL中的變數作為光的方向,那麼可以用gl_LightSource[0].position代替一致變數lightDir,程式碼如下:- varying float intensity;
- void main()
- {
- vec3 lightDir = normalize(vec3(gl_LightSource[0].position));
- intensity = dot(lightDir,gl_Normal);
- gl_Position = ftransform();
- }
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中的顏色可以用如下方式計算:- 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);
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的完整程式碼如下:- 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;
- }
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程式碼:
- varying vec3 normal;
- void main()
- {
- normal = gl_Normal;
- gl_Position = ftransform();
- }
varying vec3 normal;
void main()
{
normal = gl_Normal;
gl_Position = ftransform();
}
在片斷shader中,我們需要宣告一致變數lightDir,還需要一個易變變數接收插值後的法線。片斷shader的程式碼如下:- 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;
- }
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中,我們接收到的是一個方向正確長度錯誤的法線,為了修正這個問題,我們必須將這個法線歸一化。下面是正確實現的程式碼:
- 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;
- }
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語言形式的結構體,描述光源屬性。這些結構體組成一個陣列,儲存所有光源的資訊。
- struct gl_LightSourceParameters
- {
- vec4 ambient;
- vec4 diffuse;
- vec4 specular;
- vec4 position;
- ...
- };
- 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變為如下形式:
- varying vec3 normal;
- void main()
- {
- normal = gl_NormalMatrix * gl_Normal;
- gl_Position = ftransform();
- }
varying vec3 normal;
void main()
{
normal = gl_NormalMatrix * gl_Normal;
gl_Position = ftransform();
}
在片斷shader中,我們必須訪問光線方向來計算intensity值:- 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;
- }
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
相關文章
- GLSL著色器,來玩
- three.js 著色器材質之glsl內建函式JS函式
- Shader專題:卡通著色(一)控制顏色的藝術
- GLSL
- OpenGL 和 GLSL 在頂點著色器中動態調整裁剪平面引數的簡單程式碼示例
- Apple Pay一卡通開通教程 怎麼開通Apple Pay一卡通?APP
- three.js 著色器材質之初識著色器JS
- ShaderJoy —— 方向向量場的繪製【GLSL】
- 「Photoshop2021入門教程」等比例縮小的卡通人物
- WebGL之延遲著色Web
- 在 iOS 中使用 GLSL 實現抖音特效iOS特效
- Kotlin教程(五)型別Kotlin型別
- 在WebGL中使用GLSL實現光線追蹤Web
- 在WPF中使用著色器
- webgl 系列 —— 著色器語言Web
- PhotosRevive for mac黑白照著色工具Mac
- canvas繪製卡通人臉形象效果Canvas
- React 教程第五篇 —— stateReact
- 【opencv實戰】影象素描及卡通化OpenCV
- CSS3卡通形象程式碼例項CSSS3
- CSS3大熊貓卡通效果CSSS3
- hackyou2014 CTF web關卡通關攻略Web
- L2-023 圖著色問題
- visualstudio著色器設計器shadergraph使用
- P9384 [THUPC 2023 決賽] 著色
- CesiumJS PrimitiveAPI 高階著色入門 - 從引數化幾何與 Fabric 材質到著色器 - 上篇JSMITAPI
- CesiumJS PrimitiveAPI 高階著色入門 - 從引數化幾何與 Fabric 材質到著色器 - 下篇JSMITAPI
- MySQL教程之分組函式(五)MySql函式
- 人物的卡通渲染:方案彙總與選擇
- Abstract Pack Mac(彩色卡通元素效果fcpx外掛)Mac
- WebGL 3D概念講解(著色器)Web3D
- WebGL:使用著色器進行幾何造型Web
- three.js 著色器材質之紋理JS
- WPF網格型別畫素著色器型別
- WebGL著色器渲染小遊戲實戰Web遊戲
- [譯] Flutter 系列入門教程五:網格Flutter
- Docker教程之五Dcoker常用命令Docker
- Python開發的入門教程(五)-setPython
- MeteoInfo-Java解析與繪圖教程(五)Java繪圖