反射


  • 对环境进行采样。
  • 使用反射探测器。
  • 创建粗糙和平滑的镜子。
  • 执行盒体投影立方体贴图的采样。
  •  反射探测器之间的混合。


这是关于渲染基础的系列教程的第八部分。在这个系列教程的前面部分我们添加了对阴影的支持。这一部分会介绍间接反射。

这个系列教程是使用Unity5.4.0f3开发的。

Unity 渲染教程(八):反射

有些时候,你需要对自反射做些工作


环境贴图

目前,我们的着色器通过组合物体表面上的环境光、漫反射光和镜面反射光来对片段进行着色。这至少对于比较阴暗的表面,会产生看上去比较真实的图像但是对于自发光的表面会看起来不太对。

一些闪亮的表面比如说是镜子,特别是金属材质的时候。一个完美的镜子会反射所有的光。这意味着根本没有漫反射。只有镜面反射的存在。所以,让我们把我们的材质变成一个镜子材质,通过设置“金属”属性为1和”光滑度“属性为0.95。同时把这个材质的颜色设置为白色。

Unity 渲染教程(八):反射

一个发光的白色金属球

这么做以后,得到的结果是几乎完全黑色的表面,即使它的颜色是白色的。我们只看到一个小亮点,这是光源照射到物体直接反生发射到我们视野中的部分。而其他所有光被反射在不同的方向。如果你要将“平滑度“属性增加到1,那么高亮会消失。

这看起来不像是真正的镜子。镜子不是黑色,它们会发射东西!在这种情况下,它应该发射天空盒,显示一个蓝色的天空与灰色的地面。


间接的镜面高光

我们的球变成了黑色,因为我们只包括了直接光照。为了反射环境,我们还必须包括间接光照。具体来说,间接光照用于镜面反射。在CreateIndirectLight函数中,我们配置了Unity的UnityIndirect结构。到目前为止,我们已将其镜面分量设置为零。这就是为什么球会变成黑色!

将场景的环境亮度设置为零,以便我们可以专注于反射部分。将我们的材质再次变成比较阴暗的非金属,平滑度为0.5。然后将间接镜面高光颜色更改为一个更加明显的颜色,比如说是红色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
     #endif
 
     return indirectLight;
}

 

Unity 渲染教程(八):反射Unity 渲染教程(八):反射

间接高光颜色分别是黑色和红色的对比图,光滑度是0.5

球体这次使用的是一个红色的色调。在这种情况下,红色的深浅正好是反射率的指示。因此,我们的球体会从它的中心那块反射一些环境光到我们的视野之中。显然,它更多地会在它的边缘的地方发生反射。这是因为当视角变得更浅的时候,每个表面都会变得更具有反射性。在掠射角的情况下,大多数光被反射,一切物体都变成了镜子。这被称为菲涅耳反射。我们使用UNITY_BRDF_PBS版本来为我们进行计算。

表面越平滑,菲涅尔反射越强。当使用非常高的平滑度的时候,红色环变得非常明显。

Unity 渲染教程(八):反射Unity 渲染教程(八):反射

光滑度0.15和0.95的对比图

因为反射来自间接光,所以它与直接光源无关。这样的话,反射也与该光源的阴影无关。因此,菲涅耳反射在球体的被遮蔽的边缘中变得非常明显。

在金属的情况下,间接反射占主导地位。不再是一个黑色的球体,我们现在得到一个红色的球体。

Unity 渲染教程(八):反射Unity 渲染教程(八):反射Unity 渲染教程(八):反射

光滑度0.15、0.50和 0.95的金属材质对比图

 

对环境进行采样

为了反射出实际的环境,我们必须对天空盒的立方体贴图进行采样。它在UnityShaderVariables中定义为unity_SpecCube0。这个变量的类型取决于在HSLSupport中确定的目标平台。

使用三维向量对立方体贴图进行采样,这可以指定采样的方向。我们可以使用UNITY_SAMPLE_TEXCUBE宏来处理类型上差异。让我们在开始的时候只使用法线向量作为采样的方向。

1
2
3
4
5
#if defined(FORWARD_BASE_PASS)
     indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
     float3 envSample = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, i.normal);
     indirectLight.specular = envSample;
#endif


Unity 渲染教程(八):反射

对环境进行采样

天空盒出现在我们的球体上了,但是这太亮了。这是因为立方体贴图里面包含高动态范围颜色,这允许它包含大于1的亮度值。我们必须将样本从高动态光照渲染格式转换为RGB。

UnityCG包含一个DecodeHDR函数,我们可以使用它。高动态光照渲染数据存储在四个通道之中,使用的是RGBM格式。所以我们必须采样一个float4值,然后进行转换。

1
2
3
indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
float4 envSample = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, i.normal);
indirectLight.specular = DecodeHDR(envSample, unity_SpecCube0_HDR);

 

