Cg 程式設計/Unity/Cookie


本教程涵蓋了光空間中的投影紋理對映,這對實現聚光燈和方向光源的 Cookie 非常有用。(實際上,Unity 對任何聚光燈都使用內建 Cookie。)在 Unity 中,許多內建著色器可以處理 Cookie;本教程展示了它是如何工作的。
本教程基於“光滑鏡面高光”和“透明紋理”部分的程式碼。如果你還沒有閱讀這些教程,建議先閱讀。
在現實生活中,Gobo 是放置在光源前面的帶孔的固體材料(通常是金屬),用於控制光束或陰影的形狀。Cookie(或“cuculoris”)的作用類似,但它們放置在離光源更遠的地方,如左側影像所示。
在 Unity 中,每個光源在Inspector 視窗(選擇光源時)中都可以指定一個Cookie。這個 Cookie 實際上是一個 Alpha 紋理貼圖(參見“透明紋理”),它被放置在光源前面並隨光源移動(因此它實際上類似於 Gobo)。它允許光線穿過紋理影像 Alpha 分量為 1 的區域,並在 Alpha 分量為 0 的區域阻擋光線。Unity 對聚光燈和方向光源的 Cookie 只是方形的二維 Alpha 紋理貼圖。另一方面,點光源的 Cookie 是立方體貼圖,這裡不再討論。
為了實現 Cookie,我們必須擴充套件任何應該受到 Cookie 影響的表面的著色器。(這與 Unity 的投影機工作方式截然不同;請參見“投影機”部分。)具體來說,我們必須根據著色器照明計算中光源的 Cookie 對每個光源的光線進行衰減。這裡,我們使用“光滑鏡面高光”部分中描述的逐畫素照明;但是,該技術可以應用於任何照明計算。
為了找到 Cookie 紋理中的相關位置,必須將表面柵格化點的座標轉換為光源的座標系。這個座標系非常類似於攝像頭的裁剪座標系,如“頂點變換”部分所述。實際上,理解光源座標系的最佳方式可能是將其視為攝像機。然後,x 和 y 光座標與該假設攝像機的螢幕座標相關。從世界座標系到光座標系的點變換實際上非常容易,因為 Unity 提供了所需的 4×4 矩陣作為統一變數 _LightMatrix0。(否則,我們必須設定類似於檢視變換和投影矩陣的矩陣,這些矩陣在“頂點變換”部分中進行了討論。)
為了獲得最佳效率,應透過將 _LightMatrix0 乘以世界空間中的位置(在頂點著色器中)來執行表面點從世界空間到光空間的變換,例如:
...
uniform float4x4 _LightMatrix0; // transformation
// from world to light space (from Autolight.cginc)
...
struct vertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float4 posWorld : TEXCOORD0;
// position of the vertex (and fragment) in world space
float4 posLight : TEXCOORD1;
// position of the vertex (and fragment) in light space
float3 normalDir : TEXCOORD2;
// surface normal vector in world space
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float4x4 modelMatrix = unity_ObjectToWorld;
float4x4 modelMatrixInverse = unity_WorldToObject;
output.posWorld = mul(modelMatrix, input.vertex);
output.posLight = mul(_LightMatrix0, output.posWorld);
output.normalDir = normalize(
mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
output.pos = UnityObjectToClipPos(input.vertex);
return output;
}
除了統一變數 _LightMatrix0 的定義,以及新的輸出引數 posLight 和計算 posLight 的指令之外,這與“光滑鏡面高光”部分中的頂點著色器相同。
對於方向光源的 Cookie,我們可以直接在片段著色器中使用 posLight 中的 x 和 y 光座標作為 Cookie 紋理 _LightTexture0 的紋理座標進行查詢。然後,將得到的 Alpha 分量乘以計算出的照明;例如:
// compute diffuseReflection and specularReflection
float cookieAttenuation = 1.0;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
cookieAttenuation =
tex2D(_LightTexture0, input.posLight.xy).a;
}
// compute cookieAttenuation for spotlights here
return float4(cookieAttenuation
* (diffuseReflection + specularReflection), 1.0);
對於聚光燈,必須將 posLight 中的 x 和 y 光座標除以 w 光座標。這種除法是投影紋理對映的特徵,與攝像機的透視除法相對應,如“頂點變換”部分所述。Unity 定義矩陣 _LightMatrix0 使我們必須在除法後將兩個座標都加上
cookieAttenuation = tex2D(_LightTexture0,
input.posLight.xy / input.posLight.w
+ float2(0.5, 0.5)).a;
對於某些 GPU,使用內建函式 tex2Dproj 可能更有效,它接受 float3 中的三個紋理座標,並在紋理查詢之前將前兩個座標除以第三個座標。這種方法的一個問題是,我們必須在除以 posLight.w 後加上;但是,tex2Dproj 不允許我們在內部除以第三個紋理座標之後新增任何內容。解決方案是在除以 posLight.w 之前加上 0.5 * input.posLight.w,這相當於在除法之後加上
float3 textureCoords = float3(
input.posLight.x + 0.5 * input.posLight.w,
input.posLight.y + 0.5 * input.posLight.w,
input.posLight.w);
cookieAttenuation =
tex2Dproj(_LightTexture0, textureCoords).a;
請注意,方向光源的紋理查詢也可以透過將 textureCoords 設定為 float3(input.posLight.xy, 1.0) 來使用 tex2Dproj 實現。這將允許我們對方向光源和聚光燈使用同一個紋理查詢,在某些 GPU 上效率更高。
有時,投影紋理對映會產生一個令人不快的副作用:在投影的邊緣,GPU 使用高 mip 級別,這會導致可見的邊界(尤其是對於具有鉗位紋理座標的紋理貼圖)。避免這種情況最簡單的方法是停用紋理影像的 mip 對映:在Project 視窗中找到並選擇紋理影像;然後在Inspector 視窗中將紋理型別設定為高階,並取消選中生成 mip 對映。不要忘記點選應用按鈕。
對於完整的著色器程式碼,我們使用“光滑鏡面高光”部分中 ForwardBase 通道的簡化版本,因為 Unity 只在 ForwardBase 通道中使用沒有 Cookie 的方向光源。所有帶有 Cookie 的光源都由 ForwardAdd 通道處理。我們忽略了點光源的 Cookie,因為 _LightMatrix0[3][3] 為 1.0(但我們將其包含在下一部分中)。聚光燈始終具有 Cookie 紋理:如果使用者沒有指定 Cookie,則 Unity 會提供一個 Cookie 紋理來生成聚光燈的形狀;因此,始終應用 Cookie 是可以的。方向光源並非始終具有 Cookie;但是,如果只有一個沒有 Cookie 的方向光源,則它已在 ForwardBase 通道中處理。因此,除非有多個沒有 Cookie 的方向光源,否則我們可以假設 ForwardAdd 通道中的所有方向光源都有 Cookie。在這種情況下,完整的著色器程式碼可能是
Shader "Cg per-pixel lighting with cookies" {
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 ambient light
// and first directional light source without cookie
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 - input.posWorld.xyz);
float3 lightDirection =
normalize(_WorldSpaceLightPos0.xyz);
float3 ambientLighting =
UNITY_LIGHTMODEL_AMBIENT.rgb * _Color.rgb;
float3 diffuseReflection =
_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 = _LightColor0.rgb
* _SpecColor.rgb * pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess);
}
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")
uniform float4x4 _LightMatrix0; // transformation
// from world to light space (from Autolight.cginc)
uniform sampler2D _LightTexture0;
// cookie alpha texture map (from Autolight.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;
// position of the vertex (and fragment) in world space
float4 posLight : TEXCOORD1;
// position of the vertex (and fragment) in light space
float3 normalDir : TEXCOORD2;
// surface normal vector in world space
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float4x4 modelMatrix = unity_ObjectToWorld;
float4x4 modelMatrixInverse = unity_WorldToObject;
output.posWorld = mul(modelMatrix, input.vertex);
output.posLight = mul(_LightMatrix0, output.posWorld);
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 - 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);
}
float cookieAttenuation = 1.0;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
cookieAttenuation = tex2D(_LightTexture0,
input.posLight.xy).a;
}
else if (1.0 != _LightMatrix0[3][3])
// spotlight (i.e. not a point light)?
{
cookieAttenuation = tex2D(_LightTexture0,
input.posLight.xy / input.posLight.w
+ float2(0.5, 0.5)).a;
}
return float4(cookieAttenuation
* (diffuseReflection + specularReflection), 1.0);
}
ENDCG
}
}
Fallback "Specular"
}
之前的著色器程式碼僅限於最多包含一個無 Cookie 的方向光源的場景。此外,它沒有考慮點光源的 Cookie。編寫更通用的著色器程式碼需要為不同的光源使用不同的 ForwardAdd 通道。(請記住,ForwardBase 通道中的光源始終是無 Cookie 的方向光源。)幸運的是,Unity 提供了一種使用以下 Unity 特定指令生成多個著色器的方法(在 ForwardAdd 通道的 CGPROGRAM 之後)
#pragma multi_compile_lightpass
使用此指令,Unity 將為不同型別的光源多次編譯 ForwardAdd 通道的著色器程式碼。每次編譯都透過定義以下符號之一來區分:DIRECTIONAL、DIRECTIONAL_COOKIE、POINT、POINT_NOATT、POINT_COOKIE、SPOT。著色器程式碼應檢查定義了哪個符號(使用指令 #if defined ... #elif defined ... #endif)幷包含相應的指令。例如
Shader "Cg per-pixel lighting with cookies" {
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 ambient light
// and first directional light source without cookie
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 - input.posWorld.xyz);
float3 lightDirection =
normalize(_WorldSpaceLightPos0.xyz);
float3 ambientLighting =
UNITY_LIGHTMODEL_AMBIENT.rgb * _Color.rgb;
float3 diffuseReflection =
_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 = _LightColor0.rgb
* _SpecColor.rgb * pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess);
}
return float4(ambientLighting + diffuseReflection
+ specularReflection, 1.0);
}
ENDCG
}
Pass {
Tags { "LightMode" = "ForwardAdd" }
// pass for additional light sources
Blend One One // additive blending
CGPROGRAM
#pragma multi_compile_lightpass
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
uniform float4 _LightColor0;
// color of light source (from "Lighting.cginc")
uniform float4x4 _LightMatrix0; // transformation
// from world to light space (from Autolight.cginc)
#if defined (DIRECTIONAL_COOKIE) || defined (SPOT)
uniform sampler2D _LightTexture0;
// cookie alpha texture map (from Autolight.cginc)
#elif defined (POINT_COOKIE)
uniform samplerCUBE _LightTexture0;
// cookie alpha texture map (from Autolight.cginc)
#endif
// 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;
// position of the vertex (and fragment) in world space
float4 posLight : TEXCOORD1;
// position of the vertex (and fragment) in light space
float3 normalDir : TEXCOORD2;
// surface normal vector in world space
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float4x4 modelMatrix = unity_ObjectToWorld;
float4x4 modelMatrixInverse = unity_WorldToObject;
output.posWorld = mul(modelMatrix, input.vertex);
output.posLight = mul(_LightMatrix0, output.posWorld);
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 - input.posWorld.xyz);
float3 lightDirection;
float attenuation = 1.0;
// by default no attenuation with distance
#if defined (DIRECTIONAL) || defined (DIRECTIONAL_COOKIE)
lightDirection = normalize(_WorldSpaceLightPos0.xyz);
#elif defined (POINT_NOATT)
lightDirection = normalize(
_WorldSpaceLightPos0 - input.posWorld.xyz);
#elif defined(POINT)||defined(POINT_COOKIE)||defined(SPOT)
float3 vertexToLightSource =
_WorldSpaceLightPos0.xyz - input.posWorld.xyz;
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
#endif
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);
}
float cookieAttenuation = 1.0;
// by default no cookie attenuation
#if defined (DIRECTIONAL_COOKIE)
cookieAttenuation = tex2D(_LightTexture0,
input.posLight.xy).a;
#elif defined (POINT_COOKIE)
cookieAttenuation = texCUBE(_LightTexture0,
input.posLight.xyz).a;
#elif defined (SPOT)
cookieAttenuation = tex2D(_LightTexture0,
input.posLight.xy / input.posLight.w
+ float2(0.5, 0.5)).a;
#endif
return float4(cookieAttenuation
* (diffuseReflection + specularReflection), 1.0);
}
ENDCG
}
}
Fallback "Specular"
}
請注意,點光源的 Cookie 使用立方體紋理貼圖。這種紋理貼圖在 “反射表面”部分 中討論。
總結
[edit | edit source]恭喜,您已經瞭解了投影紋理對映最重要的方面。我們已經看到了
- 如何為方向光源實現 Cookie。
- 如何實現聚光燈(有和沒有使用者指定的 Cookie)。
- 如何為不同的光源實現不同的著色器。
進一步閱讀
[edit | edit source]如果您還想了解更多
- 關於沒有 Cookie 的光源的著色器版本,您應該閱讀 “平滑鏡面高光”部分。
- 關於紋理對映,特別是 alpha 紋理貼圖,您應該閱讀 “透明紋理”部分。
- 關於固定功能 OpenGL 中的投影紋理對映,您可以閱讀 NVIDIA 的白皮書“投影紋理對映”,作者是 Cass Everitt(可 線上獲取)。