跳轉到內容

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

來自華夏公益教科書
拉絲鋁。注意鏡面高光的形狀,它遠非圓形。

本教程涵蓋各向異性鏡面高光

它是關於光照的幾個教程之一,超出了 Phong 反射模型。然而,它基於 Phong 反射模型的光照,如“鏡面高光”部分(用於每個頂點光照)和“平滑鏡面高光”部分(用於每個畫素光照)中所述。如果您還沒有閱讀這些教程,您應該先閱讀它們。

雖然 Phong 反射模型對於紙張、塑膠和一些其他具有各向同性反射(即圓形高光)的材料來說相當不錯,但本教程特別關注具有各向異性反射(即非圓形高光)的材料,例如左側照片中的拉絲鋁。

除了 Phong 反射模型使用的大多數向量之外,我們還需要歸一化的中間向量 H,它是觀察者方向 V 和光源方向 L 之間的精確方向。

Ward 的各向異性反射模型

[編輯 | 編輯原始碼]

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。

還有兩個向量尚未描述:TBT 是表面上的刷子方向,而 BT 正交,但也在表面上。Unity 為我們提供了一個表面上的切線向量作為頂點屬性(見 “著色器除錯”部分),我們將把它用作向量 T。計算 NT 的叉積會生成一個向量 B,它與 NT 正交,正如它應該的那樣。

Ward's BRDF 模型的實現

[編輯 | 編輯來源]

我們基於 “平滑鏡面高光”章節 中的逐畫素光照著色器實現。我們還需要一個頂點輸出引數 `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 節(可在 網上 獲得)。

< Cg Programming/Unity

除非另有說明,本頁面上的所有示例原始碼均屬於公共領域。
華夏公益教科書