这篇文章讨论shader中用到的光照。
理解光照
Unity中的光照技术包括:
- 实时光照(realtime lighting)
- 烘焙后的光照贴图(baked lightmaps)
- 预计算的实时全局光照(precomputed realtime global illumination)
实时光照
Unity最基础的光照方式,能够随光线和物体移动实时变化。但是只能处理直射光,无法处理反射光,所以只是一种局部光照。
烘焙后的光照贴图
Unity可以将静态物体的光照信息(包括直射光和反射光)预先烘焙到一张贴图(lightmap)上,供运行时使用,从而避免动态计算。
lightmap的原理是:预先计算整个光照路径(只除去物体表面到摄像机的一段)。
它的优点是省去了运行时计算的代价,缺点是运行时无法改变。
另外一种烘焙方式叫光照探针(light probe)。与lightmap的不同点在于:lightmap烘焙的是射到物体表面的光;而light probe烘焙的是空间中的传播光,可用于静态物体到动态物体的反射。
Unity将光照数据存储在名为Lighting Data Asset的文件中。它是Unity 5.3新增的文件,替代原来的Snapshot。用于存储GI数据,以及render、lightmap、light probe等的引用。
预计算的实时全局光照
预计算静态物体之间所有可能的反射并编码,在运行时再生成间接光照。运行时可以随光源位置、方向、颜色的改变而改变。Unity 5.0支持的新功能,需要设置开启。
优点是既能随光源动态改变,也能降低运行时消耗;缺点是只能针对静态物体。
各种光照技术比较
三种技术典型的应用场景包括:
- 实时光照:移动的角色。
- 烘焙后的光照贴图:场景中静态的物体。
- 预计算的实时全局光照:一天之内的阳光,随时间变化而角度、强度各不相同。
在移动设备上适合用baked lightmaps,在PC上适合用precomputed realtime GI。
光源类型
名字 | 英文名 | 解释 |
点光源 | point light | 从一个点向四面八方发射,类似电灯泡 |
锥形光 | spot light | 从一个点以锥形发出,类似手电筒 |
有向光 | directional light | 以固定方向平行发出,类似太阳 |
区域光 | area light | 光线限制在一个矩形区域内,固定从一侧以任意角度发出 |
环境光 | ambient light | 光在场景中无处不在,不从特定物体发出 |
理解阴影
unity中的阴影分为实时阴影和阴影贴图。
- 实时阴影:实时光产生的阴影。要使用这种阴影,需要在产生的物体上开启投射(cast),在投影到的物体上开启接收(receive)。
- 阴影贴图:以贴图方式实现的阴影。这种方式可以降低运行时消耗,但是阴影的大小、形状都是固定的。
shader中的光照
光照是如何影响我们的视觉的呢?事实上,我们之所以能看到物体,除去那些能自发光的物体外(如灯泡),都是因为光线射到物体表面再反射到我们眼睛中。一个物体之所以呈现不同颜色,那是因为它对光的不同颜色分量的反射率不一样。例如,红色的物体对于红光几乎全部反射,而对于绿光和蓝光几乎全部吸收。所以,物体最终呈现在我们眼中的颜色,取决于反射光和自发光的叠加。
不同的光照模型对于反射光的处理大不一样。Unity中内置了两种光照模型:Lambert光照是漫反射,光线射过去向四面八方反射,适合于普通较粗糙的材质;BlingPhong光照是镜面反射,适合光滑如镜的材质。这两种光照就可以涵盖绝大多数情况。除此之外,我们也可以根据需求自己定制光照模型。
Unity中可以使用Surface Shader或者Vertex/Fragment Shader来处理光照。对于前者,需要自定义光照处理函数;对于后者,则需要在vert函数中做varying变量的赋值,而在frag函数中处理光照。对于Surface Shader和光照函数在渲染管线中的位置,可以看图(来自知乎):
可以看到,surf函数和光照都位于Fragment Shader中,两者是紧挨着的关系。
实例解析
以下均为Unity Manual中的例子。
Surface Shader中的光照
- DiffuseTexture:与Lambert光照等效的实现。自定义光照类型SimpleLambert,对应的函数在surf之后执行。计算顶点颜色涉及到的参数:片元的法向量、光入射角度、光的rgba和衰减、片元的Albedo。
Shader "Example/Diffuse Texture" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
}
SubShader {
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf SimpleLambert
half4 LightingSimpleLambert (SurfaceOutput s, half3 lightDir, half atten) {
half NdotL = dot (s.Normal, lightDir);
half4 c;
c.rgb = s.Albedo * _LightColor0.rgb * (NdotL * atten);
c.a = s.Alpha;
return c;
}
struct Input {
float2 uv_MainTex;
};
sampler2D _MainTex;
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
}
ENDCG
}
Fallback "Diffuse"
}
- SimpleSpecular:与BlingPhong光照等效的实现。计算顶点颜色涉及到的参数:视角、片元的法向量、光入射角度、光的rgba和衰减、片元的Albedo。
...ShaderLab code...
CGPROGRAM
#pragma surface surf SimpleSpecular
half4 LightingSimpleSpecular (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten) {
half3 h = normalize (lightDir + viewDir);
half diff = max (0, dot (s.Normal, lightDir));
float nh = max (0, dot (s.Normal, h));
float spec = pow (nh, 48.0);
half4 c;
c.rgb = (s.Albedo * _LightColor0.rgb * diff + _LightColor0.rgb * spec) * atten;
c.a = s.Alpha;
return c;
}
struct Input {
float2 uv_MainTex;
};
sampler2D _MainTex;
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
}
ENDCG
...ShaderLab code...
Vertex/Fragment Shader中的光照
要使用光照,首先需要在pass tag中设置LightMode,从而定义各种rendering path。常见的选择有:
关键字 | 意义 | 解释 |
ForwardBase | forward rendering | 使用关键光照信息立即渲染 |
Deferred | deferred shading | 使用所有光照信息延迟渲染 |
ShadowCaster | shadow caster | 投射阴影所必需,通常与ForwardBase或Deferred配合使用 |
- SimpleDiffuse:与Lambert光照等效。需要在vert和frag中做处理。
Shader "Lit/Simple Diffuse"
{
Properties
{
[NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Pass
{
// indicate that our pass is the "base" pass in forward
// rendering pipeline. It gets ambient and main directional
// light data set up; light direction in _WorldSpaceLightPos0
// and color in _LightColor0
Tags {"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc" // for UnityObjectToWorldNormal
#include "UnityLightingCommon.cginc" // for _LightColor0
struct v2f
{
float2 uv : TEXCOORD0;
fixed4 diff : COLOR0; // diffuse lighting color
float4 vertex : SV_POSITION;
};
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
// get vertex normal in world space
half3 worldNormal = UnityObjectToWorldNormal(v.normal);
// dot product between normal and light direction for
// standard diffuse (Lambert) lighting
half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));
// factor in the light color
o.diff = nl * _LightColor0;
return o;
}
sampler2D _MainTex;
fixed4 frag (v2f i) : SV_Target
{
// sample texture
fixed4 col = tex2D(_MainTex, i.uv);
// multiply by lighting
col *= i.diff;
return col;
}
ENDCG
}
}
}
- ShadowCasting:实现阴影投射。定义了两个Pass,LightMode分别是ForwardBase和ShadowCaster。
Shader "Lit/Shadow Casting"
{
SubShader
{
// very simple lighting pass, that only does non-textured ambient
Pass
{
Tags {"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
fixed4 diff : COLOR0;
float4 vertex : SV_POSITION;
};
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
half3 worldNormal = UnityObjectToWorldNormal(v.normal);
// only evaluate ambient
o.diff.rgb = ShadeSH9(half4(worldNormal,1));
o.diff.a = 1;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return i.diff;
}
ENDCG
}
// shadow caster rendering pass, implemented manually
// using macros from UnityCG.cginc
Pass
{
Tags {"LightMode"="ShadowCaster"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#include "UnityCG.cginc"
struct v2f {
V2F_SHADOW_CASTER;
};
v2f vert(appdata_base v)
{
v2f o;
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
return o;
}
float4 frag(v2f i) : SV_Target
{
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
}
}