视差贴图

1. 视差贴图 Parallax Mapping

1.1 视差贴图 理论介绍

视差贴图(Parallax Mapping)使用和法线贴图差不多的技术,但是基于不同的原理。和法线贴图一样,视差贴图能够极大提升表面细节,使之具有深度感。利用了视觉上的错觉,使得视差贴图对深度具有更好的表达,与法线贴图一起使用能够产生难以置信的效果。

视差贴图属于位移贴图(Displacement Mapping) 技术的一种。

1.1.1 位移贴图 — Displacement Mapping

位移贴图(Displacement Mapping)是依据存储在纹理中的几何信息来对顶点进行位移或偏移。其中一种实现方式是使用具有大约1000个顶点的平面,并根据纹理中的值(该值为高度值)来位移每个顶点。这种每个纹素就是一个高度值的纹理图叫做高度图。一张简单的砖块表面的高度贴图如下所示:

mark

整个平面上的每个顶点都根据从高度贴图采样出来的高度值进行位移,根据材质的几何属性使得平坦的平面变换成凹凸不平的表面。例如一个平坦的平面利用上面的高度贴图进行变换能得到以下结果:

mark

位移顶点有一个问题就是平面必须由很多顶点组成才能获得具有真实感的效果,否则看起来效果并不会很好。一个平坦的表面上有1000个顶点计算量太大了。我们能否不用这么多的顶点就能取得相似的效果呢?事实上,上面的表面就是用6个顶点渲染出来的(两个三角形)。上面的那个表面使用视差贴图技术渲染,该技术(位移贴图技术的一种)不需要额外的顶点数据来表达深度,它像法线贴图一样采用了一种聪明的手段来欺骗用户的眼睛。

1.1.2 法线贴图 – Normal Mapping

法线贴图的原理和实现代码具体看笔记【法线贴图】,这里主要讨论法线贴图存在的问题。法线贴图并不是把模型的面数提高了,而是使用法线贴图中的法线来计算光照,通过明暗效果作假,让观察者误以为模型有凹凸。法线贴图只能在明暗效果上作假(模拟凹凸),无法控制表面的凹凸程度。即使我们使用图像软件强制调出一个凹凸非常明显的法线贴图,通过仔细观察,会法线效果也是有问题的。

mark

上面的是一张用了法线贴图的地面,在红色圆圈的地方是有问题的。我们使用一张平面图来分析下。

mark

这是一张凸起砖块的截面图,绿色箭头表示视线的方向,白色线条表示砖块的横截面。按照常识来看,我们能看到砖块的最远的一点是蓝色点,因为蓝色点后面(红色线条部分)的砖块由于高度较低,被前面挡住了。但是从上面那张使用了法线贴图的地面效果图上可以看到,蓝色点后的砖块并没有被挡住,甚至能够看到黄色点的位置,这种效果显然是不正确的。而这是法线贴图无法避免的问题,因为上文已经说过了,法线贴图只能模拟明暗,也就是说最多只能将红色线条部分变暗(以此来模拟背光)。这就是视差贴图可以解决的一个问题,它可以让背面被遮挡住的部分完全不显示出来,除此之外还能在一定范围内调整砖块凹凸的程度。视差贴图也只是模拟作假,并没有真的改变模型表面,下面就开始分析视差贴图吧。

1.1.3 视差贴图 — Parallax Mapping

视差贴图(Parallax Mapping)技术的思想是修改纹理坐标使一个fragment的表面看起来比实际的更高或者更低,所有这些都是根据观察方向和高度贴图。 为了理解它如何工作,看看下面砖块表面的图片:

mark

这里粗糙的红线代表高度图中的数值的立体表达,向量 V⃗  V → 代表观察方向。如果平面进行实际位移,观察者会在点B看到表面。然而我们的平面实际上没有进行位移,观察方向将会在点A与平面接触。视差贴图的目的是,在A位置上的fragment不再使用点A的纹理坐标,而是使用点B的纹理坐标。随后我们用点B的纹理坐标采样,观察者就像看到了点B一样。

