【USparkle专栏】假如你深怀绝技,爱“搞点研究”,乐于共享也博采众长,咱们期待你的参加,让智慧的火花磕碰交错,让知识的传递生生不息!

一、概述

视差映射(Parallax Mapping)是一种类似于法线贴图的纹路技能,它们都能显着增强模型/纹路外表细节并赋予其凹凸感,但法线贴图所带来的凹凸感不会跟着视角改变,也不会彼此阻挠。例如,假如你看实在的砖墙,在越垂直于墙面朝向的视角,你是看不到砖之间的缝隙的,砖墙的法线贴图永久不会显现这种类型的遮挡,由于它只会改变法线来影响直接光照的成果。

实时渲染常用纹理技术总结:视差映射

仅运用法线贴图、没有正确的遮挡联络的过错作用

所以最好让凹凸感实践影响外表上每个像素的方位,咱们能够经过高度贴图来完成这个需求。

实时渲染常用纹理技术总结:视差映射

举例

最简略的办法莫过于运用很多极点,然后依据从上图中采样的高度值去偏移极点方位坐标——位移映射(Displayment Mapping) ,能够得到下图中左图的作用(极点密度为100*100)。可是这样的极点数量并非实时烘托的游戏所能承受(或许说值得优化),而极点数量过少的话就会出现十分不滑润的块状现象,如下图中右图的作用(极点密度为10*10)。所以就有聪明的人想出了能够偏移极点纹路坐标——视差映射(Parallax Mapping) ,这样咱们用一个Quad也能做出下图中左图的实在作用,先放上源码。

实时渲染常用纹理技术总结:视差映射

不同极点密度下位移映射技能的作用比照

二、原理

那怎样偏移纹路坐标来做出凹凸感呢?咱们必须从调查到的现象入手:

实时渲染常用纹理技术总结:视差映射

假设咱们真有这样一个粗糙、凹凸不平的外表(比方经过密集极点偏移后得到),那么当咱们以某一视野方向V看向外表时,咱们应该看到的是B点(即视野和高度图的交点)。但咱们前面也说了,咱们用的是一个Quad,所以实践看到的应该是A点。视差映射的作用便是偏移A处的纹路坐标到B处的纹路坐标,这样即使咱们看到的点是A,采样成果却是B处,然后模仿出高度差异,所以咱们要处理的便是如何在A处获取B处的纹路坐标。

实时渲染常用纹理技术总结:视差映射

原理

仔细调查上图,其实A、B均在视野方向V地点的直线上,所以咱们的偏移方向便是归一化的视野方向,偏移量则为A处采样高度图的成果H(A),所以偏移向量为图中P,而且咱们需求沿着纹路坐标(UV)地点的平面偏移,所以偏移量为P在平面上的投影,那么实践向A点看到的是图中的H(P),这意味着咱们得到的其实是近似B点的成果。

由于咱们需求沿着纹路坐标(UV)地点的平面偏移,那么就有必要挑选切线空间(也便是把视野方向转到切线空间再去偏移纹路坐标),这样咱们也就不必忧虑模型有任何的旋转时偏移量不沿着UV平面上了。原理见法线贴图,这便是开头强烈建议你先了解法线贴图的原因。

对任意一点的纹路坐标P、归一化的视野方向V、高度贴图采样成果h得到的偏移成果Padj:

实时渲染常用纹理技术总结:视差映射

除以Z重量是为了归一化:由于当视野越垂直于平面时,Z重量越大。可是当视野挨近平行于平面,Z重量很小,除以Z重量会使得偏移量过大,留意下图的缝隙处(运用的是最开端的高度贴图例图)。

实时渲染常用纹理技术总结:视差映射

当视野越挨近平行于平面时偏移量越大

为了改善这个问题,咱们能够约束偏移量,使其永久不大于实践的高度(如下图中偏移量本应是灰色箭头线表示的向量,而约束后则是黑色箭头线表示的向量)。方程为Padj=P+h*Vxy(也便是不除以Z重量,核算速度也更快)。

实时渲染常用纹理技术总结:视差映射

实时渲染常用纹理技术总结:视差映射

能够和上面偏移量过大的成果做比照

可是由于视野方向的XY重量仍会跟着视野方向越平行于平面而变大,所以偏移量仍会变大。

