【Unity Shaders】Shader中的光照

Forward Rendering Path的渲染細節

 

在開始後面的討論以前,先要弄懂一個問題就是Unity能夠在Forward Rendering Path中能夠處理哪些以及處理多少光照。這裏只提取官方文檔中的一些內容加以說明。html

 

在Forward Rendering中,有三種處理光照(即照亮物體)的方式:逐頂點處理,逐像素處理,球諧函數(Spherical Harmonics,SH)處理。而決定一個燈光是哪一種處理模式取決於它的類型和模式:編程

 

  • 場景中最亮的平行光老是逐像素處理的。這意味着,若是場景裏只有一個平行光,是否設置它的模式都可有可無。
  • Render Mode被設置成Not Important的光源,會按逐頂點或者球諧函數處理。經試驗,第一點中的平行光不受這點的約束。
  • Render Mode被設置成Important的光源,會按逐像素處理。
  • 如根據以上規則獲得的像素光源數量小於設置中的像素光源數量(Pixel Light Count),爲了減小亮度,會有更多的光源以逐像素的方式進行渲染。
    • 這一點我沒有讀懂,按個人實驗結果是,若是全部的光源設置成Auto,那麼逐像素光源的數目不會超過Pixel Light Count。但若是設置了Render Mode爲明確的Not Important或者Important,那麼設置Pixel Light Count彷佛沒有任何影響。
       

 

 

那在哪裏進行光照處理呢?固然是在Pass裏。Forward Rendering有兩種Pass:Base Pass,Additional Passes。這兩種Pass的圖例說明以下:數組

 

注意其中的Per-Vertex Lights/SH Lights前面我標註了可選的,這是說,咱們能夠選擇是否處理這些光源。若是咱們沒有在Base Pass中寫明相關的處理函數,那麼這些光源實際上不會對物體產生影響。另外一點就是其中橘黃色字代表的代碼,其中Tags我就不贅述了,這是基本要求。「#pragma multi_compile_fwdbase」這種在長久的實驗中代表最好是寫上它們,這會讓一些函數和宏能夠正確工做,很惋惜,如今官方沒有給出明確的文檔說明,所以咱們仍是乖乖地每次都加上它們比較好。最後,注意對於Forward Rendering來講,只有Bass Pass中處理的第一個平行光能夠有陰影效果緩存

 

從上面的圖中,咱們已經知道,因爲逐像素的光源是最重要的一種光源,所以Unity會花費一整個Pass來處理它。而對於逐頂點/SH光源來講,它們都將會在Bass Pass中處理(和最重要的平行光一塊兒)。沒份量就是這種結果。那麼,Base Pass會說,「我這麼小就讓我作這麼多東西,平行光就一個數量少就算了,SH光工做量少也算了,但頂點光也來搗亂我就不幹了,不行!我得有條件!」因而Unity規定說,最多隻有4個光源會按照逐頂點光源來處理,其餘只能按SH光源處理。bash

 

這裏很容易就弄混弄蒙了。咱們先來看官方給的狀況,即第一種狀況:全部光源都被設置成Auto。這種狀況下,Unity會自動爲光源選擇合適的類型。這時,有一個項目設置很重要就是Pixel Light Count,它決定了逐像素光的最大數目。當Pixel Light Count爲4時,就是那張著名的圖例狀況(來自官方文檔):cookie

 

 

上面的類型選擇過程大概是這樣的:首先,前Pixel Light Count(這裏是4)個光源會按照逐像素進行處理,而後最多4個逐頂點光源,剩下的就是SH光了。其中,注意每種光源之間會有重疊的狀況,這主要是爲了防止物體移動時光照產生突變。數據結構

可是,若是光源沒有被設置爲Auto,而是被指明是Important和Not Important,又會怎樣呢?(不要問我有的被設置成Auto,有的設置成Important會怎樣,你這人真討厭本身分析吧。。。)那麼,第二種狀況:自定義光源類型。首先,記住一點,這時再也不受Pixel Light Count的限制,那麼被設置成Important所有會被當成逐像素光源,一個不剩;若是被設置成Not Important,那麼最多有4個光源會被當成逐頂點光源,其餘就會被當作SH光源進行處理。app

