1 初见反射:Environment Mapping
Shiny surface 就像镜面材质一样,尤其是金属材质。一个 “完美镜面” 完全为反射的,即不包含任何漫反射,仅存在高光反射。
当我们试图将球球变成镜面材质时(metallic 设置为 1,smoothness 设置为 0.95,然后变成 solid white),会发现它呈现出一个守护甜心坏蛋的大效果:
为什么会出现坏蛋效果?
因为我们现在仅仅使用 direct light,而折射环境需要 indirect light(准确的说,是高光反射的 indirect light)。
在此前的自定义函数 CreateIndirectLight
中,配置了 UnityIndirect
structure,此时高光组件被简单设置为 0,That's whay the sphere turned out black!
in case 想要观察反射特性:
将场景的 ambient intensity 设置为 0,以便将注意力集中在 reflections;将材质再次设置回一个 “dull nomental”(smoothness 设置为 0.5);最后将 indirect specular color 设置为一个明显的颜色(比如红色)。
CreateIndirectLight function
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 在任何地方都占据主导地位。因此画面表现为红球球,而不是黑球球。
为什么球球边缘反射了更多光线(亮红色部分)?
Fresnel reflection,菲涅耳效应。
当观察角度趋向物体表面的边缘(视线与表面法线夹角增大)时,反射会增强。而在掠射角下,所有的光线都会被反射。此外,球球表面设置的光滑程度越高,菲涅耳反射越强。
此外,由于 reflection 来自间接光(indirect light),而独立于直接光,因此也同时独立于直接光的阴影。所以菲涅耳反射在原本为阴影边缘的位置表现更明显。
正在使用的 UNITY_BRDF_PBS 版本可以计算这一现象。
metallic 与 non-metallic 的镜面反射有什么区别?
diffuse
镜面反射在闪亮的非金属材料上也能很好地表现出来,但并不主导其外观。仍然可以看到大量的 diffuse;
specular
此外,金属材质的 specular reflection 是有颜色的,而非金属材质的高光则简单表现为灯光色(通常为白色)。
如何理解反射效果的 shadow 部分?
由于间接反射通过环境光产生,与直接光源的形成原因相互独立,因此即使在直接光源的阴影中,间接反射仍然可以存在。
(这里可以联想素描中明暗交界线的机制。即阴影部分的明度高于明暗交界线,因为它虽然在背光区,却依然可以收到环境光照的影响。)
Nonmetallic
Nonmetallic 的间接反射部分会让阴影区域相对更亮。因为间接光与直接光源形成原因独立,因此其光线传播不会完全被遮挡,阴影与间接反射并存。
Metallic
Metallic 的间接反射强度通常更高,甚至可以完全覆盖直射光的影响。因此非常非常光滑的金属表面上,直射光和阴影可能完全看不到;以及在完美镜面(perfect mirror)的情况下,阴影完全消失(如图三),此时镜面表面仅反射环境光,没有漫反射或散射光来表现阴影。
Anyway,在实际场景中完美镜面几乎不存在。例如,镜面上的污垢和灰尘仍会反射直射光并显示阴影。很多材料是金属和非金属的混合物(例如抛光但有氧化层的金属)。在这种情况下,既有金属的高强度间接反射,又保留了一部分非金属的直射光和阴影效果。
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
如何理解 reflect
函数计算的反射方向?
镜面反射中,入射光线与反射光线方向关于法线对称。而其中,入射方向为观察方向的反方向(这里为 -viewDir
)。
如何理解 UNITY_SAMPLE_TEXCUBE
宏?
unity_SpecCube0
的类型取决于目标平台(在 HSLSupport 中定义),而 UNITY_SAMPLE_TEXCUBE
宏则帮助 take cares of the type difference。
如何理解 DecodeHDR
函数?
直接使用注释中的 indirectLight.specular
时,会发现球球变得非常之亮。这是因为 cube map 包含了 high dynamic range(高动态范围)颜色,这允许它包含大于 1 的亮度值。所以需要这里将 HDR 格式转换为 RGB。
UnityCG 中包含了 DecodeHDR
函数,其中 HDR 数据使用 RGBM 格式存储在四个通道中,也因此需要将上方的 envSample
声明为 float4 类型(而非 float3)。
1.3 新玩具:Reflection Probe 反射探针
在 "GameObject - Light - Reflection Probe" 路径下添加一个反射探针,将它放在与球球同样的位置上。
反射探针通过渲染立方体贴图来捕捉环境,每个探针会渲染六次,对应立方体的六个面。默认为 Baked Mode。为了实时看到球球反射的情况,可以将其设置为 Realtime Mode。
Baked Mode 和 Realtime Mode 有什么区别?
Baked Mode:在编辑模式中生成 cube map 并包含在场景上的建筑中。map 只包含标记为 static 的几何体。因此,如果希望建筑在渲染为 cube map 之前,需要将其标记为 static;
Realtime Mode:实时探针在运行时渲染,可以自由控制刷新频率。频繁更新代价较高,且在编辑模式下不会更新;
此外,选择 Baked Mode 时,物体不必完全是 static 的。可以选择为特定系统(如反射探针)单独设置静态。这里的相关设置是 Reflection Probe Static。
以及,因为反射球球并不反射自己,保持 dynamic 即可。
探针的 gizmo 挡住了球球怎么办
scene view 会通过一个圆形的 gizmo 来表示 reflection probe 的存在。它的样式取决于场景设定。gizmo 有些碍眼时,可以在 scene view 中打开 gizmo 下拉菜单,找到 ReflectionProbe,点击它的 icon 关掉。
2 细说反射:非完美世界
完全光滑的表面才会产生完美的锐利反射,而表面越粗糙,反射中就有越多漫反射。暗哑的镜面会产生模糊。Unity 使用一种卷积算法生成的 mipmap 来生成这种 blur 效果。
Unity 为什么不使用传统 mipmap 进行模糊?
传统的 mipmap 是对原始图像进行降采样(down-sampling),通过平均相邻像素来逐步生成分辨率更低的图像层级。在低分辨率层级中,图像细节减少,可能导致图像呈现块状(blocky),因为每个像素代表更大的区域,细节缺失。
Unity 中环境贴图的 mipmap 使用了不同的卷积算法(convolution),目的是让每层 mipmap 显示从清晰到模糊的平滑过渡,而不是简单的像素降采样。因此,Unity 的 mipmap 不会表现为典型的块状,而是能产生更加自然的模糊反射效果。
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
如何理解 Unity_GlossyEnvironment
函数?
Unity_GlossyEnvironment
函数用于计算光滑表面的环境反射,实现基于环境贴图的高光效果:
float3 Unity_GlossyEnvironment(
samplerCUBE texCube, // 立方体贴图
half4 hdr, // HDR 参数
Unity_GlossyEnvironmentData data // 环境数据
)
其包含三部分工作:
convert from HDR(同上述 DecodeHDR
函数讲解)
convert roughness
因为粗糙度和 mipmap level 之间的关系不是线性的。Unity 使用的转换公式为 1.7r−0.7r2,其中 r 是原始的粗糙程度。
sample the cube map
对 cube map 在特定的 mipmap level 中进行采样(借用 UNITY_SAMPLE_TEXCUBE_LOD
宏)。
// what does Unity_GlossyEnvironment do
// convert roughness
float roughness = 1 - _Smoothness;
roughness *= 1.7 - 0.7 * roughness;
// sample the cube map
// UNITY_SPECCUBE_LOD_STEPS 宏:将 roughness(范围为 0 - 1)缩放到 mipmap 的范围(由该宏定义)
float4 envSample = UNITY_SAMPLE_TEXCUBE_LOD(
unity_SpecCube0, reflectionDir, roughness * UNITY_SPECCUBE_LOD_STEPS
);
// convert from HDR
indirectLight.specular = DecodeHDR(envSample, unity_SpecCube0_HDR);
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 始终与轴对齐,忽略所有旋转与缩放。
reflection probe 定义的 cube area 有什么用途?
渲染物体时,Unity 利用这些区域决定使用哪个探针;
使用该区域做 box projections(盒体投影,马上要用到的技术)。
如何使用该 cube area?
选择 probe 时可以看到场景视图中的 box。在 reflection probe 的 inspector 上方有一个 Probe Scene Editing Mode 切换按钮,左侧的按钮可以打开 box projection 边界的 gizmos。
区域可以使用边界-面-中心的小黄点来调整,也可以通过 inspector 中的 Size 和 Probe Origin 向量调整。通过调整原点可以相对于采样点移动 box。
也可以在场景中使用其他编辑模式进行调整,但这种方法比较麻烦,目前操作后无法撤消。
调整 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。
思路
其中 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
如何理解 if 判断语句 cubemapPosition.w > 0
?
是否启用盒投影(box projection)是由每个探针控制的。Unity 将该控制信息存储在立方体贴图的第四个分量中(cubemapPosition,也因此 cubemapPosition 为 float4 类型)。
当 if 语句判断 w 分量 > 0 时,启动 box projection。
如何理解语句 direction > 0 ? boxMax : boxMin
的运算逻辑?
在 shader 中,对一个向量使用 "? :" 运算符,会分别对向量的每个分量进行 "? :" 运算。
比如在下面这个例子中:
float3 test = float3(0, 0, 0.5);
float3 col1 = fixed3(1, 1, 1);
float3 col2 = fixed3(0, 0, 0);
color = (test > 0.0f) ? col1: col2;
// result: color = (0, 0, 1)
如何理解语句 UNITY_BRANCH
?
GPU 上线程是以 “线程组” 或 “波束” 的形式执行的。如果在代码中使用了分支,且不同线程的条件结果不同,会导致线程分歧,降低性能,所以编译器倾向于将 if 语句转换为条件赋值或条件移动指令,以避免分支。
所以虽然其下方是一个 if 判断语句,但是经过 GPU 编译优化,其在 OpenGL 的编译结果如下:
u_xlatb22 = 0.0 < unity_SpecCube0_ProbePosition.w;
u_xlat0.xyz = (bool(u_xlatb22)) ? u_xlat2.xyz : u_xlat0.xyz;
此时编译器会将 if 语句内的代码全部执行(都做盒投影),然后根据最终的结果根据条件来选择。
而 UNITY_BRANCH
是一个编译器提示,强制编译器在这里使用实际的分支,而不是条件赋值。
虽然 UNITY_BRANCH 并不建议使用,但由于在这里使用时,对于同一对象的渲染,其结果统一,不会导致线程分歧,又可以避免在未启用盒投影时仍然执行盒投影的计算。
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
BlendReflectionProbes.cginc
#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
如何理解 probe1 计算中的 UNITY_PASS_TEXCUBE_SAMPLER
宏?
采样纹理资源同时需要采样纹理和采样器,采样器定义了纹理采样的方式,例如过滤模式(Filtering Mode)、寻址模式(Wrap Mode)等。
因为在 Unity 中,所有 probes 通常使用相同的采样器状态,而且 GPU 对采样器数量有限制(通常是 16 个),所以此时第二个 probe 需要依赖第一个 probe 的采样器。(哪怕明面上没有这么指定,Unity 在内部可能也只为 probes 绑定了一个采样器)
因此使用 UNITY_PASS_TEXCUBE_SAMPLER 宏将第二个 probe 的纹理与咱唯一的采样器结合起来,消除报错。
float3 probe1 = Unity_GlossyEnvironment(
// UNITY_PASS_TEXCUBE(unity_SpecCube1), unity_SpecCube1_HDR, envData
UNITY_PASS_TEXCUBE_SAMPLER(unity_SpecCube1, unity_SpecCube0), unity_SpecCube0_HDR, envData
);
如何理解 UNITY_SPECCUBE_BOX_PROJECTION
和 UNITY_SPECCUBE_BLENDING
宏的优化?
在判定目标平台不支持盒投影 / 不支持混合之前停止 if 条件编译中的代码。
先问能不能,再问用不用。
4.3 设置重叠:Overlapping
让设置的 blending work 起来,需要将多个 probe 的边界重叠。
因此,调整第二个盒子使其延伸至建筑物室内,重叠区域内的球体可获得混合反射效果。(如果过渡效果不够丝滑,可以在两个 probe 间添加第三根 probe)。
如何设置 probe 和 skybox 之间的混合
将 Reflection Probe 模式从 Blend Probes 切换至 Blend Probes and Skybox,此时物体边界框超出探针的边界时,就会同 skybox 发生混合。
此外,可以使用 mesh renderer 组件的 inspector 展示被使用的探针及其权重。
5 无穷无镜:Bouncing Reflections
两张镜子面对面时会看到层层叠叠的嵌套反射,无穷无尽。Unity 中,可以通过 Bouncing 获得同样的效果。
此时地板上的镜子并不包含在反射中(因为它们不是 static 的)。将地板上的镜子设置为 static,球体则保持 dynamic(否则探测器就无法再透过球体观察,从而产生奇怪的反射)。
现在,镜子出现在单一反射探针中,但它看起来是纯黑色的,因为在渲染探针时它的环境贴图还不存在,却正试图反射自己,所以失败了!
默认情况下 Unity 的 environment map 中不包含反射,可以通过光照设置更改。Environment Settings 部分包含 "Reflection Bounces"(反弹反射)滑块,默认设置为 1。将其设置为 2。
Unity 最多可支持五次反弹。要查看实际效果,复制地面上的镜子,放到天花板上。
Unity 如何支持反弹反射?
以两次反射为例。设置为两次反弹时,Unity 会首先以正常方式渲染每个反射探针。然后使用现在可用的反射数据对它们进行第二次渲染。因此,来自落地镜的初始反射现在会包含在 environment map 中。
虽然 Unity 可以以上述方式实现嵌套反射,但数量有限(最多支持五次),并且需要大量的渲染(所以 you definitely don't want to use this at run time!)。
另外投影其实也是错误的,因为探测器的边界并没有延伸到镜子以外的虚拟空间。
APPENDIX
Anki
Reference