跳轉至內容

Cg 程式設計/Unity/多重燈光

來自華夏公益教科書,開放世界開放書籍
隧道中有限範圍內的多條地鐵燈光。

本教程涵蓋了單遍多重光源照明。特別是,它涵蓋了 Unity 所謂的 ForwardBase 通道中的“頂點燈光”。

本教程是 “平滑鏡面高光”部分 的擴充套件。如果你還沒有閱讀過該教程,你應該先閱讀它。

單遍多重燈光

[編輯 | 編輯原始碼]

“漫反射”部分 所述,Unity 的正向渲染路徑使用單獨的通道來處理最重要的光源。這些被稱為“畫素燈光”,因為內建著色器使用每畫素照明來渲染它們。所有將渲染模式設定為重要的光源都將作為畫素燈光渲染。如果質量專案設定的畫素燈光數量允許使用更多畫素燈光,那麼一些將渲染模式設定為自動的光源也會作為畫素燈光渲染。其他光源會發生什麼?Unity 的內建著色器將另外四個燈光作為頂點燈光渲染到 ForwardBase 通道中。顧名思義,內建著色器使用每頂點照明來渲染這些燈光。這就是本教程所要講的內容。(進一步的燈光將透過球諧照明來近似,這裡不討論。)

ForwardBase 通道中,四個頂點燈光(即它們的位置和顏色)由以下內建制服指定(參見 Unity 內建著色器變數的文件

  // Built-in uniforms for "vertex lights"
  uniform half4 unity_LightColor[4];
   // array of the colors of the 4 light sources 
  uniform float4 unity_4LightPosX0; 
   // x coordinates of the 4 light sources in world space
  uniform float4 unity_4LightPosY0; 
   // y coordinates of the 4 light sources in world space
  uniform float4 unity_4LightPosZ0; 
   // z coordinates of the 4 light sources in world space
  uniform float4 unity_4LightAtten0; 
   // scale factors for attenuation with squared distance

我們遵循 Unity 的內建著色器,只計算頂點燈光(假設它們是點光源)的漫反射,使用每頂點照明。這可以使用頂點著色器中的以下 for 迴圈來計算

      // Diffuse reflection by four "vertex lights"      
      float3 vertexLighting = float3(0.0, 0.0, 0.0);
      #ifdef VERTEXLIGHT_ON
      for (int index = 0; index < 4; index++)
      {  
        float4 lightPosition = float4(unity_4LightPosX0[index], 
         unity_4LightPosY0[index], 
         unity_4LightPosZ0[index], 1.0);
 
        float3 vertexToLightSource = 
         lightPosition.xyz - output.posWorld.xyz;    
        float3 lightDirection = normalize(vertexToLightSource);
        float squaredDistance = 
         dot(vertexToLightSource, vertexToLightSource);
        float attenuation = 1.0 / (1.0 + 
         unity_4LightAtten0[index] * squaredDistance);
        float3 diffuseReflection = attenuation 
         * unity_LightColor[index].rgb * _Color.rgb 
         * max(0.0, dot(output.normalDir, lightDirection));     
 
        vertexLighting = vertexLighting + diffuseReflection;
      }
      #endif

所有頂點燈光的總漫反射照明在 vertexLighting 中累加,透過將其初始化為黑色,然後在 for 迴圈結束時將每個頂點燈光的漫反射新增到 vertexLighting 的先前值。for 迴圈對於任何 C/C++/Java/JavaScript 程式設計師來說應該很熟悉。請注意,Cg 中的 for 迴圈對於某些 GPU 來說是嚴重受限的;特別是限制(這裡:0 和 4)對於某些 GPU 來說必須是常量,也就是說,你甚至不能使用制服來確定限制。(技術原因是,為了“展開”迴圈,必須在編譯時知道限制。)

這或多或少是 Unity 內建著色器中頂點燈光的計算方式。但是,請記住,沒有什麼能阻止你使用這些“頂點燈光”來計算鏡面反射或每畫素照明。

完整著色器程式碼

[編輯 | 編輯原始碼]

“平滑鏡面高光”部分 的著色器程式碼上下文中,完整著色器程式碼為

Shader "Cg per-pixel lighting with vertex lights" {
  Properties {
   _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 
      // 4 vertex lights, ambient light & first pixel light
 
     CGPROGRAM
     #pragma multi_compile_fwdbase 
     #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 _Shininess;
 
     struct vertexInput {
      float4 vertex : POSITION;
      float3 normal : NORMAL;
     };
     struct vertexOutput {
      float4 pos : SV_POSITION;
      float4 posWorld : TEXCOORD0;
      float3 normalDir : TEXCOORD1;
      float3 vertexLighting : TEXCOORD2;
     };
 
     vertexOutput vert(vertexInput input)
     {     
      vertexOutput output;
 
      float4x4 modelMatrix = unity_ObjectToWorld;
      float4x4 modelMatrixInverse = unity_WorldToObject; 
 
      output.posWorld = mul(modelMatrix, input.vertex);
      output.normalDir = normalize(
        mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
      output.pos = UnityObjectToClipPos(input.vertex);
 
      // Diffuse reflection by four "vertex lights"      
      output.vertexLighting = float3(0.0, 0.0, 0.0);
      #ifdef VERTEXLIGHT_ON
      for (int index = 0; index < 4; index++)
      {  
        float4 lightPosition = float4(unity_4LightPosX0[index], 
         unity_4LightPosY0[index], 
         unity_4LightPosZ0[index], 1.0);
 
        float3 vertexToLightSource = 
         lightPosition.xyz - output.posWorld.xyz;    
        float3 lightDirection = normalize(vertexToLightSource);
        float squaredDistance = 
         dot(vertexToLightSource, vertexToLightSource);
        float attenuation = 1.0 / (1.0 + 
         unity_4LightAtten0[index] * squaredDistance);
        float3 diffuseReflection = attenuation 
         * unity_LightColor[index].rgb * _Color.rgb 
         * max(0.0, dot(output.normalDir, lightDirection));     
 
        output.vertexLighting = 
         output.vertexLighting + diffuseReflection;
      }
      #endif
      return output;
     }
 
     float4 frag(vertexOutput input) : COLOR
     {
      float3 normalDirection = normalize(input.normalDir); 
      float3 viewDirection = normalize(
        _WorldSpaceCameraPos - input.posWorld.xyz);
      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 ambientLighting = 
        UNITY_LIGHTMODEL_AMBIENT.rgb * _Color.rgb;
 
      float3 diffuseReflection = 
        attenuation * _LightColor0.rgb * _Color.rgb 
        * max(0.0, dot(normalDirection, lightDirection));
 
      float3 specularReflection;
      if (dot(normalDirection, lightDirection) < 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
      {
        specularReflection = attenuation * _LightColor0.rgb 
         * _SpecColor.rgb * pow(max(0.0, dot(
         reflect(-lightDirection, normalDirection), 
         viewDirection)), _Shininess);
      }
 
      return float4(input.vertexLighting + 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 _Shininess;
 
     struct vertexInput {
      float4 vertex : POSITION;
      float3 normal : NORMAL;
     };
     struct vertexOutput {
      float4 pos : SV_POSITION;
      float4 posWorld : TEXCOORD0;
      float3 normalDir : TEXCOORD1;
     };
 
     vertexOutput vert(vertexInput input) 
     {
      vertexOutput output;
 
      float4x4 modelMatrix = unity_ObjectToWorld;
      float4x4 modelMatrixInverse = unity_WorldToObject; 
 
      output.posWorld = mul(modelMatrix, input.vertex);
      output.normalDir = normalize(
        mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
      output.pos = UnityObjectToClipPos(input.vertex);
      return output;
     }
 
     float4 frag(vertexOutput input) : COLOR
     {
      float3 normalDirection = normalize(input.normalDir);
 
      float3 viewDirection = normalize(
        _WorldSpaceCameraPos.xyz - input.posWorld.xyz);
      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 diffuseReflection = 
        attenuation * _LightColor0.rgb * _Color.rgb
        * max(0.0, dot(normalDirection, lightDirection));
 
      float3 specularReflection;
      if (dot(normalDirection, lightDirection) < 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
      {
        specularReflection = attenuation * _LightColor0.rgb 
         * _SpecColor.rgb * pow(max(0.0, dot(
         reflect(-lightDirection, normalDirection), 
         viewDirection)), _Shininess);
      }
 
      return float4(diffuseReflection 
        + specularReflection, 1.0);
        // no ambient lighting in this pass
     }
 
     ENDCG
   }
 
  } 
  Fallback "Specular"
}

使用 #pragma multi_compile_fwdbase#ifdef VERTEXLIGHT_ON ... #endif 是必要的,以確保當 Unity 不提供資料時,不會計算頂點照明;另見 Unity 關於 multi_compile 指令的文件

恭喜你,你已經完成了本教程。我們已經看到了

  • Unity 的頂點燈光是如何指定的。
  • 如何在 Cg 中使用 for 迴圈來計算單遍多重燈光的照明。

進一步閱讀

[編輯 | 編輯原始碼]

如果你還想了解更多

< Cg 程式設計/Unity

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