这个技巧就是描述如何从点A处得到点B处的纹理坐标。视差贴图尝试通过对从fragment到观察者的方向向量 V⃗  V → 进行缩放的方式解决这个问题,缩放的大小是A处fragment的高度。所以我们将 V⃗  V → 的长度缩放为高度图在点A处 H(A) H ( A ) 采样得来的值。下图展示了经缩放得到的向量 P⃗  P →

mark

我们随后选出 P⃗  P → 以及这个向量与平面对齐的坐标作为纹理坐标的偏移量。这种方法是可行的,因为向量 P⃗  P → 是使用从高度图得到的高度值计算出来的,所以一个fragment的高度越高位移的量越大。

这个技巧在大多数情况下都没问题,但点B是粗略估算得到的。当表面的高度变化很快的时候,看起来就不会那么真实,因为向量$P$最终不会和B接近,就像下图这样:

mark

视差贴图的另一个问题是,当表面被任意旋转(==我理解的是物体发生旋转,与法线贴图需要变换到切线空间的情况一样==)以后,很难知道应该从 P⃗  P → 获取哪个空间的坐标。我们在视差贴图中使用切线空间,在切线空间中,向量 P⃗  P → 的x和y元素总是与纹理表面对齐。如果你看了法线贴图教程,我们还是在切线空间中实现视差贴图。

将向量 V⃗  V → 变换到切线空间中,经变换的向量 P⃗  P → 中x和y元素将与表面的切线和双切线向量对齐。由于切线和双切线向量与表面纹理坐标的方向相同,我们可以用 P⃗  P → 的x和y元素作为纹理坐标的偏移量,这样就不用考虑表面的方向了。

视差贴图的理论已经介绍完了,下面是视差贴图的代码实现部分。

1.2 视差贴图 代码实现

与法线贴图教程一样,在将模型扔进GPU渲染之前,我们先计算模型顶点的切向向量(tangent)和双切线向量(bitangent)。在这一次示例中,需要使用到diffuse纹理、法线贴图以及位移贴图(displacement map),其中需要一起使用法线贴图和视差贴图。因为视差贴图是通过位移表面来产生错觉,而当光照与之不匹配时,错觉就不存在了。而又因为法线贴图通常由高度图生成,同时使用法线贴图和视差贴图能够保证光照能和位移相匹配。(这里的不匹配指的是如果光照计算仍然使用片元原来的纹理坐标去采样法线贴图的话,光照计算的结果和diffuse颜色不匹配,这里需要好好理解,为了使得匹配,法线贴图的采样应该使用位移后的纹理坐标,这样光照就能够与diffuse颜色匹配。

你可能已经注意到,上面的那个位移贴图和教程一开始的那个高度图相比是颜色是相反的。这是因为使用了高度图的反色(也叫深度贴图–depth map)去模拟深度比模拟高度更容易。下图反映了这个轻微的改变:

mark

我们再次获得A和B,但是这次我们用向量 V⃗  V → 减去点A的纹理坐标得到 P⃗  P → 。我们通过在着色器中用1.0减去从高度图中采样得到的值 来取得深度值,而不再是高度值,或者简单地在图片编辑软件中把这个纹理进行反色操作(inverse)。

视差贴图是在片元着色器(fragment)中实现的,因为三角形表面的所有位移效果都不同。在片元着色器中,我们需要计算fragment到观察者的方向向量 V⃗  V → ,所以我们需要在切线空间中的fragment位置和观察者位置。顶点着色器的代码与法线贴图中使用的顶点着色器相同。

顶点着色器代码如下:

#version 330 core
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 normal;
layout(location = 2) in vec2 texCoords;
layout(location = 3) in vec3 tangent;

out VS_OUT{
    vec3 FragPos;
    vec2 TexCoords;

    //normal mapping & parallax mapping
    vec3 TangentLightPos;
    vec3 TangentViewPos;
    vec3 TangentFragPos;
} vs_out;

uniform vec3 lightPos;
uniform vec3 viewPos;

uniform mat4 cameraSpaceMatrix;
uniform mat4 model;

void main() {
    gl_Position = cameraSpaceMatrix * model * vec4(position, 1.0f);
    vs_out.FragPos = vec3(model * vec4(position, 1.0f));
    vs_out.TexCoords = texCoords;

    //normal mapping & parallax mapping
    vec3 T = normalize(mat3(model) * tangent);
    vec3 N = normalize(mat3(model) * normal);
    T = normalize(T - dot(T, N)*N);
    vec3 B = cross(N, T);
    mat3 TBN = transpose(mat3(T, B, N));

    vs_out.TangentLightPos = TBN * lightPos;
    vs_out.TangentViewPos = TBN * viewPos;
    vs_out.TangentFragPos = TBN * vs_out.FragPos;
}


对于视差贴图来说,我们需要将顶点的位置position和在切线空间中观察者的位置viewPos发送到片元着色器中。

我们在片元着色器中实现视差贴图的逻辑,片元着色器代码看起来如下所示:

#version 330 core
out vec4 FragColor;

in VS_OUT{
    vec3 FragPos;
    vec2 TexCoords;

    //normal mapping & parallax mapping
    vec3 TangentLightPos;
    vec3 TangentViewPos;
    vec3 TangentFragPos;
}fs_in;

struct Material{
    sampler2D texture_diffuse1;
    sampler2D texture_normal1;
    sampler2D texture_height1;
};

uniform Material material;   

// parallax mapping
uniform float height_scale;

vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir);
vec3 lightingCalculation(vec3 lightDir, vec3 viewDir, vec3 color, vec3 normal);