上面聽起來很複雜,其實就是個「物競天擇」的過程。咱們能夠想象,全部的光源都在爭搶更多的計算資源,都想讓本身成爲最重要的逐像素光,再不濟點就逐頂點光,要是實在混的很差就只能當成SH光了。那麼掙到了資源又怎麼處理呢?對於逐像素光,它有一整個Pass的資源能夠揮霍,而這裏會涉及到各類光照變量和函數的使用,後面會講;對於逐頂點光和SH光來講,很惋惜,Unity並無明確的文檔來告訴咱們如何訪問它們,咱們只能經過UnityShaderVariables.cginc中的變量聲明和Surface Shader的編譯結果來「揣測」用法。這也是後面講的內容。ide

吐槽時間:雖然文檔上這麼寫,但實際過程當中仍是有不少莫名其妙的問題:函數

  • 奇葩狀況一:我在4.6.1版本中,建立一個場景包含了1個平行光+4個點光源,若是使用的Shader沒有Additional Passes的定義話,那麼4個點光源即使設置成Important,仍是會被Unity當成逐頂點光源。
  • 奇葩狀況二:若是隻定義了Additional Passes,而沒有Base Pass的話,就更奇葩了,整個Pass感受都沒有在工做,而獲得的結果像是上次緩存之類的東西。總之,請必定要先定義Base Pass再定義Additional Passes。不要任性!
  • 其餘更多奇葩等待你發現

光照變量和函數

 

在UnityShaderVariables.cginc文件中,咱們能夠找到Unity提供的和處理光照有關的變量:

CBUFFER_START(UnityLighting)


#ifdef USING_DIRECTIONAL_LIGHT

uniform fixed4 _WorldSpaceLightPos0;

#else

uniform float4 _WorldSpaceLightPos0;

#endif


uniform float4 _LightPositionRange; // xyz = pos, w = 1/range


// Built-in uniforms for "vertex lights"

float4 unity_4LightPosX0; // x coordinates of the 4 light sources in world space

float4 unity_4LightPosY0; // y coordinates of the 4 light sources in world space

float4 unity_4LightPosZ0; // z coordinates of the 4 light sources in world space

float4 unity_4LightAtten0; // scale factors for attenuation with squared distance


float4 unity_LightColor[8]; // array of the colors of the 4 light sources

float4 unity_LightPosition[8]; // apparently is not always correctly set

// x = -1

// y = 1

// z = quadratic attenuation

// w = range^2

float4 unity_LightAtten[8]; // apparently is not always correctly set

float4 unity_SpotDirection[8];


// SH lighting environment

float4 unity_SHAr;

float4 unity_SHAg;

float4 unity_SHAb;

float4 unity_SHBr;

float4 unity_SHBg;

float4 unity_SHBb;

float4 unity_SHC;

CBUFFER_END

在UnityCG.cginc能夠找到光照處理輔助函數:

// Computes world space light direction

inline float3 WorldSpaceLightDir( in float4 v );


// Computes object space light direction

inline float3 ObjSpaceLightDir( in float4 v );


// Computes world space view direction

inline float3 WorldSpaceViewDir( in float4 v );


// Computes object space view direction

inline float3 ObjSpaceViewDir( in float4 v );


float3 Shade4PointLights (

float4 lightPosX, float4 lightPosY, float4 lightPosZ,

float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,

float4 lightAttenSq,

float3 pos, float3 normal);


float3 ShadeVertexLights (float4 vertex, float3 normal);


// normal should be normalized, w=1.0

half3 ShadeSH9 (half4 normal);

下面咱們來看下如何在兩種Pass中使用上面的變量和函數處理不一樣類型的光照。

一個基本的Shader

 

下面的討論主要創建在下面的代碼下,能夠先掃一遍,這裏不用細看。它主要計算了漫反射光照和高光反射光照,還示例了逐頂點光源和SH光源的計算等。