Unity 渲染教程(八):反射

高动态光照渲染数据解码以后的效果


DecodeHDR是什么样子?

RGBM格式包含RGB三个通道,加上包含幅度因子的M通道。最终的RGB值是通过将它们乘以Unity 渲染教程(八):反射来计算。在这个公式里面,x是标量,y是指数,分别存储在解码指令的前两个分量中。

1
2
3
4
5
6
7
8
9
10
11
// Decodes HDR textures
// handles dLDR, RGBM formats
inline half3 DecodeHDR (half4 data, half4 decodeInstructions) {
     // If Linear mode is not supported we can skip exponent part
     #if defined(UNITY_NO_LINEAR_COLORSPACE)
         return (decodeInstructions.x * data.a) * data.rgb;
     #else
         return (decodeInstructions.x * pow(data.a, decodeInstructions.y)) *
             data.rgb;
     #endif
}

 

包含幅度因子的M通道的转换是必须的,这是因为当存储在纹理中的时候,会限制使用8位值来表示一个0到1范围。因此,x指令将其放大,y指令使其非线性,比如像是伽马空间那样。



跟踪反射

我们得到了正确的颜色,但是我们没有看到实际的反射。这是因为我们使用球体的法线来对环境进行采样,所以投影不依赖于视图方向。所以它就像在一个球体上涂上了环境贴图一样。

为了产生实际反射,我们必须得到从相机到物体表面的方向,并使用表面法线来反射它。我们可以使用reflect函数,就像我们在这个系列的第4部分所做的那样。在这种情况下,我们需要视图方向,因此将其作为参数添加到CreateIndirectLight。

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
UnityIndirect CreateIndirectLight (Interpolators i, float3 viewDir) {
    
     
     #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 = 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)
     );
}


Unity 渲染教程(八):反射Unity 渲染教程(八):反射

使用法线和反射方向之间的对比


使用一个反射探测器
让物体能反射天空盒很好,但如果能反射实际场景几何的话就更好了。所以让我们创建一个简单的建筑。我使用一个旋转后的四边形作为一个地板,并在它的上面放置了几个立方体柱,以及一些立方体梁。球体位于建筑物的中心。

Unity 渲染教程(八):反射

有一些东西等待反射

要看到建筑物的反射,我们必须先捕获它。这是通过反射探测器完成的,你可以通过GameObject / Light / Reflection Probe进行添加。创建一个反射探测器并将其放置在与我们的球体相同的位置。

Unity 渲染教程(八):反射Unity 渲染教程(八):反射

一个默认的反射探测器

场景视图显示场景中有具有圆形小控件的反射探测器的存在。它的外观取决于场景视图的配置。如果Gizmo阻碍我们的球体的视野,那就让我们关闭它。你可以通过打开场景视图工具栏中的Gizmo下拉菜单,向下滚动到ReflectionProbe,然后单击其图标来执行这个操作。

Unity 渲染教程(八):反射Unity 渲染教程(八):反射

关闭反射探测器的gizmo

反射探测器通过渲染一个立方体贴图来捕获环境信息。这意味着它要渲染整个场景六次,每次绘制立方体贴图的一面。在默认情况下,它的类型设置为”烘焙“。在这个模式下,立方体贴图由编辑器生成,并包含在构建中。这些立方体贴图里面仅包括静态几何。因此,我们的建筑必须是静态的,然后才能渲染到立方体贴图之中。

或者,我们可以将反射探测器的类型更改为“实时“。这种类型的探测器会在运行的时候实时渲染,你可以选择在何时渲染以及渲染的频率。还有一个自定义模式,给你完全的控制权。

虽然实时类型的探测器是最灵活的,但是当频繁更新的时候,它们也是最昂贵的。此外,实时类型的探测器不在编辑模式下更新,而烘焙类型的探测器会在探测器或者静态几何体被编辑的时候进行更新。所以,让我们坚持使用烘焙类型的探测器,并且确保我们的建筑是静态的。

对象实际上不需要是完全静态的。你可以为各种子系统的使用将物体标记为静态。在这种情况下,相关的设置为“反射探测器静态“。启用这个设置以后,对象将渲染到烘焙类型的反射探测器里面。你可以在运行的时候移动它们,但它们的反射仍然是冻结状态的。

Unity 渲染教程(八):反射

“反射探测器静态“

在建筑物被标记为静态之后,反射探测器将被更新。它会显示黑色一会儿,然后反射就会出现。反射球不是反射本身的一部分,所以保持动态。

Unity 渲染教程(八):反射

反射出来的几何体


不完美的反射

只有完美光滑的表面才会产生完美的锐利反射。表面越粗糙的话,反射扩散的越广。比较钝的镜子会产生模糊的反射。我们该如何模糊反射?

