
视差映射
视差贴图纹理化技术在效果上与
法线贴图有些相似,但基于不同的原理。 相似之处在于,与“法线贴图”一样,此技术显着增加了具有所应用纹理的表面的视觉复杂性和细节,同时对表面上存在高低差产生了似是而非的错觉。 视差贴图可与法线贴图一起很好地工作,以产生非常可靠的结果:所描述的技术比法线贴图更好地传达了浮雕效果,而法线贴图对动态照明的真实模拟进行了补充。 视差贴图几乎不能被认为是与照明模拟方法直接相关的技术,但是尽管如此,我还是选择本节来进行研究,因为该方法是法线贴图思想的逻辑发展。 我还注意到,为了解析本文,需要对法线贴图算法有一个很好的理解,尤其是切线空间或
切线空间的概念。
视差贴图属于“
位移贴图”或“压纹纹理化”技术家族,这些技术基于存储在特殊纹理贴图中的值来
偏移几何图形
的顶点。 例如,想象一下由一千个顶点组成的平面。 可以根据从纹理读取的值来移动每个值,该值代表给定点处平面的高度。 包含每个纹理像素中的高度值的这种纹理称为
高度图 。 根据砖砌表面的几何特性获得的这种地图的示例如下图所示:
通过从该贴图采样并根据高度值移动每个顶点,可以从理想平面获得一个凸面,该凸面重复原始曲面的几何参数。 因此,以一个具有足够数量的顶点的平面并从示例中应用高度图,您可以获得以下结果:
所描述的方法是简单且易于实现的,但是在处理的对象中需要高密度的顶点,否则,移位的结果将太粗糙。 而且,如果我们在每个平面上开始释放一千个或更多的顶点,那么很快我们将根本没有时间渲染我们需要的所有东西。 也许有一种算法可以让您定性地模拟天真的“置换贴图”算法的质量,而又不需要这种几何成本? 如果您站着,请坐下,因为在上图中,实际上只有六个顶点(两个三角形)! 由于使用了视差贴图技术,砖砌的浮雕被完美地模仿了,这是一种浮雕纹理化技术,不需要很多顶点就能忠实地传达表面浮雕,但是像法线贴图一样,它使用原始方法来欺骗观察者的眼睛。
该实现的主要思想是基于凝视的方向和高度图数据扭曲当前片段的纹理坐标,从而产生一种错觉,即该片段属于比实际高或低的表面部分。 为了更好地理解该原理,请看一下我们用砖头的示例图:
此处,粗红线代表高度图的值,反映了模拟砌体表面的几何特征。 向量
colororange barV 表示从表面到观察者的方向(
viewDir )。 如果飞机上确实有浮雕,那么观察者会在表面上看到一个点
\颜色蓝色B 。 但是,实际上我们有一个理想的平面,并且光线在视线方向上与点相交
colorgreenA 那是显而易见的。 视差映射任务可在一个点上移动纹理坐标
colorgreenA 使它们变得与对应于该点的坐标相同
\颜色蓝色B 。 进一步针对当前片段(对应点
colorgreenA )我们使用获得的点坐标
\颜色蓝色B 纹理采样对于所有对象都是必需的,这会造成观察者看到一个点的错觉
\颜色蓝色B 。
主要困难在于如何计算点的纹理坐标
\颜色蓝色B 在这一点上
colorgreenA 。 视差贴图提供了一种简单的缩放方法,它可以简单地缩放表面上的方向向量
colororange barV 通过片段的高度向观察者显示
colorgreenA 。 即 只是改变长度
colororange barV 使其与高度图上的样本大小匹配
\颜色绿色H(A) 对应于片段
colorgreenA 。 下图显示了缩放结果-向量
colorbrown barP :
接下来,结果向量
colorbrown barP 根据平面本身的坐标系分解为多个分量,这些分量用作原始纹理坐标的偏移量。 而且,由于向量
colorbrown barP 使用高度图的值计算,则高度值与当前片段对应的越多,偏移量越大。
在某些情况下,这种简单的技术可以提供良好的结果,但它仍然是对点位置的非常粗略的估计
\颜色蓝色B 。 如果高度图包含值急剧变化的区域,则位移的结果将变得不正确:很有可能是矢量
colorbrown barP 即使接近也不会落入该点附近
\颜色蓝色B :
基于以上所述,另一个问题仍然存在:如何确定如何正确投影向量
colorbrown barP 到任意定向的表面以获取偏移纹理坐标的分量? 最好在某个坐标系中进行计算,其中矢量展开
colorbrown barP 在分量
x和
y上,
x总是与纹理坐标系的基础相对应。 如果您仔细地完成了有关
法线贴图的课程,那么您已经猜到我们正在谈论切线空间中的计算。
将向量从表面转移到观察者
colororange barV 进入切线空间,我们得到一个修改的向量
colorbrown barP ,其成分分解将始终根据给定曲面的切线和双切线向量进行。 由于切线和双切线始终与曲面的纹理坐标系的轴对齐,因此无论曲面的方向如何,您都可以安全地使用向量的
x和
y分量
colorbrown barP 作为纹理坐标的偏移量。
但是,该理论已足够,在袖手旁观之后,我们转向了立即实施。
视差映射
为了实现,我们将使用一个简单的平面,并为其计算切线和偏置切线-我们已经可以从有关法线贴图的课程中做到这一点。 我们为纹理平面分配了多个平面:那就是
diffuse ,
normals和
offsets ,它们中的每一个都可以在适当的链接上找到。 在本课程中,我们还将使用法线贴图,因为视差贴图会产生表面形貌的错觉,如果光照不会根据形貌而变化,则很容易破坏这种错觉。 由于法线贴图通常是基于高程图创建的,因此考虑到地形,它们的结合使用可确保正确连接照明。
您可能已经注意到,上面链接中显示的位移图实际上是课程开始时显示的图的逆向。 通常仅使用这样的地图来执行视差映射,该地图与高度图-
深度图相反。 之所以发生这种情况,是因为在平面上模仿凹槽比模仿高程要容易一些。 根据此更改,视差映射工作方案也会更改:
再次看到熟悉的点
colorgreenA 和
\颜色蓝色B 但是,这次向量
colorbrown barP 通过减去向量获得
colororange barV 从一个点的纹理坐标
colorgreenA 。 可以通过从单位中减去深度样本或在任何图像编辑器中反转纹理颜色来简单地获得深度而不是高度。
视差映射在片段着色器中实现,因为三角形内每个片段的高程数据都不同。 片段着色器代码将需要计算从片段到观察者的向量
colororange barV ,因此有必要向他传达片段和观察者在切线空间中的位置。 根据有关法线贴图的课程结果,我们仍然可以使用顶点着色器,它将已减小的所有矢量转移到切线空间,我们将使用它:
#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aNormal; layout (location = 2) in vec2 aTexCoords; layout (location = 3) in vec3 aTangent; layout (location = 4) in vec3 aBitangent; out VS_OUT { vec3 FragPos; vec2 TexCoords; vec3 TangentLightPos; vec3 TangentViewPos; vec3 TangentFragPos; } vs_out; uniform mat4 projection; uniform mat4 view; uniform mat4 model; uniform vec3 lightPos; uniform vec3 viewPos; void main() { gl_Position = projection * view * model * vec4(aPos, 1.0); vs_out.FragPos = vec3(model * vec4(aPos, 1.0)); vs_out.TexCoords = aTexCoords; vec3 T = normalize(mat3(model) * aTangent); vec3 B = normalize(mat3(model) * aBitangent); vec3 N = normalize(mat3(model) * aNormal); mat3 TBN = transpose(mat3(T, B, N)); vs_out.TangentLightPos = TBN * lightPos; vs_out.TangentViewPos = TBN * viewPos; vs_out.TangentFragPos = TBN * vs_out.FragPos; }
重要的事情中,我只注意到,特别是对于视差映射的需要,有必要将
aPos和
viewPos观察者在切线空间中的位置转移到片段着色器。
在着色器内部,我们实现了视差映射算法,如下所示:
#version 330 core out vec4 FragColor; in VS_OUT { vec3 FragPos; vec2 TexCoords; vec3 TangentLightPos; vec3 TangentViewPos; vec3 TangentFragPos; } fs_in; uniform sampler2D diffuseMap; uniform sampler2D normalMap; uniform sampler2D depthMap; uniform float height_scale; vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir); void main() { vec3 viewDir = normalize(fs_in.TangentViewPos - fs_in.TangentFragPos);
我们宣布了ParallaxMapping函数,该函数获取片段的纹理坐标以及片段到观察者的向量
colororange barV 在切线空间。 该函数的结果是偏移纹理坐标,该坐标已用于漫反射纹理和法线贴图的样本。 结果,像素的漫反射颜色及其法线正确地对应于平面的已更改“几何形状”。
ParallaxMapping函数内部隐藏了什么?
vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir) { float height = texture(depthMap, texCoords).r; vec2 p = viewDir.xy / viewDir.z * (height * height_scale); return texCoords - p; }
这个相对简单的功能是该方法的字面实现,我们在上面讨论了其要点。 选取TexCoords的初始纹理坐标,并借助该坐标
选择高度(或深度)
\颜色绿色H(A) 从
depthMap获取当前片段。 计算向量
colorbrown barP viewDir向量在切线空间中获取,其对
x和
y分量对除以
z分量,并按读取的
height偏移值缩放结果。 由于位移效果通常太强,
因此还引入了height_scale
制服以提供对视差贴图效果的严重性的附加控制。 为了得到结果,我们减去结果向量
colorbrown barP 从原始纹理坐标开始。
我们将
处理将
viewDir.xy分成
viewDir.z的时刻。 由于矢量
viewDir已归一化,其分量
z位于间隔[0,1]中。 当向量几乎平行于分量的表面时,
z接近零,并且除法运算返回向量
colorbrown barP 比
viewDir接近垂直于表面的垂直线要长得多。 换句话说,我们缩放向量
colorbrown barP 因此当您以一定角度看表面时,它会增加-在这种情况下,您可以获得更真实的结果。
一些开发人员更喜欢通过除以
viewDir.z来消除缩放,因为在某些情况下,从某个角度查看时,此方法会产生错误的结果。 该技术的这种修改称为
带偏移限制的视差映射 。 在大多数情况下,方法选项的选择仍然取决于个人喜好-例如,我更忠诚于通常的视差映射算法的结果。
最终使用更改后的纹理坐标,从扩散贴图和法线贴图中进行选择,这给了我们一个很好的表面变形效果(这里的
height_scale参数选择为接近0.1):
在图像中,您可以比较法线贴图和视差贴图技术的效果。 由于“视差贴图”模拟了表面不规则性,因此根据眼睛的方向,这种技术可能会出现积木重叠的情况。
沿着纹理平面的边界也可以看到奇怪的伪像。 之所以出现它们,是因为视差映射算法移动的纹理坐标可能落在单位间隔之外,并且根据
环绕模式 ,会导致不良结果。 消除此类伪像的一种简单方法是简单地丢弃纹理坐标在单位间隔之外的所有片段:
texCoords = ParallaxMapping(fs_in.TexCoords, viewDir); if(texCoords.x > 1.0 || texCoords.y > 1.0 || texCoords.x < 0.0 || texCoords.y < 0.0) discard;
结果,所有具有偏移的纹理坐标且落入间隔[0,1]的片段都将被丢弃,并且视差映射操作的结果在视觉上将变得可以接受。 显然,这种剔除方法不是通用的,可能不适用于某些表面或纹理情况。 但是以飞机为例,它可以完美工作,并有助于增强改变飞机浮雕的效果:
样本来源在
这里 。
它看起来不错,并且该方法的性能非常好-仅从纹理中提取了一个样本! 但是该方法的简单性有很多缺点:以一定角度看飞机时,浮雕效果很容易被破坏(法线贴图也是如此),或者高度图中某些部分的值发生急剧变化:
幻觉破坏的原因在于,该算法是真实位移映射的非常粗略的近似。 但是,还有其他一些技巧可以帮助我们,即使在倾斜角度或使用高度变化较大的高度贴图时,也可以使我们获得几乎完美的效果。 例如,我们可以使用高度图中的几个样本来找到最接近该点的点
\颜色蓝色B 。
陡峭视差映射
陡峭视差映射技术是经典视差映射的逻辑发展:在算法中使用了相同的方法,但是采用了单项选择,而不是单项选择-为了更好地近似
colorbrown barP 用于计算点
\颜色蓝色B 。 由于有这些额外的样本,即使在与表面成锐角的情况下,算法的结果在视觉上也更加合理。
陡峭PM方法的基础是采用一定深度范围,并将其划分为相等大小的层。 接下来,我们遍历图层,同时在矢量方向上移动原始纹理坐标
colorbrown barP 然后从深度图制作样本,直到样本深度小于当前图层的深度时停止。 查看图表:
如您所见,我们从上到下遍历各层,对于每一层,我们将其深度与深度图中的值进行比较。 如果图层深度小于深度图的值,则表示矢量
colorbrown barP 对应于该层位于表面上方。 重复此过程,直到图层深度大于深度图中的选择为止:此时,矢量
colorbrown barP 指向模拟表面地形下方的点。
该示例显示了从第二层深度图中的选择(
D(2)=0.73 )相对于等于0.4的第二层深度仍然“更深”,这意味着搜索过程将继续。 在下一遍中,最终0.6的层深度最终证明是在深度图的样本值“之上”(
D(3)=0.37 ) 从这里我们得出结论,向量
colorbrown barP 对于变形的表面几何形状,第三层获得的最可靠位置是最可靠的位置。 您可以使用纹理坐标
T3 从向量派生
colorbrown barP ,以偏移当前片段的纹理坐标。 显然,该方法的精度随层数的增加而增加。
实现中的更改将仅影响ParallaxMapping函数,因为它已经包含了算法起作用所需的所有变量:
vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir) {
首先,我们进行初始化:设置层数,计算每层的深度,最后找到沿矢量方向的纹理坐标位移的大小
colorbrown barP ,这将需要在每一层上转移。
接下来是从顶部开始的各层的通道,直到找到深度图中位于当前层的深度值“上方”的选择:
在此代码中,我们遍历所有深度层并移动原始纹理坐标,直到从深度图中进行的选择小于当前层的深度为止。 通过基于矢量从增量的原始纹理坐标中减去来执行偏移
colorbrown barP 。 该算法的结果是纹理坐标的偏移矢量,其定义比经典的视差映射要精确得多。
使用大约10个样本,即使从某个角度观看,砖砌示例的外观也更加逼真。 但最重要的是,陡峭PM的优点在带有深度图的表面上可见,深度图的值发生了急剧变化。 例如,关于这个木制玩具,前面已经演示过:
如果您稍微分析一下视差映射技术的功能,则可以进一步改进该算法。 如果以近似法线的角度看待表面,则无需强烈移动纹理坐标,而以某个角度看时,该变化趋于最大(请在两种情况下仔细地想象观察方向)。 如果根据注视的方向参数化样本数量,则可以节省很多不需要额外样本的地方:
const float minLayers = 8.0; const float maxLayers = 32.0; float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir)));
向量
viewDir与正半轴Z的标量积的结果用于确定间隔[
minSamples ,
maxSamples ]中的
层数 ,即 注视方向确定效果所需的迭代次数(在切线空间中,正半轴Z垂直于表面)。 如果我们与表面平行,则效果将使用所有32层。
修改后的源代码在
这里 。 我还建议下载木制玩具的纹理:
漫射 ,
法线 贴图,深度图 。
并非没有方法和缺点。 由于采样数都是相同的有限值,因此不可避免会出现混叠效果,这将导致各层之间的过渡引人注目:
您可以通过增加使用的样本数量来降低伪影的严重性,但是它很快就会吞噬所有可用的视频处理器性能。 该方法有几项补充,其结果不是返回出现在表面虚拟浮雕下的第一个点,而是返回两个最近层的内插值,这使我们可以进一步阐明该点的位置
\颜色蓝色B 。
在这些方法中,最常用的两种是:
救济视差映射和
视差遮挡映射 ,其中救济PM提供最可靠的结果,但与视差遮挡映射相比,对性能的要求也更高。 由于视差遮挡贴图在质量上仍与救济PM相当,同时工作速度更快,因此他们更喜欢经常使用它。 接下来,将考虑视差遮挡映射的实现。
视差遮挡映射
视差遮挡贴图方法的所有基本原理都与陡峭PM相同,但是该方法不是使用第一层的纹理坐标(在第一层找到具有虚浮雕的相交点),而是在两层之间进行线性插值:相交之后和之前的层。 线性插值的加权系数基于当前浮雕深度与所考虑的两层深度之比。 看一下该图,可以更好地了解所有工作原理:
如您所见,所有内容都与Steep PM非常相似,仅增加了一个步骤,即对与交点相邻的两个深度层的纹理坐标进行插值。 当然,这种方法只是一个近似值,但比Steep PM更精确。
视差遮挡映射代码是Steep PM代码的补充,并且不太复杂:
[...]
在找到位于具有假想浮雕的交点之后的图层时,我们还确定了位于交点之前的图层的纹理坐标。 接下来,我们找到虚拟浮雕深度相对于所考虑的两个层的深度的位移,并将它们的比率用作与所考虑的两个层相对应的纹理坐标的进一步线性插值的权重系数。 插值结果由函数返回,以备将来使用。
视差遮挡贴图提供了令人惊讶的视觉上可靠的结果,尽管它具有小的缺陷和混叠伪像。 但是,为了牺牲速度和质量,它们并不重要,只有在靠近相机或以非常锐利的视角仔细观察表面时才会出现。
这里是一个示例代码。
视差贴图确实是一种出色的技术,它可以使您极大地增加场景的视觉细节,但是,当然,它具有伪影形式的缺点,在项目中实施该技巧时应记住。 在大多数情况下,视差贴图用于墙壁或地板之类的平坦表面上,因为要确定整个物体的轮廓并不容易,并且表面的视角通常接近垂直方向。 在这种情况下,以增加的表面细节为背景,几乎看不见视差映射缺陷。
翻译人员的奖励:
救济视差映射
由于作者提到了两种澄清Steep PM结果的方法,为了完整性,我将描述第二种方法。
像视差遮挡贴图一样,此处使用Steep PM执行的结果,即 我们知道两层之间的深度位于向量相交的真实点
colororange barV 浮雕,以及相应的纹理坐标
T2 和
T3 。 这种方法中交点估计的改进是由于使用了二进制搜索。
优化算法步骤:
- 执行陡峭的PM计算并获取纹理坐标 T2 和 T3 -在此间隔内是向量的交点 Ç ö 升ö ř 克ř Ë Ë Ñ b 一个[R V 表面形貌。 真实的交叉点标有红色十字。
- 将纹理坐标的偏移量和深度层的高度分为两个当前值。
- 从点移动纹理坐标 Ť 3 在与向量相反的方向 Ç ö 升ö ř 克ř Ë Ë Ñ b 一个[R V 由位移量决定。 通过当前图层大小值减小图层深度。
- 直接二进制搜索。 重复指定的迭代次数:
- 从深度图中选择。 将当前纹理偏移和深度层大小分成两个当前值。
- 如果样本大小大于当前图层深度,则将图层深度增加当前图层大小,并沿矢量更改纹理坐标 Ç ö 升ö ř 克ř Ë Ë Ñ b 一个[R V 到当前偏移量。
- 如果样本大小小于当前图层深度,则将图层深度减小当前图层大小,然后沿逆矢量更改纹理坐标 Ç ö 升ö ř 克ř Ë Ë Ñ b 一个[R V 到当前偏移量。
- 获得的最后一个纹理坐标是Relief PM的结果。
该图显示找到点之后
Ť 2 和
Ť 3 我们将图层的大小和纹理坐标的偏移量的大小减半,从而得到二元搜索的第一个迭代点(1)。 由于结果证明其中的样本大小大于当前图层的深度,因此我们再次将参数减半并沿
Ç ö 升ö ř 克ř Ë Ë Ñ b 一个[R V 通过纹理坐标获取点(2)
Ť p 这将是Steep PM对二进制搜索进行两次迭代的结果。
着色器代码:
自遮蔽
关于将选定光源的阴影添加到计算算法中的少量补充。我决定添加,因为从技术上讲,计算方法与上述方法相同,但是效果仍然很有趣并且增加了细节。实际上,应用了相同的Steep PM,但是搜索并没有沿着视线深入到模拟的表面,而是从表面沿着矢量到光源。ˉ 升 。
该矢量也被传递到切线空间,并用于确定纹理坐标的位移量。在该方法的输出处,在间隔[0,1]中获得了材质照明系数,该材质照明系数用于在照明计算中调制漫反射和反射镜分量。要定义具有锐利边缘的阴影,只需沿着矢量走ˉ 大号只要没有在地面下的一个点。一旦找到这样的点,我们将照度系数设为0。如果到达零深度而未遇到位于表面下方的点,则我们将照度系数等于1。要确定具有软边的阴影,必须检查矢量上的多个点ˉ 大号和表面下。阴影因子等于当前层的深度与深度图的深度之差。还应考虑以等于(1.0-stepIndex / numberOfSteps)的权重系数的形式从相关片段中删除下一个点。在每个步骤中,部分照明系数确定为:P S F i = (l a y e r h e i g h t i - h e i g h t F r o m t e x t u r e i)∗ (1.0 - iÑ ü 米小号吨ë p 小号)
最终结果是所有部分的最大照明因子:S F = m a x (P S F i)
方法的方案:此示例中三个迭代方法的进度:- 我们将总光照因子初始化为零。
- 沿着向量走一步 ˉ 大号,去体验点^ h 一 。 点深度明显小于从地图中选择的深度。 H (T L 1) -它在表面之下。在这里,我们进行了第一次检查,并记住了检查的总数,我们找到并保存了第一个局部光照因子:(1.0-1.0 / 3.0)。
- 沿着向量走一步 ˉ 大号,去体验点高b 。 点深度明显小于从地图中选择的深度。 H (T L 2) -在表面之下。第二次检查和第二部分系数:(1.0-2.0 / 3.0)。
- 我们沿着向量再迈一步,到达最小深度0。停止运动。
- 结果的定义:如果在表面下未找到任何点,则我们返回等于1的系数(无阴影)。否则,所得系数将成为计算出的部分系数的最大值。为了在照明计算中使用,我们从单位中减去该值。
着色器代码示例:
所得系数用于调制示例中使用的Blinn-Fong照明模型的结果: [...]
一张拼贴画中所有方法的比较,容量为3MB。还有视频比较:附加材料
PS:我们有一个电报会议,以协调转账。如果您有强烈的帮助翻译的愿望,欢迎您!