用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为: https://www.zhihu.com/equation?tex=+EH+%3D+%5Cdfrac%7BEJ%7D%7BIJ%7D+%5Ccdot+GH+ 虽然无法直接得到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为 https://www.zhihu.com/equation?tex=+t+%3D+%5Cdfrac%7Ba+-+c%7D%7Ba+-+c+%2B+d+-+b%7D+ 代码实现如下:
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
Parallax
视差贴图(Parallax Mapping)学习笔记
页:
[1]