unity shader 光照和阴影

以下整理主要摘自《Unity Shader 入门精要》

渲染路径

主要有: 前向渲染路径延迟渲染路径

一般使用前向渲染路径。
延迟渲染适合光源数目多,前向渲染会造成性能瓶颈的情况。不支持抗锯齿,不能处理半透明,对显卡有要求,具体选择哪种看文档
https://docs.unity3d.com/Manual/RenderingPaths.html

后文主要讨论前向渲染路径。

前向渲染路径

原理

如果一个物体在多个逐像素光源影响区域,则此物体需要执行多个Pass,每个Pass计算一个逐像素光源的光照结果。假设N个物体,受M个光源影响,整个场景共N*M个Pass,由于数目很大,故会限制每个物体的逐像素光照数目。

unity处理

在Forward Rendering中,有三种处理光照(即照亮物体)的方式:逐顶点处理,逐像素处理,球谐函数(Spherical Harmonics,SH)处理。而决定一个灯光是哪种处理模式取决于它的类型和模式。
一定数目的光源会按逐像素的方式处理,最多有4个光源按照逐定点处理,剩下的按SH。

  • 场景中最亮的平行光总是逐像素处理的。这意味着,如果场景里只有一个平行光,是否设置它的模式都无关紧要。
  • Render Mode被设置成Not Important的光源,会按逐顶点或者球谐函数处理。经试验,第一点中的平行光不受这点的约束。
  • Render Mode被设置成Important的光源,会按逐像素处理。
  • 如根据以上规则得到的像素光源数量小于设置中的像素光源数量(Pixel Light Count),为了减少亮度,会有更多的光源以逐像素的方式进行渲染。

那在哪里进行光照处理呢?当然是在Pass里。Forward Rendering有两种Pass:Base Pass,Additional Passes。这两种Pass的图例说明如下:
image
注意两个Pass的Tags和#pragma的设置为必需。如果需要点光源等也有阴影可以在Additional Pass里替换成:#pragma multi_compile_fwdadd_fullshadows

光源

分为:平行光、点光源、聚光灯、面光源

光源设置
image

光源属性 : 位置、方向、颜色、强度、衰减

  1. 平行光
    只有方向且没有衰减
  2. 点光源
    一个点发出的,向所有方向延伸的光,可用一个球表示。有衰减有位置。
  3. 聚光灯
    一个点发出的,向特定方向延伸的光,可用一个锥形表示。

在前向渲染中处理各种光源

上文说到有两种Pass,首先是Base Pass: 负责计算环境光和平行光。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Pass {
// Pass for ambient light & first pixel light (directional light)
Tags { "LightMode"="ForwardBase" }
CGPROGRAM

#pragma multi_compile_fwdbase
……

fixed4 frag(v2f i) : SV_Target {
……

fixed atten = 1.0;
return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}
}

解释: 这个LightMode决定了Pass只处理平行光,所以衰减度为1. Tags和#pragma的设置上面图例里提过。

其次是Additional Pass

1
2
3
4
5
6
7
8
9
Pass {
Tags { "LightMode"="ForwardAdd" }

Blend One One

CGPROGRAM

#pragma multi_compile_fwdadd
……

上面是必须的配置代码,Blend可以变,见上一篇(透明效果提到)将改变光照。后面是计算。

1
2
3
4
5
6
7
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
#ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif

这里分两块解释,首先是_WorldSpaceLightPos0,想得到光照方向,对于平行光来说是_WorldSpaceLightPos0.xyz,对于非平行光则是光源位置减去面片位置。得到光照方向才能继续如之前的相同计算漫反射和高光反射等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);


