【图形学】法线映射中的三平面映射技术
法线映射中的三平面映射技术对于复杂的几何体来说,用三平面映射的方法可以避免传统UV产生纹理拉伸和接缝的问题。但用在法线贴图上会产生致命的错误。
三平面映射
三平面映射一种利用世界空间位置从x、y、z三个方向对物体应用纹理的技术,又称为“uv自由纹理”,“世界空间投射纹理”等。这种技术也有对物体空间的变化,比如从物体的正上方、侧面和正面看一个物体,并且从这些方向给物体贴上一个纹理。
地形是一个很好的应用,因为它通常有复杂的几何面,基本地形通常用地形的正上方视角来做uv映射,我们称其为“单平面映射”uv。对于连绵起伏的山丘来说,这看起来不错,但是在陡峭的悬崖上,单平面映射的uv看起来就很糟糕了。
单平面映射uv,有明显的拉伸现象
三平面映射uv,无拉伸现象
问题
三平面映射不一定适用于法线映射
三平面映射法线贴图常见的简单“解决方案”是只使用网格切线。通常它看起来“足够接近”。依赖于纹理足够模糊,以至于法线错误不会很明显。但是不要这样做,有一些更便宜的方法看起来更好看。
我看到的另一种技术是尝试使用少量叉积和世界法线在顶点或片段着色器中构造与世界空间矩阵相切的方法。如果做得好,这是一个非常好的技术。但往往结果只是比naive的方法稍微好一点,因为人们并不真正了解他们在做什么。这不是我所指的更便宜的方法之一,所以很可能这也不是你想要做的。
廉价解决方案之一可以追溯到 2003 年。它来自 Nvidia 的 GPU Gems 3。
GPU Gems 3:使用 GPU 生成复杂的程序地形https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch01.html
GPU Gems 3
这是一个又好又廉价的近似值。虽然着色器代码看起来有点奇怪,但它看起来<i>不应该<i>奇怪。我很少看到它实际使用过。我怀疑这是一个糟糕的实施案例,让人们认为它不起作用。事实上,如果你在Unity 中逐字实现这种技术,它甚至会比简单的方法看起来更糟糕。
因此,在我们深入研究那篇 GPU Gems 3 文章在做什么之前,让我们先看看这种朴素的方法,以及它有什么问题。
naive方法(又名,只使用网格切线)
Naive方法
这是使用 Unity 的默认球体,定向光来自正上方,没有明显错误。在期望的地方会有颠簸和隆起,那有什么问题呢?嗯,这是最好的情况,UV 包裹在默认球体上的方式、光线方向和法线贴图的模糊方向都可以协同工作。它使一切看起来都在正常工作。或者至少工作得很好,让大多数人不关心或注意到。但是让我们更改灯光方向,用更明显的法线贴图问题就产生了。
所以现在灯光是从左边来的,但是那些颠簸是凸起还是凹下去?左边看起来像是在外面,右边看起来像是在里面,而顶部的灯光看起来像是来自完全不同的方向,这就是为什么naive的方法存在不稳定性。
为了比较,这就是它正确映射的样子。
从图上可以很明显看出凸起的部分,并且它们的光线方向都一致。这张法线贴图在混合处有些奇怪,但基本上实现了正确的映射。
对比
正切空间法线映射
侧切线
那么让我们来谈谈切线空间的法线映射是什么以及为什么naive的方法实际上不起作用。
切线空间法线贴图和法线贴图通常会给刚接触shader的人带来困惑,他们只是被认为是“魔法”。一些讨论它们的文章立即进入数学或着色器代码并且不首先用基本术语解释它并没有帮助。所以这是我的尝试。
法线贴图是相对于纹理方向的方向。除此之外还有大量的实现细节和细节,但它们并没有改变这个基本前提。对于 Unity,红、绿、蓝分别对于x、y、z轴上的值,所有这些都与纹理 UV 和顶点法线有关。这有时被称为 +Y 法线贴图。
半球+Y法线贴图
法线贴图的rgb通道代表法线xyz轴的偏移
对于实时图形,网格的顶点存储 UV 的切线或“从左到右”方向。这与顶点法线和双切线bitangent(有时称为副法线binormal)一起传递到片段着色器中,作为世界空间变换矩阵的切线。这样存储在法线贴图中的方向就可以从切线空间旋转到世界空间。关键是切线只匹配存储的纹理 UV 的方向。如果三平面投影映射 UV 与此不匹配(大多数网格保证不匹配!),那么会得到看起来像是倒置的法线,或侧向的,或面向任何其他方向。
Unity Tangent方向
在这里,我们有一个纹理,其上绘制了切线 (x) 和双切线 (y) 对齐。由于网格的切线基于 UV,因此可以很好地近似这些网格切线。下面是使用此纹理的球体网格。它在使用存储在网格中的 UV 和生成的三平面 UV 之间交替使用,两者都进行了缩放,因此纹理被多次平铺。可以在球体的左侧看到方向大致对齐,但在顶部和右侧,它们明显不同。如果再次查看这种简单的方法,就会看到这种差异导致法线贴图似乎被翻转和旋转。
关于网格切线和三平面映射的附加说明:
一般来说,如果进行三平面映射,则使用的网格不需要也不应该将 UV 存储在顶点中。通过扩展,网格也不应该有切线。对于导入到 Unity 或随 Unity 提供的网格,Unity 的默认行为是导入或生成网格的切线。这是naive的方法甚至可能的唯一原因。默认情况下在代码中创建的网格没有切线,甚至没有 UV 或法线,而且naive的方法会更加破碎。
解决方案
Swizzle
真正想要的三面映射是计算三面纹理 UV 的每个“面”的唯一切线和双切线。但是我们可以减少数据量,通过用世界轴作为切线的方式去计算,也可以交换法线的分量(又名 swizzle) ,并得到相同的结果!
// Basic Swizzle// Triplanar uvs
float2 uvX = i.worldPos.zy; // x facing plane
float2 uvY = i.worldPos.xz; // y facing plane
float2 uvZ = i.worldPos.xy; // z facing plane
// Tangent space normal maps
half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));
half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));
half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));
// Get the sign (-1 or 1) of the surface normal
half3 axisSign = sign(i.worldNormal);
// Flip tangent normal z to account for surface normal facing
tnormalX.z *= axisSign.x;
tnormalY.z *= axisSign.y;
tnormalZ.z *= axisSign.z;
// Swizzle tangent normals to match world orientation and triblend
half3 worldNormal = normalize(
tnormalX.zyx * blend.x +
tnormalY.xzy * blend.y +
tnormalZ.xyz * blend.z
);
主要要看的是前3行和最后3行。注意每个平面的 UV 和法线贴图的旋转组件的顺序是相同的。正如我们上面讨论的,切线空间法线贴图应该与其 UV 的方向对齐,因为我们使用的是世界空间uv坐标保证坐标间相互对齐。
Swizzle 方法适用于四面体,在平面,轴对齐的墙体上效果很好,但是在圆面上仍有缺陷。
接下来我们尝试加一些圆度到swizzle方法中
细节的融合
切线空间法线映射混合,特别是对于法线细节的表现,与三面映射完全无关。
有几种技术可以解决这个问题。人们想到的第一个想法是“把它们加在一起”,但并不是一个真正的解决方案,它只是把两个法线都平坦化了。一些更聪明的人可能会想“叠加混合怎么样?”它工作得很好,但它的流行纯粹源于它在 Photoshop 中的简单做法,而不是完全打破一切。这并不是因为它远远正确,甚至也不是因为它特别便宜。这是另一种比正确方法更昂贵的技术。
基本上有两种相互竞争的近似最常被使用,所谓的“Whiteout”和“ UDN”法线混合。它们在实现和结果上都非常相似。UDN 性能稍微好一点,但是会使边缘变平一点,Whiteout 看起来稍微好一点,但是性能要求更高。对于现代桌面和控制台 GPU 来说,几乎没有理由不使用 Whiteout 方法而不使用 UDN 方法。但是 UDN 混合技术仍然可以用于移动设备。
详见:http://blog.selfshadow.com/publications/blending-in-detail/
三平面法线映射
那么切线空间法线映射如何全部应用于三平面映射呢?我们可以把每个平面都与一个单独的法线贴图混合,其中一个“法线贴图”我们正在混合的是顶点法线,那篇文章中的 UDN 混合实际上特别cheap,因为它只是将 x 和 y 法线贴图值添加到顶点法线!让我们看看那是什么样子。
UDN Blend
// UDN blend// Triplanar uvs
float2 uvX = i.worldPos.zy; // x facing plane
float2 uvY = i.worldPos.xz; // y facing plane
float2 uvZ = i.worldPos.xy; // z facing plane
// Tangent space normal maps
half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));
half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));
half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));
// Swizzle world normals into tangent space and apply UDN blend.
// These should get normalized, but it's very a minor visual
// difference to skip it until after the blend.
tnormalX = half3(tnormalX.xy + i.worldNormal.zy, i.worldNormal.x);
tnormalY = half3(tnormalY.xy + i.worldNormal.xz, i.worldNormal.y);
tnormalZ = half3(tnormalZ.xy + i.worldNormal.xy, i.worldNormal.z);
// Swizzle tangent normals to match world orientation and triblend
half3 worldNormal = normalize(
tnormalX.zyx * blend.x +
tnormalY.xzy * blend.y +
tnormalZ.xyz * blend.z
);
看起来效果不错,但是UDN有一个缺点:由于计算用到的数学方法,在混合角度大于45度以上的时候会法线变平,这导致了混合后法线细节的缺失。
Whiteout Blend
在whiteout方法中,混合后没有出现法线细节损失。
// Whiteout blend// Triplanar uvs
float2 uvX = i.worldPos.zy; // x facing plane
float2 uvY = i.worldPos.xz; // y facing plane
float2 uvZ = i.worldPos.xy; // z facing plane
// Tangent space normal maps
half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));
half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));
half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));
// Swizzle world normals into tangent space and apply Whiteout blend
tnormalX = half3(
tnormalX.xy + i.worldNormal.zy,
abs(tnormalX.z) * i.worldNormal.x
);
tnormalY = half3(
tnormalY.xy + i.worldNormal.xz,
abs(tnormalY.z) * i.worldNormal.y
);
tnormalZ = half3(
tnormalZ.xy + i.worldNormal.xy,
abs(tnormalZ.z) * i.worldNormal.z
);
// Swizzle tangent normals to match world orientation and triblend
half3 worldNormal = normalize(
tnormalX.zyx * blend.x +
tnormalY.xzy * blend.y +
tnormalZ.xyz * blend.z
);
UDN对比whiteout
与whiteout相比,UDN方法混合后的法线会平坦一些,这里我们就有两个完全合理的三面法线映射近似。两者都没有明显的光线问题。对于大多数用例来说,这两者都足够好了。而且两者都比naive,swizzle方法更快。
GPU Gems 3中的技术怎么样?它实际上和上面的两个shader是一样的。它同样对法线贴图分量做swizzle,但是它去掉了法线贴图的“ z”分量,而使用了零,为什么?如果你仔细看 GPU Gems 3代码,它实际上是相同的 UDN 混合!两者实际上都没有使用法线贴图的 z 分量。它们的实现最终比我编写的 UDN 混合着色器快了几个指令,但是产生了相同的结果!
// GPU Gems 3 blend// Triplanar uvs
float2 uvX = i.worldPos.zy; // x facing plane
float2 uvY = i.worldPos.xz; // y facing plane
float2 uvZ = i.worldPos.xy; // z facing plane
// Tangent space normal maps
half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));
half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));
half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));
// Swizzle tangemt normals into world space and zero out "z"
half3 normalX = half3(0.0, tnormalX.yx);
half3 normalY = half3(tnormalY.x, 0.0, tnormalY.y);
half3 normalZ = half3(tnormalZ.xy, 0.0);
// Triblend normals and add to world normal
half3 worldNormal = normalize(
normalX.xyz * blend.x +
normalY.xyz * blend.y +
normalZ.xyz * blend.z +
i.worldNormal
);
GPU Gems 3混合是这三种shader中最快的选择。但是使用 Whiteout 混合的额外成本不太可能是可观的,即使是在移动虚拟现实中。
重定向法线映射
那么我一直显示的“地面真相”图像怎么样呢?上面链接到的 Blending in Details 文章描述了 UDN 和 Whiteout 混合之外的其他几种方法。这包括一个他们称为重定向法线映射的方法。这个方法非常棒; 它最终比 GPU Gems 3或 Whiteout 方法性能要求高一些,但是非常接近“GroundTruth”!事实上,本文中所有“GroundTruth”的示例图像都在使用这种技术!虽然它产生了一个更复杂的shader,但总体来说它仍然可能比naive的方法更快。
在Blending in Details 文章中 RNM 函数作出一些假设,关于法线贴图的结果被传递到Unity中并不是正确的。需要对原始代码进行一个小的修改。在文章的评论中,Stephen Hill 提供了使用 RNM 和 Unity 的例子。有了这个功能,三面混合着色器看起来像这样。
// Reoriented Normal Mapping blend// Triplanar uvs
float2 uvX = i.worldPos.zy; // x facing plane
float2 uvY = i.worldPos.xz; // y facing plane
float2 uvZ = i.worldPos.xy; // z facing plane
// Tangent space normal maps
half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));
half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));
half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));
// Get absolute value of normal to ensure positive tangent "z" for blend
half3 absVertNormal = abs(i.worldNormal);
// Swizzle world normals to match tangent space and apply RNM blend
tnormalX = rnmBlendUnpacked(half3(i.worldNormal.zy, absVertNormal.x), tnormalX);
tnormalY = rnmBlendUnpacked(half3(i.worldNormal.xz, absVertNormal.y), tnormalY);
tnormalZ = rnmBlendUnpacked(half3(i.worldNormal.xy, absVertNormal.z), tnormalZ);
// Get the sign (-1 or 1) of the surface normal
half3 axisSign = sign(i.worldNormal);
// Reapply sign to Z
tnormalX.z *= axisSign.x;
tnormalY.z *= axisSign.y;
tnormalZ.z *= axisSign.z;
// Triblend normals and add to world normal
half3 worldNormal = normalize(
normalX.xyz * blend.x +
normalY.xyz * blend.y +
normalZ.xyz * blend.z +
i.worldNoral
);
// Reoriented Normal Mapping for Unity3d
// http://discourse.selfshadow.com/t/blending-in-detail/21/18
float3 rnmBlendUnpacked(float3 n1, float3 n2)
{
n1 += float3( 0, 0, 1);
n2 *= float3(-1, -1, 1);
return n1*dot(n1, n2)/n1.z - n2;
}
这部分代码可以用任何切线空间内的规范化-1到1范围内的法线贴图来运行。
Ground Truth
除了重定向法线映射仍然不是“GroundTruth”,问题在于三平面法线映射的“GroundTruth”的真实定义是一个很难确定的问题。人们烘焙带切线的网格面在相同的uv上,假设任何一个投影平面本身应该看起来与它看起来一样。就像任何基本的地形shader一样。但是这种方法也是不正确的,这种投影方式是切线空间法线映射的一种不恰当的使用。只有当它被应用到一个轴对齐的方体,三面法线映射才可以被认为是真正的“GroundTruth”。
在上面描述切线空间法线映射的工作原理时,我也撒了一点小谎。特别是副切线,我通常会忽略它。我将其描述为切线纹理示例中的“绿色箭头”。这不完全是真的。对于切线空间法线映射,副切线被定义为法线和切线的叉乘。切线和副切线都应该垂直于法线,并且相互垂直。如果你看三面切线纹理的例子,你可以看到切线和双切线箭头开始倾斜,最终成为一个三角形的角落。其结果是,正切和副切都垂直于法线,但并不相互垂直。
正确地匹配切线空间的计算方法可以消除副切线和由此产生的切线空间矩阵的这种倾斜。结果是,当使用正确的切线空间时,法线方向会出现方向上的轻微旋转。当使用倾斜的切线空间时会出现不同的旋转。
问题是,任何类似这样的投影法线贴图在技术上都是错误的。正在使用的法线贴图不是在我们显示它们的相同切线空间中生成的。它们很可能形成于一个完美的平面。法线贴图应该只与它们最初计算的几何和切线空间一起使用。这意味着在地形上使用带有法线贴图的重复纹理,或者作为细节法线,或者用于三平面映射,都是不恰当的使用。因此,这些案件没有达到“GroundTruth”。当然,我们一直使用这样的法线贴图,没有人真正注意到任何错误。
所以归根结底就是要决定什么才是想要达到的“GroundTruth”。在最基本的情况下,我们需要决定法线贴图应该表示什么。是表面位移与纹理投影对齐产生的法线,还是表面法线?如果它是一个位移沿投影法线Whiteout混合方法更接近“GroundTruth”!如果它是沿表面法线的位移,那么 RNM 是更接近的方法。对于我认为 RNM 保留了更多的法线地图的细节,整体看起来更好。归根结底,这取决于个人偏好和表现。
除了naive的方法,那总是错的。
三平面法线映射的其他技术
上述方法的主要优点是,它们不需要进行矩阵旋转来在切线和世界空间之间进行转换,反之亦然。然而你可能仍然需要切向空间矢量,就像视差贴图技术一样。你是怎么得到这些的?
屏幕空间偏导数
利用屏幕空间偏导数重建切线空间旋转矩阵是可能的。这一思想来自于 Morten Mikkleson 推广的导数映射。这种技术被错误地称为“没有切线的法线映射”,但它不同于切线空间的法线映射。这种方法对三平面映射有很多好处。出于本文的目的,我不打算讨论导数映射。如果你想跳进这个兔子洞,试试下面的链接 :
http://therobotseven.com/devlog/triplanar-texturing-derivative-maps/
http://www.rorydriscoll.com/2012/01/11/derivative-maps/
然而,我们可以使用屏幕空间偏导数来帮助正切空间法线图。我们可以在片段着色器使用它们为任意投影重建切线到世界旋转矩阵。然后这可以应用到每一边的法线贴图。Christian Schüler 在本文中将这个矩阵称为余切框架,因为它与通常的切线空间中的矩阵并不完全相同。
http://www.thetenthplanet.de/archives/1180
// Cotangent frame from Screen Space Partial Derivates// Triplanar uvs
float2 uvX = i.worldPos.zy; // x facing plane
float2 uvY = i.worldPos.xz; // y facing plane
float2 uvZ = i.worldPos.xy; // z facing plane
// Tangent space normal maps
half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));
half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));
half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));
// Normalize surface normal
half3 vertexNormal = normalize(i.worldNormal);
// Calculate the cotangent frame for each plane
half3x3 tbnX = cotangent_frame(vertexNormal, i.worldPos, uvX);
half3x3 tbnY = cotangent_frame(vertexNormal, i.worldPos, uvY);
half3x3 tbnZ = cotangent_frame(vertexNormal, i.worldPos, uvZ);
// Apply cotangent frame and triblend normals
half3 worldNormal = normalize(
mul(tnormalX, tbnX) * blend.x +
mul(tnormalY, tbnY) * blend.y +
mul(tnormalZ, tbnZ) * blend.z
);
// Unity version ofhttp://www.thetenthplanet.de/archives/1180
float3x3 cotangent_frame(float3 normal, float3 position, float2 uv)
{
// get edge vectors of the pixel triangle
float3 dp1 = ddx( position );
float3 dp2 = ddy( position ) * _ProjectionParams.x;
float2 duv1 = ddx( uv );
float2 duv2 = ddy( uv ) * _ProjectionParams.x;
// solve the linear system
float3 dp2perp = cross( dp2, normal );
float3 dp1perp = cross( normal, dp1 );
float3 T = dp2perp * duv1.x + dp1perp * duv2.x;
float3 B = dp2perp * duv1.y + dp1perp * duv2.y;
// construct a scale-invariant frame
float invmax = rsqrt( max( dot(T,T), dot(B,B) ) );
// matrix is transposed, use mul(VECTOR, MATRIX)
return float3x3( T * invmax, B * invmax, normal );
}
我不一定建议使用这种方法创建三平面映射,因为它比之前的方法代价更昂贵,虽然它也可以被认为是非常接近“GroundTruth”的版本。它也有一些问题,在几何形状的多边形的法线上会出现明显的硬边。这是因为切线是从实际的几何曲面计算出来的,而不是平滑的插值值。这是否是一个问题取决于正在使用的反照率贴图,法线贴图和几何体。
在一个侧面说明,我们可以利用这一点,以获得低多边形样式的表面法线。
half3 normal = normalize(cross(dp1, dp2));
横积切线重构
如何复写在网格上计算切线?尽管我提到这是不正确的,但这是可以做到的。Unity 的地形着色器使用了一种非常类似的技术来计算顶点着色器中的切线。在这里,它是在片段着色器中完成的:
// Tangent Reconstruction// Triplanar uvs
float2 uvX = i.worldPos.zy; // x facing plane
float2 uvY = i.worldPos.xz; // y facing plane
float2 uvZ = i.worldPos.xy; // z facing plane
// Tangent space normal maps
half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));
half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));
half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));
// Get the sign (-1 or 1) of the surface normal
half3 axisSign = sign(i.worldNormal);
// Construct tangent to world matrices for each axis
half3 tangentX = normalize(cross(i.worldNormal, half3(0, axisSign.x, 0)));
half3 bitangentX = normalize(cross(tangentX, i.worldNormal)) * axisSign.x;
half3x3 tbnX = half3x3(tangentX, bitangentX, i.worldNormal);
half3 tangentY = normalize(cross(i.worldNormal, half3(0, 0, axisSign.y)));
half3 bitangentY = normalize(cross(tangentY, i.worldNormal)) * axisSign.y;
half3x3 tbnY = half3x3(tangentY, bitangentY, i.worldNormal);
half3 tangentZ = normalize(cross(i.worldNormal, half3(0, -axisSign.z, 0)));
half3 bitangentZ = normalize(-cross(tangentZ, i.worldNormal)) * axisSign.z;
half3x3 tbnZ = half3x3(tangentZ, bitangentZ, i.worldNormal);
// Apply tangent to world matrix and triblend
// Using clamp() because the cross products may be NANs
half3 worldNormal = normalize(
clamp(mul(tnormalX, tbnX), -1, 1) * blend.x +
clamp(mul(tnormalY, tbnY), -1, 1) * blend.y +
clamp(mul(tnormalZ, tbnZ), -1, 1) * blend.z
);
这与在顶点着色器中计算和使用插值值完全不同。
三面混合
基本混和
float3 blend = abs(normal.xyz);
blend /= blend.x + blend.y + blend.z;
float3 blend = abs(normal.xyz);
blend = (blend - 0.2) * 7.0;
blend = max(blend, 0);
blend /= blend.x + blend.y + blend.z;
这有助于锐化一点,但这是一个奇怪的代码位,因为 * 7.0是无用的!将7.0更改为任何非零数字或完全删除它没有任何效果,但这段代码似乎出现在我看到的半数三平面实现中。稍微思考一下就能解释为什么没有必要,一个数除以它本身总是1再乘以一些非零值并不会改变这一点。
不过 -0.2是可以的,只要省略后面的乘法就可以了。还有一个额外的优化,可以通过滥用一个点积来对除法之前的组件进行求和。
float3 blend = abs(normal.xyz);
blend = max(blend - 0.2, 0);
blend /= dot(blend, float3(1,1,1));
GPU Gem 3 风格混合在不同值下的锐度
越大的插值使拐角变得明显的尖锐,而且过高值的时候图像开始变黑,我更喜欢另一种技巧: 使用指数。如果你对一个固定的混合清晰度感到满意,使用4的硬编码功率和上面的优化选项一样快,在我看来看起来更好。
float3 blend = pow(normal.xyz, 4);
blend /= dot(blend, float3(1,1,1));
这是里有一个微妙的差异,但它的4个指令的成本相同。如果你想使用一种材料属性来更好地控制混合清晰度,那么这就有点贵了。Unity 会显示5个指令,但实际上更像是11个指令。其他硬编码的能力将会更少(每增加2个pow就多出1个指令,所以 pow (普通的,8)是5个指令,16是6个指令,等等。
还有更先进的非对称混合,也可以这样做,可能更适合一些纹理或用例。
// Asymmetric Triplanar Blendfloat3 blend = 0;// Blend for sides only
float2 xzBlend = abs(normalize(normal.xz));
blend.xz = max(0, xzBlend - 0.67);
blend.xz /= max(0.00001, dot(blend.xz, float2(1,1)));
// Blend for top
blend.y = saturate((abs(normal.y) - 0.675) * 80.0);
blend.xz *= (1 - blend.y);
如果有纹理的高度数据,还有更好的高度混合。有些人作弊,只是使用纹理亮度,可以作为一些纹理高度的体面近似值。
float3 blend = abs(normal.xyz);
blend /= dot(blend, float(1,1,1));
// Height value from each plane's texture. This is usually
// packed in to another texture or (less optimally) as a separate
// texture.
float3 heights = float3(heightX, heightY, heightZ) + (blend * 3.0);
// _HeightmapBlending is a value between 0.01 and 1.0
float height_start = max(max(heights.x, heights.y), heights.z) - _HeightmapBlending;
float3 h = max(heights - height_start.xxx, float3(0,0,0));
blend = h / dot(h, float3(1,1,1));
blend * 3.0在那里是为了减少混合区域
http://untitledgam.es/2017/01/height-blending-shader/
https://github.com/bgolus/Normal-Mapping-for-a-Triplanar-Shader/blob/master/TriplanarSurfaceShader.shader
来源:https://bgolus.medium.com/normal-mapping-for-a-triplanar-shader-10bf39dca05a
页:
[1]