还有一个问题:在大多情况下,咱们上面的做法都能得到好的成果,可是当高度快速变化时成果或许不尽善尽美:得到的成果H(P)与B点(蓝点)相差甚远。

实时渲染常用纹理技术总结:视差映射

三、完成

1. 视差映射
咱们能够依据前一个Part所讲的根本原理来进行简略的尝试,这儿咱们仍会运用法线贴图,由于我在总结法线贴图的文章中也说过,法线贴图经常依据高度贴图核算得到,但法线贴图影响的是法线,经过光照来体现凹凸细节,而视差映射是运用偏移纹路坐标来获取其他方位的采样成果来体现高度,所以二者配合就好像双剑合璧,威力大增。

float2 ParallaxMapping(float2 uv, half3 viewDir)
{
    float height = tex2D(_HeightMap, uv).r * _HeightScale;
    float2 offset = 0;
#if _OFFSETLIMIT //为了比照是否约束偏移量的作用
    offset = viewDir.xy;
#else 
    offset = viewDir.xy / viewDir.z;
#endif
    float2 p = offset * height; 
    return uv - p;
}
half3 viewDirWS = normalize(UnityWorldSpaceViewDir(positionWS));
float2 uv = i.uv.xy;
#ifdef _PARALLAXMAPPING
    half3 viewDirTS = normalize(mul(viewDirWS, float3x3(i.T2W0.xyz, i.T2W1.xyz, i.T2W2.xyz)));
    uv = ParallaxMapping(uv, viewDirTS);
#endif
//然后用偏移后的纹路坐标采样各种贴图即可

实时渲染常用纹理技术总结:视差映射

左图为仅运用法线贴图的作用,右图为参加视差映射的作用

你能够看到在偏移纹路坐标后或许在边缘的方位出现问题,由于边缘偏移后或许会超出0到1的规模,关于Quad来说,能够简略地丢掉超出规模的部分,可是关于其他复杂模型简略丢掉或许并不能处理问题。

if (uv.x > 1 || uv.y > 1 || uv.x < 0 || uv.y < 0)
    discard;

实时渲染常用纹理技术总结:视差映射

丢掉后干净了许多

在Unity的Shader源码中也为咱们提供了视差映射的函数:

// Calculates UV offset for parallax bump mapping
inline float2 ParallaxOffset( half h, half height, half3 viewDir )
{
    h = h * height - height/2.0;
    float3 v = normalize(viewDir);
    v.z += 0.42;
    return h * (v.xy / v.z);
}

虽然现在的作用现已满足好了,可是在上一个Part最终提出的两个问题仍然存在,在Real Time Rendering第四版的6.8.1节,提供了很多处理这些问题的参考资料,咱们下面总结的其中最常见的,想更深层次了解的话引荐咱们去阅览原文。

2. 峻峭视差映射
出现上一个Part最终所提到的两个问题的根本原因都是偏移量过大导致的,所以咱们能够效仿Ray Marching,运用逐渐逼近的方式寻觅到合适的偏移量,但这样就势必要屡次采样,功能耗费更大,最初采用这种思想的便是峻峭视差映射(Steep Parallax Mapping)。

如下图,将深度规模(0(平面方位)->1(最大采样深度))划分为具有相同深度h的多个层(下图层深h=0.2),求出层深h对应的纹路偏移量huv,然后从上到下遍历每一层:用huv偏移纹路坐标,对高度贴图进行采样,假如当时层的深度值小于采样的值,咱们就持续向下进行,直到当时层的深度大于高度图的采样成果,这意味着咱们找到了低于外表的第一个层(即认为检测到视野和高度图的相交方位,虽然是近似的)。

实时渲染常用纹理技术总结:视差映射

T表示遍历次数,紫色点为当时层深度值,浅蓝色点为采样的深度值