Shader "Light Test" {

Properties {

_Color ("Color", color) = (1.0,1.0,1.0,1.0)

}

SubShader {

Tags { "RenderType"="Opaque"}


Pass {

Tags { "LightMode"="ForwardBase"} // pass for 4 vertex lights, ambient light & first pixel light (directional light)


CGPROGRAM

// Apparently need to add this declaration

#pragma multi_compile_fwdbase


#pragma vertex vert

#pragma fragment frag


#include "UnityCG.cginc"

#include "Lighting.cginc"

#include "AutoLight.cginc"


uniform float4 _Color;


struct vertexInput {

float4 vertex : POSITION;

float3 normal : NORMAL;

};

struct vertexOutput {

float4 pos : SV_POSITION;

float4 posWorld : TEXCOORD0;

float3 normalDir : TEXCOORD1;

float3 lightDir : TEXCOORD2;

float3 viewDir : TEXCOORD3;

float3 vertexLighting : TEXCOORD4;

LIGHTING_COORDS(5, 6)

};


vertexOutput vert(vertexInput input) {

vertexOutput output;


output.pos = mul(UNITY_MATRIX_MVP, input.vertex);

output.posWorld = mul(_Object2World, input.vertex);

output.normalDir = normalize(mul(float4(input.normal, 0.0), _World2Object).xyz);

output.lightDir = WorldSpaceLightDir(input.vertex);

output.viewDir = WorldSpaceViewDir(input.vertex);

output.vertexLighting = float3(0.0);


// SH/ambient and vertex lights

#ifdef LIGHTMAP_OFF

float3 shLight = ShadeSH9 (float4(output.normalDir, 1.0));

output.vertexLighting = shLight;

#ifdef VERTEXLIGHT_ON

float3 vertexLight = Shade4PointLights (

unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,

unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,

unity_4LightAtten0, output.posWorld, output.normalDir);

output.vertexLighting += vertexLight;

#endif // VERTEXLIGHT_ON

#endif // LIGHTMAP_OFF


// pass lighting information to pixel shader

TRANSFER_VERTEX_TO_FRAGMENT(output);


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);

}


// LIGHT_ATTENUATION not only compute attenuation, but also shadow infos

// attenuation = LIGHT_ATTENUATION(input);

// Compare to directions computed from vertex

// viewDirection = normalize(input.viewDir);

// lightDirection = normalize(input.lightDir);


// Because SH lights contain ambient, we don't need to add it to the final result

float3 ambientLighting = UNITY_LIGHTMODEL_AMBIENT.xyz;


float3 diffuseReflection = attenuation * _LightColor0.rgb * _Color.rgb * max(0.0, dot(normalDirection, lightDirection)) * 2;


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 * _Color.rgb * pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)), 255);

}


return float4(input.vertexLighting + diffuseReflection + specularReflection, 1.0);

}

ENDCG

}


Pass{

Tags { "LightMode"="ForwardAdd"} // pass for additional light sources

ZWrite Off Blend One One Fog { Color (0,0,0,0) } // additive blending


CGPROGRAM

// Apparently need to add this declaration

#pragma multi_compile_fwdadd


#pragma vertex vert

#pragma fragment frag


#include "UnityCG.cginc"

#include "Lighting.cginc"

#include "AutoLight.cginc"


uniform float4 _Color;


struct vertexInput {

float4 vertex : POSITION;

float3 normal : NORMAL;

};

struct vertexOutput {

float4 pos : SV_POSITION;

float4 posWorld : TEXCOORD0;

float3 normalDir : TEXCOORD1;

float3 lightDir : TEXCOORD2;

float3 viewDir : TEXCOORD3;

LIGHTING_COORDS(4, 5)

};


vertexOutput vert(vertexInput input) {

vertexOutput output;


output.pos = mul(UNITY_MATRIX_MVP, input.vertex);

output.posWorld = mul(_Object2World, input.vertex);

output.normalDir = normalize(mul(float4(input.normal, 0.0), _World2Object).xyz);

output.lightDir = WorldSpaceLightDir(input.vertex);

output.viewDir = WorldSpaceViewDir(input.vertex);


// pass lighting information to pixel shader

vertexInput v = input;

TRANSFER_VERTEX_TO_FRAGMENT(output);


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);

}


