Cg 程式設計/Unity/拉絲金屬

本教程涵蓋各向異性鏡面高光。
它是關於光照的幾個教程之一,超出了 Phong 反射模型。然而,它基於 Phong 反射模型的光照,如“鏡面高光”部分(用於每個頂點光照)和“平滑鏡面高光”部分(用於每個畫素光照)中所述。如果您還沒有閱讀這些教程,您應該先閱讀它們。
雖然 Phong 反射模型對於紙張、塑膠和一些其他具有各向同性反射(即圓形高光)的材料來說相當不錯,但本教程特別關注具有各向異性反射(即非圓形高光)的材料,例如左側照片中的拉絲鋁。

Gregory Ward 在其作品“測量和建模各向異性反射”中發表了一個合適的各向異性反射模型,計算機圖形學(SIGGRAPH ’92 論文集),第 265-272 頁,1992 年 7 月。(該論文的副本可在網上獲得。)該模型根據 BRDF(雙向反射分佈函式)來描述反射,它是一個四維函式,描述了來自任何方向的光線是如何反射到任何其他方向的。他的 BRDF 模型包含兩項:一個漫反射項,它是,以及一個更復雜的鏡面反射項。
讓我們先看看漫反射項:只是一個常數(大約 3.14159),而指定了漫反射率。原則上,每個波長都需要一個反射率;然而,通常為三個顏色分量(紅色、綠色和藍色)中的每一個指定一個反射率。如果我們包括常數,僅僅代表漫反射材質顏色,我們第一次在“漫反射”部分中看到它,但它也出現在 Phong 反射模型中(參見“鏡面高光”部分)。您可能想知道為什麼因子 max(0, L·N) 沒有出現在 BRDF 中。答案是 BRDF 的定義方式是不包含這個因子(因為它實際上不是材料的屬性),但在進行任何光照計算時,應該將其與 BRDF 相乘。
因此,為了對不透明材料實現給定的 BRDF,我們必須將 BRDF 的所有項乘以 max(0, L·N) 並且 - 除非我們想實現物理上正確的光照 - 我們可以將任何常數因子替換為使用者指定的顏色,這通常比物理量更容易控制。
對於他的 BRDF 模型的鏡面項,Ward 在他的論文的等式 5b 中給出了一個近似值。我稍微修改了它,使其使用歸一化的表面法線向量 N,歸一化的觀察者方向 V,歸一化的光源方向 L 以及歸一化的中間向量 H,它是 (V + L) / |V + L|。使用這些向量,Ward 對鏡面項的近似值變為
這裡, 是鏡面反射率,它描述了鏡面高光的顏色和強度; 和 是描述高光形狀和大小的材料常數。由於所有這些變數都是材料常數,我們可以將它們組合成一個常數 。因此,我們得到一個稍短的版本
請記住,在著色器中實現它時,我們仍然必須將此 BRDF 項乘以 L·N,如果 L·N 小於 0,則將其設定為 0。此外,如果 V·N 小於 0,即我們從“錯誤”的一側查看表面,它也應該為 0。
還有兩個向量尚未描述:T 和 B。T 是表面上的刷子方向,而 B 與 T 正交,但也在表面上。Unity 為我們提供了一個表面上的切線向量作為頂點屬性(見 “著色器除錯”部分),我們將把它用作向量 T。計算 N 和 T 的叉積會生成一個向量 B,它與 N 和 T 正交,正如它應該的那樣。
我們基於 “平滑鏡面高光”章節 中的逐畫素光照著色器實現。我們還需要一個頂點輸出引數 `tangentDir` 來表示切線向量 **T**(即刷痕方向),並且我們在頂點著色器中計算 `viewDir` 以節省片元著色器中的指令。在片元著色器中,我們計算另外兩個方向:`halfwayVector` 表示半程向量 **H**,`binormalDirection` 表示副法線向量 **B**。屬性包括 `_Color` 代表 ,`_SpecColor` 代表 ,`_AlphaX` 代表 ,`_AlphaY` 代表 。
片元著色器與 “平滑鏡面高光”章節 中的版本非常相似,只是它計算了 `halfwayVector` 和 `binormalDirection`,並且實現了鏡面部分的不同方程。此外,該著色器僅計算一次點積 **L**·**N** 並將其儲存在 `dotLN` 中,以便可以重複使用而無需重新計算。程式碼如下
float4 frag(vertexOutput input) : COLOR
{
float3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(_WorldSpaceLightPos0.xyz);
}
else // point or spot light
{
float3 vertexToLightSource =
_WorldSpaceLightPos0.xyz - input.posWorld.xyz;
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
float3 halfwayVector =
normalize(lightDirection + input.viewDir);
float3 binormalDirection =
cross(input.normalDir, input.tangentDir);
float dotLN = dot(lightDirection, input.normalDir);
// compute this dot product only once
float3 ambientLighting =
UNITY_LIGHTMODEL_AMBIENT.rgb * _Color.rgb;
float3 diffuseReflection =
attenuation * _LightColor0.rgb * _Color.rgb
* max(0.0, dotLN);
float3 specularReflection;
if (dotLN < 0.0) // light source on the wrong side?
{
specularReflection = float3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
float dotHN = dot(halfwayVector, input.normalDir);
float dotVN = dot(input.viewDir, input.normalDir);
float dotHTAlphaX =
dot(halfwayVector, input.tangentDir) / _AlphaX;
float dotHBAlphaY = dot(halfwayVector,
binormalDirection) / _AlphaY;
specularReflection =
attenuation * _LightColor0.rgb * _SpecColor.rgb
* sqrt(max(0.0, dotLN / dotVN))
* exp(-2.0 * (dotHTAlphaX * dotHTAlphaX
+ dotHBAlphaY * dotHBAlphaY) / (1.0 + dotHN));
}
return float4(ambientLighting + diffuseReflection
+ specularReflection, 1.0);
}
注意 `sqrt(max(0, dotLN / dotVN))` 這個項,它是從 乘以 得到的。這確保了所有值都大於 0。
完整的著色器程式碼
[edit | edit source]完整的著色器程式碼僅定義了適當的屬性,並添加了另一個用於切線的頂點輸入引數。此外,它需要一個帶有累加混合但沒有環境光的第二通道,用於處理額外的光源。
Shader "Cg anisotropic per-pixel lighting" {
Properties {
_Color ("Diffuse Material Color", Color) = (1,1,1,1)
_SpecColor ("Specular Material Color", Color) = (1,1,1,1)
_AlphaX ("Roughness in Brush Direction", Float) = 1.0
_AlphaY ("Roughness orthogonal to Brush Direction", Float) = 1.0
}
SubShader {
Pass {
Tags { "LightMode" = "ForwardBase" }
// pass for ambient light and first light source
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
uniform float4 _LightColor0;
// color of light source (from "Lighting.cginc")
// User-specified properties
uniform float4 _Color;
uniform float4 _SpecColor;
uniform float _AlphaX;
uniform float _AlphaY;
struct vertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float4 posWorld : TEXCOORD0;
// position of the vertex (and fragment) in world space
float3 viewDir : TEXCOORD1;
// view direction in world space
float3 normalDir : TEXCOORD2;
// surface normal vector in world space
float3 tangentDir : TEXCOORD3;
// brush direction in world space
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float4x4 modelMatrix = unity_ObjectToWorld;
float4x4 modelMatrixInverse = unity_WorldToObject;
output.posWorld = mul(modelMatrix, input.vertex);
output.viewDir = normalize(_WorldSpaceCameraPos
- output.posWorld.xyz);
output.normalDir = normalize(
mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
output.tangentDir = normalize(
mul(modelMatrix, float4(input.tangent.xyz, 0.0)).xyz);
output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
return output;
}
float4 frag(vertexOutput input) : COLOR
{
float3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(_WorldSpaceLightPos0.xyz);
}
else // point or spot light
{
float3 vertexToLightSource =
_WorldSpaceLightPos0.xyz - input.posWorld.xyz;
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
float3 halfwayVector =
normalize(lightDirection + input.viewDir);
float3 binormalDirection =
cross(input.normalDir, input.tangentDir);
float dotLN = dot(lightDirection, input.normalDir);
// compute this dot product only once
float3 ambientLighting =
UNITY_LIGHTMODEL_AMBIENT.rgb * _Color.rgb;
float3 diffuseReflection =
attenuation * _LightColor0.rgb * _Color.rgb
* max(0.0, dotLN);
float3 specularReflection;
if (dotLN < 0.0) // light source on the wrong side?
{
specularReflection = float3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
float dotHN = dot(halfwayVector, input.normalDir);
float dotVN = dot(input.viewDir, input.normalDir);
float dotHTAlphaX =
dot(halfwayVector, input.tangentDir) / _AlphaX;
float dotHBAlphaY = dot(halfwayVector,
binormalDirection) / _AlphaY;
specularReflection =
attenuation * _LightColor0.rgb * _SpecColor.rgb
* sqrt(max(0.0, dotLN / dotVN))
* exp(-2.0 * (dotHTAlphaX * dotHTAlphaX
+ dotHBAlphaY * dotHBAlphaY) / (1.0 + dotHN));
}
return float4(ambientLighting + diffuseReflection
+ specularReflection, 1.0);
}
ENDCG
}
Pass {
Tags { "LightMode" = "ForwardAdd" }
// pass for additional light sources
Blend One One // additive blending
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
uniform float4 _LightColor0;
// color of light source (from "Lighting.cginc")
// User-specified properties
uniform float4 _Color;
uniform float4 _SpecColor;
uniform float _AlphaX;
uniform float _AlphaY;
struct vertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float4 posWorld : TEXCOORD0;
// position of the vertex (and fragment) in world space
float3 viewDir : TEXCOORD1;
// view direction in world space
float3 normalDir : TEXCOORD2;
// surface normal vector in world space
float3 tangentDir : TEXCOORD3;
// brush direction in world space
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float4x4 modelMatrix = unity_ObjectToWorld;
float4x4 modelMatrixInverse = unity_WorldToObject;
output.posWorld = mul(modelMatrix, input.vertex);
output.viewDir = normalize(_WorldSpaceCameraPos
- output.posWorld.xyz);
output.normalDir = normalize(
mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
output.tangentDir = normalize(
mul(modelMatrix, float4(input.tangent.xyz, 0.0)).xyz);
output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
return output;
}
float4 frag(vertexOutput input) : COLOR
{
float3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(_WorldSpaceLightPos0.xyz);
}
else // point or spot light
{
float3 vertexToLightSource =
_WorldSpaceLightPos0.xyz - input.posWorld.xyz;
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
float3 halfwayVector =
normalize(lightDirection + input.viewDir);
float3 binormalDirection =
cross(input.normalDir, input.tangentDir);
float dotLN = dot(lightDirection, input.normalDir);
// compute this dot product only once
float3 diffuseReflection =
attenuation * _LightColor0.rgb * _Color.rgb
* max(0.0, dotLN);
float3 specularReflection;
if (dotLN < 0.0) // light source on the wrong side?
{
specularReflection = float3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
float dotHN = dot(halfwayVector, input.normalDir);
float dotVN = dot(input.viewDir, input.normalDir);
float dotHTAlphaX =
dot(halfwayVector, input.tangentDir) / _AlphaX;
float dotHBAlphaY = dot(halfwayVector,
binormalDirection) / _AlphaY;
specularReflection =
attenuation * _LightColor0.rgb * _SpecColor.rgb
* sqrt(max(0.0, dotLN / dotVN))
* exp(-2.0 * (dotHTAlphaX * dotHTAlphaX
+ dotHBAlphaY * dotHBAlphaY) / (1.0 + dotHN));
}
return float4(diffuseReflection
+ specularReflection, 1.0);
}
ENDCG
}
}
Fallback "Specular"
}
總結
[edit | edit source]恭喜你完成了一個相當高階的教程!我們已經瞭解了
- 什麼是 BRDF(雙向反射分佈函式)。
- 什麼是 Ward 用於各向異性反射的 BRDF 模型。
- 如何實現 Ward 的 BRDF 模型。
進一步閱讀
[edit | edit source]如果你想了解更多關於
- 使用 Phong 反射模型進行光照,你應該閱讀 “鏡面高光”章節。
- 逐畫素光照(即 Phong 著色),你應該閱讀 “平滑鏡面高光”章節。
- Ward 的 BRDF 模型,你應該閱讀他的文章“測量和建模各向異性反射”,計算機圖形學(SIGGRAPH ’92 論文集),第 265-272 頁,1992 年 7 月。(該論文的副本可在 網上 獲得。)或者你可以閱讀 Randi Rost 等人 2009 年由 Addison-Wesley 出版的三版“OpenGL 著色語言”的第 14.3 節,或者閱讀 Wolfgang Engel、Jack Hoxley、Ralf Kornmann、Niko Suni 和 Jason Zink 的“程式設計頂點、幾何和畫素著色器”(第二版,2008 年)照明章節的第 8 節(可在 網上 獲得)。