float2 ParallaxMapping(float2 uv, float3 viewDir)
{
    // 优化:依据视角来决议分层数(由于视野方向越垂直于平面,纹路偏移量较少,不需求过多的层数来保持精度)
    float layerNum = lerp(_MaxLayerNum, _MinLayerNum, abs(dot(float3(0,0,1), viewDir)));//层数
    float layerDepth = 1 / layerNum;//层深
    float2 deltaTexCoords = 0;//层深对应偏移量
#if _OFFSETLIMIT //建议运用偏移量约束,不然视野方向越平行于平面偏移量过大,分层显着
    deltaTexCoords = viewDir.xy / layerNum * _HeightScale;
#else
    deltaTexCoords = viewDir.xy / viewDir.z / layerNum * _HeightScale;
#endif
    float2 currentTexCoords = uv;//当时层纹路坐标
    float currentDepthMapValue = tex2D(_HeightMap, currentTexCoords).w;//当时纹路坐标采样成果
    float currentLayerDepth = 0;//当时层深度
    // unable to unroll loop, loop does not appear to terminate in a timely manner
    // 上面这个过错是在循环内运用tex2D导致的,需求加上unroll来约束循环次数或许改用tex2Dlod
    // [unroll(100)]
    while(currentLayerDepth < currentDepthMapValue)
    {
        currentTexCoords -= deltaTexCoords;
        // currentDepthMapValue = tex2D(_HeightMap, currentTexCoords).r;
        currentDepthMapValue = tex2Dlod(_HeightMap, float4(currentTexCoords, 0, 0)).r;
        currentLayerDepth += layerDepth;
    }
    return currentTexCoords;
}

现在的作用就几近实在了:

实时渲染常用纹理技术总结:视差映射

右图为峻峭视差映射,现在最小层数为10,最大层数为30

可是当咱们以越平行与外表的视点去看时,即使层数会随视角添加,但仍有很显着的分层现象:

实时渲染常用纹理技术总结:视差映射

实时渲染常用纹理技术总结:视差映射

最简略的办法便是持续添加层数,但这势必会大大影响功能(事实上,现在现已很严重了)。有些旨在修复这个问题的办法:不必低于外表的第一个层,而是在相交前后的深度层之间(高于外表的最终一个层和低于外表的第一个层之间)进行插值找出更匹配的相交方位。两种最盛行的处理办法叫做浮雕视差映射(Relief Parallax Mapping)和视差遮挡映射(Parallax Occlusion Mapping),Relief Parallax Mapping更准确一些,可是比Parallax Occlusion Mapping功能开销更多,咱们来看看这两种方案。

3. 视差遮挡映射
以相交前后的深度层的高度贴图采样值与两层的深度值之间的间隔作为线性插值的权重,然后对前后两层对应的纹路坐标进行线性插值即可。如下图的H(T3)和H(T2),两个分别由蓝线、紫线、黄线的相似三角形,蓝线的长度即高度贴图采样值和对应层深度的间隔,这样咱们就能够依据相似三角形得到紫线之间的比例,直接能够对应到纹路坐标偏移成果(即Tp对应的偏移量,故愈加挨近相交点)。

实时渲染常用纹理技术总结:视差映射

// 峻峭视差映射的代码
//......
// get texture coordinates before collision (reverse operations)
float2 prevTexCoords = currentTexCoords + deltaTexCoords;
// get depth after and before collision for linear interpolation
float afterDepth  = currentDepthMapValue - currentLayerDepth;
float beforeDepth = tex2D(_HeightMap, prevTexCoords).r - currentLayerDepth + layerDepth;
// interpolation of texture coordinates
float weight = afterDepth / (afterDepth - beforeDepth);
float2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight);
return finalTexCoords;  

实时渲染常用纹理技术总结:视差映射

十分完美,没有分层现象

4. 浮雕视差映射
在说浮雕视差映射之前咱们先来看看浮雕映射:咱们不像峻峭视差映射那样分层,而是经过二分法在深度规模(0->1)之间寻觅最佳值:

实时渲染常用纹理技术总结:视差映射

二分法

如图,咱们取AB的中点1,用1替换掉B,再取1和A之间的中点2,用2替掉A,再取1和2的中点3,即咱们想要的视野和高度图的交点,这便是二分法的流程。可是在某些情况下,或许出现问题:

实时渲染常用纹理技术总结:视差映射

视野和高度图或许有多个交点

在图中视野方向,咱们运用二分法就会得到3,可是实践上3现已被遮挡了,咱们得到的应该是上面那个蓝点。这时咱们能够运用峻峭视差映射的成果:如下图,先经过峻峭视差映射找到低于外表的第一个层(3),再和A做二分查找,这便是为什么被称为浮雕视差映射。

实时渲染常用纹理技术总结:视差映射