// LIGHT_ATTENUATION not only compute attenuation, but also shadow infos

// attenuation = LIGHT_ATTENUATION(input);

// Compare to directions computed from vertex

// viewDirection = normalize(input.viewDir);

// lightDirection = normalize(input.lightDir);


float3 diffuseReflection = attenuation * _LightColor0.rgb * _Color.rgb * max(0.0, dot(normalDirection, lightDirection)) * 2;


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 * _Color.rgb * pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)), 255);

}


return float4(diffuseReflection + specularReflection, 1.0);

}

ENDCG

}

}

FallBack "Diffuse"

}​​​​​​​

​​​​​​​Base Pass

回想一下,上面咱們說過在Bass Pass中,咱們能夠處理所有三種光照:處理第一個平行光做爲逐像素光處理,處理全部的逐頂點光,處理其餘全部SH光。還有很重要的一點就是,咱們還要處理環境光、陰影等。一句話,因爲Additional Passes只能處理逐像素光,若是你想要其餘光照效果,都須要在Bass Pass中處理。

環境光

這裏的環境光指的是咱們在Edit->Render Setting裏面的Ambient Light的值。在Shader中獲取它很容易,只須要訪問全局變量UNITY_LIGHTMODEL_AMBIENT便可。它是全局變量,所以在在哪一個Pass裏訪問均可以,但環境光只須要加一次便可,所以咱們只須要在Bass Pass中疊加到其餘顏色上便可。

陰影和光照衰減

Base Pass還有一個很是重要的做用就是添加陰影。上面提到過,對於Forward Rendering來講,只有Bass Pass中處理的第一個平行光能夠有陰影效果。也就是說,錯過了這裏就不會獲得陰影信息了。程序中模擬陰影主要是依靠一張Shadow Map,裏面記錄了從光源出發距離它最近的深度信息。Unity很貼心地提供了這樣的一張紋理(_ShadowMapTexture),不用咱們本身再編程實現了。

與陰影的實現相似,Unity還提供了一張紋理(_LightTexture0),這張紋理包含了光照衰減(attenuation)。

因爲陰影和光照衰減都是對紋理進行採樣,而後將結果乘以顏色值,所以Unity把這兩步合併到一個宏中,讓咱們經過一個宏調用就能夠解決這兩個問題。既然是對紋理採樣,那麼首先就要知道頂點對應的紋理座標,Unity一樣是經過宏來輔助咱們完成的,咱們只須要在v2f(vertexOutput)中添加關於宏LIGHTING_COORDS便可。而後,爲了計算頂點對應的兩張紋理上的座標,須要在vert函數裏面調用一個新的宏:TRANSFER_VERTEX_TO_FRAGMENT。

這個過程當中使用的宏定義都在AutoLight.cginc文件中。

一個完整的過程以下:

首先咱們必須聲明Pass和#pragma,這樣才能夠保證Unity會正確填充紋理和座標:
ForwardAdd Pass也是相似的。

Tags { "LightMode"="ForwardBase"} // pass for 4 vertex lights, ambient light & first pixel light (directional light)


CGPROGRAM

// Apparently need to add this declaration

#pragma multi_compile_fwdbase

定義光照紋理和陰影紋理的紋理座標:
即上面的最後一行,LIGHTING_COORDS(5, 6)。5和6指明變量的存儲位置。這個宏的定義會根據光源類型、有無cookie發生變化。

struct vertexOutput {

float4 pos : SV_POSITION;

float4 posWorld : TEXCOORD0;

float3 normalDir : TEXCOORD1;

float3 lightDir : TEXCOORD2;

float3 viewDir : TEXCOORD3;

float3 vertexLighting : TEXCOORD4;

LIGHTING_COORDS(5, 6)

};

