GLSL 程式設計/Unity/凹凸表面的光照

本教程涵蓋法線貼圖。
它是關於紋理技術的系列教程中的第一個,該技術超越了二維表面(或表面層)。在本教程中,我們從法線貼圖開始,這是一種非常成熟的技術,即使在粗糙的多邊形網格上也能模擬小凹凸的光照。本教程的程式碼基於“平滑鏡面高光”部分和“紋理球體”部分.
左側描繪的卡拉瓦喬的畫作是關於聖多馬的疑惑,他直到將手指放在基督的肋旁才相信基督復活。使徒們的皺眉不僅象徵著這種疑惑,而且透過常見的面部表情清楚地傳達了這種疑惑。然而,為什麼我們知道他們的額頭實際上是皺起的,而不是用一些明暗線條畫出來的?畢竟,這只是一幅平面畫作。事實上,觀眾直覺地假設它們是皺眉的,而不是畫出來的眉毛,即使這幅畫本身允許兩種解釋。教訓是:光滑表面的凹凸通常可以透過光照本身來令人信服地傳達,而無需任何其他線索(陰影、遮擋、視差效果、立體聲等)。
法線貼圖試圖透過根據一些虛擬凹凸改變表面法線向量來傳達光滑表面上的凹凸(即具有插值法線的粗糙三角形網格)。當使用這些修改後的法線向量計算光照時,觀眾通常會感知到虛擬凹凸,即使渲染的是完全平坦的三角形。這種幻覺當然會失效(尤其是在輪廓線處),但在許多情況下它非常令人信服。
更具體地說,代表虛擬凹凸的法線向量首先被編碼到紋理影像中(即法線貼圖)。然後,片段著色器在紋理影像中查詢這些向量,並根據它們計算光照。就是這樣。當然,問題在於在紋理影像中編碼法線向量。有不同的可能性,片段著色器必須適應用於生成法線貼圖的特定編碼。

好訊息是,您可以使用 Unity 從灰度影像輕鬆建立法線貼圖:在您喜歡的繪圖程式中建立灰度影像,並對錶面的常規高度使用特定的灰色,對凹凸使用較淺的灰色,對凹陷使用較深的灰色。確保不同灰色之間的過渡平滑,例如透過模糊影像。當您使用資產 > 匯入新資產匯入影像時,在檢查器檢視中將紋理型別更改為法線貼圖,並選中從灰度生成。單擊應用後,預覽應顯示帶有紅色和綠色邊緣的藍色影像。或者,您可以匯入左側的編碼法線貼圖(不要忘記取消選中從灰度生成框)。
不太好的訊息是,片段著色器必須執行一些計算才能解碼法線。首先,紋理顏色儲存在二維紋理影像中,即只有 alpha 分量 和一個可用顏色分量。顏色分量可以作為紅色、綠色或藍色分量訪問,在所有情況下,都會返回相同的值。在這裡,我們使用綠色分量 ,因為 Unity 也使用它。兩個分量, 和 ,儲存為 0 到 1 之間的數字;但是,它們表示 -1 到 1 之間的座標 和 。對映是
and
從這兩個分量中,可以計算出三維法線向量 n 的第三個分量 ,因為它們被歸一化為單位長度
如果我們選擇軸沿著光滑法線向量(從頂點著色器中設定的法線向量插值得到)的軸,則只有“+”解是必要的,因為我們無法渲染具有向內指向法線向量的表面。片元著色器中的程式碼片段可能如下所示
vec4 encodedNormal = texture2D(_BumpMap,
_BumpMap_ST.xy * textureCoordinates.xy
+ _BumpMap_ST.zw);
vec3 localCoords =
vec3(2.0 * encodedNormal.ag - vec2(1.0), 0.0);
localCoords.z = sqrt(1.0 - dot(localCoords, localCoords));
// approximation without sqrt: localCoords.z =
// 1.0 - 0.5 * dot(localCoords, localCoords);
對於使用 OpenGL ES 的裝置,解碼實際上更簡單,因為 Unity 在這種情況下不使用雙分量紋理。因此,對於移動平臺,解碼變為
vec4 encodedNormal = texture2D(_BumpMap,
_BumpMap_ST.xy * textureCoordinates.xy
+ _BumpMap_ST.zw);
vec3 localCoords = 2.0 * encodedNormal.rgb - vec3(1.0);
但是,本教程的其餘部分(以及“凹凸表面的投影”部分)將只涵蓋(桌面)OpenGL。

