PA7 Reflections
Shader 与渲染算法 - 环境互动 - 反射
1 初见反射:Environment Mapping
Shiny surface 就像镜面材质一样,尤其是金属材质。一个 “完美镜面” 完全为反射的,即不包含任何漫反射,仅存在高光反射。
当我们试图将球球变成镜面材质时(metallic 设置为 1,smoothness 设置为 0.95,然后变成 solid white),会发现它呈现出一个守护甜心坏蛋的大效果:

in case 想要观察反射特性:
将场景的 ambient intensity 设置为 0,以便将注意力集中在 reflections;将材质再次设置回一个 “dull nomental”(smoothness 设置为 0.5);最后将 indirect specular color 设置为一个明显的颜色(比如红色)。
UnityIndirect CreateIndirectLight (Interpolators i) {
UnityIndirect indirectLight;
indirectLight.diffuse = 0;
indirectLight.specular = 0;
#if defined(VERTEXLIGHT_ON)
indirectLight.diffuse = i.vertexLightColor;
#endif
#if defined(FORWARD_BASE_PASS)
indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
indirectLight.specular = float3(1, 0, 0); // here's new
#endif
return indirectLight;
}
此时,红色就表示了 reflectivity。
金属材质下,indirect reflections 在任何地方都占据主导地位。因此画面表现为红球球,而不是黑球球。

1.1 粗糙的想法 Rough Idea

反射真实环境,一个简单的想法是对 skybox cube 进行采样。
设置反射方向:使用 reflect 函数。需传递视角方向 viewDir;
采样 skybox cube:借助
unity_SpecCube0
变量(定义在 UnityShaderVariables 中)进行采样;HDR 解码:采样结果使用
DecodeHDR
函数进行向 RGB 格式的转换。
1.2 CODING
#if !defined(REFLECTION_ENVIRONMENTMAPPING_LIGHT)
#define REFLECTION_ENVIRONMENTMAPPING_LIGHT
#define UNITY_PBS_USE_BRDF3
#include "UnityPBSLighting.cginc"
#include "AutoLight.cginc"
...
UnityIndirect CreateIndirectLight (Interpolators i, float3 viewDir) {
UnityIndirect indirectLight;
indirectLight.diffuse = 0;
indirectLight.specular = 0;
#if defined(VERTEXLIGHT_ON)
indirectLight.diffuse = i.vertexLightColor;
#endif
#if defined(FORWARD_BASE_PASS)
indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
float3 reflectionDir = reflect(-viewDir, i.normal);
float4 envSample = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, reflectionDir);
// indirectLight.specular = envSample;
indirectLight.specular = DecodeHDR(envSample, unity_SpecCube0_HDR);
#endif
return indirectLight;
}
...
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
...
return UNITY_BRDF_PBS(
albedo, specularTint,
oneMinusReflectivity, _Smoothness,
i.normal, viewDir,
CreateLight(i), CreateIndirectLight(i, viewDir)
);
}
...
#endif
1.3 新玩具:Reflection Probe 反射探针
在 "GameObject - Light - Reflection Probe" 路径下添加一个反射探针,将它放在与球球同样的位置上。

反射探针通过渲染立方体贴图来捕捉环境,每个探针会渲染六次,对应立方体的六个面。默认为 Baked Mode。为了实时看到球球反射的情况,可以将其设置为 Realtime Mode。
2 细说反射:非完美世界
完全光滑的表面才会产生完美的锐利反射,而表面越粗糙,反射中就有越多漫反射。暗哑的镜面会产生模糊。Unity 使用一种卷积算法生成的 mipmap 来生成这种 blur 效果。

2.1 粗糙的想法 Rough Idea
使用 mipmap 对环境贴图进行采样,重点在对 roughness 的处理(roughness 与 mipmap level 的关系)。
具体实现中,可使用 Unity_GlossyEnvironment
函数,配合 Unity_GlossyEnvironmentData
structure,一键式搞定 mipmap 环境采样。
2.2 CODING
#if !defined(REFLECTION_ROUGHMIRROR_LIGHT)
#define REFLECTION_ROUGHMIRROR_LIGHT
#define UNITY_PBS_USE_BRDF3
#include "UnityPBSLighting.cginc"
#include "AutoLight.cginc"
...
UnityIndirect CreateIndirectLight (Interpolators i, float3 viewDir) {
UnityIndirect indirectLight;
indirectLight.diffuse = 0;
indirectLight.specular = 0;
#if defined(VERTEXLIGHT_ON)
indirectLight.diffuse = i.vertexLightColor;
#endif
#if defined(FORWARD_BASE_PASS)
indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
float3 reflectionDir = reflect(-viewDir, i.normal);
Unity_GlossyEnvironmentData envData;
envData.roughness = 1 - _Smoothness;
envData.reflUVW = reflectionDir;
indirectLight.specular = Unity_GlossyEnvironment(
UNITY_PASS_TEXCUBE(unity_SpecCube0), unity_SpecCube0_HDR, envData
);
#endif
return indirectLight;
}
...
#endif
2.3 What's more:凹凸镜面 Bumpy Mirror
除了使用 smoothness 去表现粗糙镜面,也可以使用 normal maps 添加更大的变形。使用法线扰动来决定反射方向即可。

