Vulkan_法线映射、视差映射、陡视差映射和视差遮挡映射

47 篇文章 9 订阅

本部分主要实现基于纹理信息的多种纹理映射方法:法向映射、视差映射、陡视差映射和视差遮挡映射(质量最好,性能最差)来模拟深度。

贴图技术

首先,本部分场景仅是简单的创建一个平面及加载纹理,基本的vulkan加载创建等就不再啰嗦,直接进入主题GLSL实现。

下面的顶点和片元着色器是视差映射和自阴影的基础模板:顶点着色器把光照向量和摄像机向量变换到切空间。片元着色器调用视差映射的相关函数,然后计算自阴影系数,并计算最终光照后的颜色值。

首先来看一下:顶点着色器

#version 450

layout (location = 0) in vec3 inPos;
layout (location = 1) in vec2 inUV;
layout (location = 2) in vec3 inNormal;
layout (location = 3) in vec3 inTangent;
layout (location = 4) in vec3 inBiTangent;

layout (binding = 0) uniform UBO 
{
	mat4 projection;
	mat4 view;
	mat4 model;
	vec4 lightPos;
	vec4 cameraPos;
} ubo;

layout (location = 0) out vec2 outUV;
layout (location = 1) out vec3 outTangentLightPos;
layout (location = 2) out vec3 outTangentViewPos;
layout (location = 3) out vec3 outTangentFragPos;

void main(void) 
{
	gl_Position = ubo.projection * ubo.view * ubo.model * vec4(inPos, 1.0f);
	outTangentFragPos = vec3(ubo.model * vec4(inPos, 1.0));   
	outUV = inUV;
		
	vec3 T = normalize(mat3(ubo.model) * inTangent);
	vec3 B = normalize(mat3(ubo.model) * inBiTangent);
	vec3 N = normalize(mat3(ubo.model) * inNormal);
	mat3 TBN = transpose(mat3(T, B, N));

	outTangentLightPos = TBN * ubo.lightPos.xyz;
	outTangentViewPos  = TBN * ubo.cameraPos.xyz;
	outTangentFragPos  = TBN * outTangentFragPos;
}

此处很简单,都是之前有介绍过的内容,其中TBN矩阵的转换原理不明白的可以参照前文GLSL-TBN矩阵

接下来我们逐步来看片元着色器,其中入口main及管线数据如下:

#version 450

layout (binding = 1) uniform sampler2D sColorMap;
layout (binding = 2) uniform sampler2D sNormalHeightMap;

layout (binding = 3) uniform UBO 
{
	float heightScale;
	float parallaxBias;
	float numLayers;
	int mappingMode;
} ubo;

layout (location = 0) in vec2 inUV;
layout (location = 1) in vec3 inTangentLightPos;
layout (location = 2) in vec3 inTangentViewPos;
layout (location = 3) in vec3 inTangentFragPos;

layout (location = 0) out vec4 outColor;

void main(void) 
{
	vec3 V = normalize(inTangentViewPos - inTangentFragPos);
	vec2 uv = inUV;

	if (ubo.mappingMode == 0) {
		//"常规贴图"
		outColor = texture(sColorMap, inUV);
	} else {
	    //case 1: //"法线贴图"
		switch(ubo.mappingMode) {
			case 2:	//"视差贴图"
				uv = parallaxMapping(inUV, V);
				break;
			case 3:	//"陡峭视差贴图"
				uv = steepParallaxMapping(inUV, V);
				break;
			case 4: //"视差阻塞贴图"
				uv = parallaxOcclusionMapping(inUV, V);
				break;
		}

		//在(可能)丢弃前执行取样:这是为了避免非均匀控制流中的隐式导数。
		vec3 normalHeightMapLod = textureLod(sNormalHeightMap, uv, 0.0).rgb;
		vec3 color = texture(sColorMap, uv).rgb;

		//在纹理坐标异常处舍弃片段
		if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
			discard;
		}

		vec3 N = normalize(normalHeightMapLod * 2.0 - 1.0);
		vec3 L = normalize(inTangentLightPos - inTangentFragPos);
		vec3 R = reflect(-L, N);
		vec3 H = normalize(L + V);  
   
		vec3 ambient = 0.3 * color; //环境光
		vec3 diffuse = max(dot(L, N), 0.0) * color;//漫反射
		vec3 specular = vec3(0.15) * pow(max(dot(N, H), 0.0), 32.0);//镜面反射

		outColor = vec4(ambient + diffuse + specular, 1.0f);
	}	
}