上面的代碼來自Forward Add中的一段。注意上面從新定義了一個結構v,這是由於TRANSFER_VERTEX_TO_FRAGMENT中,若是該光源非平行光,就須要利用頂點位置來計算衰減(平行光不須要計算衰減),而頂點的訪問,宏裏直接使用了v,這意味着咱們必須在上下文中提供一個名爲v的頂點數據結構。固然,咱們能夠直接把vertexInput的命名換成v就不須要這樣轉換了。。。

而後,須要在vert函數中計算正確的紋理座標:

vertexOutput vert(vertexInput input) {

vertexOutput output;


......


// pass lighting information to pixel shader

vertexInput v = input;

TRANSFER_VERTEX_TO_FRAGMENT(output);


return output;

}

最後,咱們在frag函數中請求獲得陰影或衰減值:

attenuation = LIGHT_ATTENUATION(input);

Unity就是使用了這三個宏來完成陰影和衰減的計算的。咱們來看一下這三個宏究竟是個什麼東東。這裏僅以不開啓cookie的平行光和點光源爲例:

#ifdef POINT

#define LIGHTING_COORDS(idx1,idx2) float3 _LightCoord : TEXCOORD##idx1; SHADOW_COORDS(idx2)

uniform sampler2D _LightTexture0;

uniform float4x4 _LightMatrix0;

#define TRANSFER_VERTEX_TO_FRAGMENT(a) a._LightCoord = mul(_LightMatrix0, mul(_Object2World, v.vertex)).xyz; TRANSFER_SHADOW(a)

#define LIGHT_ATTENUATION(a) (tex2D(_LightTexture0, dot(a._LightCoord,a._LightCoord).rr).UNITY_ATTEN_CHANNEL * SHADOW_ATTENUATION(a))

#endif


#ifdef DIRECTIONAL

#define LIGHTING_COORDS(idx1,idx2) SHADOW_COORDS(idx1)

#define TRANSFER_VERTEX_TO_FRAGMENT(a) TRANSFER_SHADOW(a)

#define LIGHT_ATTENUATION(a) SHADOW_ATTENUATION(a)

#endif

#define SHADOW_COORDS(idx1) float4 _ShadowCoord : TEXCOORD##idx1;

#define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_World2Shadow[0], mul(_Object2World,v.vertex));

#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)

能夠發現,對於點光源來講,會計算兩種紋理,即光照衰減紋理和陰影紋理,並在最後計算attenuation的時候,就是將兩種紋理的採樣結果相乘。而對於平行光來講更加簡單,因爲平行光沒有衰減,所以只須要計算陰影紋理就能夠了。

再次強調如下,Forward Rendering來講,只有Bass Pass中處理的第一個平行光能夠有陰影效果。例如,下面左圖中的平行光能夠投射出陰影,而右圖中即使小球在光源和小蘋果的中間也不會產生任何陰影:

 

逐頂點光照

其實逐頂點光照就是一個名字,Unity把這些所謂的「逐頂點光照」的數據存儲在一些變量中,咱們徹底能夠按逐像素的方式來處理它們。固然,處於性能的考慮,咱們一般仍是會在頂點函數階段處理它們,所以把它們稱爲逐頂點光照。

逐頂點光照涉及的變量和函數有兩組。這裏的組別主要是依靠Unity提供的頂點光照計算函數使用的變量來歸類的。

第一組以下:

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


對應的函數以下:

float3 Shade4PointLights (

float4 lightPosX, float4 lightPosY, float4 lightPosZ,

float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,

float4 lightAttenSq,

float3 pos, float3 normal)

