/细节法线混合方式

细节法线混合方式

Hello . 大家好

今天是网上一篇法线混合的分享

         
1
the x, why z
 
这是一个看似简单的问题:给定两个法线贴图,如何组合它们?特别是,如何以一致的方式向基础法线贴图添加细节?我们将研究几种流行的方法,并介绍一种新方法,重新定向法线贴图,它的作用略有不同。
研究虽然不是所有答案都考虑到,但希望我们会鼓励你重新检查你当前正在做的事情,无论是在运行时还是正在创建
2
法线的混合
 
混合在视频游戏渲染中一次又一次地出现。常见用途包括:材质之间的过渡、解决tiling的贴图、通过褶皱贴图模拟局部变形以及向表面添加微观细节。我们先讨论最后一项
混合的确切方法还得看实际情况。对于albedo贴图,线性插值通常是有意义的,但法线贴图是另一回事。由于数据代表方向,我们不能像处理颜色那样简单地独立处理通道。有时为了快读或方便而忽视这一点,然后导致了糟糕的结果。
3
线性混合
 
为了直接的观察到,让我们看一个简单的例子,以一种简单的方式将高频细节添加到基础法线贴图(圆锥体):
         
从左到右:base map,detail map 和线性混合后的结果
这里我们只是解压法线贴图,将它们加在一起,重新规范化,最后为了可视化重新打包:
float3 n1 = tex2D(texBase,   uv).xyz*2 - 1;
float3 n2 = tex2D(texDetail, uv).xyz*2 - 1;
float3 r  = normalize(n1 + n2);
return r*0.5 + 0.5;
输出类似于平均,并且由于纹理完全不同,我们最终“展平”了基本方向和细节。即使在简单的情况下,这也会导致不直观的行为,例如当其中一个法线输入是平坦的时,我们希望它没有效果,但我们却转向了[0,0,1]。
4
overlay(叠加)混合
 
美术上一个常见的替代方案是叠加混合:
下面是参考代码
float3 n1 = tex2D(texBase,   uv).xyz;
float3 n2 = tex2D(texDetail, uv).xyz;
float3 r  = n1 < 0.5 ? 2*n1*n2 : 1 - 2*(1 - n1)*(1 - n2);
r = normalize(r*2 - 1);
return r*0.5 + 0.5;
虽然看起来确实有整体改善,但混合后的法线看起来仍然不正确。不过这并不奇怪,因为我们仍在独立处理通道。事实上,除了比其他 Photoshop 混合模式表现得更好之外,没有理由使用叠加,这也是它受到一些美术师喜欢的原因。
5
偏导数混合
 
如果我们可以使用高度而不是法线贴图,就会更好处理,因为标准操作可以预测地运行。但是高度贴图在创建过程中并不总是可用,直接用于着色可能不切实际。
幸运的是,可以通过使用偏导数 (PD) 来获得相同的结果,偏导数是根据法线贴图本身简单地计算出来的。
一些参考代码
float3 n1 = tex2D(texBase,   uv).xyz*2 - 1;
float3 n2 = tex2D(texDetail, uv).xyz*2 - 1;
float2 pd = n1.xy/n1.z + n2.xy/n2.z; // Add the PDs
float3 r  = normalize(float3(pd, 1));
return r*0.5 + 0.5;
实际上,为了更稳定,第三行和第四行可以换成下面的代码
float3 r = normalize(float3(n1.xy*n2.z + n2.xy*n1.z, n1.z*n2.z));
通过图三可以看出,输出显然比之前好得多。正如我们所期望的那样,现在合并后的贴图类似于原来的扰动版本。通过简单地将偏导数加在一起,平坦的正常情况也可以正确处理。
但是这个过程并不完美,因为锥体表面的细节仍然很柔和。也就是说,当它用于在材质之间淡入淡出时效果很好。
float2 pd = lerp(n1.xy/n1.z, n2.xy/n2.z, blend);
float3 r = normalize(float3(pd, 1));
6
whiteout混合
 
