纯手码字翻译大佬教程. 因为parallax算法和RayMarching算法相似(都为步进采样),就放在一个专栏里了,自己理解的地方有注明.
原文链接 :https://github.com/UPBGE/upbge/issues/1009
这个教程介绍了怎么用GLSL使用不同的视差贴图技术(也可以在DX中运用)。下面要介绍的技术有:视差贴图,偏移限制视差贴图,陡峭视差贴图,浮雕视差贴图以及视差映射贴图(POM),同样这篇文章介绍了怎么在视差中加自投影。下面的图片描绘了在简单灯光下视差贴图和法线贴图的差别:
基础视差贴图
在计算机图形学中视差贴图是法线贴图的进阶版,也就是不仅仅改变光的行为还在平面中制造了3D细节的假象.并没有额外生成模型.在之前展示的视差和法线的对比图中.您可能会认为视差移动了原来的模型,实际上是移动了UV后采样了diffuse贴图和normal贴图.
使用视差贴图需要一张高度图,高度图储存了每个像素表面的高度信息。可以描述成距离表面的深度,在这种情况里必须反转高度图. 这个教程里把高度图的信息视作深度信息.黑色(0)代表了没有孔,白色(1)代表了孔的最大深度.
在下面的视差贴图范例中我们使用三张贴图:heightMap,diffuseMap还有normalMap,通常normalMap是由heightMap生成的.在我们的范例中高度图作为深度图使用.所以在生成normalMap之前.您必须反转高度图.您可以把normalMap和heightMap合并成一张图(heightMap作为Alpha通道),但是本教程为了方便一点使用了三张不同的贴图.下面是视差的贴图范例:
视差贴图技术的主要任务是通过移动UV修改贴图来让平面看着像3D. 这个效果会在片段着色器中显示的所有像素进行计算. 如下图.Level0代表没有坑洞.Level1代表最深的孔. 模型实际上没有变而且一直在Level0处.曲线代表的是储存在高度图中的值.当下的像素坐落在黄色方形处.这个像素对应的UV(TextureCoordinates)是T0.V是从摄像机到该像素的向量.如果按照T0采样高度图的话能得到值H(T0) = 0.55.值不等于0。所以该像素并不在表面. 在像素之下有坑. 所以你必须延申V到由高度图定义曲面的交点.这个交点正在深度为H,UV位T1的位置,然后用T1去采样diffuseMap和normalMap.
因此所有视差算法的目标都是去计算摄像机向量V和高度图所定义表面的交点。
视差贴图基础shader
视差贴图的计算必须在切线空间里(和法线一样).所以灯光(L)和摄像机(V)的向量必须转换到切线空间.然后用视差技术计算新的UV坐标,然后您可以用这个新的UV坐标计算子投影以及采样这个像素的diffuseMap和normalMap.在这个例子中执行视差算法的函数是parallaxMapping() ,计算自投影的函数是parallaxSoftShadowMultiplier().光照模型用的是Blinn_Phong光照模型.法线方程是normalMappingLighting().下面的顶点和片段着色器可以当作是视差贴图算法的基本架构.顶点着色器变换了L,V到切线空间.片段着色器调用了视差算法函数,然后计算自投影最后计算光照:
// Basic vertex shader for parallax mapping
#version 330
// attributes
layout(location = 0) in vec3 i_position; // xyz - position
layout(location = 1) in vec3 i_normal; // xyz - normal
layout(location = 2) in vec2 i_texcoord0; // xy - texture coords
layout(location = 3) in vec4 i_tangent; // xyz - tangent, w - handedness
// uniforms
uniform mat4 u_model_mat;
uniform mat4 u_view_mat;
uniform mat4 u_proj_mat;
uniform mat3 u_normal_mat;
uniform vec3 u_light_position;
uniform vec3 u_camera_position;
// data for fragment shader
out vec2 o_texcoords;
out vec3 o_toLightInTangentSpace;
out vec3 o_toCameraInTangentSpace;
///
void main(void)
{
// transform to world space
vec4 worldPosition = u_model_mat * vec4(i_position, 1);
vec3 worldNormal = normalize(u_normal_mat * i_normal);
vec3 worldTangent = normalize(u_normal_mat * i_tangent.xyz);
// calculate vectors to the camera and to the light
vec3 worldDirectionToLight = normalize(u_light_position - worldPosition.xyz);
vec3 worldDirectionToCamera = normalize(u_camera_position - worldPosition.xyz);
// calculate bitangent from normal and tangent
vec3 worldBitangnent = cross(worldNormal, worldTangent) * i_tangent.w;
// transform direction to the light to tangent space
o_toLightInTangentSpace = vec3(
dot(worldDirectionToLight, worldTangent),
dot(worldDirectionToLight, worldBitangnent),
dot(worldDirectionToLight, worldNormal)
);
// transform direction to the camera to tangent space
o_toCameraInTangentSpace= vec3(
dot(worldDirectionToCamera, worldTangent),
dot(worldDirectionToCamera, worldBitangnent),
dot(worldDirectionToCamera, worldNormal)
);
// pass texture coordinates to fragment shader
o_texcoords = i_texcoord0;
// calculate screen space position of the vertex
gl_Position = u_proj_mat * u_view_mat * worldPosition;
}
// basic fragment shader for Parallax Mapping
#version 330
// data from vertex shader
in vec2 o_texcoords;
in vec3 o_toLightInTangentSpace;
in vec3 o_toCameraInTangentSpace;
// textures
layout(location = 0) uniform sampler2D u_diffuseTexture;
layout(location = 1) uniform sampler2D u_heightTexture;
layout(location = 2) uniform sampler2D u_normalTexture;
// color output to the framebuffer
out vec4 resultingColor;
// scale for size of Parallax Mapping effect
uniform float parallaxScale; // ~0.1
//
// Implements Parallax Mapping technique
// Returns modified texture coordinates, and last used depth
vec2 parallaxMapping(in vec3 V, in vec2 T, out float parallaxHeight)
{
// ...
}
//
// Implements self-shadowing technique - hard or soft shadows
// Returns shadow factor
float parallaxSoftShadowMultiplier(in vec3 L, in vec2 initialTexCoord,
in float initialHeight)
{
// ...
}
//
// Calculates lighting by Blinn-Phong model and Normal Mapping
// Returns color of the fragment
vec4 normalMappingLighting(in vec2 T, in vec3 L, in vec3 V, float shadowMultiplier)
{
// restore normal from normal map
vec3 N = normalize(texture(u_normalTexture, T).xyz * 2 - 1);
vec3 D = texture(u_diffuseTexture, T).rgb;
// ambient lighting
float iamb = 0.2;
// diffuse lighting
float idiff = clamp(dot(N, L), 0, 1);
// specular lighting
float ispec = 0;
if(dot(N, L) > 0.2)
{
vec3 R = reflect(-L, N);
ispec = pow(dot(R, V), 32) / 1.5;
}
vec4 resColor;
resColor.rgb = D * (ambientLighting + (idiff + ispec) * pow(shadowMultiplier, 4));
resColor.a = 1;
return resColor;
}
/
// Entry point for Parallax Mapping shader
void main(void)
{
// normalize vectors after vertex shader
vec3 V = normalize(o_toCameraInTangentSpace);
vec3 L = normalize(o_toLightInTangentSpace);
// get new texture coordinates from Parallax Mapping
float parallaxHeight;
vec2 T = parallaxMapping(V, o_texcoords, parallaxHeight);
// get self-shadowing factor for elements of parallax
float shadowMultiplier = parallaxSoftShadowMultiplier(L, T, parallaxHeight - 0.05);
// calculate lighting
resultingColor = normalMappingLighting(T, L, V, shadowMultiplier);
}
视差和视差偏移限制
最简单版本的视差技术是由原来的UV通过一步计算得到接近交点的UV,简单的称之为视差贴图.只有当高度图平滑而且不包含很多小细节时,才能给出或多或少的有效结果.另一种情况,向量V和N之间的夹角过大时,视差的影响将无效,近似视差的计算过程如下:
- 从高度图中获得H(T0),初始UV 坐标T0.
- 根据摄像机向量V和初始高度H(T0)来偏移初始UV坐标.
偏移UV是通过以下方法完成的.由于V向量在切线空间中,并且切线空间是沿着纹理坐标的梯度建立的.所以V.xy可以不做任何变换就用作沿着V偏移UV坐标的方向.而V.z是法线分量.它垂直于表面.您可以用V.xy除以V.z.这就是视差贴图中UV的原始计算. 或者您可以去掉V.z,这样的实现被称为带偏移限制的视差贴图. 这可以减少在V和N之间的角度较高的怪异结果.那么您可以把V.xy添加到原始的UV上,这样就得到了沿着V方向偏移的新UV.
您可以用一个变量来控制视差的效果.同样的.必须乘上V.xy.最有效的数值范围是0到0.5。更高的数值在多数情况下都是错误的(如下图).您也可以让值为负.这种情况必须翻转normalMap的b通道.那么偏移UV计算的最后公式是:
下面的图片描述了高度图的深度H(T0)是如何确定UV沿着V方向的偏移量的.在这里TP是错误的结果,因为视差贴图仅是求近似值,而无意寻找V和表面交点的确切位置.
此算法的主要优势就是仅多了一次对高度图的采样而提高GLSLshader的性能.下面是简单视差贴图shader的实现函数:
vec2 parallaxMapping(in vec3 V, in vec2 T, out float parallaxHeight)
{
// get depth for this fragment
float initialHeight = texture(u_heightTexture, o_texcoords).r;
// calculate amount of offset for Parallax Mapping
vec2 texCoordOffset = parallaxScale * V.xy / V.z * initialHeight;
// calculate amount of offset for Parallax Mapping With Offset Limiting
texCoordOffset = parallaxScale * V.xy * initialHeight;
// return modified texture coordinates
return o_texcoords - texCoordOffset;
}
陡峭视差贴图
陡峭视差贴图不同于简单近似视差贴图,不需要检查有效性和相关性.但是检查结果是否更接近有效值.这个方法的主要思路是将表面的深度分为等高的数层.然后从最上面的层数开始采样高度图.每次沿着V方向移动UV.如果采样点低于表面(当下层的深度大于采样深度),然后停止采样并使用最后使用的UV作为陡峭视差贴图的结果.
下图是陡峭视差贴图的实例.深度分为8层.每层高度为0.125.每层的UV偏移量为V.xy/V.z*Scale/numLayers.从最上面的层开始检查.也就是像素所在的层(黄色方块),以下为计算流程:
- 高度图中的深度大于层深度(点在表面之上),因此继续迭代采样.
- 沿着V方向偏移UV.选择深度等于0.125的下一个层.深度H(T1) = 0.625,高度图中的深度大于层深度,因此继续迭代采样.
- 沿着V方向偏移UV.选择深度等于0.25的下一个层.深度H(T2) = 0.4,高度图中的深度大于层深度,因此继续迭代采样.
- 沿着V方向偏移UV.选择深度等于0.375的下一个层.深度H(T3) = 0.2,高度图中的深度小于层深度,因此当下沿着V的采样点在表面之下,我们找到了最接近交点的采样点.Tp= T3.
从上图可以看到纹理坐标T3距离V与表面的交点距离还是很远.但是与基础视差相比更加接近有效结果.如果需要更精确的结果.可以增加采样层数.
陡峭视差的最大缺点在于它将深度分成若干层.如果层数比较大.那么性能会下降.如果层数太小.您会注意右图所示的锯齿.您可以根据V和N的角度来差值最小,最大采样来确定层数.性能和锯齿的问题可以在浮雕视差或者映射视差贴图(POM)中得到解决.这些内容会在后面的教程中介绍.
下面是陡峭视差的shader实现:
vec2 parallaxMapping(in vec3 V, in vec2 T, out float parallaxHeight)
{
// determine number of layers from angle between V and N
const float minLayers = 5;
const float maxLayers = 15;
float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0, 0, 1), V)));
// height of each layer
float layerHeight = 1.0 / numLayers;
// depth of current layer
float currentLayerHeight = 0;
// shift of texture coordinates for each iteration
vec2 dtex = parallaxScale * V.xy / V.z / numLayers;
// current texture coordinates
vec2 currentTextureCoords = T;
// get first depth from heightmap
float heightFromTexture = texture(u_heightTexture, currentTextureCoords).r;
// while point is above surface
while(heightFromTexture > currentLayerHeight)
{
// to the next layer
currentLayerHeight += layerHeight;
// shift texture coordinates along vector V
currentTextureCoords -= dtex;
// get new depth from heightmap
heightFromTexture = texture(u_heightTexture, currentTextureCoords).r;
}
// return results
parallaxHeight = currentLayerHeight;
return currentTextureCoords;
}