此处定义了多中贴图映射的方法入口。

1、常规贴图(texture mapping)

其中最简单的便是常规贴图(ubo.mappingMode == 0),直接采样纹理贴图:
可见如下效果:
在这里插入图片描述

2、法线映射(Normal mapping)

在三维计算机图形学中,法线贴图(Normal mapping)是一种模拟凹凸处光照效果的技术,是凸凹贴图的一种实现。法线贴图可以在不添加多边形的前提下,为模型添加细节。常见的使用场景是为低多边形模型改善外观、添加细节,此时的法线贴图一般根据高多边形模型或高度贴图生成。
法线贴图通常以普通RGB图像的形式存储,其中的R、G、B分量分别对应法线的X、Y、Z坐标。

在我们的程序中,从main中我们可以看到,在法线映射的时候(ubo.mappingMode == 1),我们直接textureLod使用显式的细节级别执行纹理查找从高度贴图中进行采样。执行结果如下图:
在这里插入图片描述
从上图中,我们可以明显的看出在相对于常规贴图,法线贴图具有明显的凹凸改善效果。

3、视差映射(parallax mapping)

视差映射技术的主要任务是修改纹理坐标,让平面看起来像是立体的。主要计算都是在Fragment Shader中进行。看看下面的图片。水平线0.0表示完全没有凹陷的深度,水平线1.0表示凹陷的最大深度。实际的几何体并没改变,其实一直都在0.0水平线上。图中的曲线代表了高度图中存储的高度数据。

设当前点片元是图片中用黄色方块高亮出来的那个点,这个点的纹理坐标是T0。向量V是从摄像机到点的方向向量。用坐标T0在高度图上采样,你能得到这个点的高度值H(T0)=0.55。这个值不是0,所以点并不是在表面上,而是凹陷下去的。所以你得把向量V继续延长直到与高度图定义出来的表面最近的一个交点。这个交点我们说它的深度就是H(T1),它的纹理坐标就是T1。所以我们就应该用T1的纹理坐标去对颜色和法线贴图进行采样。

所以说,所有视差映射技术的主要目的,就是要精确的计算摄像机的向量V和高度图定义出来的表面的交点
在这里插入图片描述
视差映射的计算是在切空间进行的(跟法线映射一样)。所以指向光源的向量(L)和指向摄像机的向量(V)应该先被变换到切空间。在用视差映射计算出来新的纹理坐标之后,你可以用这个坐标来计算自阴影,可以从漫反射贴图读取颜色以及从发现贴图读取法线。

在着色器中代码为

//"视差贴图"
vec2 parallaxMapping(vec2 uv, vec3 viewDir) 
{
	float height = 1.0 - textureLod(sNormalHeightMap, uv, 0.0).a;
	vec2 p = viewDir.xy * (height * (ubo.heightScale * 0.5) + ubo.parallaxBias) / viewDir.z;
	return uv - p;  
}

运行,可见如下效果:
在这里插入图片描述
可以看到像比如法线贴图,视差贴图更进一步的处理了图片的凹凸细节。

4、陡视差映射(steep parallax mapping)

陡峭视差映射,不像简单的视差映射近似,并不只是简单粗暴的对纹理坐标进行偏移而不检查合理性和关联性,会检查结果是否接近于正确值。这种方法的核心思想是把表面的深度切分成等距的若干层。然后从最顶端的一层开始采样高度图,每一次会沿着V的方向偏移纹理坐标。如果点已经低于了表面(当前的层的深度大于采样出的深度),停止检查并且使用最后一次采样的纹理坐标作为结果。