3 死抠反射:Box Projection
在场景中添加多个球球(共享一个探针)。此时会发现所有球球上的反射都是一样的。
解决方案很粗暴,给每个球球一个单独的探针即可:

But what about a flat mirror?
反射完全不匹配。此时的反射方向看起来是正确的,但比例和位置是错误的。假想如果对每个片段都是用一个探针肯定不会有问题,但是 we only have one probe。
对于无限远的物体(比如天空盒),前述近似办法就已足够,但对于附近事物的反射,就需要新的方法。

假设一间可以获取 surface position 的房间,并且任意位置可取得反射方向。向量最终将与 cube 边缘某处相交。用数学方法计算出该交点,然后构建一个从房间中心到该点的向量。利用这个向量对 cube map 进行采样,最终得到正确的反射。
将反射范围限制在一个盒子里
通过计算采样点和探针之间的向量来修正反射方向
通过数学计算确定该向量在 cube map 上的交点
3.1 启用 Reflection Probe Box
reflection probes(反射探针)有尺寸和探针原点,使得其可以在世界空间中依赖其位置,定义一个 cube area(立方体区域)。reflection probes 始终与轴对齐,忽略所有旋转与缩放。
调整 box 使其覆盖建筑物内部,拉伸至支柱并延伸到最高点。可以将它调大一点,以防场景视图中的 gizmos 因 Z-fighting 而闪烁。
完成后,启动 Box Projection 复选框。

3.2 天才的想法 Genius Idea
问题
现在采样 Reflection Probe 对应的起点是 Reflection Probe 的中心点 R,方向则是当前像素点 P(position)计算的视线方向对应的反射向量 direction(PK)。
如果按照 direction 方向直接采样 Reflection Probe,那么就会是下图 RL(平行于 PK)方向。而真正的采样方向是 RK。

思路
有 RK = RP + PK;
其中 RP 已知,PK 方向已知,长度 t 未知;
因为有直线公式 K = P + dir(PK) * t,且 K 为 reflection box bound
由此思路为:求 K -> 求 t -> 求 PK -> 求 RK

另外,Unity 通过体对角线定义 box:
3.3 CODING
#if !defined(REFLECTION_BOXPROJECTION_LIGHT)
#define REFLECTION_BOXPROJECTION_LIGHT
#define UNITY_PBS_USING_BRDF3
#include "UnityPBSLighting.cginc"
#include "AutoLight.cginc"
...
// [dir(PK)] direction => reflectionDir
// [P] position => i.worldPos
// [R] cubemapPosition => unity_SpecCube0_ProbePosition
float3 BoxProjection (
float3 direction, float3 position,
float4 cubemapPosition, float3 boxMin, float3 boxMax
) {
UNITY_BRANCH
if (cubemapPosition.w > 0) {
// t = (K - P) / dir(PK)
// K: 包围盒上的坐标值
float3 factors = ((direction > 0 ? boxMax : boxMin) - position) / direction;
// AABB 算法,求最小交点
float scalar = min(min(factors.x, factors.y), factors.z);
// PK: direction * scalar
// RP: position - cubemapPosition
// RK: PK + RP
direction = direction * scalar + (position - cubemapPosition);
}
return direction;
}
UnityIndirect CreateIndirectLight (Interpolators i, float3 viewDir) {
UnityIndirect indirectLight;
indirectLight.diffuse = 0;
indirectLight.specular = 0;
#if defined(VERTEXLIGHT_ON)
indirectLight.diffuse = i.vertexLightColor;
#endif
#if defined(FORWARD_BASE_PASS)
indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
float3 reflectionDir = reflect(-viewDir, i.normal);
Unity_GlossyEnvironmentData envData;
envData.roughness = 1 - _Smoothness;
// envData.reflUVW = reflectionDir;
envData.reflUVW = BoxProjection(
reflectionDir, i.worldPos,
unity_SpecCube0_ProbePosition,
unity_SpecCube0_BoxMin, unity_SpecCube0_BoxMax
);
indirectLight.specular = Unity_GlossyEnvironment(
UNITY_PASS_TEXCUBE(unity_SpecCube0), unity_SpecCube0_HDR, envData
);
#endif
return indirectLight;
}
...
#endif
4 混合反射:Blending Reflection Probes
在建筑里 reflection 的效果很好,如果是将球球移出 probe 的范围,球球就会突然切换为反射 skybox。想要在室内外得到一个比较好的反射,就需要使用多个反射探针。
预期管理:这样做会好很多,但是在两个不同的探针区域间,还是会有一些突然和清晰的过渡。

