反射效果的实现总结

一、反射的实现原理分类

首先要说明下反射向量,指的是视线的镜面反射向量,如下图所示,

实际上,人眼看向一个物体表面的时候,在该位置(上图O点)的反射信息,来自于视线的反射方向,因为光线会从该反射方向打到物体表面,最终进入人眼。一般情况下,我们假定反射角和入射角相等。以下所有涉及到的反射向量, 都是指的视线反射向量,不是光线反射向量。

1.1 CubeMap

天空盒就是一个CubeMap,我们可以假定天空盒是一个环境反射来源,也可以指定另外的CubeMap,用反射向量采样这个CubeMap就能得到反射颜色。CubeMap虽然比较简单,但是也能出很好的效果,而且不需要实时计算反射信息,性能很好。
优点:实现简单,效率高,只需要额外的CubeMap存储反射信息;适用于多种情况,不仅仅限于平面反射。
缺点:反射信息固定,没有变化。

1.2 反射探针

这种方式需要在场景内布置反射探针,用来采集反射信息。渲染时候,在Shader内根据反射探针来获得反射信息。反射探针如果是实时的,性能就会很差,这个时候可以考虑降低反射探针的更新频率或者使用烘焙模式的反射探针。因为反射探针的输出就是CubeMap,因此烘焙模式的反射探针,本质上和CubeMap没有区别。
优点:直接利用引擎计算反射信息,不需要额外工作;适用于多种情况,不仅仅限于平面反射。
缺点:实时反射探针性能差,计算一次反射探针需要朝着6个方向渲染场景,Drawcall增加6倍,性能太差;烘焙反射探针无法变化。

1.3 平面反射

这种方式限制于只能在平面上做反射。如果要求在凹凸不平的表面上实现反射效果,则不太适合。基本思路是将场景根据平面对称镜像一次,具体实现上是将生成的反射矩阵乘以到原场景摄像机的世界到相机空间的矩阵,然后用新的相机再渲染一次场景生成RT。然后在屏幕空间内采样这个RT,得到的像素值作为反射信息。
其实,使用反射探针也能实现平面反射的效果,原理是将探针的位置放在摄像机在平面的对称位置。可以参考大佬的这篇文章,关于反射探头的实时反射。实现难度相对平面反射低很多,不过实时探针比平面反射性能差6倍,优化起来难度太大。
优点:反射效果最好,最真实接近平面反射。
缺点:需要额外渲染一次场景,DrawCall翻倍。

1.4 屏幕空间反射(SSR)

屏幕空间反射的基本原理比较简单,也就是在屏幕空间内通过深度法线纹理恢复世界空间坐标。然后,沿着反射向量方向做步进,也就是所谓的RayMarching,检查当前深度是否已经超过深度纹理对应的值,如果超过,表面已经碰到物体了,那么取当前步进到的颜色值作为反射结果即可。
优点:适用于多种情况,不仅仅限于平面反射;DrawCall不变。
缺点:需要额外的深度和法线纹理,在前向渲染中这不是免费的,需要多渲染一次场景得到深度和法线纹理;效果一般;无法反射屏幕之外的信息;实现比较复杂,移动平台下性能差(步进相交的计算量大),很可能跑不起来;带宽增加。

SSR应该是更适合于延迟渲染的一个反射效果实现方案,毕竟可以免费得到深度和法线纹理。

1.5 屏幕空间平面反射

这个是平面反射在屏幕空间下的一个实现。
SSPR大体的实现思路如下,
1、用当前屏幕UV从深度图重建当前世界坐标,将世界坐标以反射平面进行对称翻转
2、使用翻转后的世界坐标的计算屏幕UV
3、对当前屏幕纹理进行采样得到ReflectColor保存到一张新的ColorRT中,保存位置是翻转后的世界坐标的屏幕UV
4、在反射平面的Shader中用屏幕UV对ColorRT进行采样得到反射颜。
5、在反射平面的Shader中将反射颜色和着色结果进行组合得到最终颜色。


如上大佬的图能够基本说明SSPR的实现思路。UAV write即是3的输出。关键点和难点是要得到步骤三的ColorRT,并且要正确高效。网上有不少博客说的是如何正确高效实现前三步,基本上要使用Computer Shader,图形接口要求是vulkan/metal。具体实现比较复杂,不在此详细说明。

优点:效果较高;性能比SSR好;DrawCall不变。
缺点:对硬件要求高;需要额外的ColorRT,带宽和内存增加;只适用于平面反射。

