笔者介绍:姜雪伟,IT公司技术合伙人。IT高级讲师,CSDN社区专家,特邀编辑。畅销书作者;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术具体解释》电子工业出版社等。

CSDN视频网址:http://edu.csdn.net/lecturer/144

继续接着上文的问题。先给读者展示一副图效果例如以下所看到的:
OpenGL核心之视差映射-LMLPHP
问题的解决办法是这仅仅是一个大致近似的视差映射。

另一些技巧让我们在陡峭的高度上能够获得差点儿完美的结果,即使当以一定角度观看的时候。比如。我们不再使用单一样本。取而代之使用多样本来找到近期点B会得到如何的结果?

陡峭视差映射(Steep Parallax Mapping)是视差映射的扩展,原则是一样的,但不是使用一个样本而是多个样本来确定向量P¯到B。

它能得到更好的结果,它将总深度范围分布到同一个深度/高度的多个层中。

从每一个层中我们沿着P¯方向移动採样纹理坐标,直到我们找到了一个採样得到的低于当前层的深度值的深度值。

看看以下的图片:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvanh3MTY3/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="" />

我们从上到下遍历深度层,我们把每一个深度层和储存在深度贴图中的它的深度值进行对照。

假设这个层的深度值小于深度贴图的值,就意味着这一层的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¯的方向进行移动。

然后我们遍历全部层,从上開始,知道找到小于这一层的深度值的深度贴图值:

// get initial values
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的纹理坐标减去最后的偏移量,来得到终于的经过位移的纹理坐标向量,这次就比传统的视差映射更精确了。

有10个样本砖墙从一个角度看上去就已经非常好了,可是当有一个强前面展示的木制表面一样陡峭的表面时,陡峭的视差映射的威力就显示出来了:

OpenGL核心之视差映射-LMLPHP

我们能够通过对视差贴图的一个属性的利用。对算法进行一点提升。当垂直看一个表面的时候纹理时位移比以一定角度看时的小。

我们能够在垂直看时使用更少的样本。以一定角度看时添加样本数量:

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

这里我们得到viewDir和正z方向的点乘,使用它的结果依据我们看向表面的角度调整样本数量(注意正z方向等于切线空间中的表面的法线)。假设我们所看的方向平行于表面,我们就是用32层。

你能够在这里找到最新的像素着色器代码。

这里也提供木制玩具箱的表面贴图:diffuse、法线、深度。

陡峭视差贴图相同有自己的问题。

由于这个技术是基于有限的样本数量的。我们会遇到锯齿效果以及图层之间有明显的断层:

OpenGL核心之视差映射-LMLPHP

我们能够通过添加样本的方式降低这个问题,可是非常快就会花费非常多性能。有些旨在修复这个问题的方法:不适用低于表面的第一个位置。而是在两个接近的深度层进行插值找出更匹配B的。

两种最流行的解决方法叫做Relief Parallax Mapping和Parallax Occlusion Mapping,Relief Parallax Mapping更精确一些,可是比Parallax Occlusion Mapping性能开销很多其它。由于Parallax Occlusion Mapping的效果和前者几乎相同可是效率更高。因此这样的方式更常常使用。所以我们将在以下讨论一下。

视差遮蔽映射(Parallax Occlusion Mapping)和陡峭视差映射的原则相同,但不是用触碰的第一个深度层的纹理坐标。而是在触碰之前和之后,在深度层之间进行线性插值。

我们依据表面的高度距离啷个深度层的深度层值的距离来确定线性插值的大小。

看看以下的图片就能了解它是如何工作的:

你能够看到大部分和陡峭视差映射一样,不一样的地方是有个额外的步骤,两个深度层的纹理坐标环绕着交叉点的线性插值。这也是近似的。可是比陡峭视差映射更精确。

视差遮蔽映射的代码基于陡峭视差映射。所以并不难:

[...] // 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;

在对(位移的)表面几何进行交叉,找到深度层之后。我们获取交叉前的纹理坐标。然后我们计算来自对应深度层的几何之间的深度之间的距离。并在两个值之间进行插值。线性插值的方式是在两个层的纹理坐标之间进行的基础插值。函数最后返回终于的经过插值的纹理坐标。

视差遮蔽映射的效果非常好。虽然有一些能够看到的轻微的不真实和锯齿的问题,这仍是一个好交易。由于除非是放得非常大或者观察角度特别陡,否则也看不到。

OpenGL核心之视差映射-LMLPHP

最后把视线该效果的源码给读者展示一下,首先展示的顶点着色器代码:

#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;
}

片段着色器代码例如以下所看到的:

#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 bool parallax;
uniform float height_scale; vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{
// number of depth layers
const float minLayers = 10;
const float maxLayers = 20;
float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir)));
// 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 / viewDir.z * height_scale;
vec2 deltaTexCoords = P / numLayers; // get initial values
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;
} // -- parallax occlusion mapping interpolation from here on
// 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;
} void main()
{
// Offset texture coordinates with Parallax Mapping
vec3 viewDir = normalize(fs_in.TangentViewPos - fs_in.TangentFragPos);
vec2 texCoords = fs_in.TexCoords;
if(parallax)
texCoords = ParallaxMapping(fs_in.TexCoords, viewDir); // discards a fragment when sampling outside default texture region (fixes border artifacts)
if(texCoords.x > 1.0 || texCoords.y > 1.0 || texCoords.x < 0.0 || texCoords.y < 0.0)
discard; // Obtain normal from normal map
vec3 normal = texture(normalMap, texCoords).rgb;
normal = normalize(normal * 2.0 - 1.0); // Get diffuse color
vec3 color = texture(diffuseMap, texCoords).rgb;
// Ambient
vec3 ambient = 0.1 * color;
// Diffuse
vec3 lightDir = normalize(fs_in.TangentLightPos - fs_in.TangentFragPos);
float diff = max(dot(lightDir, normal), 0.0);
vec3 diffuse = diff * color;
// Specular
vec3 reflectDir = reflect(-lightDir, normal);
vec3 halfwayDir = normalize(lightDir + viewDir);
float spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0); vec3 specular = vec3(0.2) * spec;
FragColor = vec4(ambient + diffuse + specular, 1.0f);
}

视差贴图是提升场景细节非常好的技术,可是使用的时候还是要考虑到它会带来一点不自然。大多数时候视差贴图用在地面和墙壁表面,这样的情况下查明表面的轮廓并不easy。同一时候观察角度往往趋向于垂直于表面。

这样视差贴图的不自然也就非常难能被注意到了,对于提升物体的细节能够祈祷难以置信的效果。

05-17 15:44