可是仍然能优化,由于峻峭视差映射现已能得到相交前后的深度层了(高于外表的最终一个层和低于外表的第一个层,比方上图中2、3),那咱们直接在这两个深度层之间进行二分查找即可:经过代码足以理解,其实便是更细分了,所以比视差遮挡映射更准确。仍然有细微的分层,但根本看不见了。而且由于相邻两层之间深度差异便是层深,所以也不必像视差遮挡映射相同核算高于外表的最终一个方位,不过显然后者不需求再细分而是插值,所以功能要更好。

// 峻峭视差映射的代码
//......
// 二分查找
float2 halfDeltaTexCoords = deltaTexCoords / 2;
float halfLayerDepth = layerDepth / 2;
currentTexCoords += halfDeltaTexCoords;
currentLayerDepth += halfLayerDepth;
int numSearches = 5; // 5次根本上就最好了,再多也看不出来了
for(int i = 0; i < numSearches; i++)
{
halfDeltaTexCoords = halfDeltaTexCoords / 2;
halfLayerDepth = halfLayerDepth / 2;
currentDepthMapValue = tex2D(_HeightMap, currentTexCoords).r;
if(currentDepthMapValue > currentLayerDepth)
{
currentTexCoords -= halfDeltaTexCoords;
currentLayerDepth += halfLayerDepth;
}
else
{
currentTexCoords += halfDeltaTexCoords;
currentLayerDepth -= halfLayerDepth;
}
}
return currentTexCoords;

实时渲染常用纹理技术总结:视差映射

实时渲染常用纹理技术总结:视差映射

5. 参加暗影
最能体现遮挡的莫过于暗影了,而且也是十分必要的,现在咱们运用的砖墙由于偏移深度较小,所以没有自遮挡的暗影看上去也很好,可是参加暗影后作用要更棒(当然更适用于偏移深度较大的情况):

实时渲染常用纹理技术总结:视差映射

为了让暗影更显着我加大了高度/深度缩放以及暗影的强度

做暗影的思路更简略了,咱们能够运用视差遮挡映射的成果,反过来向上找相交点,假如有则意味着被遮挡了,而且暗影的强度能够依据相交点个数决议,由于越深越简单被遮挡,相交点个数越多,暗影就越强,这样能够做出明暗滑润过渡的暗影。

// 输入的initialUV和initialHeight均为视差遮挡映射的成果
float ParallaxShadow(float3 lightDir, float2 initialUV, float initialHeight)
{
float shadowMultiplier = 1; //默许没有暗影
if(dot(float3(0, 0, 1), lightDir) > 0) //Lambert
{
                //依据光线方向决议层数(道理和视野方向相同)
float numLayers = lerp(_MaxLayerNum, _MinLayerNum, abs(dot(float3(0, 0, 1), lightDir)));
float layerHeight = 1 / numLayers; //层深
float2 texStep = 0; //层深对应偏移量
        #if _OFFSETLIMIT
        texStep = _HeightScale * lightDir.xy / numLayers;
        #else
                texStep = _HeightScale * lightDir.xy / lightDir.z / numLayers;
        #endif
                // 持续向上找是否还有相交点
float currentLayerHeight = initialHeight - layerHeight; //当时相交点前的最终层深
float2 currentTexCoords = initialUV + texStep;
float heightFromTexture = tex2D(_HeightMap, currentTexCoords).r;
                float numSamplesUnderSurface = 0; //核算被遮挡的层数
while(currentLayerHeight > 0) //直到到达外表
{
if(heightFromTexture <= currentLayerHeight) //采样成果小于当时层深则有交点
numSamplesUnderSurface += 1; 
currentLayerHeight -= layerHeight;
currentTexCoords += texStep;
heightFromTexture = tex2Dlod(_HeightMap, float4(currentTexCoords, 0, 0)).r;
}
shadowMultiplier = 1 - numSamplesUnderSurface / numLayers; //依据被遮挡的层数来决议暗影强度
}
return shadowMultiplier;
}

实时渲染常用纹理技术总结:视差映射

可是现在的暗影偏硬,且有分层作用

软暗影的做法:优化都在注释中,能够和上面的代码比照。重点是不依据相交层数决议暗影的强度!!!