陡峭视差映射的工作方式在下面的图片上举例。深度被分割成8个层,每层的高度值是0.125。每层的纹理坐标偏移是V.xy/V.z * scale/numLayers。从顶层黄色方块的位置开始检查,下面是手动计算步骤:

  1. 层的深度为0,高度图深度H(T0)大约为0.75。采样到的深度大于层的深度,所以开始下一次迭代。
  2. 沿着V方向偏移纹理坐标,选定下一层。层深度为0.125,高度图深度H(T1)大约为0.625。采样到的深度大于层的深度,所以开始下一次迭代。
  3. 沿着V方向偏移纹理坐标,选定下一层。层深度为0.25,高度图深度H(T2)大约为0.4。采样到的深度大于层的深度,所以开始下一次迭代。
  4. 沿着V方向偏移纹理坐标,选定下一层。层深度为0.375,高度图深度H(T3)大约为0.2。采样到的深度小于层的深度,所以向量V上的当前点在表面之下。我们找到了纹理坐标Tp=T3是实际交点的近似点。

在这里插入图片描述
从上图你能看到,其实纹理坐标T3还是离交点挺远的。但是这个纹理坐标已经比视差映射要接近正确结果了。如果你想得到更精确的结果,增加层的数量。

陡峭视差映射的主要优势在于它把深度切分成了有限数量的层。如果层数很多,那性能就会低。但如果层数少,就会有明显的锯齿现象产生,就像下面这张图一样。你也可以根据摄像机向量V和多边形法向N之间的夹角来动态的决定层的数量。

//"陡峭视差贴图"
vec2 steepParallaxMapping(vec2 uv, vec3 viewDir) 
{
	float layerDepth = 1.0 / ubo.numLayers;
	float currLayerDepth = 0.0;
	vec2 deltaUV = viewDir.xy * ubo.heightScale / (viewDir.z * ubo.numLayers);
	vec2 currUV = uv;
	float height = 1.0 - textureLod(sNormalHeightMap, currUV, 0.0).a;
	for (int i = 0; i < ubo.numLayers; i++) {
		currLayerDepth += layerDepth;
		currUV -= deltaUV;
		height = 1.0 - textureLod(sNormalHeightMap, currUV, 0.0).a;
		if (height < currLayerDepth) {
			break;
		}
	}
	return currUV;
}

运行,可见这种视图看起来会更加逼真,显示效果会好点:
在这里插入图片描述

5、视差遮挡映射( parallax occlusion mapping)

视差遮蔽映射(POM)是陡峭视差映射的另一个改进版本。
在这里插入图片描述
视差遮蔽映射简单的对陡峭视差映射的结果进行插值。请看上图,POM使用相交之后的层深度(0.375,陡峭视差映射停止迭代的层),上一个采样深度H(T2)和下一个采样深度H(T3)。从图片中你能看到,视差遮蔽映射的插值结果是在视向量V和H(T2)和H(T3)高度的连线的交点上。这个交点已经足够接近实际交点(标记为绿色的点)了。

图片对应的手动计算步骤:

  1. nextHeight = H(T3) - currentLayerHeight
  2. prevHeight = H(T2) - (currentLayerHeight - layerHeight)
  3. weight = nextHeight / (nextHeight - prevHeight)
  4. Tp = T(T2) weight + T(T3) (1.0 - weight)

视差遮蔽映射可以使用相对较少的采样次数产生很好的结果。但视差遮蔽映射比浮雕视差映射更容易跳过高度图中的小细节,也更容易在高度图数据产生大幅度的变化时得到错误的结果。

//"视差遮蔽贴图"
vec2 parallaxOcclusionMapping(vec2 uv, vec3 viewDir) 
{
	float layerDepth = 1.0 / ubo.numLayers;
	float currLayerDepth = 0.0;
	vec2 deltaUV = viewDir.xy * ubo.heightScale / (viewDir.z * ubo.numLayers);
	vec2 currUV = uv;
	float height = 1.0 - textureLod(sNormalHeightMap, currUV, 0.0).a;
	for (int i = 0; i < ubo.numLayers; i++) {
		currLayerDepth += layerDepth;
		currUV -= deltaUV;
		height = 1.0 - textureLod(sNormalHeightMap, currUV, 0.0).a;
		if (height < currLayerDepth) {
			break;
		}
	}
	vec2 prevUV = currUV + deltaUV;
	float nextDepth = height - currLayerDepth;
	float prevDepth = 1.0 - textureLod(sNormalHeightMap, prevUV, 0.0).a - currLayerDepth + layerDepth;
	return mix(currUV, prevUV, nextDepth / (nextDepth - prevDepth));
}

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值