在 SIGGRAPH07 上,Christopher Oat 描述了 AMD Ruby:用于添加褶皱的方法
代码看起来很像第二种形式的PD代码,只是组件没有按z的比例缩放xy:
float3 r = normalize(float3(n1.xy + n2.xy, n1.z*n2.z));
通过此修改,锥体上的细节更加明显,而平面法线仍然直观地起作用。
 
7
UDN混合
 
最后,Unreal Developer Network 上出现了一个更简单的混合模式,UDN混合。
与上一种技术的唯一变化是它去掉了n2.z的乘法
也可以把它看作线性混合,除此以外我们只从细节法线上加了x和y。
正如我们稍后将看到的,这可以在Whiteout上节省一些着色器指令,这对于低端平台总是有用的。然而,它也会导致一些细节减少而不是更平坦的基础法线—注意角落上最坏情况下的结果(尽管这可能不会被注意到)。事实上,总体而言,通过上图可以发现Whiteout的视觉差异在这里很难察觉。
   
8
我们的RNM方案
 
现在对于我们自己的方法。我们一直在研究下面属性,以便为美术师提供直观的表现:
  • 逻辑:操作有明确的数学基础(例如几何解释)
  • 处理特殊情况:如果其中一个法线贴图是平坦的,则输出与另一个法线贴图匹配
  • 不展平:保留两个法线贴图的强度
虽然 Whiteout 解决方案看起来效果很好,但在第一点和最后一点上没有达到。
为了实现这些目标,我们的方法涉及旋转(或重新定向)细节贴图,使其遵循基础法线贴图的“surface”,就像当物体在模型空间或世界空间中被照明时切线空间法线由底层几何体变换一样。我们将此称为重定向法线贴图(RNM)。这是与最后两种技术相比的结果:
RNM混合