纹理可以具有mipmap,这是原始图像的下采样版本。当以全尺寸进行观察的时候,较高的mipmap因此会产生模糊的图像。这些会是块状的图像,但是Unity使用不同的算法来生成环境贴图的mipmap。这些卷积图表示从锐利到模糊的良好过渡。

Unity 渲染教程(八):反射Unity 渲染教程(八):反射Unity 渲染教程(八):反射Unity 渲染教程(八):反射Unity 渲染教程(八):反射Unity 渲染教程(八):反射

Mipmap等级从0到5的对比图



粗糙的镜子

我们可以使用UNITY_SAMPLE_TEXCUBE_LOD宏在特定的mipmap等级对立方体贴图进行采样。环境立方体贴图使用三线性过滤,因此我们可以在相邻的级别之间进行混合。这允许我们根据材质的平滑度来选择mipmap。材质越粗糙,我们应该使用更高的mipmap等级。

因为粗糙度从0变化到1的,我们必须通过我们使用的mipmap的范围来缩放这个值。Unity使用UNITY_SPECCUBE_LOD_STEPS宏来确定此范围,因此我们也使用它。

1
2
3
4
float roughness = 1 - _Smoothness;
float4 envSample = UNITY_SAMPLE_TEXCUBE_LOD(
     unity_SpecCube0, reflectionDir, roughness * UNITY_SPECCUBE_LOD_STEPS
);

 

UNITY_SPECCUBE_LOD_STEPS在哪里定义?

UnityShaderVariables将UNITY_SPECCUBE_LOD_STEPS定义为6,除非它之前已在其他位置定义过了。所以你可以在自己的着色器中定义它,在导入其他文件之前。Unity的着色器不在其他任何地方定义它,所以他们总是使用6这个值。环境贴图的实际大小没有考虑在内。

Unity 渲染教程(八):反射

光滑度为 0.5 时候的效果
实际上,粗糙度和mipmap等级之间的关系不是线性的。Unity使用的转换公式是 Unity 渲染教程(八):反射 ,其中r是原始的粗糙度。

Unity 渲染教程(八):反射

1
2
3
4
5
float roughness = 1 - _Smoothness;
roughness *= 1.7 - 0.7 * roughness;
float4 envSample = UNITY_SAMPLE_TEXCUBE_LOD(
     unity_SpecCube0, reflectionDir, roughness * UNITY_SPECCUBE_LOD_STEPS
);


Unity 渲染教程(八):反射Unity 渲染教程(八):反射Unity 渲染教程(八):反射

光滑度为0.5、0.75和0.95时候的效果

UnityStandardBRDF导入文件包含了Unity_GlossyEnvironment函数。它包含了所有用于转换粗糙度的代码,对立方体贴图进行采样,并从高动态光照渲染进行转换。所以让我们使用这个函数代替我们自己的代码。

要将立方体贴图作为参数传递,我们必须使用UNITY_PASS_TEXCUBE macrp。这需要处理类型差异。 此外,粗糙度和反射方向必须被封装在Unity_GlossyEnvironmentData结构之中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
         indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
         float3 reflectionDir = reflect(-viewDir, i.normal);
//      float roughness = 1 - _Smoothness;
//      roughness *= 1.7 - 0.7 * roughness;
//      float4 envSample = UNITY_SAMPLE_TEXCUBE_LOD(
//          unity_SpecCube0, reflectionDir, roughness * UNITY_SPECCUBE_LOD_STEPS
//      );
//      indirectLight.specular = DecodeHDR(envSample, unity_SpecCube0_HDR);
         Unity_GlossyEnvironmentData envData;
         envData.roughness = 1 - _Smoothness;
         envData.reflUVW = reflectionDir;
         indirectLight.specular = Unity_GlossyEnvironment(
             UNITY_PASS_TEXCUBE(unity_SpecCube0), unity_SpecCube0_HDR, envData
         );



Unity_GlossyEnvironment做了什么不同的事情吗?