{

// to light vectors

float4 toLightX = lightPosX - pos.x;

float4 toLightY = lightPosY - pos.y;

float4 toLightZ = lightPosZ - pos.z;

// squared lengths

float4 lengthSq = 0;

lengthSq += toLightX * toLightX;

lengthSq += toLightY * toLightY;

lengthSq += toLightZ * toLightZ;

// NdotL

float4 ndotl = 0;

ndotl += toLightX * normal.x;

ndotl += toLightY * normal.y;

ndotl += toLightZ * normal.z;

// correct NdotL

float4 corr = rsqrt(lengthSq);

ndotl = max (float4(0,0,0,0), ndotl * corr);

// attenuation

float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);

float4 diff = ndotl * atten;

// final color

float3 col = 0;

col += lightColor0 * diff.x;

col += lightColor1 * diff.y;

col += lightColor2 * diff.z;

col += lightColor3 * diff.w;

return col;

}

調用的話代碼以下:​​​​​​​

float3 vertexLight = Shade4PointLights (

unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,

unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,

unity_4LightAtten0, output.posWorld, output.normalDir);

注意其中頂點位置和法線方向都是指在世界座標系下的。
第二組變量:​​​​​​​

float4 unity_LightPosition[8]; // apparently is not always correctly set

// x = -1

// y = 1

// z = quadratic attenuation

// w = range^2

float4 unity_LightAtten[8]; // apparently is not always correctly set

float4 unity_SpotDirection[8];

函數:​​​​​​​

float3 ShadeVertexLights (float4 vertex, float3 normal)

{

float3 viewpos = mul (UNITY_MATRIX_MV, vertex).xyz;

float3 viewN = mul ((float3x3)UNITY_MATRIX_IT_MV, normal);

float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;

for (int i = 0; i < 4; i++) {

float3 toLight = unity_LightPosition[i].xyz - viewpos.xyz * unity_LightPosition[i].w;

float lengthSq = dot(toLight, toLight);

float atten = 1.0 / (1.0 + lengthSq * unity_LightAtten[i].z);

float diff = max (0, dot (viewN, normalize(toLight)));

lightColor += unity_LightColor[i].rgb * (diff * atten);

}

return lightColor;

}

用法:

vertexLight = ShadeVertexLights(input.vertex, input.normal)


注意其中的頂點座標和法線方向是在對象座標系下的。並且,其計算結果包含了環境光。。。

這兩組函數看起來作了同樣的工做,但其實Forward Rendering咱們只能夠選擇第一組。下面是官方文檔中的解釋:

Forward rendering helper functions in UnityCG.cginc

These functions are only useful when using forward rendering (ForwardBase or ForwardAdd pass types).

  • float3 Shade4PointLights (...) - computes illumination from four point lights, with light data tightly packed into vectors. Forward rendering uses this to compute per-vertex lighting.

Vertex-lit helper functions in UnityCG.cginc

These functions are only useful when using per-vertex lit shaders (「Vertex」 pass type).

  • float3 ShadeVertexLights (float4 vertex, float3 normal) - computes illumination from four per-vertex lights and ambient, given object space position & normal.

文檔裏說的很清楚,對於Forward Rendering來講,咱們應該使用Shade4PointLights來計算最多四個逐頂點光照,並且只能計算Point Lights和Spot Lights,若是一個平行光被設置成逐頂點光源,那麼是不會被計算的。換句話說,咱們應該使用unity_4LightPosX0、unity_4LightPosY0、unity_4LightPosZ0、unity_4LightAtten0這些數據來訪問逐頂點的光源數據。而另外一組是在Vertex Pass(e.g. Tags { "LightMode"="Vertex"})中使用的。

還有有一些須要咱們瞭解的地方

  • Unity給出的函數只是爲了方便咱們提供的一種計算方法,能夠看出來Shade4PointLights中,只是按逐頂點的方法(即只需在vert函數中提供頂點位置和法線)計算了漫反射方向的光照,但咱們也徹底能夠本身根據這些光照變量處理逐頂點光源,例如添加高光反射等等。
  • 咱們甚至還能夠按照逐像素的方式來處理它們,即在frag函數裏訪問並計算它們。只要你願意,沒有什麼能夠阻止你這麼作。(就是這麼任性。)

