UE5视差材质与POM实战:高度图、UV偏移和性能优化
发布时间:2026/11/5 4:04:12 锦皓数字建站

简介这份资源是面向虚幻引擎5开发者与视觉美术方向学习者的视差材质实例工程针对材质编辑器中「高度图驱动深度错觉」这一进阶主题提供可直接打开运行的参考实现。包内共478个文件以uasset材质与贴图资产为主辅以umap示例关卡、ini工程配置、udd与dat缓存数据及uproject工程文件另含少量ush着色器源码与json、png辅助素材压缩包约918MB属于体量完整、可直接对照调试的项目级素材。目前已有1127人学习下载适合具备一定材质节点基础、希望突破平面贴图质感瓶颈的中级用户。资源覆盖九宫格分区域控制与单图版两种实现路线可观察高度图、视差偏移强度、视差映射向量与切线空间法线如何协同决定表面起伏工程中的示例关卡便于逐项修改参数体会强度过大导致的失真边界并对比简化工作流与可定制程度之间的取舍。读者可据此搭建自己的凹凸砖墙、地面裂缝或浮雕面板效果也能将其作为材质函数复用模板嵌入既有项目验证性能开销。1. 一块平墙看出砖缝深度UE5 视差材质到底替我们省了什么第一次把 UE5 视差材质贴到一面只有一个面的墙上时我心里是打鼓的法线贴图给了它明暗看着还行可一蹲下去、一走近砖缝立刻变回一张贴纸。视差材质解决的就是这件事——用一张高度图在像素级别把 UV 偏移掉让平面上不同部位“假装”处在不同深度镜头移动时砖缝会跟着视线错开。它适合建筑内景、地砖、砖墙、金属格栅这类大面积、视角又贴近的东西不想为了几厘米厚度去堆面数这是最划算的解法。这套虚幻5 材质方案真正难的从来不是节点长什么样而是高度图有没有做对、步进搜索有没有收敛。2. 拆开视差的数学UV 偏移、步进搜索与高度图的三个前提2.1 三种做法摆一起BumpOffset、单步视差与 POM 的边界先把可选路径列清楚不然很容易一上来就冲着最贵的那条路走。方案高度采样次数会不会遮挡典型用途常见翻车点BumpOffset 节点1不会远景墙面、移动端、次要物件高度大的地方 UV 直接串到隔壁砖单步视差自写1不会中景、砖缝浅的墙面掠射角下整片糊POM 步进 二分832 35会靠深度搜索主角级墙面、地砖、铭牌步数不够出层状条纹视差 Pixel Depth Offset同上会且能写深度需要和角色、雾、接触阴影交互偏移量给大了穿模真几何 / 置换0真遮挡特写、需要轮廓面数、内存直接爆炸UE5 里硬件细分被拿掉了想靠细分把平面“拱”出厚度这条路已经走不通剩下的现实选择基本就是 POM 加深度写入。所以选型的关键不是“哪个更准”而是“这块面在屏幕上占多大”。占屏超过四分之一、镜头会贴脸看的用 POM只在走廊尽头晃一下的BumpOffset 足够不要浪费采样。引擎自带一个ParallaxOcclusionMapping节点在材质编辑器里直接搜名字就能找到暴露了 Steps、Min Steps、Max Steps、Height Ratio 这些参数输出偏移后的 UV。它的质量够用但我一般还是自己写一遍 Custom 节点原因是自带节点的步数是常量输入我想按视角动态调另外自己写才能顺手把命中高度吐出来喂给 Pixel Depth Offset。2.2 高度图不是灰度随便画通道、sRGB 与压缩设置视差效果有八成问题出在高度图本身节点基本是无辜的。约定先统一白1.0代表最高、离观察者最近黑0.0代表最深的凹槽。这条约定跟引擎自带节点一致反了也不要紧把 UV 偏移的符号取反或者把高度值1 - h一下就行——但必须全项目统一不然一半材质凸、一半材质凹返工很痛苦。导入设置有几点是硬要求。sRGB 一定要关掉高度是线性数据不是颜色勾着 sRGB 会让中间调整体偏移砖缝的深度关系就歪了。压缩设置选 Masks 或 Grayscale别用默认的 DXT1/DXT5块压缩在灰度渐变上会产生阶梯步进搜索走到这些阶梯上会抖出一条条亮线这个现象我第一次遇到时盯着代码看了两个小时。8 位灰度只有 256 级步数上到 16 以上就能看出台阶条件允许就用 16 位灰度导入。还有一条容易被忽略非平铺的单张装饰板、铭牌、浮雕高度图四周必须留一圈压到 0 的边框宽度大概 48 像素。因为 UV 偏移会把采样点推出 [0,1]一推出界就采到对面的像素边缘会被拉出一条“果冻”似的色带。平铺的砖墙反而没这个问题偏移到隔壁 tile 本来就是正确的行为。提示从法线贴图反推高度图这件事能不做就不做。法线是梯度信息积分回来会累积误差做出来的高度图中间总是鼓一块砖缝的锐利边缘全丢了。要么让美术直接出灰度高度要么用能导出高度的素材。2.3 手写一次 UV 偏移把 ViewDir 转到切线空间视差的本质是一句话观察方向越斜同一个像素对应的高度面偏移越多。所以第一步是把相机方向转到切线空间——因为 UV 就活在切线空间里世界空间的偏移量跟 UV 没有可比性。做法是在材质里放一个Transform节点输入CameraVectorWSSource 选 World Space、Destination 选 Tangent Space切线基由VertexNormalWS、VertexTangentWS、VertexBinormalWS三个节点提供。输出就是一个 xyz 的切线空间视线方向习惯上叫 ViewDirTS其中 z 分量表示“有多正对着看”。拿到方向之后单步视差的偏移量可以这么算// Inputs: UV (float2), HeightMap (Texture2DObject), ViewDirTS (float3), HeightRatio (float) // 输出偏移后的 UV float3 V normalize(ViewDirTS); // 掠射角保护V.z 接近 0 时偏移量会指数级放大必须夹住 float vz max(abs(V.z), 0.08); // 采样高度LOD 固定 0避免 mip 把高度抹平 float h Texture2DSampleLevel(HeightMap, HeightMapSampler, UV, 0).r; // 负号决定凸还是凹反了就把这里取正 float2 offset -(V.xy / vz) * HeightRatio * h; return UV offset;HeightRatio是这个材质最重要的手感参数它把“高度 01”映射成 UV 空间里的偏移比例。砖墙常用 0.020.05地砖 0.030.08浮雕铭牌可以到 0.1。它跟物体的实际尺寸没有直接关系所以换一个模型就得重调这也是我在做材质实例时一定把它暴露出来的原因。把CameraVectorWS转过切线空间之后你会发现平放的平面和竖着的墙面同一个 HeightRatio 看起来深度差很多——这很正常因为平面的切线基方向不同倾斜视角下 V.xy 的模长不一样。想要跨物件统一观感就得按物件尺寸换算实际高度再反推 HeightRatio这是后面性能那章会提到的做法。Texture2DSampleLevel(..., 0)里的 0 是硬编码 LOD别省。高度图一旦被采到高 mip砖缝的锐利边就被平均掉了步进会变得又平又黏。3. 从零搭一个可用的视差材质参数、Custom 节点与深度写入3.1 参数集怎么切把美术要调的和你不想让他动得分来参数设计比代码更容易埋雷。我一般把参数分三层给美术天天拧的、只在材质实例里分档的、编译期决定的。最典型的错误就是把步数做成普通 Scalar 参数结果美术在实例里拉到 32整个项目的所有实例都按 32 步编译性能预算当场失控。参数类型默认说明HeightMapTexture无灰度高度sRGB 关闭、Masks 压缩HeightRatioScalar0.03手感参数允许美术在 0.010.1 调PdoScaleScalar0.01深度写入强度按物件尺寸给StepQualityStatic SwitchMid分 Low/Mid/High 三档编译期决定步数RefineStepsStatic Switch3二分修正次数High 档才开 5UseParallaxStatic Switchtrue低配机型直接关掉退回法线Static Switch和普通 Scalar 的区别是前者在编译期就折叠掉分支不同档位会编出不同的 shader后者是运行时插值所有实例共享最贵的那份。材质质量等级Material Quality Level配合Quality Switch节点也能做类似的事运行时切r.MaterialQualityLevel就能整体降档做多平台适配时这个比 Static Switch 更好用因为不用给每个材质单独做变体。材质域记得设成 Surface混合模式 Opaque着色模型用默认的 Default Lit 就行。视差不需要特殊的光照模型它只改 UV 和深度跟光照管线是解耦的。3.2 POM 的 Custom 节点写法步进 二分修正单步偏移在掠射角会崩因为一个像素的偏移量可能横跨好几条砖缝采样点直接跳到错误的位置上。POM 的做法是从高度 1 开始沿着视线方向一步一步往下走每一步采样一次当前 UV 的高度一旦采样到的高度大于等于当前射线高度就说明撞到表面了停。// Inputs: UV (float2), HeightMap (Texture2DObject), ViewDirTS (float3), // HeightRatio (float), Steps (float), RefineSteps (float) float2 uv0 UV; float3 V normalize(ViewDirTS); float vz max(abs(V.z), 0.08); float2 maxOffset -(V.xy / vz) * HeightRatio; float steps clamp(Steps, 4.0, 32.0); float stepSize 1.0 / steps; float2 stepUV maxOffset * stepSize; // 射线从顶端往下走 float rayH 1.0; float2 curUV uv0; float2 prevUV uv0; float prevRayH 1.0; [loop] for (int i 0; i 32; i) { if (i (int)steps) break; // LOD 固定 0防止 mip 把砖缝抹平 float h Texture2DSampleLevel(HeightMap, HeightMapSampler, curUV, 0).r; // 撞上了采样高度 射线高度 if (h rayH) break; prevUV curUV; prevRayH rayH; rayH - stepSize; curUV stepUV; } // 二分修正在最后一段里逼近交点35 次就够 [loop] for (int j 0; j 8; j) { if (j (int)RefineSteps) break; float2 midUV (curUV prevUV) * 0.5; float midRayH (rayH prevRayH) * 0.5; float h Texture2DSampleLevel(HeightMap, HeightMapSampler, midUV, 0).r; if (h midRayH) { curUV midUV; rayH midRayH; } else { prevUV midUV; prevRayH midRayH; } } return curUV;几个细节值得单独说。循环上限我写成常量 32再用if (i steps) break提前跳出而不是写成i steps的动态循环——动态循环在部分平台上会被编译器强制展开成 32 份指令数直接翻倍编译时间也跟着涨这个坑我在一个开放世界项目里踩得很实。[loop]属性是告诉编译器别自动展开配合 break 用是安全的。maxOffset里的负号跟高度图的约定绑定高度图白为高、且相机在物体正前方时UV 应该往视线反方向偏。第一次调的时候建议随便找一张砖墙把HeightRatio拉到 0.2肉眼确认凹凸方向然后把符号固定下来别再动。二分修正那几行看起来很像玄学其实就是在“最后一次没撞上的 UV”和“撞上的 UV”之间取中点重采样判断交点在哪一半重复几次误差就缩到 1/2^n。RefineSteps 给 3 就够给 5 肉眼几乎看不出差别但采样次数多了 60%。return出来的 UV 拿去接所有需要视差的贴图BaseColor、Normal、Roughness。注意 Normal 贴图一般也要跟着偏移不然光影和凹凸对不上砖缝会亮得不自然。3.3 接上 Pixel Depth Offset 与阴影让轮廓不再“贴纸”只改 UV 的视差有个致命伤深度缓冲里这面墙仍然是平的。结果是角色贴墙走过去脚下该有的遮挡阴影没有雾气和半透明物体穿过砖缝时也没有正确的穿插关系。解法是把“视差把表面推远了多远”写进Pixel Depth Offset引脚。命中的高度是现成的步进结束时rayH就是交点高度1 - rayH大致就是从顶面往下陷进去的比例。乘一个按物件尺寸给的标量接到 PDO// 接在上面 POM 代码的返回处多输出一个 float // PdoScale 单位近似为世界单位下的最大凹陷深度 float depth (1.0 - rayH) * PdoScale; return depth;PdoScale在米级砖墙上通常 0.0050.02超过 0.05 就会开始穿模角色站在墙前会被“吸”进去一截。PDO 的代价不小它会让深度预pass和 DXR 的命中判断都多一层所以只在主角级材质上开。阴影这块Shadow Pass Switch节点是后悔药。阴影 pass 里材质会用一套降级的计算如果你在里面也跑 32 步阴影贴图的渲染成本会翻好几倍更糟的是偏移后的 UV 让阴影深度对不上自阴影出现噪点。做法是Shadow Pass Switch的 Default 接完整的 POM 结果Shadow 分支接原始 UV 或者便宜的单步偏移。Nanite 网格上还有一层额外注意PDO 生效但基于距离场的 AO 和间接光对深度偏移不敏感砖缝里该黑的地方不会自己变黑。我一般在 BaseColor 里叠一张 AO 贴图或者把高度值直接乘进基础色做压暗视觉上补回来。3.4 性能开关步数、LOD 与移动端降级POM 的开销是逐像素的而且跟屏幕占比成正比。16 步加 3 次二分就是 19 次纹理采样一面铺满屏幕的地板光视差就能吃掉一多半的 base pass 时间。分档我一般这么给近景主角材质 High24 步 3 次二分中景 Mid12 步 2 次远景和移动端 Low4 步或直接用 BumpOffset。三档通过Static Switch编成三个变体由Quality Switch在运行时按r.MaterialQualityLevel挑打包时按平台设默认值。移动端还有一条硬限制部分低端 GPU 对循环内的纹理采样支持不好Custom 节点会编译失败或者退化成单次采样。稳妥做法是用FeatureLevelSwitch节点ES3.1 分支直接输出 BumpOffset 的结果SM5 分支才走 POM。同一个材质两个分支视觉上有差异但不会崩。4. 避坑清单视差翻车最多的五个现场4.1 边缘拉出“果冻”色带现象一块非平铺的装饰铭牌四周边缘被拉出放射状的彩色条纹镜头一动条纹跟着动。原因UV 偏移把采样点推出了 [0,1]纹理寻址是 Wrap于是采到了对面边缘的像素如果高度图边缘不是 0偏移量在边缘处还是最大值拉得更狠。解决高度图四周留 48 像素压到 0 的边框在 Custom 节点里对输出 UV 做clamp(curUV, 0.001, 0.999)HeightRatio 在边缘区域用顶点色或距离场做一个衰减。三条一起上最稳只做第一条往往还不够。4.2 掠射角糊成一团或者满屏噪点现象正对着墙看很干净一贴墙侧着看砖缝变成一片糊偶尔还闪出噪点。原因V.xy / V.z在 V.z 趋近 0 时数值爆炸偏移量一步跨过好几条砖缝步进搜索直接跳过真实交点再加上高度图被采到高 mip边缘糊掉交点判断在相邻两像素间跳变就是你看到的噪点。解决max(abs(V.z), 0.08)把掠射角压住代价是极端角度下视差变弱但视觉上没人会盯着 88 度的墙面看高度采样锁 LOD 0步数按视角动态加越斜给越多步下一章会写。4.3 阴影和视差对不上砖缝里一片死黑或者完全不黑现象砖缝的凹凸看得出但角色靠近时地上没有对应的遮挡阴影或者反过来砖缝里出现一圈圈噪点状的自阴影。原因前者是没接 Pixel Depth Offset深度缓冲还是平的后者是阴影 pass 里也跑了完整的 POM偏移后的 UV 让阴影深度和主 pass 对不上。解决主角级材质接 PDO强度按物件尺寸给 0.0050.02用Shadow Pass Switch把阴影分支换成原始 UV 或单步偏移砖缝的暗部靠 AO 贴图或高度值压基础色补别指望距离场 AO 能识别 PDO。4.4 一进大场景就掉帧Profile 里找不到元凶现象编辑器里单看材质很流畅一进实际场景帧率掉一半Stat GPU里 Base Pass 时间暴涨。原因步数做成了普通 Scalar 参数美术在实例里拉到 32所有实例共享最贵的编译结果加上大面积地面 overdraw逐像素 30 多次采样直接压垮。解决步数改成Static Switch或Quality Switch分档用 Shader Complexity 视图确认红色区域是不是集中在地面和墙面上对只在远景出现的墙面换用 BumpOffset 版本别一套材质打天下。4.5 编辑器里好好的打包到移动端完全没效果现象PC 上预览正常Android/iOS 打包后视差消失或者材质直接编译报错。原因移动端着色器模型对循环内纹理采样支持有限Custom 节点里的动态循环被拒绝另外材质质量等级的默认值在移动端可能被设成 LowQuality Switch直接走了降级分支。解决用FeatureLevelSwitch给 ES3.1 单独一个 BumpOffset 分支循环上限写成常量、用 break 提前退出打包前在移动预览渲染级别下检查一遍材质编译日志别等出包才发现。5. 进阶自适应步数加多层合并把视差压在 2ms 以内固定 16 步的问题是正对着看的时候8 步就收敛了剩下 8 步纯浪费侧着看的时候16 步又不够。按视角给步数是性价比最高的一刀。// 视线越平V.z 越小需要的步数越多 float facing saturate(abs(normalize(ViewDirTS).z)); float steps lerp(28.0, 8.0, facing); // 正面 8 步掠射 28 步 steps floor(steps / 4.0) * 4.0; // 量化到 4 的倍数减少变体量化这一步别省。连续变化的步数会让分支发散同一个 warp 里不同像素跑不同次数GPU 实际按最长的算等于没省。取 4 的倍数之后实际只会出现三四种组合编译和调度都可控。多层材质是另一个省法。砖墙和灰缝如果各做一套 POM就是两倍的采样。常见做法是先把两张高度图合并成一张取max因为砖面比灰缝高用合并图跑一次步进拿到 UV然后两张颜色图都用这个 UV 采样再按命中高度做个权重混合。视觉上灰缝的深度是对的代价只多了一次纹理采样。场景步数策略参考耗时1080p 半屏主角墙面贴脸看自适应 828 3 次二分1.21.8 ms中景地砖固定 12 2 次二分0.60.9 ms远景墙体BumpOffset 单步0.1 ms 以内表里的数字是某台开发机上的量级别当基准只用来判断“值不值得开”。真要卡预算还是看Stat GPU和 Shader Complexity 的实际颜色。验证方法很简单做个摄像机沿墙平移的关卡看砖缝边缘有没有“游动”和跳变再把HeightRatio从 0 拉到 0.2如果边缘拉伸先崩说明高度图边框没处理好最后把r.MaterialQualityLevel依次切 0/1/2确认三档都编得出来、观感差异能接受。血泪经验是我现在的习惯是任何视差材质在交给美术之前强制走一遍“高度图边框检查 掠射角截图 移动端预览编译”这三步少一步都要在后面返工。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。