// 输入的initialUV和initialHeight均为视差遮挡映射的成果
float ParallaxShadow(float3 lightDir, float2 initialUV, float initialHeight)
{
    float shadowMultiplier = 0;
    if (dot(float3(0, 0, 1), lightDir) > 0) //只算正对阳光的面
    {
        // 依据光线方向决议层数(道理和视野方向相同)
float numLayers = lerp(_MaxLayerNum, _MinLayerNum, abs(dot(float3(0, 0, 1), lightDir)));
float layerHeight = initialHeight / numLayers; //从当时点开端核算层深(没必要以整个规模)
        float2 texStep = 0; //层深对应偏移量
    #if _OFFSETLIMIT
texStep = _HeightScale * lightDir.xy / numLayers;
    #else
        texStep = _HeightScale * lightDir.xy / lightDir.z / numLayers;
    #endif
        // 持续向上找是否有相交点
float currentLayerHeight = initialHeight - layerHeight; //当时相交点前的最终层深
float2 currentTexCoords = initialUV + texStep;
float heightFromTexture = tex2D(_HeightMap, currentTexCoords).r;
int stepIndex = 1; //向上查找次数
        float numSamplesUnderSurface = 0; //核算被遮挡的层数
while(currentLayerHeight > 0) //直到到达外表
{
    if(heightFromTexture < currentLayerHeight) //采样成果小于当时层深则有交点
            {
numSamplesUnderSurface += 1;              
                float atten = (1 - stepIndex / numLayers); //暗影的衰减值:越挨近顶部(或许说浅处),暗影强度越小
                // 以当时层深到高度贴图采样值的间隔作为暗影的强度并乘以暗影的衰减值
float newShadowMultiplier = (currentLayerHeight - heightFromTexture) * atten;
shadowMultiplier = max(shadowMultiplier, newShadowMultiplier);
    }
    stepIndex += 1;
    currentLayerHeight -= layerHeight;
    currentTexCoords += texStep;
    heightFromTexture = tex2Dlod(_HeightMap, float4(currentTexCoords, 0, 0)).r;
}
if(numSamplesUnderSurface < 1) //没有交点,则不在暗影区域
    shadowMultiplier = 1;
else 
    shadowMultiplier = 1 - shadowMultiplier;
    }
    return shadowMultiplier;
}

实时渲染常用纹理技术总结:视差映射

十分完美的软暗影

四、制造办法

1. 程序纹路的灰度值
能够运用很多程序生成纹路的技能(噪声、SDF、核算几何…….)

2. 经过明暗联络核算
咱们运用的色彩贴图(Albedo/Diffuse)中常常包含了很丰富的明暗细节,如Photo Shop>滤镜>3D>生成凹凸(高度)图,能够运用的一个信息是自带的明暗联络(比方下图墙面的缝隙是黑的)

实时渲染常用纹理技术总结:视差映射

实时渲染常用纹理技术总结:视差映射

3. 手绘+运用图画处理

4. 用高精度模型生成
咱们前面说了在游戏开发中常见的做法是在建模软件中制造高精度的模型,调好作用后简化成低精度网格导入引擎运用,而高精度模型本身就运用了很多极点体现细节,能够运用雕琢东西做出,能够展UV后把修改量写入一张贴图作为高度贴图导出。

五、应用

视差映射是提升场景细节十分好的技能,能够寻求难以置信的作用,可是运用的时分仍是要考虑到它会带来一点不自然,所以大多数时分视差映射用在地上和墙面外表,这些情况下查明外表的概括并不简单,一起调查方向往往趋向于垂直于外表。这样视差映射的不自然也就很难能被留意到了。

1. 墙面:PS生成

实时渲染常用纹理技术总结:视差映射

色彩贴图-法线贴图-视差映射

实时渲染常用纹理技术总结:视差映射

2. 地势裂缝:手绘

基于视差映射的地势裂缝

3. 动态云雾模仿:运用噪声

运用视差映射的动态云雾

4. 地势上的轨道:动态生成

实时渲染常用纹理技术总结:视差映射


这是侑虎科技第1403篇文章,感谢作者别看着我笑了供稿。欢迎转发共享,未经作者授权请勿转载。假如您有任何独特的见地或许发现也欢迎联络咱们,一起讨论。(QQ群:465082844)

作者主页:www.zhihu.com/people/LuoX…

再次感谢别看着我笑了的共享,假如您有任何独特的见地或许发现也欢迎联络咱们,一起讨论。(QQ群:465082844)