好啦,說完了理論咱們來看下視覺效果是怎樣的。咱們在場景裏放了一個小蘋果+一個球,而且放了四個不一樣顏色的點光源,只輸出Shade4PointLights的結果以下(左圖爲逐頂點光照,右圖爲逐像素光照):

 

能夠看出來,逐頂點光源從視覺效果上不如逐像素光源,但性能更好。

那麼,還有一個問題,即支持計算的逐頂點光源數目最多爲4個,定義的存儲逐頂點光源信息的變量數組也只有4維。也就是說,若是場景裏被設置(或者排序後獲得的數目)成逐頂點光源的數目大於4個,那麼Unity會對它們進行排序,把其中最重要的4個光源存儲到那些變量中。但這種排序方法Unity沒有文檔進行說明,而從實驗結果來看,這個排序結果和光的顏色、密度、距離都有關。例如,若是咱們再加一個藍色光源,能夠發現不會對結果有任何變化:

而若是咱們調整它的顏色、密度、或者位置時,因爲排序結果發生變化,就會生成光照突變(左圖爲改變顏色,右圖爲改變密度):

  

SH光照

那些既不是逐像素光又不是逐頂點光的光源,若是想對物體產生影響,就只能按SH光照進行處理。宮鬥失敗就是這個結果。Unity裏和計算SH光有關的變量和函數以下:

// SH lighting environment

float4 unity_SHAr;

float4 unity_SHAg;

float4 unity_SHAb;

float4 unity_SHBr;

float4 unity_SHBg;

float4 unity_SHBb;

float4 unity_SHC;​​​​​​​
// normal should be normalized, w=1.0

half3 ShadeSH9 (half4 normal)

{

half3 x1, x2, x3;


// Linear + constant polynomial terms

x1.r = dot(unity_SHAr,normal);

x1.g = dot(unity_SHAg,normal);

x1.b = dot(unity_SHAb,normal);


// 4 of the quadratic polynomials

half4 vB = normal.xyzz * normal.yzzx;

x2.r = dot(unity_SHBr,vB);

x2.g = dot(unity_SHBg,vB);

x2.b = dot(unity_SHBb,vB);


// Final quadratic polynomial

float vC = normal.x*normal.x - normal.y*normal.y;

x3 = unity_SHC.rgb * vC;

return x1 + x2 + x3;

}

調用代碼以下:

float3 shLight = ShadeSH9 (float4(output.normalDir, 1.0));

關於SH光照的實現細節我沒有研究,有興趣的能夠查資料理解下上面函數的含義。以前有網友留言告訴我一篇文章。但太長了我沒看。。。還有論壇中的一個帖子,能夠看看裏面的代碼初步瞭解一下。

咱們以以前的例子爲例,看一下只輸出SH光照的結果。下面左圖中,是隻有四個光源的狀況,能夠看出此時並無任何SH光,這是由於這四個光源此時被當作是逐頂點光照。這裏物體顏色非黑是由於unity_SHAr、unity_SHAg、unity_SHAb包含了環境光數據,而非真正的光照形成的,所以理論上只要包含了計算SH光照的代碼就不須要在最後結果上添加上面提到的環境光了。右圖則是增長了4個新的Not Important光源後的SH光照結果。

 

咱們將逐頂點光照和SH光照結合在一塊兒,代碼以下:

// SH/ambient and vertex lights

#ifdef LIGHTMAP_OFF

float3 shLight = ShadeSH9 (float4(output.normalDir, 1.0));

output.vertexLighting = shLight;

#ifdef VERTEXLIGHT_ON

float3 vertexLight = Shade4PointLights (

unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,

unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,

unity_4LightAtten0, output.posWorld, output.normalDir);

output.vertexLighting += vertexLight;

#endif // VERTEXLIGHT_ON

#endif // LIGHTMAP_OFF

其中,須要添加#ifdef這些聲明是爲了保證,在Unity不提供這些數據時能夠不用計算這些光照。

咱們把二者相加的結果輸出,能夠獲得如下的結果:

Additional Passes