Whiteout混合
UDN混合
细节上的差异很明显,特别是阴影部分
需要明确的是,我们并不是唯一想到这一点的人。Renaldas Zioma 在 GDC 上提出了本质上相同的想法—作为 Unity 技术演示的一部分,为在皮肤上添加毛孔而开发的 。也就是说,与Unity方法相比,我们的方法有一些优势,我们将在深入实施后进行解释      
9
The Nitty Gritty
 


 
假设我们有一个几何法线s, 基本法线t和次要(或细节)法线u. 我们可以计算重新定向的法线r通过构建一个旋转的变换s到t,然后将其应用于u:
重定向细节法线(左),使其遵循基础法线贴图(右)
我们可以用四元数来实现
旋转u然后可以以标准方式执行
这里可以简化为:
由于我们在线性空间中操作,按照惯例s=[0,0,1],因此我们把(1带入到(4)中并简化得到
进一步优化
下面是用HLSL对公式7的实现,
float3 t = tex2D(texBase,   uv).xyz*float3( 2,  2, 2) + float3(-1, -1,  0);
float3 u = tex2D(texDetail, uv).xyz*float3(-2, -2, 2) + float3( 1,  1, -1);
float3 r = t*dot(t, u)/t.z - u;
return r*0.5 + 0.5;
这种方法的一个潜在的好处是u的长度被保留如果t是单位长度,所以如果u也是单位长度,那么不需要归一。然而,由于量化、压缩、mipmapping 和过滤,这在实践中不太可能成立。我们可能看不到对漫反射着色的重大影响,但它确实会影响节能镜面反射。鉴于此,我们建议对结果进行归一化:
float3 r = normalize(t*dot(t, u) - u*t.z);
     
10
细节
 
在准备这篇文章时,我们了解到 Jeppe Revall Frisvad发表的一篇论文,该论文使用相同的策略来旋转局部向量。下面是适用于 HLSL 的相关代码:
float3 n1 = tex2D(texBase,   uv).xyz*2 - 1;
 float3 n2 = tex2D(texDetail, uv).xyz*2 - 1;
 float a = 1/(1 + n1.z);
 float b = -n1.x*n1.y*a;
 // Form a basis
 float3 b1 = float3(1 - n1.x*n1.x*a, b, -n1.x);
 float3 b2 = float3(b, 1 - n1.y*n1.y*a, -n1.y);
float3 b3 = n1;
if (n1.z < -0.9999999) // Handle the singularity
{
    b1 = float3( 0, -1, 0);
    b2 = float3(-1,  0, 0);
}
// Rotate n2 via the basis
float3 r = n2.x*b1 + n2.y*b2 + n2.z*b3;
return r*0.5 + 0.5;
我们的版本是为 GPU 编写的,在这种情况下需要较少的 ALU 操作,而 Jeppe 的实现适用于可以重用base map的情况,例如蒙特卡洛采样。另一件需要注意的事情是,当基础法线的z分量是−1时, Jeppe 会检查这一点,但在我们的例子中,我们可以在美术流程中避免
更重要的是,我们可以讨论z应该≥0,但是这并不是我们这个方法的真正输出。一个潜在的问题是,如果在创作过程中使用重新定向,然后压缩为双分量法线贴图格式,因为重定向的时候通常假定z是≥0. 最直接的解决方法是修复z到0并在压缩前重新归一化。
至于阴影,我们还没有看到z值的负面影响——比如重新定向的法线指向表面的位置。
   
11
unity blending
 

现在让我们回到 Unity 技术演示所采用的方法。与 Jeppe 一样,Renaldas 也使用基础来变换第二个法线。通过围绕y和x轴旋转基础法线来生成矩阵的其他两行:
float3 n1 = tex2D(texBase,   uv).xyz*2 - 1;
 float3 n2 = tex2D(texDetail, uv).xyz*2 - 1;
 float3x3 nBasis = float3x3(
     float3(n1.z, n1.y, -n1.x), // +90 degree rotation around y axis
     float3(n1.x, n1.z, -n1.y), // -90 degree rotation around x axis
     float3(n1.x, n1.y,  n1.z));
 float3 r = normalize(n2.x*nBasis[0] + n2.y*nBasis[1] + n2.z*nBasis[2]);
return r*0.5 + 0.5;
正常情况当n1为[0,0,±1]时,从这两个方向中的任意一个偏移得越远,效果越弱。下图显示了当我们向x轴旋转n1并且在n2的上半球(+z)移动一些点
Unity(第一行)四元数变换(第二行)
相反,四元数变换没有这样的问题。这也反映在混合输出中:
重定向法线贴图(左)与unity方法(右)
   
12
性能
 
提供具有代表性的性能数据是不太可能的,因为它在很大程度上取决于许多因素:平台、周围代码、法线贴图编码的选择(这可能是你的游戏所独有的),甚至可能是shader。
作为参考,我们采用了各种技术,比如减去纹理读取和重新打包,并针对 Shader Model 3.0 虚拟指令集对其进行了优化。以下是它们在指令数方面的表现:
光照下的演示模型可以参考
https://blog.selfshadow.com/sandbox/normals.html
   
13
结论
 
根据分析和结果,我们很清楚线性和叠加混合在细节法线贴图方面没有优势。即使在 GPU  cycles非常宝贵的情况下,UDN也是一个更好的选择,而且它应该也很容易在 Photoshop 中重现。
通过上面可以发现Whiteout优于UDN可能取决于我们的纹理和着色模型 - 在我们的示例中,几乎没有将它们分开。除此之外,RNM可以保留更多细节,而且指令成本相似,因此希望是个可行的替代方案。
除了两种组件格式之外,我们还没有介绍褪色策略、与视差映射的集成或镜面反射抗锯齿。这些是未来想要解决的主题

- End -

本文来自微信公众号“Thepoly”(ID:gh_8e14f20c7af9)。大作社经授权转载,该文观点仅代表作者本人,大作社平台仅提供信息存储空间服务。