void main() {
    vec3 lightPos = fs_in.TangentLightPos;
    vec3 viewPos = fs_in.TangentViewPos;
    vec3 FragPos = fs_in.TangentFragPos;

    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 lightDir = normalize(lightPos - FragPos);

    //offset texture coordinates with Parallax Mapping
    vec2 texCoords = ParallaxMapping(fs_in.TexCoords, viewDir);

    //then sample textures with new texture coords
    vec3 color = texture(material.texture_diffuse1, texCoords);
    vec3 normal = texture(material.texture_normal1, texCoords);
    normal = normalize(normal*2.0 - 1.0);

    //proceed with lighting code
    [...]
}

定义了函数ParallaxMapping(),fragment的纹理坐标和在切线空间中观察向量 V⃗  V → 作为输入。这个函数返回的是位移后的纹理坐标。然后我们使用这些位移后的纹理坐标对diffuse贴图和法线贴图进行采样。最后fragment的diffuse颜色和法线向量就正确对应于表面位移后的位置(这样就能够保证光照计算结果正确,因为光照计算使用的也是位移后的法线向量)。

以下是函数ParallaxMapping()的实现部分:

vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir){
    float height =  texture(material.texture_height1, texCoords).r;    
    vec2 p = viewDir.xy * (height * height_scale);
    p.y = -p.y;
    return texCoords - p;   
}

上述的函数实现就涵括了上面讨论的细节。在该函数中,使用片元原来的纹理坐标对深度图(depthMap)进行采样,得到该片元的深度值 H(A) H ( A ) 。然后我们需要计算出二维向量 P⃗  P → ,该向量通过变量viewDir的x、y部分除以z部分得到相应x、y,再用H(A)对x、y值进行缩放,这里求出的向量就是下图中的向量 AP A P → 。这里面还引入了一个uniform变量height_scale来进行额外的控制,因为如果没有一个缩放参数,视差的效果就会过于强烈。然后我们用原来的纹理坐标减去偏移向量 P⃗  P → 得到最终位移后的纹理坐标。

mark

我们知道向量viewDir是单位向量,那么viewDir.z在范围[0,1]之间。当向量viewDir几乎与表面平行时,即平行于表面去观察表面的时候,viewDir.z接近于0,并使得viewDir.x / viewDir.z的值 比当viewDir垂直于平面的时候要大得多,这也就使得当向量viewDir几乎与表面平行时,计算出来的偏移向量p也就很大,这样当前片元就会去使用较远的纹理坐标;当向量viewDir与表面垂直时,计算出来的偏移向量p就很小,当前片元使用附近的纹理坐标。当然上面都是相对于height值不变的情况下讨论的。

