用Unity实现Parallax Mapping

用Unity实现Parallax Mapping

使用normal map可以增加物体表面的凹凸程度,但是以一个比较小的角度观察物体表面,还是会缺乏一些细节。parallax mapping就是通过给每个像素添加上高度信息,在采样时根据高度信息和视线方向对uv进行一定的偏移,来达到模拟凹凸的效果。

首先来看没有parallax mapping,只使用normal map的效果:

在这里插入图片描述

由于纹理坐标是在切线空间的,我们首先需要把视线方向变换到切线空间:

		float3x3 objectToTangent = float3x3(
			v.tangent.xyz,
			cross(v.normal, v.tangent.xyz) * v.tangent.w,
			v.normal
		);
		i.tangentViewDir = mul(objectToTangent, ObjSpaceViewDir(v.vertex));

然后对uv坐标进行偏移:

		i.tangentViewDir = normalize(i.tangentViewDir);
		i.uv.xy += i.tangentViewDir.xy * _ParallaxStrength;

此时的效果如下:

在这里插入图片描述

接下来,我们再把纹理的高度信息加进去,让比较高的高度偏移更大,显得更凸,让比较低的高度偏移为负,显得更凹:

		float height = tex2D(_ParallaxMap, i.uv.xy).g;
		height -= 0.5;
		height *= _ParallaxStrength;
		i.uv.xy += i.tangentViewDir.xy * height;

此时效果如下:

在这里插入图片描述

但是这种情况下,_ParallaxStrength参数调的过大会导致撕裂感明显:

在这里插入图片描述

出现的原因,本质上是我们直接拿tangentViewDir的xy分量来作为uv的偏移量,而这个是不准的。如图:

在这里插入图片描述

EI为视线向量,E为没有高度时本来的采样点,但由于高度阻挡,视线向量和阻挡物相交于G,因此此时的采样点应当为H,即uvOffset为EH。EJ为视线向量在uv上的投影,即tangentViewDir.xy,而IJ为视线向量的高度,即tangentViewDir.z。那么,实际上真正的uvOffset,即EH为:
E H = E J I J ⋅ G H EH = \dfrac{EJ}{IJ} \cdot GH EH=IJEJGH
虽然无法直接得到GH,但可以拿原始采样点E的高度作为估计,所以我们有

i.tangentViewDir.xy /= i.tangentViewDir.z;
i.uv.xy += i.tangentViewDir.xy * height;

为了防止在极小的角度时z分量趋向于零而导致的问题,我们可以手动加上一个bias:

			i.tangentViewDir.xy /= (i.tangentViewDir.z + PARALLAX_BIAS);

在这里插入图片描述

由上图可知,我们是拿原始采样点的高度来估计实际视线遮挡物的高度的,因而依旧是不准确的。我们可以使用raymarching方法来进行精确估计,例如使用逐步逼近的方式,即每次前进一小步长,然后比较当前遮挡物的高度和视线的高度,如果当前视线不再被物体遮挡,则停止前进,返回上一个也就是最后一个被物体遮挡的uvOffset作为结果。由于这样实现比较麻烦,我们可以逆向思考,即每次后退一小步长,这样只要当视线被物体遮挡时,即可返回结果,如图所示:

在这里插入图片描述

	viewDir.xy /= (viewDir.z + PARALLAX_BIAS);
	float2 uvOffset = 0;
	float stepSize = 1.0 / PARALLAX_RAYMARCHING_STEPS;
	float2 uvDelta = viewDir * (stepSize * _ParallaxStrength);

	float stepHeight = 1;
	float surfaceHeight = GetParallaxHeight(uv);

	for (
		int i = 1;
		i < PARALLAX_RAYMARCHING_STEPS && stepHeight > surfaceHeight;
		i++
	) {		
		uvOffset -= uvDelta;
		stepHeight -= stepSize;
		surfaceHeight = GetParallaxHeight(uv + uvOffset);
	}

	return uvOffset;

在这里插入图片描述

