GLSL 程式設計/Unity/Cookie


本教程涵蓋了光空間中的投影紋理對映,它有助於為聚光燈和方向光源實現 Cookie。(事實上,Unity 對任何聚光燈都使用內建 Cookie。)
本教程基於“平滑鏡面高光”部分和“透明紋理”部分的程式碼。如果你還沒有閱讀這些教程,你應該先閱讀它們。
在現實生活中,Gobo 是放置在光源前方的帶有孔洞的材料(通常是金屬),用於操縱光束或陰影的形狀。Cookie(或“cuculoris”)的功能類似,但放置在遠離光源的位置,如左側影像所示。
在 Unity 中,可以在選擇光源時在檢查器檢視中為每個光源指定一個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 mat4 _Object2World; // model matrix
uniform mat4 _World2Object; // inverse model matrix
uniform vec4 _WorldSpaceLightPos0;
// direction to or position of light source
uniform vec4 _LightColor0;
// color of light source (from Lighting.cginc)
uniform mat4 _LightMatrix0; // transformation
// from world to light space (from Autolight.cginc)
varying vec4 position;
// position of the vertex (and fragment) in world space
varying vec4 positionInLightSpace;
// position of the vertex (and fragment) in light space
varying vec3 varyingNormalDirection;
// surface normal vector in world space
#ifdef VERTEX
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object; // unity_Scale.w
// is unnecessary because we normalize vectors
position = modelMatrix * gl_Vertex;
positionInLightSpace = _LightMatrix0 * position;
varyingNormalDirection = normalize(vec3(
vec4(gl_Normal, 0.0) * modelMatrixInverse));
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
除了統一變數_LightMatrix0和變化量positionInLightSpace的定義以及計算positionInLightSpace的指令外,這與“平滑鏡面高光”部分中的頂點著色器相同。
對於方向光源的 Cookie,我們可以直接在片段著色器中使用positionInLightSpace中的x 和 y 光座標作為 Cookie 紋理_LightTexture0的紋理座標進行查詢。然後,將得到的 Alpha 分量乘以計算得到的光照;例如
// compute diffuseReflection and specularReflection
float cookieAttenuation = 1.0;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
cookieAttenuation = texture2D(_LightTexture0,
vec2(positionInLightSpace)).a;
}
// compute cookieAttenuation for spotlights here
gl_FragColor = vec4(cookieAttenuation
* (diffuseReflection + specularReflection), 1.0);
除了vec2(positionInLightSpace),我們還可以使用positionInLightSpace.xy來獲取一個二維向量,其中包含光空間中的x 和 y 座標。
對於聚光燈,必須將positionInLightSpace中的x 和 y 光座標除以w 光座標。這種除法是投影紋理對映的特徵,對應於攝像頭的透視除法,如“頂點變換”部分中所述。Unity 定義了矩陣_LightMatrix0,因此在除法後,我們必須在兩個座標中都新增
cookieAttenuation = texture2D(_LightTexture0,
vec2(positionInLightSpace) / positionInLightSpace.w
+ vec2(0.5)).a;
對於某些 GPU,使用內建函式texture2DProj可能更高效,該函式接受一個vec3中的三個紋理座標,並在紋理查詢之前將前兩個座標除以第三個座標。這種方法的一個問題是,我們必須在除以positionInLightSpace.w之後新增;但是,texture2DProj不允許我們在內部除以第三個紋理座標後新增任何內容。解決方案是在除以positionInLightSpace.w之前新增0.5 * positionInLightSpace.w,這對應於在除法後新增
vec3 textureCoords = vec3(vec2(positionInLightSpace)
+ vec2(0.5 * positionInLightSpace.w),
positionInLightSpace.w);
cookieAttenuation =
texture2DProj(_LightTexture0, textureCoords).a;
請注意,透過將textureCoords設定為vec3(vec2(positionInLightSpace), 1.0),也可以使用texture2DProj實現方向光的紋理查詢。這將允許我們對方向光和聚光燈使用相同的紋理查詢,這在某些 GPU 上更高效。
對於完整的著色器程式碼,我們使用“平滑鏡面高光”部分中ForwardBase傳遞的簡化版本,因為 Unity 僅在ForwardBase傳遞中使用沒有 Cookie 的方向光。所有帶有 Cookie 的光源都由ForwardAdd傳遞處理。我們忽略了點光源的 Cookie,對於點光源,_LightMatrix0[3][3]為1.0(但在下一節中包含了它們)。聚光燈始終具有 Cookie 紋理:如果使用者沒有指定 Cookie 紋理,Unity 會提供一個 Cookie 紋理以生成聚光燈的形狀;因此,始終應用 Cookie 是可以的。方向光並不總是具有 Cookie;但是,如果只有一個沒有 Cookie 的方向光源,那麼它將在ForwardBase傳遞中進行處理。因此,除非有多個沒有 Cookie 的方向光源,否則我們可以假設ForwardAdd傳遞中的所有方向光源都具有 Cookie。在這種情況下,完整的著色器程式碼可以是
Shader "GLSL 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
GLSLPROGRAM
// User-specified properties
uniform vec4 _Color;
uniform vec4 _SpecColor;
uniform float _Shininess;
// The following built-in uniforms (except _LightColor0)
// are also defined in "UnityCG.glslinc",
// i.e. one could #include "UnityCG.glslinc"
uniform vec3 _WorldSpaceCameraPos;
// camera position in world space
uniform mat4 _Object2World; // model matrix
uniform mat4 _World2Object; // inverse model matrix
uniform vec4 _WorldSpaceLightPos0;
// direction to or position of light source
uniform vec4 _LightColor0;
// color of light source (from Lighting.cginc)
varying vec4 position;
// position of the vertex (and fragment) in world space
varying vec3 varyingNormalDirection;
// surface normal vector in world space
#ifdef VERTEX
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object; // unity_Scale.w
// is unnecessary because we normalize vectors
position = modelMatrix * gl_Vertex;
varyingNormalDirection = normalize(vec3(
vec4(gl_Normal, 0.0) * modelMatrixInverse));
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
vec3 normalDirection = normalize(varyingNormalDirection);
vec3 viewDirection =
normalize(_WorldSpaceCameraPos - vec3(position));
vec3 lightDirection =
normalize(vec3(_WorldSpaceLightPos0));
vec3 ambientLighting =
vec3(gl_LightModel.ambient) * vec3(_Color);
vec3 diffuseReflection = vec3(_LightColor0) * vec3(_Color)
* max(0.0, dot(normalDirection, lightDirection));
vec3 specularReflection;
if (dot(normalDirection, lightDirection) < 0.0)
// light source on the wrong side?
{
specularReflection = vec3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
specularReflection = vec3(_LightColor0)
* vec3(_SpecColor) * pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess);
}
gl_FragColor = vec4(ambientLighting
+ diffuseReflection + specularReflection, 1.0);
}
#endif
ENDGLSL
}
Pass {
Tags { "LightMode" = "ForwardAdd" }
// pass for additional light sources
Blend One One // additive blending
GLSLPROGRAM
// User-specified properties
uniform vec4 _Color;
uniform vec4 _SpecColor;
uniform float _Shininess;
// The following built-in uniforms (except _LightColor0)
// are also defined in "UnityCG.glslinc",
// i.e. one could #include "UnityCG.glslinc"
uniform vec3 _WorldSpaceCameraPos;
// camera position in world space
uniform mat4 _Object2World; // model matrix
uniform mat4 _World2Object; // inverse model matrix
uniform vec4 _WorldSpaceLightPos0;
// direction to or position of light source
uniform vec4 _LightColor0;
// color of light source (from Lighting.cginc)
uniform mat4 _LightMatrix0; // transformation
// from world to light space (from Autolight.cginc)
uniform sampler2D _LightTexture0;
// cookie alpha texture map (from Autolight.cginc)
varying vec4 position;
// position of the vertex (and fragment) in world space
varying vec4 positionInLightSpace;
// position of the vertex (and fragment) in light space
varying vec3 varyingNormalDirection;
// surface normal vector in world space
#ifdef VERTEX
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object; // unity_Scale.w
// is unnecessary because we normalize vectors
position = modelMatrix * gl_Vertex;
positionInLightSpace = _LightMatrix0 * position;
varyingNormalDirection = normalize(vec3(
vec4(gl_Normal, 0.0) * modelMatrixInverse));
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
vec3 normalDirection = normalize(varyingNormalDirection);
vec3 viewDirection =
normalize(_WorldSpaceCameraPos - vec3(position));
vec3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(vec3(_WorldSpaceLightPos0));
}
else // point or spot light
{
vec3 vertexToLightSource =
vec3(_WorldSpaceLightPos0 - position);
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
vec3 diffuseReflection =
attenuation * vec3(_LightColor0) * vec3(_Color)
* max(0.0, dot(normalDirection, lightDirection));
vec3 specularReflection;
if (dot(normalDirection, lightDirection) < 0.0)
// light source on the wrong side?
{
specularReflection = vec3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
specularReflection = attenuation * vec3(_LightColor0)
* vec3(_SpecColor) * pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess);
}
float cookieAttenuation = 1.0;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
cookieAttenuation = texture2D(_LightTexture0,
vec2(positionInLightSpace)).a;
}
else if (1.0 != _LightMatrix0[3][3])
// spotlight (i.e. not a point light)?
{
cookieAttenuation = texture2D(_LightTexture0,
vec2(positionInLightSpace) / positionInLightSpace.w
+ vec2(0.5)).a;
}
gl_FragColor = vec4(cookieAttenuation
* (diffuseReflection + specularReflection), 1.0);
}
#endif
ENDGLSL
}
}
// The definition of a fallback shader should be commented out
// during development:
// Fallback "Specular"
}
之前的著色器程式碼僅限於最多隻有一個沒有 Cookie 的方向光源的場景。 此外,它沒有考慮點光源的 Cookie。 編寫更通用的著色器程式碼需要為不同的光源使用不同的 ForwardAdd 傳遞。(請記住,ForwardBase 傳遞中的光源始終是沒有任何 Cookie 的方向光源。) 幸運的是,Unity 提供了一種透過使用以下 Unity 特定的指令(在 ForwardAdd 傳遞中的 GLSLPROGRAM 之後)來生成多個著色器的方法
#pragma multi_compile_lightpass
使用此指令,Unity 將為不同型別的燈光源多次編譯 ForwardAdd 傳遞的著色器程式碼。 每次編譯都由以下符號之一的定義來區分:DIRECTIONAL、DIRECTIONAL_COOKIE、POINT、POINT_NOATT、POINT_COOKIE、SPOT。 著色器程式碼應該檢查哪個符號已定義(使用指令 #if defined ... #elif defined ... #endif)幷包含相應的指令。 例如
Shader "GLSL 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
GLSLPROGRAM
// User-specified properties
uniform vec4 _Color;
uniform vec4 _SpecColor;
uniform float _Shininess;
// The following built-in uniforms (except _LightColor0)
// are also defined in "UnityCG.glslinc",
// i.e. one could #include "UnityCG.glslinc"
uniform vec3 _WorldSpaceCameraPos;
// camera position in world space
uniform mat4 _Object2World; // model matrix
uniform mat4 _World2Object; // inverse model matrix
uniform vec4 _WorldSpaceLightPos0;
// direction to or position of light source
uniform vec4 _LightColor0;
// color of light source (from Lighting.cginc)
varying vec4 position;
// position of the vertex (and fragment) in world space
varying vec3 varyingNormalDirection;
// surface normal vector in world space
#ifdef VERTEX
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object; // unity_Scale.w
// is unnecessary because we normalize vectors
position = modelMatrix * gl_Vertex;
varyingNormalDirection = normalize(vec3(
vec4(gl_Normal, 0.0) * modelMatrixInverse));
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
vec3 normalDirection = normalize(varyingNormalDirection);
vec3 viewDirection =
normalize(_WorldSpaceCameraPos - vec3(position));
vec3 lightDirection =
normalize(vec3(_WorldSpaceLightPos0));
vec3 ambientLighting =
vec3(gl_LightModel.ambient) * vec3(_Color);
vec3 diffuseReflection = vec3(_LightColor0) * vec3(_Color)
* max(0.0, dot(normalDirection, lightDirection));
vec3 specularReflection;
if (dot(normalDirection, lightDirection) < 0.0)
// light source on the wrong side?
{
specularReflection = vec3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
specularReflection = vec3(_LightColor0)
* vec3(_SpecColor) * pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess);
}
gl_FragColor = vec4(ambientLighting
+ diffuseReflection + specularReflection, 1.0);
}
#endif
ENDGLSL
}
Pass {
Tags { "LightMode" = "ForwardAdd" }
// pass for additional light sources
Blend One One // additive blending
GLSLPROGRAM
#pragma multi_compile_lightpass
// User-specified properties
uniform vec4 _Color;
uniform vec4 _SpecColor;
uniform float _Shininess;
// The following built-in uniforms (except _LightColor0)
// are also defined in "UnityCG.glslinc",
// i.e. one could #include "UnityCG.glslinc"
uniform vec3 _WorldSpaceCameraPos;
// camera position in world space
uniform mat4 _Object2World; // model matrix
uniform mat4 _World2Object; // inverse model matrix
uniform vec4 _WorldSpaceLightPos0;
// direction to or position of light source
uniform vec4 _LightColor0;
// color of light source (from Lighting.cginc)
uniform mat4 _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
varying vec4 position;
// position of the vertex (and fragment) in world space
varying vec4 positionInLightSpace;
// position of the vertex (and fragment) in light space
varying vec3 varyingNormalDirection;
// surface normal vector in world space
#ifdef VERTEX
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object; // unity_Scale.w
// is unnecessary because we normalize vectors
position = modelMatrix * gl_Vertex;
positionInLightSpace = _LightMatrix0 * position;
varyingNormalDirection = normalize(vec3(
vec4(gl_Normal, 0.0) * modelMatrixInverse));
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
vec3 normalDirection = normalize(varyingNormalDirection);
vec3 viewDirection =
normalize(_WorldSpaceCameraPos - vec3(position));
vec3 lightDirection;
float attenuation = 1.0;
// by default no attenuation with distance
#if defined DIRECTIONAL || defined DIRECTIONAL_COOKIE
lightDirection = normalize(vec3(_WorldSpaceLightPos0));
#elif defined POINT_NOATT
lightDirection =
normalize(vec3(_WorldSpaceLightPos0 - position));
#elif defined POINT || defined POINT_COOKIE || defined SPOT
vec3 vertexToLightSource =
vec3(_WorldSpaceLightPos0 - position);
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
#endif
vec3 diffuseReflection =
attenuation * vec3(_LightColor0) * vec3(_Color)
* max(0.0, dot(normalDirection, lightDirection));
vec3 specularReflection;
if (dot(normalDirection, lightDirection) < 0.0)
// light source on the wrong side?
{
specularReflection = vec3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
specularReflection = attenuation * vec3(_LightColor0)
* vec3(_SpecColor) * pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess);
}
float cookieAttenuation = 1.0;
// by default no cookie attenuation
#if defined DIRECTIONAL_COOKIE
cookieAttenuation = texture2D(_LightTexture0,
vec2(positionInLightSpace)).a;
#elif defined POINT_COOKIE
cookieAttenuation = textureCube(_LightTexture0,
vec3(positionInLightSpace)).a;
#elif defined SPOT
cookieAttenuation = texture2D(_LightTexture0,
vec2(positionInLightSpace)
/ positionInLightSpace.w + vec2(0.5)).a;
#endif
gl_FragColor = vec4(cookieAttenuation *
(diffuseReflection + specularReflection), 1.0);
}
#endif
ENDGLSL
}
}
// The definition of a fallback shader should be commented out
// during development:
// Fallback "Specular"
}
請注意,點光源的 Cookie 使用的是立方體紋理貼圖。 這種紋理貼圖在 “反射表面”部分 中討論。
摘要
[edit | edit source]恭喜,您已經瞭解了投影紋理對映的最重要方面。 我們已經看到了
- 如何為方向光源實現 Cookie。
- 如何實現聚光燈(有或沒有使用者指定的 Cookie)。
- 如何為不同的光源實現不同的著色器。
進一步閱讀
[edit | edit source]如果您仍然想了解更多
- 關於沒有 Cookie 的燈光著色器版本,您應該閱讀 “平滑鏡面高光”部分。
- 關於紋理對映,尤其是 alpha 紋理貼圖,您應該閱讀 “透明紋理”部分。
- 關於固定功能 OpenGL 中的投影紋理對映,您可以閱讀 NVIDIA 的白皮書“投影紋理對映”,作者為 Cass Everitt(可以在 網上 獲取)。