OpenGL视差贴图

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_44800780/article/details/102466178

参考:

https://learnopenglcn.github.io/05%20Advanced%20Lighting/05%20Parallax%20Mapping/
视差贴图(Parallax Mapping)技术和法线贴图差不多,但它有着不同的原则。
和法线贴图一样视差贴图能够极大提升表面细节,使之具有深度感。它也是利用了视错觉,然而对深度有着更好的表达,与法线贴图一起用能够产生难以置信的效果。

视差贴图属于位移贴图技术的一种,它对根据储存在纹理中的几何信息对顶点进行位移或偏移。一种实现的方式是比如有1000个顶点,根据纹理中的数据对平面特定区域的顶点的高度进行位移。这样的每个纹理像素包含了高度值纹理叫做高度贴图。一张简单的砖块表面的高度贴图如下所示:
在这里插入图片描述
整个平面上的每个顶点都根据从高度贴图采样出来的高度值进行位移,根据材质的几何属性平坦的平面变换成凹凸不平的表面。例如一个平坦的平面利用上面的高度贴图进行置换能得到以下结果:
在这里插入图片描述

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

视差贴图背后的思想是修改纹理坐标使一个fragment的表面看起来比实际的更高或者更低,所有这些都根据观察方向和高度贴图。为了理解它如何工作,看看下面砖块表面的图片:
在这里插入图片描述
这里粗糙的红线代表高度贴图中的数值的立体表达,向量V代表观察方向。如果平面进行实际位移,观察者会在点B看到表面。然而我们的平面没有实际上进行位移,观察方向将在点A与平面接触。视差贴图的目的是,在A位置上的fragment不再使用点A的纹理坐标而是使用点B的。随后我们用点B的纹理坐标采样,观察者就像看到了点B一样

视差贴图尝试通过对从fragment到观察者的方向向量V进行缩放的方式解决这个问题,缩放的大小是A处fragment的高度。所以我们将V的长度缩放为高度贴图在点A处H(A)采样得来的值。得到向量P

随后选出P以及这个向量与平面对齐的坐标作为纹理坐标的偏移量。这能工作是因为向量P是使用从高度贴图得到的高度值计算出来的,所以一个fragment的高度越高位移的量越大。

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

视差贴图的另一个问题是,当表面被任意旋转以后很难指出从P获取哪一个坐标。我们在视差贴图中使用了另一个坐标空间,这个空间P向量的x和y元素总是与纹理表面对齐。我们实现它的方法,是的,我们还是在切线空间中实现视差贴图。

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

视差贴图

我们将使用一个简单的2D平面,在把它发送给GPU之前我们先计算它的切线和副切线向量,我们将向平面贴diffuse纹理、法线贴图以及一个位移贴图,这个例子中我们将视差贴图和法线贴图连用。因为视差贴图生成表面位移了的幻觉,当光照不匹配时这种幻觉就被破坏了。法线贴图通常根据高度贴图生成,法线贴图和高度贴图一起用能保证光照能和位移想匹配。
使用反色高度贴图(也叫深度贴图)去模拟深度比模拟高度更容易。下图反映了这个轻微的改变:
在这里插入图片描述

高度为0.0的平面是渲染时的实际面片,棕色的线则代表了原始几何体的实际高度。这里我们一共需要三张纹理:法线纹理,高度纹理以及颜色纹理。
A点是我们在面片上看到的点,B点则是该几何体应该被我们所观察到的点,很明显A点和B点对应的纹理坐标是不同的,为了模拟实际的几何体表面,我们需要估计A B之间纹理坐标的偏移,这里我们粗暴的将偏移量通过如下公式计算:
在这里插入图片描述
viewDir 是归一化之后的视线向量,H(A)代表A点的高度
factor 这个参数,一般设置为0.1
顶点着色器:

#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;
layout (location = 4) in vec3 bitangent;

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(position, 1.0f);
    vs_out.FragPos   = vec3(model * vec4(position, 1.0));   
    vs_out.TexCoords = texCoords;    

    vec3 T   = normalize(mat3(model) * tangent);
    vec3 B   = normalize(mat3(model) * bitangent);
    vec3 N   = normalize(mat3(model) * normal);
    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;
    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()
{           
    // Offset texture coordinates with Parallax Mapping
    vec3 viewDir   = normalize(fs_in.TangentViewPos - fs_in.TangentFragPos);
    vec2 texCoords = ParallaxMapping(fs_in.TexCoords,  viewDir);

    // then sample textures with new texture coords
    vec3 diffuse = texture(diffuseMap, texCoords);
    vec3 normal  = texture(normalMap, texCoords);
    normal = normalize(normal * 2.0 - 1.0);
    // proceed with lighting code
    [...]    
}

我们定义了一个叫做ParallaxMapping的函数,它把fragment的纹理坐标作和切线空间中的fragment到观察者的方向向量为输入。这个函数返回经位移的纹理坐标。

然后我们使用这些经位移的纹理坐标进行diffuse和法线贴图的采样。最后fragment的diffuse颜色和法线向量就正确的对应于表面的经位移的位置上了

vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{ 
    float height =  texture(depthMap, texCoords).r;    
    vec2 p = viewDir.xy / viewDir.z * (height * height_scale);
    return texCoords - p;    
}

有一个地方需要注意,就是viewDir.xy除以viewDir.z那里。因为viewDir向量是经过了标准化的,viewDir.z会在0.0到1.0之间的某处。当viewDir大致平行于表面时,它的z元素接近于0.0,除法会返回比viewDir垂直于表面的时候更大的P向量。所以基本上我们增加了P的大小,当以一个角度朝向一个表面相比朝向顶部时它对纹理坐标会进行更大程度的缩放;这回在角上获得更大的真实度。

最后的纹理坐标随后被用来进行采样(diffuse和法线)贴图,下图所展示的位移效果中height_scale等于1:
在这里插入图片描述

在视差贴图的那个平面里你仍然能看到在边上有古怪的失真。原因是在平面的边缘上,纹理坐标超出了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;

在这里插入图片描述
当从一个角度看过去的时候,会有一些问题产生(和法线贴图相似),陡峭的地方会产生不正确的结果,从下图你可以看到:
在这里插入图片描述

问题的原因是这只是一个大致近似的视差映射。还有一些技巧让我们在陡峭的高度上能够获得几乎完美的结果,即使当以一定角度观看的时候。例如,我们不再使用单一样本,取而代之使用多样本来找到最近点B会得到怎样的结果

陡峭视差映射

陡峭视差映射(Steep Parallax Mapping)是视差映射的扩展,原则是一样的,但不是使用一个样本而是多个样本来确定向量P到B
它能得到更好的结果,它将总深度范围分布到同一个深度/高度的多个层中。从每个层中我们沿着P方向移动采样纹理坐标,直到我们找到了一个采样得到的低于当前层的深度值的深度值。看看下面的图片:

在这里插入图片描述
从上到下遍历深度层,我们把每个深度层和储存在深度贴图中的它的深度值进行对比。如果这个层的深度值小于深度贴图的值,就意味着这一层的P¯向量部分在表面之下。我们继续这个处理过程直到有一层的深度高于储存在深度贴图中的值:这个点就在(经过位移的)表面下方

第二层(D(2) = 0.73)的深度贴图的值仍低于第二层的深度值0.4
所以我们继续。下一次迭代,这一层的深度值0.6大于深度贴图中采样的深度值(D(3) = 0.37)。我们便可以假设第三层向量P是可用的位移几何位置。我们可以用从向量P3¯的纹理坐标偏移T3来对fragment的纹理坐标进行位移。你可以看到随着深度曾的增加精确度也在提高。

为实现这个技术,我们只需要改变ParallaxMapping函数,因为所有需要的变量都有了:

vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{ 
    // number of depth layers
    const float numLayers = 10;
    // calculate the size of each layer
    float layerDepth = 1.0 / numLayers;
    // depth of current layer
    float currentLayerDepth = 0.0;
    // the amount to shift the texture coordinates per layer (from vector P)
    vec2 P = viewDir.xy * height_scale; 
    float deltaTexCoords = P / numLayers;

    [...]     
}

我们先定义层的数量,计算每一层的深度,最后计算纹理坐标偏移,每一层我们必须沿着P的方向进行移动。

然后我们遍历所有层,从上开始,直到找到小于这一层的深度值的深度贴图值:

vec2  currentTexCoords     = texCoords;
float currentDepthMapValue = texture(depthMap, currentTexCoords).r;

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

return texCoords - currentTexCoords;

这里我们循环每一层深度,直到沿着P向量找到第一个返回低于(位移)表面的深度的纹理坐标偏移量。从fragment的纹理坐标减去最后的偏移量,来得到最终的经过位移的纹理坐标向量,这次就比传统的视差映射更精确了

我们可以通过对视差贴图的一个属性的利用,对算法进行一点提升。当垂直看一个表面的时候纹理时位移比以一定角度看时的小。我们可以在垂直看时使用更少的样本,以一定角度看时增加样本数量:

const float minLayers = 8;
const float maxLayers = 32;
float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir)));

陡峭视差贴图同样有自己的问题。因为这个技术是基于有限的样本数量的,我们会遇到锯齿效果以及图层之间有明显的断层:
在这里插入图片描述

我们可以通过增加样本的方式减少这个问题,但是很快就会花费很多性能。

视差遮蔽映射

视差遮蔽映射(Parallax Occlusion Mapping)和陡峭视差映射的原则相同,但不是用触碰的第一个深度层的纹理坐标,而是在触碰之前和之后,在深度层之间进行线性插值。我们根据表面的高度距离哪个深度层的深度层值的距离来确定线性插值的大小。看看下面的图片就能了解它是如何工作的:


现在我们不再简单的取T3作为偏移纹理了,根据一个比值R来对纹理坐标进行线性插值,现在图中的H()代表该点处实际高度与该层高度的差值的绝对值。
在这里插入图片描述

[...] // 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(depthMap, prevTexCoords).r - currentLayerDepth + layerDepth;

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

return finalTexCoords;

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_44800780/article/details/102466178