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 关于前向渲染的参考。