它执行与我们做的相同的操作,但它有一些基于目标平台和一些其他设置的变化。此外,它还包含了一些注释和禁用代码,这触及了mipmaps如何创建的细节。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
half3 Unity_GlossyEnvironment (
     UNITY_ARGS_TEXCUBE(tex), half4 hdr, Unity_GlossyEnvironmentData glossIn
) {
#if UNITY_GLOSS_MATCHES_MARMOSET_TOOLBAG2 && (SHADER_TARGET >= 30)
     // TODO: remove pow, store cubemap mips differently
     half roughness = pow(glossIn.roughness, 3.0 / 4.0);
#else
     // MM: switched to this
     half roughness = glossIn.roughness;
#endif
     // spec power to the square root of real roughness
     //roughness = sqrt(sqrt(2/(64.0+2)));
 
#if 0
     // m is the real roughness parameter
     float m = roughness*roughness;
     // smallest such that 1.0+FLT_EPSILON != 1.0
     // (+1e-4h is NOT good here. is visibly very wrong)
     const float fEps = 1.192092896e-07F;
     // remap to spec power. See eq. 21 in -->
     float n =  (2.0 / max(fEps, m * m)) - 2.0;
     // remap from n_dot_h formulatino to n_dot_r.
     // See section "Pre-convolved Cube Maps vs Path Tracers" --> https://
     // s3.amazonaws.com/docs.knaldtech.com/knald/1.0.0/lys_power_drops.html
     n /= 4;
     // remap back to square root of real roughness
     roughness = pow( 2 / (n + 2), 0.25);
#else
     // MM: came up with a surprisingly close approximation
     // to what the #if 0'ed out code above does.
     roughness = roughness * (1.7 - 0.7 * roughness);
#endif
 
#if UNITY_OPTIMIZE_TEXCUBELOD
     half4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(tex, glossIn.reflUVW, 4);
     if (roughness > 0.5)
         rgbm = lerp(rgbm, UNITY_SAMPLE_TEXCUBE_LOD(tex, glossIn.reflUVW, 8),
             2 * roughness - 1);
     else
         rgbm = lerp(UNITY_SAMPLE_TEXCUBE(tex, glossIn.reflUVW), rgbm,
             2 * roughness);
#else
     half mip = roughness * UNITY_SPECCUBE_LOD_STEPS;
     half4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(tex, glossIn.reflUVW, mip);
#endif
 
     return DecodeHDR_NoLinearSupportInSM2 (rgbm, hdr);
}

最后的优化部分是用于PVR类型的图形处理器,以避免相关的纹理读取。 为了使它能够正常工作,它需要反射向量来传递给内插值器。

DecodeHDR_NoLinearSupportInSM2函数只是负责转发到DecodeHDR,但是针对Shader Model 2.0优化过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Decodes HDR textures
// handles dLDR, RGBM formats
// Modified version of DecodeHDR from UnityCG.cginc
inline half3 DecodeHDR_NoLinearSupportInSM2 (
     half4 data, half4 decodeInstructions
) {
     // If Linear mode is not supported we can skip exponent part
 
     // In Standard shader SM2.0 and SM3.0 paths are always using different
     // shader variations
     // SM2.0: hardware does not support Linear, we can skip exponent part
     #if defined(UNITY_NO_LINEAR_COLORSPACE) && (SHADER_TARGET < 30)
         return (data.a * decodeInstructions.x) * data.rgb;
     #else
         return DecodeHDR(data, decodeInstructions);
     #endif
}

 

凹凸不平的镜子

除了使用平滑度来表示粗糙的镜子,你当然可以使用法线贴图来添加更大的变形。因为我们使用扰动的法线来确定反射方向,使用法线贴图来添加更大的变形是没问题的。

Unity 渲染教程(八):反射Unity 渲染教程(八):反射Unity 渲染教程(八):反射

光滑度分别为0.5、 0.75和0.95的凹凸不平的镜子的对比图



金属与非金属的对比

金属和非金属表面可以产生清晰的反射,它们看起来不同。镜面反射可以在发光的电介质材质上表现得很好,但它们不会支配它们的外观。仍然有大量的漫反射可以看到。

Unity 渲染教程(八):反射Unity 渲染教程(八):反射Unity 渲染教程(八):反射

光滑度为0.5、0.75和0.95的非金属的对比图


回想一下,金属会它的镜面反射发生带上颜色,而非金属不会。这对于镜面高光,以及镜面的环境反射也是如此。

Unity 渲染教程(八):反射Unity 渲染教程(八):反射

红色的金属和非金属


镜子和阴影

正如我们前面所看到的那样,间接反射与表面的直接光照无关。这对于其他阴影区域最为明显。在非金属的情况下,这仅仅导致视觉上更明亮的表面。你仍然可以看到直接光照投射的阴影。

Unity 渲染教程(八):反射Unity 渲染教程(八):反射Unity 渲染教程(八):反射

光滑度为0.5、0.75和1的非金属的对比图

相同的规则适用于金属,但间接反射占据了主导地位。因此,直接光照和阴影随着光泽增加而消失。完美的镜子上没有阴影。

Unity 渲染教程(八):反射Unity 渲染教程(八):反射Unity 渲染教程(八):反射

光滑度为0.5、0.75和1的金属的对比图


虽然这在物理上是正确的,但现实生活很少是完美的。举个简单的例子来说,你可以看到直接光照和粘在一个完美的镜子上的污垢和灰尘产生的阴影。并且有许多材质是金属和电介质组分的混合物。你可以通过将金属滑块设置在0和1之间的某个位置来模拟此问题。

Unity 渲染教程(八):反射

