视差映射
全库目录221
本文目录15
给一块砖墙平面贴上颜色和法线,斜看时砖缝仍停在原来的位置。我们让观察射线进入砖缝的高度层,找到交点,再把这一个交点交给材质采样和朝光查询。一张平面纹理可以描述颜色,也可以描述表面的局部形状。法线贴图改变着色方向,视差映射移动纹理坐标,位移映射才真正改变几何位置。三者解决的问题和产生的遮挡不同。
高度图、深度图和法线贴图
高度图(Height Map)保存表面相对基准面的高度。常见约定是白色高、黑色低。深度图在视差算法中常表示从表面顶层向内的距离,因此可能使用:
资产和 Shader 必须使用同一约定。约定相反时,凸起会显示成凹陷。
法线贴图保存的是局部表面方向。它能改变漫反射和高光,却不能改变纹理坐标,也不能产生真实轮廓和几何遮挡。高度图可以离线转换成法线贴图,但法线积分回高度通常没有唯一解。
切线空间与UV尺度
UV、高度图和法线贴图都附着在表面的切线坐标系中。这里把观察方向 定义为从表面指向相机,并让切线空间的 。变换后:
- 表示视线沿纹理平面的移动方向;
- 表示视线离开表面的程度;
- UV 偏移可以直接沿 计算。
TBN 的生成规则、镜像 UV 手性和法线贴图 Y 轴约定仍由 法线贴图、IBL、Cubemap环境采样 负责。视差和法线必须采样同一组偏移后的 UV,否则凹凸位置会错开。
沿视线寻找高度层交点
基础视差映射
把表面看成厚度为 的高度层。使用一次高度采样估计视线在层内走过的距离:
是归一化深度走完一层时对应的UV缩放,不直接等同于世界高度。若T/B为单位正交方向,一单位UV分别覆盖L_u、L_v米,物理厚度为H米,就应分别使用H/L_u、H/L_v;UV存在剪切时需要完整表面参数化的度量。下述标量heightScale假定两方向单位尺度相同。 防止掠射角除以接近零的 。减号取决于观察向量定义和纹理坐标方向,结果反向时应先检查空间和符号。
下面的 GLSL 函数使用白色为高处的高度图,输入观察方向已经归一化到切线空间:
vec2 ParallaxUV(
sampler2D heightMap,
vec2 uv,
vec3 viewDirTS,
float heightScale)
{
float depth = 1.0 - texture(heightMap, uv).r;
float safeZ = max(viewDirTS.z, 0.05);
vec2 ray = viewDirTS.xy / safeZ;
return uv - ray * depth * heightScale;
}这段代码只有一次高度采样,适合较浅、变化平缓的表面。高度变化大时,一次采样无法找到视线与高度场的真实交点。取一维截面,起点u=0.6,整层位移rayUV=0.2,深度场D(u)=u。一次采样得到深度0.6,于是返回u=0.48;但这个位置的表面深度是0.48,射线已经走到0.6,二者并不相交。真实交点满足t=D(0.6−0.2t),解得t=0.5、u=0.5。错误来自用起点的深度代替了沿途不断改变的深度。逐层采样正是为了重新比较这两份数据。
Offset Limiting
基础公式在掠射角会因为 产生很大的偏移。Offset Limiting 省略除法,直接使用 :
偏移更稳定,斜视时的深度感也会减弱。它适合高度很浅、稳定性优先的材质。
陡峭视差映射
陡峭视差映射(Steep Parallax Mapping)把深度范围 分成多层。算法沿切线空间视线逐层移动 UV,并比较“当前射线深度”和“高度场深度”。第一次穿过表面时停止。射线从顶层向内行进,因而算法追踪的是纹理定义的局部高度层,几何剪影仍由原网格决定。
观察越接近表面,射线在 UV 平面走得越远,需要更多层:
固定层数容易在正视时浪费采样,在掠射角出现台阶。动态层数把预算放到更困难的角度。
视差遮蔽映射
视差遮蔽映射(Parallax Occlusion Mapping,POM)在首次穿过后,使用穿过前后的两个样本做线性插值,得到更连续的交点。
令归一化射线深度t从0增加到1,UV平面中的射线写成 uv(t)=uv0-rayUV*t,其中 rayUV=viewDirTS.xy/max(viewDirTS.z,0.05)*uvDepthScale。深度函数D以黑色表示顶层,残差 F(t)=D(uv(t))-t。F为正时射线仍在表面上方,首次变为非正时取得交点区间。
取整数层数N,每步t=i/N。若起点D(uv0)=0,交点就是uv0,应立即返回;否则保存上一步的正残差before。当本步残差error非正时,区间内线性比例为 w=before/(before-error),在前后UV之间按w插值。分母由正残差减非正残差构成,正常区间内严格为正。这个比例来自区间内的线性残差F(w)=(1−w)before+w·error,令它为零即可得到w。比如前后残差为0.1和−0.15,零点在区间的0.4处;取两端中点则仍有−0.025的残差。插值消除了局部线性模型中的台阶,曲面和高频高度场仍有模型误差。
bool Outside(vec2 uv) { return any(lessThan(uv,vec2(0))) || any(greaterThan(uv,vec2(1))); }
struct PomHit { vec2 uv; float depth; bool valid; };
PomHit PomUV(vec2 origin, vec3 view, vec2 dx, vec2 dy) {
if (isolatedPatch!=0 && Outside(origin)) return PomHit(origin,0.0,false);
int layers = clamp(int(round(mix(float(maxLayers),float(minLayers),view.z))),1,128);
vec2 ray = view.xy / max(view.z,0.05) * uvDepthScale;
float before = textureGrad(depthMap,origin,dx,dy).r;
if (before <= 0.0) return PomHit(origin,0.0,true);
vec2 previous = origin;
float previousDepth = 0.0;
for (int i=1; i<=layers; ++i) {
float depth = float(i)/float(layers);
vec2 current = origin-ray*depth;
if (isolatedPatch!=0 && Outside(current)) return PomHit(current,depth,false);
float error = textureGrad(depthMap,current,dx,dy).r-depth;
if (error <= 0.0) {
float t = before/(before-error);
return PomHit(mix(previous,current,t),mix(previousDepth,depth,t),true);
}
previous=current;
previousDepth=depth;
before=error;
}
return PomHit(origin-ray,1.0,true);
}这是GLSL 4.50片元机制函数,view是已归一化、Z为正的切线空间观察方向;全局minLayers/maxLayers给层数预算,uvDepthScale是两方向尺度,depthMap采样值有限且处于[0,1]。dx/dy在一致控制流中由原始UV求得。isolatedPatch=0表示连续平铺,纹理Sampler使用Wrap;非零表示独立的[0,1]面片,越界返回valid=false。采样步跨出边界时采用保守无命中,最后一小段内的交点也可能被舍弃,减小步长可降低边缘侵蚀。有效返回值包含同一交点的UV与归一化深度:顶层为0,区间内按同一权重插值;在有限精度下未取得交叉时回退到底层1。这份接口供后续材质采样、位置重建和朝光查询共同消费。完整声明与诊断输出见pom.frag,宿主创建纹理、Sampler、参数缓冲和图形管线。
恒定深度D=0.3、起点u=0.6、rayUV=0.2时,交点t=0.3、u=0.54,与层数无关的线性插值应恢复该值。若高度信号在两次采样间快速起伏,首次采样到的区间仍可能跳过更前面的交点;增加层数降低漏检,并不提供任意高度场的严格求交保证。
UV 导数和 Mipmap
POM 会在循环中多次读取变化后的 UV。循环次数或分支在相邻像素间不一致时,隐式导数的 LOD 选择可能不稳定。可以在进入循环前保存原始 UV 的 dFdx/dFdy,再使用 textureGrad;HLSL中对应SampleGrad。保存原导数能稳定采样层级,但没有包含视差变换本身的导数,掠射变形处仍是足迹近似,需结合远处渐隐与实际画面检查。
高度图也需要 Mipmap,但普通平均会改变高低峰形状。远处通常允许减弱高度缩放或层数,把稳定性和采样成本放在细小视差之前。
交点的材质与光照输入
砖缝交点已经确定,材质和光照必须消费同一份结果。独立面片的valid=false先由材质入口丢弃;选择平面回退时,统一改用原UV、零深度并跳过高度自阴影。有效交点先用于贴图采样,再恢复光照需要的位置,朝光查询因此有了确定的起点。
材质采样沿用进入步进前保存的UV梯度:基础色、粗糙度与切线空间法线都在hit.uv用显式梯度采样。直接让这些采样隐式求导,会把不同像素的命中跳变和循环分支混入LOD选择;高度图稳定并不保证其余贴图稳定。若材质图采用uvMaterial=scale*hit.uv+offset,采样梯度也分别乘同一scale。法线图解码并归一化后,通过与步进相同的T、B、N变到着色空间;其法线用于BRDF,几何法线与高度厚度仍负责交点重建。沿用原始梯度是稳定的过滤近似,掠射处强拉伸仍需靠前文的衰减和采样预算控制。
命中位置与光照输入
点光方向需要命中位置时,可以从同一份交点恢复局部虚拟表面。设原网格位置为P0,世界单位切线为T、B,几何法线为N,一单位UV覆盖Lu、Lv米,高度层厚度H米;令duv=hit.uv-originUV,得到Phit=P0+T*(duv.x*Lu)+B*(duv.y*Lv)-N*(H*hit.depth)。它与前文两方向尺度(H/Lu,H/Lv)配套。保留步进中的未环绕UV差,Wrap只在纹理寻址时发生;先把UV夹回[0,1]再求差,会在平铺接缝产生一个纹理周期的错误位移。
例如Lu=Lv=1米、H=0.1米,交点UV相对原点移动−0.02、0,深度0.5,则虚拟位置沿T移动−0.02米并沿N向内0.05米。点光方向改用normalize(lightPosition-Phit),距离衰减也使用同一Phit,再转到TBN求高度场可见性。朝光查询的长度由下一节按这份位置确定;只改方向而保留原距离会混用两个表面。材质法线仍从命中UV读取,TBN由原表面提供,因此这是局部平面上的虚拟着色位置,光栅深度、轮廓和场景碰撞仍来自网格。邻近光源与厚层的几何差异可由这两种位置的差直接观察。
观察方向的Z小于0.05时,前文用分母下限压短了UV路径;此时Phit恢复的是这条受限路径上的虚拟交点,不再严格位于相机穿过P0的视线上。用同一Phit计算点光只能保持近似内部一致,不能消除掠射处的形状压缩。若需要严格的视线求交,应使用真实Vz并配足采样与边界处理,或在进入该角度前把高度效果渐隐到零。
高度场自阴影
观察射线求出了可见点,但这个点到光源之间也可能穿过凸起。仍采用深度向内为正的约定,设命中位置为uvHit、命中深度为tHit,光方向L从表面指向光源,且Lz>0。让q表示朝光走过的归一化垂直深度,则光线上的位置为:
朝光查询读取PomHit中已有的UV与深度。正视时所有深度映到同一UV,因此只保存UV无法恢复这条光线的起点;POM在首次交叉时保存的深度在这里成为必要输入。
方向光的查询终点qEnd=tHit;点光则先计算从命中点到光源沿N方向的正距离zLight,以物理厚度H换成qLight=zLight/H,取qEnd=min(tHit,qLight)。光位于层内时,查询在光源处结束;光位于层外时,在离开高度层处结束。H必须为正,高度效果为零时直接走原表面光照。下述循环以q=qEnd/shadowSteps作为首个正偏移,到qEnd结束;qEnd=0直接返回可见。每步读取D(uv(q));如果t(q)>D(uv(q))+bias,光线点处在表面内部,判为遮挡。bias以归一化深度计量,用于容纳交点误差。它过大会漏掉浅凸起,过小会把起点误差判成自遮挡。Lz接近零时路径很长,可限制采样预算并渐隐该近似;Lz≤0的直射贡献按本页单面表面模型置零。
例如砖缝命中深度0.6,点光在深度0.4,qEnd只有0.2;深度0.3处的凸起位于光源之后,不应参与这次遮挡查询。若仍查到顶层q=0.6,这个凸起会错误地使砖缝变暗。光与Phit重合时方向不可归一化,按光源半径模型处理或跳过该退化光样本;位于表面背侧的光仍按单面模型没有直射贡献。
例如tHit=0.6,在q=0.2处光线深度为0.4。若采样表面D=0.3、bias=0.01,0.4>0.31,光线已经进入凸起,返回阴影;若D=0.5,则该点仍在空处,应继续步进。固定步数同样会漏过两次采样之间的薄凸起,所以这只是高度场内的硬阴影近似。远处物体投下的阴影仍由场景阴影系统提供。可以将两者的可见性相乘,再作用于直射项;环境光不能直接套用这条单方向光线的结果。
float HeightVisibility(PomHit hit, vec3 light, float qEnd, vec2 dx, vec2 dy) {
if (!hit.valid) return 0.0;
if (light.z<=0.0) return 0.0;
qEnd=clamp(qEnd,0.0,hit.depth);
if (qEnd<=0.0) return 1.0;
vec2 ray=light.xy/max(light.z,0.05)*uvDepthScale;
int steps=clamp(shadowSteps,1,128);
float visibility=1.0;
for (int i=1;i<=steps;++i) {
float q=qEnd*float(i)/float(steps);
vec2 uv=hit.uv+ray*q;
if (isolatedPatch!=0 && Outside(uv)) break;
if (hit.depth-q>textureGrad(depthMap,uv,dx,dy).r+shadowBias) {
visibility=0.0;
break;
}
}
// Fade the finite-budget approximation to visible at grazing light angles.
return mix(1.0,visibility,smoothstep(0.0,0.05,light.z));
}函数接收上一节的同一份PomHit,light为归一化切线空间朝光方向;全局shadowSteps给采样预算,shadowBias为非负归一化深度偏置。qEnd由光类型和同一命中位置产生;采样足迹沿用显式梯度。独立面片越界结束查询,片外不再延展本高度场。Z在0到0.05之间时,路径除法下限限制长度,同时将阴影近似渐隐到可见,避免有限步数的长路径在掠射处跳变;这会漏掉部分真实遮挡,直射余弦仍会压低掠射贡献。独立面片越界时必须按资产边界结束查询,不能把Clamp延展出的边缘当成真实阻挡物。
完整片元入口先在一致控制流中计算导数,再连续调用两个函数:
vec2 dx=dFdx(inputUV), dy=dFdy(inputUV);
vec3 view=normalize(inputViewTS);
if (view.z<=0.0) { outputColor=vec4(inputUV,0,1); return; }
PomHit hit=PomUV(inputUV,view,dx,dy);
if (!hit.valid) discard;
float visibility=HeightVisibility(hit,normalize(lightTS),hit.depth,dx,dy);
outputColor=vec4(hit.uv,hit.depth,visibility);这里演示方向光,故终点使用hit.depth。入口在采样和光照之前处理无命中;平面回退规则见“交点的材质与光照输入”。诊断输出的RG是交点UV,B是归一化深度,A是高度场可见性。正常材质将该可见性与场景阴影相乘后用于直射项。
点光入口先按前文恢复Phit,再计算朝光方向、距离和qEnd,最后调用HeightVisibility。这样从砖墙观察射线得到的一份交点,连续决定了材质位置、光照衰减和遮挡区间。
边界和局限
- 平铺材质在采样时Wrap,射线位置保持连续。独立面片在步进中检查UV边界,越界时返回无命中并由材质决定丢弃或回退原表面;Clamp只有在资产明确把边缘延展为高度场时才成立,并对观察射线和自阴影使用相同延展规则。
- 视差只改变采样位置,模型轮廓、深度缓冲和碰撞保持原几何形状。
- 自阴影步进只检查当前高度层,采样数和偏移决定漏光与自遮挡误差。
- 大高度、薄轮廓和近距离剪影适合几何位移或 Tessellation。
- 掠射角需要限制偏移、增加层数或渐隐效果,避免纹理拉伸和 UV 跳变。
- 动态循环的成本随角度和内容变化,需要在目标 GPU 上检查 Shader 指令、纹理采样和 Wave 发散。
实现与验证
- 使用斜光照的单个四边形,准备棋盘色、高度和法线三张可对齐纹理。
- 依次显示原始 UV、基础视差、陡峭视差和 POM。
- 固定相机后把层数设为 4、16、64,观察台阶和采样成本。
- 将高度缩放设为 0,确认所有纹理回到原始 UV。
- 旋转相机到掠射角,检查偏移方向、UV 越界和层数变化。
- 单独显示偏移后的 UV 与最终深度误差,定位凹凸翻转和交点错误。
一维数值推演、自阴影阻挡与漏检反例见 retained_review/pom/check.py,运行 python -B 知识库/examples/retained_review/pom/check.py。该程序只验证本文标量关系,不替代纹理采样或GPU图像检查。
CPU模型pom_line检验顶层零深度、恒定深度和线性高度场交点,运行 python -B 知识库/examples/revision_06_08/test_reference.py。GLSL片段已用glslangValidator编译为SPIR-V;该结果仅证明着色器编译通过,纹理过滤、画面与GPU成本仍为尚未验证。
相关主题
- 空间变换
- 渲染帧流程
- 法线贴图、IBL、Cubemap环境采样
- 纹理采样与过滤
- Tessellation与位移
- 纹理图集、Texture Streaming、Virtual Texture
- 帧瓶颈诊断
参考资料
- Natalya Tatarchuk,Practical Parallax Occlusion Mapping:SIGGRAPH2006,射线步进、首次交叉与线性重建;2026-10-22核读PDF第28—33页的步进与交点重建。
- GLSL 4.60规范:2026-10-22核读第8.9.3节显式梯度与textureGrad表;本文样例使用GLSL4.50并实际编译。