二、Unity对反射效果支持

2.1 CubeMap

Unity自带的Shader或许有支持,实现起来也很简单,关键代码如下:

1
2
half3 reflectVector = reflect(-viewDirectionWS, normalWS);
half3 reflectColor = SAMPLE_TEXTURECUBE(_Cubemap, sampler_Cubemap, reflectVector).rgb;

2.2 反射探针

目前,Unity内置管线和Urp支持反射探针,HDRP管线还支持一种特殊的平面反射探针,平面反射探针猜测是针对平面反射这种特殊情况的一种优化手段。
场景内布置了反射探针后,Urp管线中反射信息是存储在叫做unity_SpecCube0的内置CubeMap中。Shader中需要采样该CubeMap获得反射信息,Urp代码中搜索函数GlossyEnvironmentReflection,可以得到如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
half3 GlossyEnvironmentReflection(half3 reflectVector, half perceptualRoughness, half occlusion)
{
#if !defined(_ENVIRONMENTREFLECTIONS_OFF)
half mip = PerceptualRoughnessToMipmapLevel(perceptualRoughness);
half4 encodedIrradiance = SAMPLE_TEXTURECUBE_LOD(unity_SpecCube0, samplerunity_SpecCube0, reflectVector, mip);

//TODO:DOTS - we need to port probes to live in c# so we can manage this manually.
#if defined(UNITY_USE_NATIVE_HDR) || defined(UNITY_DOTS_INSTANCING_ENABLED)
half3 irradiance = encodedIrradiance.rgb;
#else
half3 irradiance = DecodeHDREnvironment(encodedIrradiance, unity_SpecCube0_HDR);
#endif

return irradiance * occlusion;
#endif // GLOSSY_REFLECTIONS

return _GlossyEnvironmentColor.rgb * occlusion;
}

上述函数三个关键点,根据粗糙度计算mipmap,采样光照探针,解析HDR贴图。我们重点关注的是mipmap计算,mipmap大家都知道,越远的地方,贴图采样率越低效果才好,看起来越模糊,没有锐利的毛刺感觉;同时粗糙度刚好可以表示这个概率,粗糙度越低越接近镜面,那么反射效果更接近光滑镜子的效果,粗糙度越高,反射效果越模糊。
计算Mipmap的概念可以应用到所有的反射效果实现中,不仅仅反射探针

2.3 平面反射

Unity没有发现支持,需要自己实现或者找第三方实现。后续会写文章介绍如何实现。

2.4 屏幕空间反射

Unity没有发现支持,需要自己实现或者找第三方实现。后续会写文章介绍如何实现。

2.5 屏幕空间平面反射

Unity没有发现支持,需要自己实现或者找第三方实现。后续会写文章介绍如何实现。

三、UE4对反射效果支持

3.1 CubeMap

UE4的材质编辑器可以实现。

3.2 反射探针

UE4有盒子和球形的反射探针

3.3 平面反射

UE4有Planar Reflection Actor,放入场景中即可。不过先要在工程设置中开启平面反射。

3.4 屏幕空间反射

UE4默认是启用屏幕空间反射的。不过是可以在工程设置或者配置文件中关闭的。

3.5 屏幕空间平面反射

目前没有发现UE4支持这个特性。

四、反射颜色与物体颜色的组合

4.1 Mipmap

计算Mipmap,模拟粗糙度的效果,这个在反射探针中已经有说明。

4.2 菲涅尔效果

获得反射颜色后,可以根据菲涅尔定律与物体本身的着色结果进行一定的组合即可。不过,不一定完全照搬菲涅尔效果的近似公式,比如Schlick菲涅耳近似等式。不过关键点在于强度必须是NdotV的函数,最简单的方式是计算出NdotV,对NdotV取反或者1-NdotV,因为入射角越大反射光越强,同时提供一个最大最小值来限制强度范围。也可以自定义其它跟NdotV负相关的函数来在反射颜色和物体颜色之间进行插值,来得到想要的效果。

以下是一个同时应用了粗糙度计算Mipmap和菲涅尔效果的反射平面,

五、参考资料

1、Unity Shader-反射效果(CubeMap,Reflection Probe,Planar Reflection,Screen Space Reflection)
2、关于反射探头的实时反射
3、Unity URP 移动平台的屏幕空间平面反射(SSPR)趟坑记
4、URP下屏幕空间平面反射(ScreenSpacePlanarReflection)学习笔记