金属和电介质组分为0.75的金属材质,一个有一点脏的镜子


对盒子的投影

我们目前有一个反射球和一个反射探测器。两者都悬停在我们建筑的中心。让我们添加一些球体,将它们放置在内部正方形区域的边缘附近。但是让我们坚持只使用一个探测器,位于建筑的中心。

Unity 渲染教程(八):反射

所有的反射都是相同的

这里面的反射有些问题。他们都看起来一样。视角稍有不同,但所有球体都反射了环境,就好像它们位于建筑物的中心。他们其实不是位于建筑物的中心,但反射探测器是位于建筑物的中心!

如果我们想要更逼真的反射,那么我们必须为每个球体创建一个探测器,将它们放置在适当的位置。这样,每个球体从其自己的角度获得环境地图。

Unity 渲染教程(八):反射

每个球体一个探测器,带有不同的反射内容

虽然这个效果更好,但它仍然不是完全真实的反射。为了实现完全真实的反射的效果,我们必须对我们渲染的每个片段使用一个反射探测器。这将需要位于球体表面上的许多反射探测器。幸运的是,近似对球体来说效果不是太坏。但是如果是平面镜的话效果是怎么样的?

首先,去除中心反射探测器以外的所有部件。然后创建一个四边形并将其定位,使其覆盖建筑物的内部并接触支柱的中点。然后把它变成镜子,观察反射。

Unity 渲染教程(八):反射

地板反射不正确

反射的内容根本不匹配!方向显示是正确的,但大小和位置都是错误的。如果我们给每个片段使用一个反射探测器,反射效果会很好。但是我们只有一个反射探测器。这种近似对于实际是无限远的东西是足够的,就好比说天空盒。但它不适合对附近的事物进行反射。

当一个环境无限远的时候,我们在确定它的反射的时候不需要关心视角的位置。但是当大多数环境就在附近的时候,我们需要小心视角的位置。假设我们在一个空的立方体房间的中间有一个反射探测器。它的环境贴图包含这个房间的墙壁、地板和天花板。如果立方体贴图和房间对齐的话,那么立方体贴图的每个面都与墙壁、地板或天花板中的一个完全对应。

接下来,假设我们在这个房间的任何地方有一个表面位置和一个反射方向。向量将最终与立方体的边缘相交。我们可以用一点数学来计算这个交点。然后我们可以从房间的中心到这一点构造一个向量。使用这个向量,我们可以对立方体贴图进行采样,并以正确的反射结束。

Unity 渲染教程(八):反射

投影来找到采样的方向

房间不必是一个立方体,也能正常工作。任何矩形形状都可以,像是我们建筑物的内部。但是,房间和立方体贴图必须对齐。


反射探测器的盒子.

反射探测器具有大小和探测器起点的位置,这些信息相对于其位置在世界空间中限定了一个立方区域。这个立方区域总是轴对齐的,这意味着这个立方区域忽略所有的旋转。同时它也忽略缩放。

这个立方区域用于两个目的。首先,Unity使用这些区域来决定在渲染对象时使用哪个探测器。第二,这个区域用于盒子的投影,这是我们要做的。

选择探测器后,可以在场景视图中显示盒子。在反射探测器的检查器的顶部是Probe场景编辑模式切换。点击左边的按钮可以打开盒子投影边界的小控件。

Unity 渲染教程(八):反射Unity 渲染教程(八):反射

盒子的投影边界

你可以使用边界面中心的黄点来调整它们。你也可以通过在检查器中编辑大小和探测器的原点矢量来调整它们。通过调整探测器的原点位置,可以相对于采样点来移动盒子。你也可以在场景中使用其他编辑模式来调整它,但是它很精巧,目前不能很好地与撤消一起工作。

调整盒子,使这个盒子覆盖建筑物的内部,触摸柱子,并达到最高点。我把这个盒子做的稍微大一点,以防止由于与场景视图中的gizmo出现Z冲突而发生闪烁。

Unity 渲染教程(八):反射Unity 渲染教程(八):反射

调整后的边界

当我们在这个位置的时候,启用Box Projection,因为这是我们想做的。



调整采样的方向

为了计算对盒子的投影,我们需要初始反射方向、我们采样的位置、立方体贴图的位置以及盒子的边界。为我们的着色器添加一个函数,在CreateIndirectLight上面的位置。

1
2
3
4
5
6
float3 BoxProjection (
     float3 direction, float3 position,
     float3 cubemapPosition, float3 boxMin, float3 boxMax
) {
     return direction;
}

首先,调整盒子的边界,使盒子的边界相对于表面位置。

1
2
3
boxMin -= position;
boxMax -= position;
return direction;  

 

