Unity渲染教程(六):凹凸度
译者:白芷(白芷)审校:王磊(未来的未来)
?扰动法线来模拟凹凸不平的情况。
?从高度字段来计算法线。
?对法线贴图进行采样和混合。
?从切线空间转换到世界空间。
这是关于渲染基础的系列教程的第六部分。这个系列教程的上一部分讲的是法对多个光源的支持。在这篇文章里面,我们将创建具有更复杂错觉的曲面。
这个教程是使用Unity5.4.0f3开发的。
它看起来不像是一个光滑的球体了。
1 凹凸贴图
我们可以使用反射率纹理创建具有复杂颜色图案的材质。我们可以使用法线来调整表面的曲率。通过使用这些工具,我们可以生产各种表面。然而,单个三角形的表面将总是平滑的。它只能在三个法线向量之间进行插值。因此它不能表示粗糙或是有变化的表面。当抛弃反射率纹理并仅使用纯色的时候,这变得显而易见。这个平面的一个很好的例子是一个简单的四边形。将一个简单的四边形添加到场景中,并通过围绕X轴旋转90°使这个简单的四边形指向上方。给这个简单的四边形施加我们的光照材质,没有纹理并且是全白色调。
完美的平面。
因为默认的天空盒非常的明亮,很难看到其他光源的贡献。所以,在这个教程让我们关闭默认的天空盒。你可以通过在光照设置中将环境亮度降低为零来实现这一点。然后只启用主方向光源。在场景视图中找到一个好的观点的话,你可以在四边形看到一些光差。
没有环境光,只有主方向光源的效果。
我们如何使这个四边形看起来不平?我们可以通过将阴影烘烤到反射率纹理中来伪造粗糙度。然而,这将是完全静态的。如果灯光改变,或是物体移动的话,那么阴影也应该相应的发生变化。如果没有相应的发生变化,那么幻觉会被打破。在镜面高光反射的情况下,即使相机也不允许移动。
我们可以改变法线,创造这是个曲面的错觉。但每个四边形只有四个法线,每个顶点一个。这只能产生平滑的过渡。如果我们想要一个变化和粗糙的表面,我们需要更多的法线。
我们可以将我们的四边形细分成更小的四边形。这给了我们更多的法线。事实上,一旦我们有更多的顶点,我们也可以移动它们。那么我们就不需要粗糙的错觉了,我们可以做出一个实际的粗糙表面!但是子三角形仍有同样的问题。我们要继
续细分这些子三角形吗?这将导致巨大的网格与大量的三角形。这在创建三维模型的时候很好,但在游戏中实时使用是不可行的。
高度贴图
与平坦表面相比,粗糙表面具有不均匀的高度。如果我们将这个高度数据存储在纹理中,我们可以使用它为每个片段生成法向量,而不是只能在每个顶点有一个法向量。这个想法被称为凹凸贴图,最初由詹姆斯·布林(James Blinn)提出的。这里是我们的大理石纹理相应的高度图。它是一个RGB纹理,每个通道设置为相同的值。使用默认导入设置将这张高度图导入到项目中。
大理石纹理相应的高度图。
向My First Lighting着色器添加_HeightMap纹理属性。由于它将使用与我们的反射率纹理相同的UV坐标,因此它不需要自己的缩放和偏移参数。默认纹理并不重要,只要它是均匀的就可以。我们会使用灰色。
1 Properties {
2 3 4 5 6 7 8 9 10 11 12 13
_Tint ("Tint", Color) = (1, 1, 1, 1)
_MainTex ("Albedo", 2D) = "white" {}
[NoScaleOffset] _HeightMap ("Heights", 2D) = "gray" {}
[Gamma] _Metallic ("Metallic", Range(0, 1)) = 0
_Smoothness ("Smoothness", Range(0, 1)) = 0.1
}
带有高度贴图的材质。
将匹配的变量添加到My Lighting 的导入文件之中,以便我们可以访问纹理。 让我们看看它的外观,把它添加到反射率中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 float4 _Tint;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _HeightMap;
…
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
i.normal = normalize(i.normal);
float3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
float3 albedo = tex2D(_MainTex, i.uv).rgb * _Tint.rgb;
albedo *= tex2D(_HeightMap, i.uv);
…
}
像使用颜色贴图一样使用高度贴图。
调整法线
因为我们片段的法线将变得更复杂,让我们将他们的初始化移动到一个单独的函数中去。另外,摆脱高度贴图的测试代码。
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 void InitializeFragmentNormal(inout Interpolators i) {
i.normal = normalize(i.normal);
}
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
InitializeFragmentNormal(i);
float3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
float3 albedo = tex2D(_MainTex, i.uv).rgb * _Tint.rgb;
// albedo *= tex2D(_HeightMap, i.uv);
…
}
因为我们目前正在使用一个位于XZ 平面的四边形,所以它的法向量总是(0,1,0)。 所以我们可以使用一个常量法向量,忽略顶点数据。让我们现在做这个事情,以后再担心法线有不同的方向。
1 2 3 4 5 6 7 void InitializeFragmentNormal(inout Interpolators i) {
i.normal = float3(0, 1, 0);
i.normal = normalize(i.normal);
}
我们如何在这里导入高度数据?一个比较直观简单的方法是使用高度作为法线的Y分量,在归一化之前。
1 2 3 4 5 6 7 8 9
void InitializeFragmentNormal(inout Interpolators i) {
float h = tex2D(_HeightMap, i.uv);
i.normal = float3(0, h, 0);
i.normal = normalize(i.normal);
}
使用高度作为法线的Y分量。
这个方法不行,因为归一化将每个向量转换回(0,1,0)。黑线出现在高度为零的地方,因为在这些情况下归一化会失败。我们需要一个不同的方法。
1.1有限差分
因为我们使用的是纹理数据,所以我们有的是二维数据。有U和V的尺寸。高度可以被认为是向上的第三维度。我们可以说纹理表示了一个函数f(u,v)= h。
让我们从限制我们只有U维度开始做这个事情。因此,函数被减少到f(u)= h。我们可以从这个函数中导出法向量吗?
如果我们知道这个函数的斜率,那么我们可以使用它来计算它在任何点的法线。斜率由h的变化率定义。这是它的导数,h'。因为h是函数的结果,h'也是函数的结果。因此,我们有导数函数f'(u)= h'。
不幸的是,我们不知道这些函数是什么。但我们可以近似这些函数。我们可以比较纹理中两个不同点的高度。举个简单的例子来说,在两端,使用U坐标0和1。这两个样本之间的差异是这些坐标之间的变化率。表达为函数的话,也就是f(1)
-f(0)。我们可以使用它来构造一个切线向量。
从到的切线向量。
这当然是对真正的切线向量的一个非常粗略的近似。它将整个纹理视为线性斜率。我们可以通过对靠近在一起的两点进行采样来做得更好。举个简单的例子来
说,U坐标0和1/2。这两点之间的变化率是,在每半个单位U的跨度上。因为它更容易处理每个整个单位的变化率,我们除以点之间的距离,
所以我们有。这样我们就得到了
切向量:。
一般来说,我们必须相对于我们渲染的每个片段的U坐标来这么做。到下一个点的距离由恒定增量来定义。因此,导数函数近似为f'(u)≈f(u +δ)-f(u)δ。
δ越小,我们越逼近真实的导数函数。当然,它不能变成零,但当它到达它的理论极限,你会得到。这种近似导数的方法称为有限差分法。这样,我们可以在任何点建立切向量,。
从切线到法向量
我们可以在我们的着色器中使用δ的什么值?最小的明显差异将覆盖我们纹理的单个纹理。我们可以通过带有_TexelSize 后缀的float4变量在着色器中检索这个信息。Unity 会设置这些变量,类似于_ST 变量。 1 2 3 sampler2D _HeightMap;
float4 _HeightMap_TexelSize; 什么东西存储在_TexelSize 变量之中?
它的前两个分量包含的是纹理像素大小,作为U 和V 的分数。其他两个分量包含的是像素的数量。举个简单的例子来说,在256×128纹理的情况下,它将包含(0.00390625,0.0078125,256,128)。
现在我们可以对纹理进行两次采样,计算高度导数,并构造一个切线矢量。让我们直接使用它作为我们的法线向量。 1 2 3 4 5 6 7 8 9 10 11 float2 delta = float2(_HeightMap_TexelSize.x, 0);
float h1 = tex2D(_HeightMap, i.uv);
float h2 = tex2D(_HeightMap, i.uv + delta);
i.normal = float3(1, (h2 - h1) / delta.x, 0);
i.normal = normalize(i.normal);
实际上,因为我们进行了归一化,我们可以通过δ来缩放切线向量。这消去了一个除法并提高了精度。
1 n ormal = float3(delta.x, h
2 - h1, 0);
使用切线向量作为法线向量。
我们得到一个非常明显的结果。这是因为高度有一个单位的跨度,产生了非常陡的斜坡。由于扰动的法线实际上不改变表面,我们不想要这样巨大的差异。我们可以通过任意因子来缩放高度。让我们将范围缩小到单个纹素。我们可以通过将高度差乘以δ,或者通过在切线中简单地将δ替换为1来实现。