#ifdef USING_DIRECTIONAL_LIGHT
fixed atten = 1.0;
#else
#if defined (POINT)
float3 lightCoord = mul(_LightMatrix0, float4(i.worldPos, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#elif defined (SPOT)
float4 lightCoord = mul(_LightMatrix0, float4(i.worldPos, 1));
fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#else
fixed atten = 1.0;
#endif
#endif

return fixed4((diffuse + specular) * atten, 1.0);
}

解释2: 这里atten(衰减)的计算分三种,平行光已解释过。点光源&聚光灯:由于计算衰减公式比较麻烦,Unity内置了一张映射表,计算的时候用_LightMatrix0将点转换到光源空间,用该坐标在映射表里采样,最终得到衰减值。使用dot(lightCoord,lightCoord).rr作为坐标的原因是,这样可以用事先平方省下一次开方。效果如下。

image

阴影

原理

ShadowMap技术:把摄像机与光源重合,看不见的地方即是阴影区,生成一张深度图(后文称为阴影纹理),之后只要是接受阴影的物体,会把顶点变到光源空间下,比对自己的深度和深度图此xy的深度,若是自己的深度更大,则处于阴影中。
Unity获得ShadowMap的方式:调用LightMode为ShadowCaster的Pass。因为此Pass比较通用,所以可以使用Fallback,在Fallback中找到此Pass。
以下是在unity标准shader中Mobile-VertexLit.shader内找到的Pass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Pass 
{
Name "ShadowCaster"
Tags { "LightMode" = "ShadowCaster" }

ZWrite On ZTest LEqual Cull Off

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
}

unity设置

  1. 光源设置 ShadowType决定此光源阴影模式;
  2. 物体的Mesh Renderer组件的CastShadows参数和Receive Shadows参数决定是否发射/接受阴影。
    image

接収阴影 using内置宏

前文提到用Fallback可以用unity内置的Pass实现阴影显示,然而也可以调用unity内置宏来编写阴影显示的shader。

后面会用到三个内置宏所以在之前需要加上引用:

1
#include "AutoLight.cginc"

顶点输出增加内置宏SHADOW_COORDS,增加用于阴影纹理采样的坐标。

1
2
3
4
5
6
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
SHADOW_COORDS(2)
};

顶点输出钱使用内置宏TRANSFER_SHADOW计算

1
2
3
4
5
6
v2f vert(a2v v) {
v2f o;
……
TRANSFER_SHADOW(o);
return o;
}

查看TRANSFER_SHADOW源代码可看到,a2v必须包含vertex且名字必须为v,而且v2f必须包含a.pos,否则会GG。运算结果是存储顶点转换到光源空间后的坐标

1
#define TRANSFER_SHADOW(a) a._ShadowCoord = mul( unity_World2Shadow[0], mul( _Object2World, v.vertex ) );

这里用到内置宏SHADOW_ATTENUATION()来计算阴影值,算出来以后跟其它颜色相乘。计算原理就是前文说过的对阴影纹理采样。

1
2
3
4
5
fixed4 frag(v2f i) : SV_Target {
……
fixed shadow = SHADOW_ATTENUATION(i);
return fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0);
}

统一光照和阴影

无论是光照还是阴影,都用到了了从光源出发的纹理以及在光源空间的坐标等,故统一管理与计算。unity又发了福利 UNITY_LIGHT_ATTENUATION
所以在Base Pass 和Additional Pass 里都这么写就好:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
SHADOW_COORDS(2) //!!
};

v2f vert(a2v v) {
v2f o;
……
TRANSFER_SHADOW(o);
return o;
}

fixed4 frag(v2f i) : SV_Target {
……
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}

如此计算出来的atten就是原来的衰减颜色*阴影颜色了。

透明+多重光照+阴影

……整合好一看代码太长了不想贴,就说下思路……其实就是把光照的两个Pass都加上,分别和之前AlphaTest或者AlphaBend代码合并下,即可。
不行有几行还是得记录下…

1
2
3
4
5
6
7
8
9
10
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha

struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2; //!!!
SHADOW_COORDS(3) //!!!
};

计算之类的还是跟之前一样啦╮(╯▽╰)╭

两个光源效果图
image

别人的文章:
Shader中的光照
Unity3D光照前置知识——Rendering Paths(渲染路径)及LightMode(光照模式)译解
Unity3D内建参数文档翻译与解析