接下来,我们必须对方向向量进行放缩,使其从它的位置指向所需的交叉点。让我们先考虑X方向。如果方向的X分量为正,则它指向最大的边界。否则,它指向最小边界。用方向的X分量划分适当的界限给了我们需要的标量。当方向为负的时候,这也是有效的,因为最小边界也是负的,在除法之后会产生一个为正的结果。

1
2
3
boxMin -= position;
boxMax -= position;
float x = (direction.x > 0 ? boxMax.x : boxMin.x) / direction.x;

对于Y方向和Z方向也是如此。

1
2
3
float x = (direction.x > 0 ? boxMax.x : boxMin.x) / direction.x;
float y = (direction.y > 0 ? boxMax.y : boxMin.y) / direction.y;
float z = (direction.z > 0 ? boxMax.z : boxMin.z) / direction.z;

我们现在有三个标量,但那个是正确的那个标量?这取决于哪个标量最小。哪个标量最小表示着哪个边界面最近。

Unity 渲染教程(八):反射

选择最小的因子

1
2
float z = (direction.z > 0 ? boxMax.z : boxMin.z) / direction.z;
float scalar = min(min(x, y), z);

 

当其中一个分数为零的时候会发生什么?

可能的结果是,方向矢量分量中的一个或两个是零。这将产生无效的结果,这不会使它超过选择的最小值。

现在我们可以通过将缩放后的方向添加到位置中来找到交点。通过从那里减去立方体贴图的位置,我们得到新的投影采样方向。

Unity 渲染教程(八):反射

找到新的投影方向

1
2
float scalar = min(min(x, y), z);
return direction * scalar + (position - cubemapPosition);


新的方向是否必须规一化?

可以使用任何非零矢量对立方体贴图进行采样。硬件的立方体贴图的采样基本上与我们刚刚做的一样。它要确定向量指向哪个面,然后执行除法以找到与立方体贴图面的交点。它使用该点的适当坐标来对立方体贴图面的纹理进行采样。

我们可以通过在单个float3表达式中组合三个候选因子来简化这个代码,直到选择适当的界限之后再做减法和除法。

1
2
3
4
5
6
7
8
float3 BoxProjection (
     float3 direction, float3 position,
     float3 cubemapPosition, float3 boxMin, float3 boxMax
) {
     float3 factors = ((direction > 0 ? boxMax : boxMin) - position) / direction;
     float scalar = min(min(factors.x, factors.y), factors.z);
     return direction * scalar + (position - cubemapPosition);
}

 

现在使用我们在CreateIndirectLight中的新函数来修改环境的采样向量。

1
2
3
4
5
envData.reflUVW = BoxProjection(
     reflectionDir, i.worldPos,
     unity_SpecCube0_ProbePosition,
     unity_SpecCube0_BoxMin, unity_SpecCube0_BoxMax
);


Unity 渲染教程(八):反射

投影之后的反射

我们的平面镜调整后的反射看起来好多了。但重要的是,反射表面不会延伸超出探测器的界限。缩放镜像,使其与边界完全匹配,并触及柱子。

Unity 渲染教程(八):反射

调整后的地板镜像

柱子的反射现在完全匹配真实的情况了。至少它的位置是正好在镜子的边缘和探测器的边界。距离更远的一切都是不对齐的,因为这些点的投影是错误的。这是我们用单个反射探测器所能做的最好的结果。

另一件明显错误的事情是,我们看到地板上的镜子反射了部分地板。这是因为环境地图是从底部的镜子上方的角度进行渲染的。这可以通过将探测器的原点降低到仅稍微高于镜子来修正,同时保持它们的边界还是正确的。

Unity 渲染教程(八):反射

探测器的中心被降低以后的效果图

虽然这样一个比较低的采样点对于地板上的镜子是更好的,但是对于浮动的球体来说就不是那么好。所以,让我们再次移动探测器,并看看球体的反射效果。

Unity 渲染教程(八):反射Unity 渲染教程(八):反射

一个投影后的探测器与九个未投影的探测器的对比效果图

事实证明,一个单箱投影的探测器产生的反射非常类似于九个单独的探测器所产生的效果!所以盒体投影是一个非常方便的技巧,尽管它不是完美的。



可选的投影

是否使用盒体投影会根据每个探测器的情况来确定不同,这由盒体投影的切换开关控制。Unity将此信息存储在立方贴图所在位置的第四个组件之中。如果这个分量大于零的话,则探测器应使用盒体投影。让我们使用if语句来处理这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
float3 BoxProjection (
     float3 direction, float3 position,
     float4 cubemapPosition, float3 boxMin, float3 boxMax
) {
     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);
     }
     return direction;
}

 

即使我们使用了if语句,这并不意味着编译后的代码也会包含一个分支。举个简单的例子来说,OpenGLCore用一个条件赋值结尾,但是它不是一个分支。

Unity 渲染教程(八):反射

Direct3D11也是如此。

