本教程涵盖了在一个通道中的多光源照明。特别地,它涵盖了Unity所谓的在ForwardBase通道中的“顶点光”。
本教程是章节“平滑镜面高光”的扩展。如果你没有读过,你应该优先去看看。
一个通道中的多光源
如章节“漫反射”中讨论的,Unity的前向渲染路径会为最重要的光源使用独立通道。这被称作“像素光”因为内置着色器会用逐像素光照渲染它们。渲染模式设置为重要的所有光源会被当作像素光来渲染。如果Quality project settings中像素光数量允许更多的像素光,那么渲染模式设置为Auto的部分光源也会被当作像素光来渲染。如果Quality project settings中像素光数量允许更多的像素光,那么渲染模式设置为Auto的部分光源也会被当作像素光来渲染。那其它的光源会发生什么?Unity内置的着色器会在ForwardBase通道中作为顶点光照来渲染额外的4个灯光。顾名思义,内置着色器会用逐顶点光照渲染这些灯光。以上就是本教程所要讲的东西。(进一步的光照近似球面谐波光照,它不包括在这里。)
在ForwardBase通道中,4个顶点光(也就是它们的位置和颜色)是由以下的内置uniforms(参考Unity内置着色器变量的文档)来指定的:
// Built-in uniforms for "vertex lights"
uniform half4 unity_LightColor[4];
// array of the colors of the 4 light sources
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
我们遵循Unity的内置着色器,只使用逐顶点光照通过顶点光源计算漫反射(假设它们是点光源)。这个可以用下面的在顶点着色器中的for循环来计算:
// Diffuse reflection by four "vertex lights"
float3 vertexLighting = float3(0.0, 0.0, 0.0);
#ifdef VERTEXLIGHT_ON
for (int index = 0; index < 4; index++)
{
float4 lightPosition = float4(unity_4LightPosX0[index],
unity_4LightPosY0[index],unity_4LightPosZ0[index], 1.0);
float3 vertexToLightSource = lightPosition.xyz - output.posWorld.xyz;
float3 lightDirection = normalize(vertexToLightSource);
float squaredDistance = dot(vertexToLightSource, vertexToLightSource);
float attenuation = 1.0 / (1.0 + unity_4LightAtten0[index] * squaredDistance);
float3 diffuseReflection = attenuation * unity_LightColor[index].rgb * _Color.rgb
* max(0.0, dot(output.normalDir, lightDirection));
vertexLighting = vertexLighting + diffuseReflection;
}
#endif
所有顶点光产生的总漫射光照是在vertexLighting中累积的,通过把它初始化为黑色并且在for循环的最后把每个顶点光照添加到之前vertexLighting的值上。任何一个C/C++/JavaScrip程序员都应该熟悉for循环。注意,Cg中的for循环在一些GPU上是严重受限的;特别是对于一些GPU,界限(这里是0到4)必须是常量,也就是你甚至不能使用uniforms来定义界限。(技术上的原因是为了“展开”循环,在编译时间就要知道这个界限。)
上面或多或少解释了如何在Untiy的内置着色器中计算顶点光照。但是,请记住什么都阻止不了你用这些“顶点光”计算镜面反射或逐像素光照。
完整的着色器代码
在章节“光滑镜面高光”着色器代码的基础上,完整的着色器代码如下:
Shader "Cg 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" } // 4个顶点光的通道,环境光和第一个像素光
CGPROGRAM
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
uniform float4 _LightColor0;
// color of light source (from "Lighting.cginc")
// 用户自定义属性
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;
float3 vertexLighting : TEXCOORD2;
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float4x4 modelMatrix = _Object2World;
float4x4 modelMatrixInverse = _World2Object;
output.posWorld = mul(modelMatrix, input.vertex);
output.normalDir = normalize(
mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
// 4个"顶点光"的漫反射
output.vertexLighting = float3(0.0, 0.0, 0.0);
#ifdef VERTEXLIGHT_ON
for (int index = 0; index < 4; index++)
{
float4 lightPosition = float4(unity_4LightPosX0[index],
unity_4LightPosY0[index],
unity_4LightPosZ0[index], 1.0);
float3 vertexToLightSource =
lightPosition.xyz - output.posWorld.xyz;
float3 lightDirection = normalize(vertexToLightSource);
float squaredDistance =
dot(vertexToLightSource, vertexToLightSource);
float attenuation = 1.0 / (1.0 +
unity_4LightAtten0[index] * squaredDistance);
float3 diffuseReflection = attenuation
* unity_LightColor[index].rgb * _Color.rgb
* max(0.0, dot(output.normalDir, lightDirection));
output.vertexLighting =
output.vertexLighting + diffuseReflection;
}
#endif
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 ambientLighting =
UNITY_LIGHTMODEL_AMBIENT.rgb * _Color.rgb;
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);
}
return float4(input.vertexLighting + 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")
// 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 = _Object2World;
float4x4 modelMatrixInverse = _World2Object;
output.posWorld = mul(modelMatrix, input.vertex);
output.normalDir = normalize(
mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
return output;
}
float4 frag(vertexOutput input) : COLOR
{
float3 normalDirection = normalize(input.normalDir);
float3 viewDirection = normalize(
_WorldSpaceCameraPos.xyz - 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);
}
return float4(diffuseReflection
+ specularReflection, 1.0);
// no ambient lighting in this pass
}
ENDCG
}
}
Fallback "Specular"
}
#pragma multi_compile_fwdbase和#ifdef VERTEXLIGHT_ON … #endif的使用是必要的,为了确保Unity没有提供光照数据时顶点光照不会被计算;同样可以参考Unity multi_compile directives的文档。
总结
恭喜,你又完成了一章的学习。我们学习了:
- 如何指定Untiy的顶点光。
- 在Cg中如何使用for循环来计算在一个通道中的多光源的光照。