Unity 使用每個表面點的區域性表面座標系來指定法線貼圖中的法線向量。該區域性座標系的軸由世界空間中的光滑插值法線向量 N 給出,而平面是該表面切平面,如左側影像所示。具體而言,軸由 Unity 提供給頂點的切線屬性 T 指定(參見“著色器的除錯”部分中關於屬性的討論)。給定和軸,軸可以透過頂點著色器中的叉積計算,例如 B = N × T。(字母 B 指的是這個向量的傳統名稱“副法線”。)
請注意,法線向量 N 使用模型矩陣的逆矩陣的轉置從物體空間變換到世界空間(因為它與表面正交;參見“應用矩陣變換”部分),而切線向量 T 指定表面上點之間的方向,因此使用模型矩陣變換。副法線向量 B 代表第三類向量,其變換方式不同。(如果你真的想知道:與“B×”對應的斜對稱矩陣 B 像二次型一樣變換。)因此,最好的選擇是首先將 N 和 T 變換到世界空間,然後使用變換後的向量的叉積在世界空間中計算 B。
使用世界空間中歸一化的方向 T、B 和 N,我們可以輕鬆地形成一個矩陣,該矩陣將法線貼圖的任何法線向量 n 從區域性表面座標系對映到世界空間,因為該矩陣的列只是軸的向量;因此,將 n 對映到世界空間的 3×3 矩陣為
這些計算由頂點著色器執行,例如,以這種方式執行
varying vec4 position;
// position of the vertex (and fragment) in world space
varying vec4 textureCoordinates;
varying mat3 localSurface2World; // mapping from
// local surface coordinates to world coordinates
#ifdef VERTEX
attribute vec4 Tangent;
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object; // unity_Scale.w
// is unnecessary because we normalize vectors
localSurface2World[0] = normalize(vec3(
modelMatrix * vec4(vec3(Tangent), 0.0)));
localSurface2World[2] = normalize(vec3(
vec4(gl_Normal, 0.0) * modelMatrixInverse));
localSurface2World[1] = normalize(
cross(localSurface2World[2], localSurface2World[0])
* Tangent.w); // factor Tangent.w is specific to Unity
position = modelMatrix * gl_Vertex;
textureCoordinates = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
計算 binormal 中的因子 Tangent.w 特定於 Unity,即 Unity 提供切線向量和法線貼圖,因此我們必須進行此乘法。
在片元著色器中,我們將 localSurface2World 中的矩陣乘以 n。例如,使用以下程式碼行
vec3 normalDirection =
normalize(localSurface2World * localCoords);
使用世界空間中的新法線向量,我們可以像在“光滑鏡面高光”部分中一樣計算光照。
完整的著色器程式碼
[edit | edit source]此著色器程式碼只是集成了所有程式碼片段,並使用我們針對畫素光照的標準雙通道方法。
Shader "GLSL normal mapping" {
Properties {
_BumpMap ("Normal Map", 2D) = "bump" {}
_Color ("Diffuse Material Color", Color) = (1,1,1,1)
_SpecColor ("Specular Material Color", Color) = (1,1,1,1)
_Shininess ("Shininess", Float) = 10
}
SubShader {
Pass {
Tags { "LightMode" = "ForwardBase" }
// pass for ambient light and first light source
GLSLPROGRAM
// User-specified properties
uniform sampler2D _BumpMap;
uniform vec4 _BumpMap_ST;
uniform vec4 _Color;
uniform vec4 _SpecColor;
uniform float _Shininess;
// The following built-in uniforms (except _LightColor0)
// are also defined in "UnityCG.glslinc",
// i.e. one could #include "UnityCG.glslinc"
uniform vec3 _WorldSpaceCameraPos;
// camera position in world space
uniform mat4 _Object2World; // model matrix
uniform mat4 _World2Object; // inverse model matrix
uniform vec4 _WorldSpaceLightPos0;
// direction to or position of light source
uniform vec4 _LightColor0;
// color of light source (from "Lighting.cginc")
varying vec4 position;
// position of the vertex (and fragment) in world space
varying vec4 textureCoordinates;
varying mat3 localSurface2World; // mapping from local
// surface coordinates to world coordinates
#ifdef VERTEX
attribute vec4 Tangent;
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object; // unity_Scale.w
// is unnecessary because we normalize vectors
localSurface2World[0] = normalize(vec3(
modelMatrix * vec4(vec3(Tangent), 0.0)));
localSurface2World[2] = normalize(vec3(
vec4(gl_Normal, 0.0) * modelMatrixInverse));
localSurface2World[1] = normalize(
cross(localSurface2World[2], localSurface2World[0])
* Tangent.w); // factor Tangent.w is specific to Unity
position = modelMatrix * gl_Vertex;
textureCoordinates = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
// in principle we have to normalize the columns of
// "localSurface2World" again; however, the potential
// problems are small since we use this matrix only to
// compute "normalDirection", which we normalize anyways
vec4 encodedNormal = texture2D(_BumpMap,
_BumpMap_ST.xy * textureCoordinates.xy
+ _BumpMap_ST.zw);
vec3 localCoords =
vec3(2.0 * encodedNormal.ag - vec2(1.0), 0.0);
localCoords.z = sqrt(1.0 - dot(localCoords, localCoords));
// approximation without sqrt: localCoords.z =
// 1.0 - 0.5 * dot(localCoords, localCoords);
vec3 normalDirection =
normalize(localSurface2World * localCoords);
vec3 viewDirection =
normalize(_WorldSpaceCameraPos - vec3(position));
vec3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(vec3(_WorldSpaceLightPos0));
}
else // point or spot light
{
vec3 vertexToLightSource =
vec3(_WorldSpaceLightPos0 - position);
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
vec3 ambientLighting =
vec3(gl_LightModel.ambient) * vec3(_Color);
vec3 diffuseReflection =
attenuation * vec3(_LightColor0) * vec3(_Color)
* max(0.0, dot(normalDirection, lightDirection));
vec3 specularReflection;
if (dot(normalDirection, lightDirection) < 0.0)
// light source on the wrong side?
{
specularReflection = vec3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
specularReflection = attenuation * vec3(_LightColor0)
* vec3(_SpecColor) * pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess);
}
gl_FragColor = vec4(ambientLighting
+ diffuseReflection + specularReflection, 1.0);
}
#endif
ENDGLSL
}
Pass {
Tags { "LightMode" = "ForwardAdd" }
// pass for additional light sources
Blend One One // additive blending
GLSLPROGRAM
// User-specified properties
uniform sampler2D _BumpMap;
uniform vec4 _BumpMap_ST;
uniform vec4 _Color;
uniform vec4 _SpecColor;
uniform float _Shininess;
// The following built-in uniforms (except _LightColor0)
// are also defined in "UnityCG.glslinc",
// i.e. one could #include "UnityCG.glslinc"
uniform vec3 _WorldSpaceCameraPos;
// camera position in world space
uniform mat4 _Object2World; // model matrix
uniform mat4 _World2Object; // inverse model matrix
uniform vec4 _WorldSpaceLightPos0;
// direction to or position of light source
uniform vec4 _LightColor0;
// color of light source (from "Lighting.cginc")
varying vec4 position;
// position of the vertex (and fragment) in world space
varying vec4 textureCoordinates;
varying mat3 localSurface2World; // mapping from
// local surface coordinates to world coordinates
#ifdef VERTEX
attribute vec4 Tangent;
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object; // unity_Scale.w
// is unnecessary because we normalize vectors
localSurface2World[0] = normalize(vec3(
modelMatrix * vec4(vec3(Tangent), 0.0)));
localSurface2World[2] = normalize(vec3(
vec4(gl_Normal, 0.0) * modelMatrixInverse));
localSurface2World[1] = normalize(
cross(localSurface2World[2], localSurface2World[0])
* Tangent.w); // factor Tangent.w is specific to Unity
position = modelMatrix * gl_Vertex;
textureCoordinates = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
// in principle we have to normalize the columns of
// "localSurface2World" again; however, the potential
// problems are small since we use this matrix only to
// compute "normalDirection", which we normalize anyways
vec4 encodedNormal = texture2D(_BumpMap,
_BumpMap_ST.xy * textureCoordinates.xy
+ _BumpMap_ST.zw);
vec3 localCoords =
vec3(2.0 * encodedNormal.ag - vec2(1.0), 0.0);
localCoords.z = sqrt(1.0 - dot(localCoords, localCoords));
// approximation without sqrt: localCoords.z =
// 1.0 - 0.5 * dot(localCoords, localCoords);
vec3 normalDirection =
normalize(localSurface2World * localCoords);
vec3 viewDirection =
normalize(_WorldSpaceCameraPos - vec3(position));
vec3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(vec3(_WorldSpaceLightPos0));
}
else // point or spot light
{
vec3 vertexToLightSource =
vec3(_WorldSpaceLightPos0 - position);
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
vec3 diffuseReflection =
attenuation * vec3(_LightColor0) * vec3(_Color)
* max(0.0, dot(normalDirection, lightDirection));
vec3 specularReflection;
if (dot(normalDirection, lightDirection) < 0.0)
// light source on the wrong side?
{
specularReflection = vec3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
specularReflection = attenuation * vec3(_LightColor0)
* vec3(_SpecColor) * pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess);
}
gl_FragColor =
vec4(diffuseReflection + specularReflection, 1.0);
}
#endif
ENDGLSL
}
}
// The definition of a fallback shader should be commented out
// during development:
// Fallback "Bumped Specular"
}
請注意,我們使用了在“紋理化球體”部分中解釋的平鋪和偏移一致變數 _BumpMap_ST,因為此選項通常對凹凸貼圖特別有用。
總結
[edit | edit source]恭喜你!你完成了本教程!我們已經瞭解了
- 人類對形狀的感知通常依賴於光照。
- 什麼是法線貼圖。
- Unity 如何對法線貼圖進行編碼。
- 片元著色器如何解碼 Unity 的法線貼圖並將其用於逐畫素光照。
如果您還想了解更多
- 關於紋理對映(包括平鋪和偏移),您應該閱讀 “紋理球體”部分。
- 關於使用 Phong 反射模型進行逐畫素光照,您應該閱讀 “平滑鏡面高光”部分。
- 關於變換法向量,您應該閱讀 “應用矩陣變換”部分。
- 關於法線貼圖,您可以閱讀 Mark J. Kilgard: “A Practical and Robust Bump-mapping Technique for Today’s GPUs”,GDC 2000:Advanced OpenGL Game Development,該文章可在 網上獲取。