Unity 渲染教程(八):反射

 

我们可以通过在我们自己的分支之前插入UNITY_BRANCH宏来请求实际会有一个分支。尽管在着色器中应该尽量避免分支,但在这种情况下有分支并不是那么糟糕。这是因为条件是均匀的。对象的所有片段使用的是相同的探测器设置,因此最终采取相同的分支。

1
2
3
4
UNITY_BRANCH
if (cubemapPosition.w > 0) {
    
}


OpenGL核心现在包含了一个明显的分支。

 Unity 渲染教程(八):反射


Direct3D11也是如此。

Unity 渲染教程(八):反射

 

 

是不是有一个Unity函数用于盒体投影?

是有这么一个函数。UnityStandardUtils里面包含了BoxProjectedCubemapDirection函数。它做的就是我们刚才所做的同样的事情,包括分支。但它也对反射方向参数进行了归一化,这是不必要的。这就是为什么我们不使用它的原因。


都反射探测器进行混合

现在我们在建筑物内部有了很好的反射,但是建筑物外面怎么样的?一旦你移动一个球体超出探测器的范围之外,它会将反射的内容切换到天空盒。

Unity 渲染教程(八):反射

位于探测器盒体内部和外部的球体

探测器和天空盒之间的切换是非常突然的。我们可以增加探测器的盒体,这样它也可以覆盖建筑物外面的空间。然后,我们可以移动一个球体进出建筑物,它的反射内容将逐渐改变。然而,探测器的位置位于建筑物的内部。如果要在建筑物的外面使用它会产生非常奇怪的反射效果。

Unity 渲染教程(八):反射

探测器的盒体变大以后的效果

为了在建筑物的内部和外部都得到合适的反射,我们必须使用多个反射探测器。

Unity 渲染教程(八):反射

第二反射探测器

这些反射是有意义的,但在两个不同的探测器之间的区域仍然会有突然和非常清楚的过渡。



在探测器之间进行插值

Unity为着色器提供了两个反射探测器的数据,因此我们可以在它们之间进行混合。第二个反射探测器是unity_SpecCube1。我们可以对环境贴图和内插值进行采样,这取决于哪个反射探测器起到更主要的效果。Unity为我们计算了这个值,并将插值器的结果存储在unity_SpecCube0_BoxMin的第四个坐标中。当仅使用第一个反射探测器的时候,它会被设置为1,如果有混合的话,则设置为较低的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
envData.reflUVW = BoxProjection(
     reflectionDir, i.worldPos,
     unity_SpecCube0_ProbePosition,
     unity_SpecCube0_BoxMin, unity_SpecCube0_BoxMax
);
float3 probe0 = 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
);
float3 probe1 = Unity_GlossyEnvironment(
     UNITY_PASS_TEXCUBE(unity_SpecCube1), unity_SpecCube0_HDR, envData
);
indirectLight.specular = lerp(probe1, probe0, unity_SpecCube0_BoxMin.w);


上面的代码很可能会产生一个编译错误。显然,samplerunity_SpecCube1变量是不存在的。这是因为访问纹理的话需要纹理资源和采样器,第二个反射探测器没有任何的纹理资源和采样器。相反,它依赖于第一个反射探测器的采样器。

使用UNITY_PASS_TEXCUBE_SAMPLER宏将第二个反射探测器针的纹理与我们唯一的采样器相结合。这样我们就摆脱了错误。

1
2
3
4
float3 probe1 = Unity_GlossyEnvironment(
     UNITY_PASS_TEXCUBE_SAMPLER(unity_SpecCube1, unity_SpecCube0),
     unity_SpecCube0_HDR, envData
);

 

Unity 渲染教程(八):反射

仍然没有发生混合


重叠探测器的盒体

为了让混合起作用,多个反射探测器的边界必须重叠。所以调整第二个反射探测器的边界,以便这个反射探测器的区域延伸到建筑物上。重叠区域中的球体应该获得混合反射。网格渲染器组件的检查器还会显示正在使用的反射探测器及其权重。

Unity 渲染教程(八):反射Unity 渲染教程(八):反射

开启了混合功能的发生重叠的反射探测器的盒体

如果这个过渡不够顺利的话,你可以在其他两个反射探测器之间添加第三个反射探测器。这个反射探测器的盒体要与另外两个反射探测器的盒体重叠。所以当球体移动到建筑物的外面的时候,你首先要混合内部反射探测器和中间的反射探测器,以及在中间的反射探测器和外部的反射探测器之间做混合。

Unity 渲染教程(八):反射Unity 渲染教程(八):反射

三个反射探测器

我们也可以在反射探测器和天空盒之间进行混合。你必须将物体的模式从“反射探测器”模式更改为“反射探测器和天空盒相结合”的模式。 当物体的包围盒部分地位于探测器的边界之外的时候,将会发生混合。