4.1 粗糙的想法 Rough Idea
Unity 支持在两张 environment map 中采样,其插值 interpolator 存储在 unity_SpecCube0_BoxMin
的第四个分量中,用以做采样权重的系数。
当 interpolator 值为 1 时,仅对第一张 map 采样;数值越小,第一张 map 的权重越低。
为 probe0 和 probe1 分开计算环境反射函数
Unity_GlossyEnvironment
。注意 probe1 需单独创建其环境数据envData
;当 interpolator 小于 1 时,使用插值函数
lerp
,根据probe0
,probe1
和interpolator
计算indirectLight.specular
;否则使
indirectLight.specular
直接等于 probe0。
4.2 CODING
#if !defined(REFLECTION_BLENDREFLECTIONPROBES_LIGHT)
#define REFLECTION_BLENDREFLECTIONPROBES_LIGHT
#define UNITY_PBS_USE_BRDF3
#include "UnityPBSLighting.cginc"
#include "AutoLight.cginc"
...
float3 BoxProjection (
float3 direction, float3 position,
float4 cubemapPosition, float3 boxMin, float3 boxMax
) {
#if UNITY_SPECCUBE_BOX_PROJECTION
UNITY_BRANCH
if (cubemapPosition.w > 0) {
float3 factors =
((direction > 0 ? boxMax : boxMin) - position) / direction;
float scalar = min(min(factors.x, factors.y), factors.z);
direction = direction * scalar + (position - cubemapPosition);
}
#endif
return direction;
}
UnityIndirect CreateIndirectLight (Interpolators i, float3 viewDir) {
UnityIndirect indirectLight;
indirectLight.diffuse = 0;
indirectLight.specular = 0;
#if defined(VERTEXLIGHT_ON)
indirectLight.diffuse = i.vertexLightColor;
#endif
#if defined(FORWARD_BASE_PASS)
indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
float3 reflectionDir = reflect(-viewDir, i.normal);
Unity_GlossyEnvironmentData envData;
envData.roughness = 1 - _Smoothness;
envData.reflUVW = BoxProjection(
reflectionDir, i.worldPos,
unity_SpecCube0_ProbePosition,
unity_SpecCube0_BoxMin, unity_SpecCube0_BoxMax
);
= Unity_GlossyEnvironment(
UNITY_PASS_TEXCUBE(unity_SpecCube0), unity_SpecCube0_HDR, envData
);
envData.reflUVW = BoxProjection(
reflectionDir, i.worldPos,
unity_SpecCube1_ProbePosition,
unity_SpecCube1_BoxMin, unity_SpecCube1_BoxMax
);
#if UNITY_SPECCUBE_BLENDING
float interpolator = unity_SpecCube0_BoxMin.w;
UNITY_BRANCH // 优化
if(interpolator < 0.99999) {
float3 probe1 = Unity_GlossyEnvironment(
UNITY_PASS_TEXCUBE_SAMPLER(unity_SpecCube1, unity_SpecCube0), unity_SpecCube0_HDR, envData
);
indirectLight.specular = lerp(probe1, probe0, interpolator);
} else {
indirectLight.specular = probe0;
}
#else
indirectLight.specular = probe0;
#endif
#endif
return indirectLight;
}
...
#endif
4.3 设置重叠:Overlapping
让设置的 blending work 起来,需要将多个 probe 的边界重叠。
因此,调整第二个盒子使其延伸至建筑物室内,重叠区域内的球体可获得混合反射效果。(如果过渡效果不够丝滑,可以在两个 probe 间添加第三根 probe)。
此外,可以使用 mesh renderer 组件的 inspector 展示被使用的探针及其权重。
5 无穷无镜:Bouncing Reflections
两张镜子面对面时会看到层层叠叠的嵌套反射,无穷无尽。Unity 中,可以通过 Bouncing 获得同样的效果。
此时地板上的镜子并不包含在反射中(因为它们不是 static 的)。将地板上的镜子设置为 static,球体则保持 dynamic(否则探测器就无法再透过球体观察,从而产生奇怪的反射)。
现在,镜子出现在单一反射探针中,但它看起来是纯黑色的,因为在渲染探针时它的环境贴图还不存在,却正试图反射自己,所以失败了!

默认情况下 Unity 的 environment map 中不包含反射,可以通过光照设置更改。Environment Settings 部分包含 "Reflection Bounces"(反弹反射)滑块,默认设置为 1。将其设置为 2。
Unity 最多可支持五次反弹。要查看实际效果,复制地面上的镜子,放到天花板上。

虽然 Unity 可以以上述方式实现嵌套反射,但数量有限(最多支持五次),并且需要大量的渲染(所以 you definitely don't want to use this at run time!)。
另外投影其实也是错误的,因为探测器的边界并没有延伸到镜子以外的虚拟空间。
APPENDIX
Anki
Reference
puppet-master: Unity Shader 反射效果(理解 Box Projection 帮大忙)
Last updated