也有一些人在上述等式中不使用viewDir.z,因为普通视差贴图会在角上产生不想要的结果,而这个技术也被称为有偏移量限制的视差贴图(Parallax Mapping with Offset Limitting)。选择哪一个技术属于个人偏好问题,这里使用的是普通视差贴图。

==在示例代码中使用的是viewDir.xy * height * height_scale,没有除以viewDir.z是因为实验观察发现没有viewDir.z的时候显示效果更好看一些。还有一个bug就是需要将点p的y坐标取反,如上述代码所示,不然最终的显示效果是你从上往下看,是看到砖块的下面,从下往上看,是看到砖块的上面,与现实完全相反,取反后显示的结果与事实相符,至于原理在哪,感觉问题出在viewDir的y轴的方向问题上,还没有去细想,(以后待解决)。==

形成一个理解是:视差贴图是通过对当前片元使用的纹理坐标 ++根据观察向量的方向和深度图采样的值++ 做一些偏移,偏移的方向就是观察向量的方向,因此上述的向量p都是基于观察向量的方向上做的,向量p的大小由深度图采样的值决定。

mark

结合上面这个图,当观察方向和绿色线e的方向一样时,为了方便讨论,蓝点所在的白线就是我们上面的灰色平面(片元所在的平面),观察蓝点的时候,该点的深度值较小,该点会使用稍微往左边的纹理坐标;当观察蓝点往左边的片元时,片元的深度值可以看出是越来越大,使得这些片元使用越来越远的纹理坐标,红色线上所对应的纹理坐标就不会被完全使用,这和实际的观察现象相一致,就是在背面的纹理很难被观察到,甚至观察不到。这应该是视差贴图比法线贴图更具有深度感的原因吧!

最后的纹理坐标将会被用来对diffuse贴图和法线贴图进行采样,设置height_scale值为0.1,产生的效果图如下图所示:

mark

这里你会看到只用法线贴图和与视差贴图相结合的法线贴图的不同之处。因为视差贴图尝试模拟深度,它实际上能够根据你观察它们的方向使砖块叠加到其他砖块上。

在视差贴图中,会看到在边缘上有古怪的失真。原因是在平面的边缘上进行采样的话,根据纹理的环绕方式,位移后的纹理坐标就有可能超出范围[0,1],从而导致了不真实的结果。解决的方法是当它超出默认纹理坐标范围进行采样的时候就丢弃这个fragment:

texCoords = ParallaxMapping(fs_in.TexCoords,  viewDir);
if(texCoords.x > 1.0 || texCoords.y > 1.0 || texCoords.x < 0.0 || texCoords.y < 0.0)
    discard;

丢弃了超出默认范围的纹理坐标的所有fragment,视差贴图的表面边缘给出了正确的结果。注意,这个解决方法不适用于所有类型的表面,仅仅当处理的情况是一个平面的时候,这个解决方法能够显示出很好的结果,其他表面有可能效果略差,如下图是最终的显示结果:

mark

1.3 示例代码

示例代码已上传到github的Cephei项目上,
1. main.cpp
2. parallaxMappingvs.glsl
3. parallaxMappingfs.glsl

1.4 存在的问题

视差贴图技术的应用看起来的效果不错,并且运行速度也很快,因为只需额外使用一个纹理贴图(深度图)就能使得视差贴图正常工作。但是也会出现一些问题,当以一个倾斜的角度去看的话,陡峭的地方会产生不正确的结果,如下图所示:

mark

造成上述问题的原因是这只是对位移贴图的一个大致近似,即在上面的原理图中,使用点P去近似点B。使用一些额外的技巧使得即使在陡峭的高度变化中也能够获得完美正确的结果。例如,不再使用单一样本采样,而是使用多个样本来找到最近点B的话,其结果会是怎样?

2. 浮雕贴图 Relief Mapping

2.1 浮雕贴图 原理