最後,咱們來談談Additional Passes中的逐像素光。咱們須要知道的是,其實在Base Pass中咱們也須要處理逐像素光,但咱們能夠明確的知道這個逐像素光只能是第一個平行光。而在Additional Passes中,逐像素光多是平行光、點光源、聚光燈光源(Spot Light)。這裏不討論使用了LightMap或者開啓了Cookie的狀況。

一樣,這裏的逐像素光其實也只是一個名字,Unity只是負責把所謂的逐像素光的數據放到一些變量中,可是,沒有什麼能夠阻止咱們是在vert中計算仍是在frag中計算。

注意:想要Additional Passes是疊加在Bass Pass上的話(通常人的目的都是這個),請確保你給Pass添加了合適的混合模式。例如:

Pass{

Tags { "LightMode"="ForwardAdd"} // pass for additional light sources

ZWrite Off Blend One One Fog { Color (0,0,0,0) } // additive blending​​​​​​​

對於逐像素光照,咱們最長使用的變量和函數以下:

來自UnityShaderVariables.cginc:

uniform float4 _WorldSpaceLightPos0;

uniform float3 _WorldSpaceCameraPos;

來自Lighting.cginc:

fixed4 _LightColor0;

來自UnityCG.cginc(文檔說明):​​​​​​​

// Computes world space light direction

inline float3 WorldSpaceLightDir( in float4 v );

// Computes object space light direction

inline float3 ObjSpaceLightDir( in float4 v );

// Computes world space view direction

inline float3 WorldSpaceViewDir( in float4 v );

// Computes object space view direction

inline float3 ObjSpaceViewDir( in float4 v );

能夠發現,只有函數給出了明確的文檔說明,其餘都只能靠Unity內部Shader的結構來揣測了。

咱們先無論這些變量和函數,先來想一想咱們到底想利用逐像素光照來計算什麼,在哪裏計算。最多見的需求就是計算光源方向和視角方向,而後再進行漫反射和高光反射的計算。在Unity裏在哪裏計算這些方向彷佛從視覺上沒有太大的區別,理論上在vert中計算比在frag中計算更快一點。但計算位置的選擇決定了咱們能夠如何使用上面的變量和函數。

能夠注意到,Unity提供的函數都是在vert函數中的輔助函數,即都是隻須要提供頂點位置就能夠獲得光照方向和視角方向的。也就是說,若是咱們想要在vert函數中就計算各個方向的值,能夠這麼作:

output.lightDir = WorldSpaceLightDir(input.vertex);

output.viewDir = WorldSpaceViewDir(input.vertex);

固然,上面是獲得世界座標系下的用法,咱們也能夠獲得對象座標系下的,看需求便可。這些函數其實也是利用了_WorldSpaceLightPos0和_WorldSpaceCameraPos而已。例如WorldSpaceLightDir的定義以下:

// Computes world space light direction

inline float3 WorldSpaceLightDir( in float4 v )

{

float3 worldPos = mul(_Object2World, v).xyz;

#ifndef USING_LIGHT_MULTI_COMPILE

return _WorldSpaceLightPos0.xyz - worldPos * _WorldSpaceLightPos0.w;

#else

#ifndef USING_DIRECTIONAL_LIGHT

return _WorldSpaceLightPos0.xyz - worldPos;

#else

return _WorldSpaceLightPos0.xyz;

#endif

#endif

}

其中,因爲平行光的方向不隨頂點位置發生變化,所以直接使用_WorldSpaceLightPos0.xyz便可,此時裏面存儲的其實就是平行光的方向,而非位置。同時,_WorldSpaceLightPos0.w能夠代表該光源的類型,若是爲0表示是平行光,爲1表示是點光源或者聚光燈光源。所以,咱們經常能夠看到相似下面的代碼:

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;

lightDirection = normalize(vertexToLightSource);

}

實際上是和WorldSpaceLightDir函數的意義是同樣的。

_LightColor0就沒什麼可說的了,就是存儲了該逐像素光的顏色。

相關文章
相關標籤/搜索