不过这种方式依旧存在一定的误差。首先是步长的选取,如果步长选的过大,在高度变化频繁的区域,可能就会错过遮挡的点;其次我们是拿若干步长后遮挡物的高度作为结果的,而它并不能精确表示视线被遮挡时的高度,如图所示:

在这里插入图片描述

针对这个问题,我们可以采用插值的方式解决。只需记录前一次步长遮挡物的高度和视线高度,此时视线高度是大于遮挡物高度的;而当前步长则是遮挡物高度大于视线高度,因此在前一步长到当前步长的过程中,遮挡物高度越来越高,而视线高度越来越低,在某个点它们的值必然会出现相等的情况。问题就转化为了求两条线段的交点问题,如图所示:

在这里插入图片描述

在这里插入图片描述

进而可以得到插值的系数t为
t = a − c a − c + d − b t = \dfrac{a - c}{a - c + d - b} t=ac+dbac
代码实现如下:

	viewDir.xy /= (viewDir.z + PARALLAX_BIAS);
	float2 uvOffset = 0;
	float stepSize = 1.0 / PARALLAX_RAYMARCHING_STEPS;
	float2 uvDelta = viewDir * (stepSize * _ParallaxStrength);

	float stepHeight = 1;
	float surfaceHeight = GetParallaxHeight(uv);

	float2 prevUVOffset = uvOffset;
	float prevStepHeight = stepHeight;
	float prevSurfaceHeight = surfaceHeight;

	for (
		int i = 1;
		i < PARALLAX_RAYMARCHING_STEPS && stepHeight > surfaceHeight;
		i++
	) {
         prevUVOffset = uvOffset;
		prevStepHeight = stepHeight;
		prevSurfaceHeight = surfaceHeight;
        
		uvOffset -= uvDelta;
		stepHeight -= stepSize;
		surfaceHeight = GetParallaxHeight(uv + uvOffset);
	}

	float prevDifference = prevStepHeight - prevSurfaceHeight;
	float difference = surfaceHeight - stepHeight;
	float t = prevDifference / (prevDifference + difference);
	uvOffset = prevUVOffset - uvDelta * t;

	return uvOffset;

在这里插入图片描述

不过,上面这种做法利用了一个假设,就是遮挡物的高度在一个步长区间内是线性变化的,如果遮挡物的高度变化频繁,依旧可能是有问题的。我们还可以尝试使用类似二分查找的思路来解决这个问题,即步长不是一成不变的,在到达当前步长后,每次都以一半的步长进行下去,当视线高度和遮挡物高度的大小关系发生变化时,调整步长的方向,直到一定的迭代次数之后,返回结果,如图所示:

在这里插入图片描述

代码实现如下:

	viewDir.xy /= (viewDir.z + PARALLAX_BIAS);
	float2 uvOffset = 0;
	float stepSize = 1.0 / PARALLAX_RAYMARCHING_STEPS;
	float2 uvDelta = viewDir * (stepSize * _ParallaxStrength);

	float stepHeight = 1;
	float surfaceHeight = GetParallaxHeight(uv);

	for (
		int i = 1;
		i < PARALLAX_RAYMARCHING_STEPS && stepHeight > surfaceHeight;
		i++
	) {		
		uvOffset -= uvDelta;
		stepHeight -= stepSize;
		surfaceHeight = GetParallaxHeight(uv + uvOffset);
	}

	for (int i = 0; i < PARALLAX_RAYMARCHING_SEARCH_STEPS; i++) {
		uvDelta *= 0.5;
		stepSize *= 0.5;

		if (stepHeight < surfaceHeight) {
			uvOffset += uvDelta;
			stepHeight += stepSize;
		}
		else {
			uvOffset -= uvDelta;
			stepHeight -= stepSize;
		}
		surfaceHeight = GetParallaxHeight(uv + uvOffset);
	}
	return uvOffset;

在这里插入图片描述

如果你觉得我的文章有帮助,欢迎关注我的微信公众号:Game_Develop_Forever

Reference

[1] Parallax

[2] 视差贴图(Parallax Mapping)学习笔记

猜你喜欢

转载自blog.csdn.net/weixin_45776473/article/details/125245873