浮雕贴图(Relief Mapping),又可以称之为陡峭视差贴图(Steep Parallax Mapping)。陡峭视差贴图(Steep Parallax Mapping)是视差贴图的扩展,它们都是使用相同的原理,但是陡峭视差贴图不是使用一个样本采样,而是采样多个样本使得点P到达点B。即使有陡峭的高度变化,该贴图技术也能够给出很好显示效果,因为样本数量的增多能够提高贴图的精确度(这里精确度应该是指点P更接近于点B)。

陡峭视差贴图的基本思想是将整个深度值范围(即范围[0,1])划分为多个具有相同高度的层(layer),沿着视觉方向$\vec{P}$与各层发生相交,使用 交点对应的纹理坐标对深度图采样得到的深度值 与 该层的深度值作比较,直到采样的深度值比当前层的深度值要小时,停止迭代。 如下图所示:

mark

原理解释:从上到下遍历每一层,并对于每一层来说,将该层的深度值与从深度图采样得到的深度值进行比较。如果当前层的深度值要比从深度图采样得到的深度值要小的话,说明当前层与向量 P⃗  P → 的交点(就是上图中紫色的点)不在模型表面的下方,继续遍历到下一层重复上述的操作,直到当前层的深度值要比从深度图采样得到的深度值要大时(此时说明交点处于模型表面的下方),才停止遍历。

在上述的例子中,可以看到第二层(D(2) = 0.73)的深度图的深度值仍低于第二层的深度值0.4,所以我们继续。下一次迭代,这一层的深度值0.6要大于从深度图中采样的深度值(D(3) = 0.37)。我们便可以假设第三层向量 P⃗  P → 是可行的。可以使用纹理坐标偏移量 T3 T 3 来对片元原来的纹理坐标进行位移或偏移。可以看到随着层数的增加,精确度也随之提高。

2.2 浮雕贴图 代码实现

为了实现浮雕贴图(又称陡峭视差贴图),只需要在视差贴图的基础上做些修改即可,因为所有需要使用到的变量都已经提供好了。在浮雕贴图实现中,只需要改变函数ParallaxMapping(),为了和视差贴图的代码做个区别,这里重新定义了一个新的函数reliefMapping(),不影响理解:

函数reliefMapping()的实现代码如下:

vec2 reliefMapping(vec2 texCoords, vec3 viewDir) {
    const float numLayers = 10;         //number of depth layers
    float layerDepth = 1.0 / numLayers; //calculate the size of each layer
    float currentLayerDepth = 0.0;      //depth of  current layer

    //the amount to shift the texture coordinates per layer
    vec2 P = viewDir.xy * height_scale;
    vec2 deltaTexCoords = P / numLayers;

    [....]
}

在上述实现中,定义了层的数量,每一个层的高度(或深度),以及每一层前进时纹理坐标的偏移量。

然后从上往下遍历所有层,直到深度图采样得到的深度值比该层的深度值要小时,遍历结束。下面是函数reliefMapping()剩余的实现代码:

      // get initial values
    vec2 currentTexCoords = texCoords;
    float currentDepthMapValue = texture(material.texture_height1, currentTexCoords).r;

    while (currentDepthMapValue > currentLayerDepth) {
       //shift texture coordinates along direction of P
       currentTexCoords -= deltaTexCoords;
       //get depthmap value at current texture coordinates;
       currentDepthMapValue = texture(material.texture_height1, currentTexCoords).r;
       //get depth of next layer
       currentLayerDepth += layerDepth;
    }

    return currentTexCoords;

这种分层的实现方法比之前的视差贴图要更加精确。(指近似点P与实际观察到点B更加接近。)显示的效果图如下所示:

mark

这里可以利用视差贴图的一个特性来优化算法。这个特性是指当垂直看向表面的时候,存在比较少的纹理位移(displacement);而当一定角度看向表面的时候,存在较多的纹理位移。因此当垂直看向表面时,使用更少的样本,而当一定的角度看向表面的时候,使用更多的样本。

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轴正方向(它们都处于切线空间,(0,0,1)是表面的法线方向)做点积,并根据看向表面的角度来调整样本的数量(这里使用点积的结果来判断角度)。如果所看的方向平行于表面,这里就是使用32层(32个样本)。

