GLSL 程式設計/Unity/多盞燈光
本教程涵蓋了**在一次渲染中使用多個光源進行照明**。特別地,它涵蓋了 Unity 中的所謂“頂點燈光”,在 `ForwardBase` 渲染通道中使用。
本教程是對 “平滑鏡面高光”章節 的擴充套件。如果你還沒有閱讀過該教程,建議你首先閱讀它。
如 “漫反射”章節 中所述,Unity 的前向渲染路徑對最重要的光源使用單獨的渲染通道。這些通道被稱為“畫素燈光”,因為內建著色器使用逐畫素照明來渲染它們。所有將**渲染模式**設定為**重要**的光源都會被渲染為畫素燈光。如果**質量**專案設定中的**畫素燈光數量**允許使用更多畫素燈光,那麼一些**渲染模式**設定為**自動**的光源也會被渲染為畫素燈光。其他光源會怎樣?Unity 的內建著色器將四個額外的燈光作為**頂點燈光**渲染在 `ForwardBase` 通道中。顧名思義,內建著色器使用逐頂點照明來渲染這些燈光。這就是本教程的主題。(更多燈光將透過球諧光照進行近似,這裡不涵蓋。)
不幸的是,訪問這四個頂點燈光(即它們的位置和顏色)的方式有些不明確。以下是在 Windows 和 MacOS X 上 Unity 3.4 中看似有效的做法
// Built-in uniforms for "vertex lights"
uniform vec4 unity_LightColor[4];
// array of the colors of the 4 light sources
uniform vec4 unity_4LightPosX0;
// x coordinates of the 4 light sources in world space
uniform vec4 unity_4LightPosY0;
// y coordinates of the 4 light sources in world space
uniform vec4 unity_4LightPosZ0;
// z coordinates of the 4 light sources in world space
uniform vec4 unity_4LightAtten0;
// scale factors for attenuation with squared distance
// uniform vec4 unity_LightPosition[4] is apparently not
// always correctly set in Unity 3.4
// uniform vec4 unity_LightAtten[4] is apparently not
// always correctly set in Unity 3.4
根據你的平臺和 Unity 版本,你可能需要使用 `unity_LightPosition[4]` 來代替 `unity_4LightPosX0`、`unity_4LightPosY0` 和 `unity_4LightPosZ0`。類似地,你可能需要使用 `unity_LightAtten[4]` 來代替 `unity_4LightAtten0`。需要注意的是,以下內容不可用:任何 Cookie 紋理或到燈光空間的變換(因此也沒有聚光燈的方向)。此外,燈光位置的第四個分量也不可用;因此,無法確定一個頂點燈光是方向光、點光還是聚光燈。
在這裡,我們遵循 Unity 的內建著色器,僅使用逐頂點照明計算頂點燈光產生的漫反射。這可以透過在頂點著色器中使用以下 for 迴圈來實現
vertexLighting = vec3(0.0, 0.0, 0.0);
for (int index = 0; index < 4; index++)
{
vec4 lightPosition = vec4(unity_4LightPosX0[index],
unity_4LightPosY0[index],
unity_4LightPosZ0[index], 1.0);
vec3 vertexToLightSource =
vec3(lightPosition - position);
vec3 lightDirection = normalize(vertexToLightSource);
float squaredDistance =
dot(vertexToLightSource, vertexToLightSource);
float attenuation = 1.0 / (1.0 +
unity_4LightAtten0[index] * squaredDistance);
vec3 diffuseReflection =
attenuation * vec3(unity_LightColor[index])
* vec3(_Color) * max(0.0,
dot(varyingNormalDirection, lightDirection));
vertexLighting = vertexLighting + diffuseReflection;
}
所有頂點燈光產生的總漫反射光照在 `vertexLighting` 中累加,首先將其初始化為黑色,然後在 for 迴圈結束時將每個頂點燈光的漫反射新增到 `vertexLighting` 的先前值。對於任何 C/C++/Java/JavaScript 程式設計師來說,for 迴圈應該很熟悉。需要注意的是,for 迴圈有時受到嚴格限制;特別是迴圈限制(這裡為 0 和 4)必須是常量,即你甚至不能使用 uniform 來確定迴圈限制。(技術原因是,為了“展開”迴圈,迴圈限制必須在編譯時已知。)
這或多或少是 Unity 內建著色器中計算頂點燈光的方式。但是,請記住,沒有什麼能阻止你使用這些“頂點燈光”來計算鏡面反射或逐畫素照明。
在 “平滑鏡面高光”章節 的著色器程式碼中,完整的著色器程式碼如下
Shader "GLSL 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
GLSLPROGRAM
#pragma multi_compile_fwdbase
// 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")
// Built-in uniforms for "vertex lights"
uniform vec4 unity_LightColor[4];
uniform vec4 unity_4LightPosX0;
// x coordinates of the 4 light sources in world space
uniform vec4 unity_4LightPosY0;
// y coordinates of the 4 light sources in world space
uniform vec4 unity_4LightPosZ0;
// z coordinates of the 4 light sources in world space
uniform vec4 unity_4LightAtten0;
// scale factors for attenuation with squared distance
// Varyings
varying vec4 position;
// position of the vertex (and fragment) in world space
varying vec3 varyingNormalDirection;
// surface normal vector in world space
varying vec3 vertexLighting;
#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;
// Diffuse reflection by four "vertex lights"
vertexLighting = vec3(0.0, 0.0, 0.0);
#ifdef VERTEXLIGHT_ON
for (int index = 0; index < 4; index++)
{
vec4 lightPosition = vec4(unity_4LightPosX0[index],
unity_4LightPosY0[index],
unity_4LightPosZ0[index], 1.0);
vec3 vertexToLightSource =
vec3(lightPosition - position);
vec3 lightDirection = normalize(vertexToLightSource);
float squaredDistance =
dot(vertexToLightSource, vertexToLightSource);
float attenuation = 1.0 / (1.0 +
unity_4LightAtten0[index] * squaredDistance);
vec3 diffuseReflection =
attenuation * vec3(unity_LightColor[index])
* vec3(_Color) * max(0.0,
dot(varyingNormalDirection, lightDirection));
vertexLighting = vertexLighting + diffuseReflection;
}
#endif
}
#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 ambientLighting =
vec3(gl_LightModel.ambient) * vec3(_Color);
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);
}
gl_FragColor = vec4(vertexLighting + ambientLighting
+ diffuseReflection + specularReflection, 1.0);
}
#endif
ENDGLSL
}
Pass {
Tags { "LightMode" = "ForwardAdd" }
// pass for additional "pixel lights"
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")
// Varyings
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;
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);
}
gl_FragColor = vec4(diffuseReflection
+ specularReflection, 1.0);
}
#endif
ENDGLSL
}
}
// The definition of a fallback shader should be commented out
// during development:
// Fallback "Specular"
}
使用 `#pragma multi_compile_fwdbase` 和 `#ifdef VERTEXLIGHT_ON ... #endif` 看起來是必要的,以確保當 Unity 不提供資料時不會計算頂點照明。
恭喜你已經完成了本教程。我們已經瞭解了
- Unity 中頂點燈光的指定方式。
- 如何使用 for 迴圈在 GLSL 中計算一次渲染中多個燈光的照明。
如果你想了解更多
- 關於著色器程式碼的其他部分,請閱讀 “平滑鏡面高光”章節。
- 關於 Unity 的前向渲染路徑以及在 `ForwardBase` 通道中計算的內容,請閱讀 Unity 關於前向渲染的參考。