Unity 渲染教程(06):凹凸度

更新时间:2023-04-24 17:31:01 阅读量: 实用文档 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

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来实现。

1 n ormal = float3(1, h

2 - h1, 0);

缩放后的高度。

这开始看起来不错,但光照是错误的。太黑了。这是因为我们直接使用切线向量作为法线向量。要将切线向量转为向上的法线向量,我们必须围绕Z轴旋转切线向量90°。

1 n ormal = float3(h1 - h2, 1, 0);

使用实际的法线向量。

这个向量旋转是如何工作的?

你可以通过交换向量的X和Y分量,并翻转新的X分量的符号,来对一个二维向量做一个逆时针90°的旋转。这样我们就得到了。

对一个二维向量做一个逆时针90°的旋转。

1.5中心差分 我们使用有限差分近似法来创建法向量。具体地做法是,通过使用正向差分法。我们选取一点,然后沿着一个方向来确定斜率。我们得到的结果就是法线在该方向上的偏移。为了获得更好的法线近似,我们可以在两个方向上偏移采样点。这将线性近似集中在当前点上,并且被称为中心差分法。 这将导数函数更改为 。

1 2 3 4 5 6 7 float2 delta = float2(_HeightMap_TexelSize.x * 0.5, 0);

float h1 = tex2D(_HeightMap, i.uv - delta);

float h2 = tex2D(_HeightMap, i.uv + delta);

normal = float3(h1 - h2, 1, 0);

这会轻微地移动凹凸度贴图,因此它们可以更好地与高度字段对准。除此之外,它们的形状没有发生改变。

1.6同时使用两个维度

我们创建的法线只考虑了沿U 方向的变化。我们一直使用函数f

(u ,v )相对于u 轴的偏导数。这就是f'u (u ,v ),或简称f'u 。我们还可以通过使用f'v 沿着V 方向创建法线。 在这种情况下,切向量为而法线向量是。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 float2 du = float2(_HeightMap_TexelSize.x * 0.5, 0);

float u1 = tex2D(_HeightMap, i.uv - du);

float u2 = tex2D(_HeightMap, i.uv + du);

i.normal = float3(u1 - u2, 1, 0);

float2 dv = float2(0, _HeightMap_TexelSize.y * 0.5);

float v1 = tex2D(_HeightMap, i.uv - dv);

float v2 = tex2D(_HeightMap, i.uv + dv);

i.normal = float3(0, 1, v1 - v2);

normal = normalize(i.normal);

沿着V 方向的法线。

我们现在可以访问沿着U 方向和V 方向的切线。总之,这些向量描述了在我们的片段所在处的高度场的表面。通过计算它们的叉积,我们找到二维高度场的法向量。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 float2 du = float2(_HeightMap_TexelSize.x * 0.5, 0);

float u1 = tex2D(_HeightMap, i.uv - du);

float u2 = tex2D(_HeightMap, i.uv + du);

float3 tu = float3(1, u2 - u1, 0);

float2 dv = float2(0, _HeightMap_TexelSize.y * 0.5);

float v1 = tex2D(_HeightMap, i.uv - dv);

float v2 = tex2D(_HeightMap, i.uv + dv);

float3 tv = float3(0, v2 - v1, 1);

i.normal = cross(tv, tu);

i.normal = normalize(i.normal);

完整的法线的效果。

什么是叉积?

两个向量之间的叉积在几何上定义为A×B = || A || || B || sin(θ)N。这里N 是垂直于包含A向量和B向量的平面的单位矢量。所以N是我们想要的法线向量。

||A || || B || sinθ这个部分缩放这个向量。它就像点积,除了它包含矢量之间的角度的正弦,而不是余弦之外。如果两个向量都是单位长度,并且它们之间的角度是90°,则结果是1。由于很可能不是这种情况,因此我们必须对叉积运算的结

果进行归一化。只要矢量之间的角度不是0°和180°,这个方法就有效,因为如果矢量之间的角度是0°和180°的话,正弦为零。

在代数上,对于三维向量,叉积被定义为。

1 f loat crossProduct = v1.yzx * v2.zxy - v1.zxy * v2.yzx;

在视觉上,产生的矢量的绝对量值对应于可以用两个矢量形成的平行四边形的表面积。

叉积。

需要注意的是,A×B = -B×A。这意味着结果的方向取决于向量的顺序。因为我们想要我们的向量指向上,我们必须使用cross(ty,tx),而不是cross(tx,ty)。

当你用切线向量计算叉积的时候,你会看到

。因此,我们可以直接构造向量,而不必依赖于叉积函数。

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 void InitializeFragmentNormal(inout Interpolators i) {

float2 du = float2(_HeightMap_TexelSize.x * 0.5, 0);

float u1 = tex2D(_HeightMap, i.uv - du);

float u2 = tex2D(_HeightMap, i.uv + du);

// float3 tu = float3(1, u2 - u1, 0);

float2 dv = float2(0, _HeightMap_TexelSize.y * 0.5);

float v1 = tex2D(_HeightMap, i.uv - dv);

float v2 = tex2D(_HeightMap, i.uv + dv);

// float3 tv = float3(0, v2 - v1, 1);

// i.normal = cross(tv, tu);

i.normal = float3(u1 - u2, 1, v1 - v2);

i.normal = normalize(i.normal);

}

2 法线贴图

当凹凸贴图起作用的时候,我们必须执行多个纹理采样和有限差分计算。这似乎是一种浪费,因为最终的法线向量应该总是相同的。为什么所有这些工作每帧都要做一次?我们可以只做一次,并将得到的法线向量存储在纹理上。 这是否与纹理过滤相冲突?

双线性滤波和三线性滤波将在法线向量之间进行混合,就像法线在三角形内部进行内插值一样。因此,我们必须对采样的法线进行归一化。

你还必须确保每个mipmap包含有效的法线。你不能简单地对纹理进行缩样,就好像它包含颜色数据一样。矢量也必须被归一化。Unity会负责这个事情。这意味着我们需要一个法线贴图。我可以提供一个,但我们可以让Unity为我们做这个工作。将高度贴图的纹理类型更改为法线贴图。Unity自动切换纹理以使用三线性滤波,并假设我们想使用灰度图像数据来生成法线贴图。这正是我们想要的,但我们要将凹凸度更改为更低的值,如0.05。

从高度数据生成法线向量。

应用导入设置以后,Unity 将计算法线贴图。原始高度图仍然存在,但Unity 内部使用的是生成的贴图。

像我们在将法线可视化为颜色时所做的那样,它们必须进行调整以适应0-1范围内。 因此它们被存储为N + 12。 这表明平坦区域将呈现浅绿色。然而,它们看起来是浅蓝色的。这是因为法线贴图最常见的约定是将向上方向存储在Z 分量中。所以从Unity 的角度来看,Y 坐标和Z 坐标是交换的。

对法线贴图进行采样

因为法线贴图与高度图完全不同,请相应地重命名着色器属性。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Properties {

_Tint ("Tint", Color) = (1, 1, 1, 1)

_MainTex ("Albedo", 2D) = "white" {}

[NoScaleOffset] _NormalMap ("Normals", 2D) = "bump" {}

// [NoScaleOffset] _HeightMap ("Heights", 2D) = "gray" {}

[Gamma] _Metallic ("Metallic", Range(0, 1)) = 0

_Smoothness ("Smoothness", Range(0, 1)) = 0.1

}

本文来源:https://www.bwwdw.com/article/u0oq.html

Top