2.3 示例代码

示例代码已上传到github的Cephei项目上,
1. main.cpp
2. reliefMappingvs.glsl
3. reliefMappingfs.glsl

2.4 存在的问题

陡峭视差贴图同样存在自己的问题。因为陡峭视差贴图是基于有限的样本数量,因此会遇到锯齿效果以及层与层之间有明显的断层:

mark

可以通过增加样本数量的方法来减少上述问题的发生,但是这会带来很大的性能开销。目前有几种方法去解决这些问题,其中一个是:不去使用第一个在表面以下的交点(指该层的深度值大于交点从深度图采样得到的深度值的情况),而是使用相邻两层的交点的插值作为最终的 P P 点,这样P点更加接近于 B B 点。

两种最流行的解决方法叫做浮雕视差贴图(Relief Parallax Mapping)视差遮蔽贴图(Parallax Occlusion Mapping)。其中浮雕视差贴图比视差遮蔽贴图要更精确一些,但是比视差遮蔽贴图性能开销更大。因为视差遮蔽贴图的效果和前者差不多,但效率更高,因此这种方法(视差遮蔽贴图)更经常使用。下面介绍视差遮蔽贴图。

3. 视差遮蔽贴图 Parallax Occlusion Mapping

3.1 视差遮蔽贴图 原理

视差遮蔽贴图(Parallax Occlusion Mapping)和陡峭视差贴图(也叫浮雕贴图)使用相同的原理,但是不是使用第一个在表面以下的交点的纹理坐标,而是在当前层交点的纹理坐标与前一层交点的纹理坐标之间做线性插值。线性插值的权重是基于表面高度分别与各自层之间的距离,原理图如下所示:

mark

从这可以看出视差遮蔽贴图与陡峭视差贴图大致相同,不过视差遮蔽贴图要多了一步:在两层的纹理坐标之间做一个线性插值,虽然这也是近似,但是会比陡峭视差贴图要更加精确。

3.2 视差遮蔽贴图 实现代码

视差遮蔽贴图的代码是在陡峭视差贴图的代码基础上有所增加:

[...]  //steep parallax mapping code here

// get texture coordinates before collision (reverse operations)
vec2 prevTexCoords = currentTexCoords + deltaTexCoords;

// get depth after and before collision for linear interpolation
float afterDepth = currentDepthMapValue - currentLayerDepth;
float beforeDepth = texture(material.texture_height1, prevTexCoords).r - currentLayerDepth + layerDepth;

// interpolation of texture coordinates
float weight = afterDepth / (afterDepth - beforeDepth);
vec2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight);

return finalTexCoords;

上述代码的逻辑是:首先获得与模型表面相交的第一个层的纹理坐标$T_3$,然后再去获取这一层的前一层的纹理坐标$T_2$,然后分别去计算这两层的深度值与对应模型表面的深度值之间的距离afterDepthbeforeDepth,指上图中的紫色点与蓝色点的连线,根据这两个距离值计算出权重值,在$T_2$$T_3$之间做线性插值。上述函数的返回值就是片元最终使用的纹理坐标。这里需要注意的是:afterDepth是负值,beforeDepth是正值,所以计算weight的公式可以理解成 afterDepth在这两个距离值之和中所占的比例,如果afterDepth比较大,那么实际的交点很可能在前面,此时最终的纹理坐标更偏向于prevTexCoords;如果afterDepth比较小的情况则相反。

视差遮蔽贴图带来非常好的效果,虽然还是能够看到一些轻微的不真实和锯齿问题,但这是在性能和效果之间最好的权衡(相对于浮雕视差贴图(Relief Parallax Mapping)性能消耗大来说),而且这些视觉问题只有在放得非常大或者观察角度特别陡的情况下才发生,否则也看不到。

mark