Unity 渲染教程(八):反射Unity 渲染教程(八):反射

对一个反射探测器和天空盒进行混合


其他反射探测器的模式是怎么样的?

 “关闭“表示物体根本不会使用探测器。它总是使用天空盒。

“简单“则是禁用混合。它总是使用最重要的探测器,或是天空盒。


优化

对两个探测器进行采样需要做很多的工作。只有当有东西要混合的时候,我们才应该这样做。因此,需要添加一个基于插值器的分支。Unity也在标准着色器中做到了这一点。再次强调下,它是一个概率均匀的分支。

1
2
3
4
5
6
7
8
9
10
11
12
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;
}


当目标平台被认为不能处理它的时候,Unity的着色器也会禁用混合。这由UNITY_SPECCUBE_BLENDING进行控制,在可以进行混合的时候这个值会定义为1,否则的话为0。我们可以通过使用预处理器条件块仅在需要时包括对应的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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


 

我们不应该使用#ifdefinedUNITY_SPECCUBE_BLENDING)吗?

不应该,因为UNITY_SPECCUBE_BLENDING是始终定义的。在这种情况下,实际的定义很重要。A 1表示真,而a 0表示假。

对于盒体投影存在一个类似的优化,这是基于UNITY_SPECCUBE_BOX_PROJECTION的定义来决定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}  

 

这两个值在哪里定义?

它们由编辑器基于目标平台进行定义。此外,当面向3.0以下的着色器模型的时候,UnityStandardConfig将它们设置为0。

1
2
3
4
5
6
7
8
9
10
11
12
13
// "platform caps" defines that were moved to editor,
// so they are set automatically when compiling shader
// UNITY_SPECCUBE_BOX_PROJECTION
// UNITY_SPECCUBE_BLENDING
 
// still add safe net for low shader models,
// otherwise we might end up with shaders failing to compile
#if SHADER_TARGET < 30
     #undef UNITY_SPECCUBE_BOX_PROJECTION
     #define UNITY_SPECCUBE_BOX_PROJECTION 0
     #undef UNITY_SPECCUBE_BLENDING
     #define UNITY_SPECCUBE_BLENDING 0
#endif

 

 

多次反射

当你有两个镜子彼此面对的时候,你最终会看到看起来无尽的嵌套反射。我们可以在Unity中得到相同的结果吗?

Unity 渲染教程(八):反射

没有多次反射的效果

我们的镜子不包括在反射本身,因为它们不是静态的。所以让我们把底部的镜子设为静态。而球应该保持动态,否则的话探测器不能再忽视它们,会产生奇怪的反射。

Unity 渲染教程(八):反射

地板镜是静态的,会产生黑色的反光

镜子现在显示在我们的单个反射探测器里面,但它看起来全部是黑的。这是因为在渲染反射探测器的时候,镜子的环境贴图不存在。它试图反射自己,然后失败了!

默认情况下,Unity不包括环境贴图中的反射。但这可以通过光照设置进行更改。环境设置部分包含反射反弹的滑块,默认情况下设置为1。让我们将它设置为2。

Unity 渲染教程(八):反射Unity 渲染教程(八):反射

发生了两次反射以后的效果

当设置为两次反射的时候,Unity首先正常渲染每个反射探测器。然后,使用现有的反射数据再次渲染它们。因此,来自地面镜的初始反射现在包括在环境贴图之中。

Unity支持支持最多五次反弹。而这需要大量的渲染,所以你绝对不想在运行的时候使用这个!如果要在实际环境中看到个五次反弹的效果,可以复制地面镜,把它变成一个天花板上的镜子。

Unity 渲染教程(八):反射

同时有地面镜和天花板上的镜子的环境里面发生五次反弹的效果

所以在Unity中可以得到嵌套的反射,但是它们是有限的。此外,投影是错误的,因为反射探测器的边界没有延伸到镜子之外的虚拟空间之中。



有了所有这些限制,反射这个功能还有用吗?

我们在这一节教程中专注于反射本身,所以我们看到了反射里面包含的所有的缺陷。通过反射功能造出一个完美的镜子不太实际,但有了这些微妙的反射就可以让镜子很逼真。知道了它们的极限所在,你就可以找出有效地使用它们的时间和地点。

反射探测器是向场景中添加反射的默认方法,也是最方便的方法,但它不是唯一的方法。如果你需要强大的平面镜,一种与反射探测器不同的方法是从虚拟观察者的角度渲染场景,并将其用作镜子的纹理。另一种方法是对场景几何做镜像。你可以通过这种方式获得很好的结果,但是这些方法有很多的限制,不像反射探测器那样通用。然后还有屏幕空间反射。我们将在添加对延迟渲染的支持之后讲解屏幕空间反射。


这个系列的下一篇教程是:复杂的材质。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值