开篇胡扯
之前在学习入门精要的时候,看到阴影部分各种名字超长的内置宏和看不明白的采样方式就想直接跳过这一章了(当时想的是,反正用到的时候把这些内置宏复制一遍出来就可以了)。直到前段时间面试被问到unity内部阴影到底是怎么实现的,才发现自己对阴影一无所知,刚好最近有时间,准备再次认真的学习一下阴影相关的知识。
1.阴影必须的三要素
想要产生阴影,至少需要三个物体的支持:
- 光源(废话)
- 阴影产生(投射?)者
- 阴影接收者
阴影的产生与消失与这三个物体息息相关,所以当场景中阴影消失时要先查找一下是不是这三个物体没有打开对应的开关。
light组件中的mode和shadow type选项都会影响阴影的产生,mesh组件重的cast shadows表示是否向其他物体投射阴影,receive shadows表示这个mesh是否接收其他物体的阴影。
2.阴影是怎么产生的
我们知道,阴影是由于物体遮住了光的传播,不能穿过不透明物体而形成的较暗区域。
而在unity中,阴影使用的是一种Shadow Map的技术。大概意思就是把相机放在光源位置,相机看不到的地方,就是这个光源的阴影区域。
3.阴影产生着(投射者)
上面提到过unity使用的是Shadow Map技术,unity要把相机放在光源位置,然后计算一张该点的深度图。如果按照正常流程来说,我们要把物体的渲染流程全都走一遍来写入深度,得到shadowmap,但是这么做无疑会做很多多余的计算(很多和深度无关的计算如光照模型等)。
unity使用了一个额外的pass来专门更新光源的shadowmap:这个pass就是LightMode标签被设置为ShadowCaster的pass
。所以,在unity中,如果没有这个pass,并且没有指定Fallback或指定的Fallback中没有LightMode标签为ShadowCaster的pass时,该物体就无法向其他物体投射阴影。
Tags { "LightMode" = "ShadowCaster" }
对于不透明物体,我们一般可以使用unity中写好的shader作为Fallback,这样在渲染的时候unity会自己去Fallback中寻找ShadowCaster的pass来渲染阴影。但是对于透明物体或者是使用了透明度测试的材质,使用默认的ShadowCaster就会得到一些错误的结果,这个时候就要我们根据自己的需要来实现ShadowCaster的pass,比如在片元着色器进行透明度测试等。
4.阴影接收者
在传统的阴影映射纹理中,我们会在正常渲染的pass中把顶点转换到光源空间下,然后对光源的shadowmap进行采样,再把采样结果与顶点的深度进行对比,如果顶点深度大于采样结果,那么说明该顶点在阴影内。
unity使用了不同的阴影采样技术:屏幕空间的阴影映射技术
。但是不是所有平台unity都会使用这种技术,因为这种技术需要显卡支持MRT。那么屏幕空间的阴影映射技术到底做了什么呢?
unity首先会调用LightMode为ShadowCaster的pass得到光源的阴影映射纹理(shadowmap)
和摄像机的深度纹理
。然后根据阴影映射纹理和相机的深度纹理得到屏幕空间的阴影图
。如果摄像机的深度图中记录的深度大于转换到阴影映射纹理中的深度值,就说明该表面可见但是处于阴影中。
如果我们想要某个物体接受来自其他物体的阴影,只需要在shader中对这张屏幕空间的阴影图
进行采样就可以了。因为阴影图是基于屏幕空间的,所以我们在采样的时候要把表面坐标从模型空间变换到屏幕坐标空间
。
那么怎么对阴影图进行采样呢?unity其实已经帮我们封装好了采样的函数。我们可以直接使用三个宏指令,就可以完成对阴影的采样。三个宏指令分别是:SHADOW_COORDS(用在顶点输出结构体内)
、TRANSFER_SHADOW(用在顶点着色器)
、SHADOW_ATTENUATION(用在片元着色器)
。这三个指令都是在AutoLight.cginc
中定义的,所以我们在使用前要记得添加include。
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
//参数为下一个插值寄存器的索引
SHADOW_COORDS(2)
};
v2f vert(a2v v) {
v2f o;
//你的定点着色器逻辑
// Pass shadow coordinates to pixel shader
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target {
//片元着色器逻辑
fixed shadow = SHADOW_ATTENUATION(i);
//结果计算
}
AutoLight中定义的宏指令:
// ---- Screen space direction light shadows helpers (any version)
#if defined (SHADOWS_SCREEN)
#if defined(UNITY_NO_SCREENSPACE_SHADOWS)
UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture);
#define TRANSFER_SHADOW(a) a._ShadowCoord = mul( unity_WorldToShadow[0], mul( unity_ObjectToWorld, v.vertex ) );
inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord)
{
#if defined(SHADOWS_NATIVE)
fixed shadow = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, shadowCoord.xyz);
shadow = _LightShadowData.r + shadow * (1-_LightShadowData.r);
return shadow;
#else
unityShadowCoord dist = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, shadowCoord.xy);
// tegra is confused if we use _LightShadowData.x directly
// with "ambiguous overloaded function reference max(mediump float, float)"
unityShadowCoord lightShadowDataX = _LightShadowData.x;
unityShadowCoord threshold = shadowCoord.z;
return max(dist > threshold, lightShadowDataX);
#endif
}
#else // UNITY_NO_SCREENSPACE_SHADOWS
UNITY_DECLARE_SCREENSPACE_SHADOWMAP(_ShadowMapTexture);
#define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos);
inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord)
{
fixed shadow = UNITY_SAMPLE_SCREEN_SHADOW(_ShadowMapTexture, shadowCoord);
return shadow;
}
#endif
#define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)
#endif
由于这些宏会使用上下文变量来进行相关计算,我们在编写shader时需要保证自己定义的变量名与宏使用的名称相匹配:a2v结构体(顶点输入结构体)的顶点坐标变量名必须是vertex,顶点着色器中a2v(顶点输入)结构体的名字必须是v,且v2f(顶点输出结构体)的顶点位置必须为pos
。
5.统一管理阴影与光照衰减
在片元着色器中我们使用了SHADOW_ATTENUATION
宏进行阴影处理(对屏幕空间阴影图进行采样)。unity还封装了一个宏,可以进行统一的光照衰减计算与阴影计算,那就是UNITY_LIGHT_ATTENUATION
宏,这个宏需要三个参数,第一个是光照的衰减atten,这个参数我们不用在外部声明,宏内部会自己声明该变量并填充衰减值后传出,第二个参数是我们在片元着色器中拿到的顶点输出结构体,第三个参数是世界空间的坐标,这个坐标用来计算光源空间下的坐标。这个宏针对不同类型的光源和情况声明了多个版本,所以我们在使用的时候不需要在Additional Pass判断光源类型,代码也得以统一。
// UNITY_LIGHT_ATTENUATION not only compute attenuation, but also shadow infos
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
总结
又看了一遍书中关于阴影的部分,确实收益良多。但是还有很多东西现在搞不清楚,比如为什么在生成光源阴影映射纹理的时候要绘制很多次同样的物体,而且不能合批,是不是阴影映射纹理也默认进行了LOD处理呢?
补充:
在渲染阴影的时候,会绘制四次renderjobdir,这里的阴影也是使用了一种类似于LOD的手法进行处理,生成四份光源空间下的深度图。在游戏运行时根据相机距离来决定最终要采样哪一种质量的阴影贴图,这样可以在游戏执行时动态的优化效率。但是同样付出的代价就是要多绘制几次,也就是多一些DC,同样用来存储的空间也对应的要增大一些。
那么这个LOD的设置在哪呢?在quality setting中我们可以看到有相关的shadows的设置信息,在这里就可以设置shadow cascades数量啦。