视差贴图是一种能够很好提升场景细节的技术,但是在使用该贴图技术的时候需要考虑到它所带来的不自然。大多数情况下,视差贴图技术应用于地面和墙壁表面,而在这种情况下,视角一般都是垂直于表面而且不容易观察到表面的轮廓。这样视差贴图的不自然情况也就很难被注意到,同时视差贴图能够极大提升物体的细节。

要离表面远一些,这样显示效果最好,当你离表面越来越近的时候,会发现表面的一些细节在移动(不正确的),因此离表面特别近的时候就会出现上面的不自然现象.

3.3 示例代码

示例代码已上传到github的Cephei项目上,
1. main.cpp
2. parallaxOcclusionMappingvs.glsl
3. parallaxOcclusionMappingfs.glsl

4. 总结

在之前的渲染中,一般是使用普通的纹理贴图来获取模型表面的diffuse值(颜色值),但这样显得不真实。后来就引入了法线贴图,在光照计算中之前是直接使用了顶点的法向量来进行光照计算,而在法线贴图的应用中,光照计算中使用的法向量来自于法线贴图采样的值,而顶点原来的法向量在这里仅仅用于生成切线坐标系的基,如果没有光照计算,法线贴图在这里就没有任何效果上的提升,反而会影响性能(好吧,很微不足道的影响)。同样的,法线贴图在光照计算过程中提供片元的法向量,然后在明暗效果上起到立体的感觉。然而在视觉效果上没有很大的深度感,因此就出现使用视差贴图等技术。视差贴图仅仅比法线贴图多使用了一个高度图(heightmap),实现的效果比法线贴图要好,但是计算代价也随之增大。法线贴图是改变了顶点光照计算过程中使用的法向量,而视差贴图是改变了顶点采样diffuse贴图和法线贴图时使用的纹理坐标。

  1. 视差贴图(Parallax Mapping):最简单的视差贴图,利用点A的深度值作为视角向量V的长度,使用该向量的终点P去近似 交点B,点A的纹理坐标使用的是点P的纹理坐标。从图中可以发现点P近似 点B,也只是大概去近似,有很大的误差,因此后面几种方法都是怎么使得点P更加精确地去近似 点B。

    mark

    1. 浮雕贴图(Relief Mapping):也叫陡峭视差贴图(Steep Parallax Mapping),也属于视差贴图的一种。摒弃了视差贴图简单粗暴的近似方法,浮雕贴图是将深度图的深度划分为等高度的各层,沿着视角向量的方向去与各层相交,其交点就是下图紫色的点,找到第一个是在模型表面以下的交点P,该交点的纹理坐标就是我们需要使用的。如下图所示点$T_0$的纹理坐标使用的是点 T3 T 3 的纹理坐标,可以看出交点P与点B(==就是上面图中的蓝色的点B==)比第一种方法稍微精确了些。

    mark

    1. 视差遮蔽贴图(Parallax Occlusion Mapping):视差遮蔽贴图在浮雕贴图的基础上,稍微修改了下近似的方法,它不是使用第一个在表面以下的交点的纹理坐标,而是在当前层交点的纹理坐标 T3 T 3 与前一层交点的纹理坐标 T2 T 2 之间做线性插值。线性插值的权重由 H(T3)H(T3)+H(T2) H ( T 3 ) H ( T 3 ) + H ( T 2 ) 决定H(T_3)和H(T_2)就是下图中紫色和蓝色之间的连线的距离),最终的近似点能够很好的近似点B。

    mark

    上述的贴图技术都是在片元着色器中实现的,因为在片元着色器中才需要使用到纹理坐标。各种视差贴图最终返回的是当前片元位移后的纹理坐标,然后使用该纹理坐标去对diffuse贴图和法线贴图进行采样

    讨论一下计算代价:法线贴图和视差贴图的计算代价较小,仅仅是对两张贴图进行采样,而浮雕贴图和视差遮蔽贴图的计算代价比较大,随着层的数量增加,其计算代价也随之增大

    参考链接:
    1. learn-opengl. Parallax-Mapping
    2. 视差贴图(Parallax Mapping)

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值