Unity中基于视差遮蔽映射(POM)的动态云海效果实现与优化 📅 2026/7/11 20:01:15 1. 项目概述当视差映射遇上天空在Unity中制作天空和云层很多开发者会立刻想到使用天空盒Skybox或者粒子系统。但如果你想要一种更具沉浸感、能随着摄像机移动而展现出真实深度和体积感的云海传统的贴图就显得有些“平”了。这正是“基于POM的视差云效果”要解决的问题。简单来说它利用一张看似普通的2D云层纹理通过一种名为“视差遮蔽映射”Parallax Occlusion Mapping简称POM的着色器技术在GPU上实时计算出逼真的三维凹凸和遮挡关系让云层看起来像是拥有真实的厚度和结构而不仅仅是贴在天空背景上的一张画。这个效果的核心价值在于它用极低的几何体成本通常只是一个平面或球面实现了接近复杂模型才能达到的视觉深度。想象一下在飞行模拟器或开放世界游戏中你驾驶飞机穿梭于云层之间云朵的边缘会因为你的视角变化而产生真实的遮挡和位移这种动态的立体感是静态天空盒无法比拟的。它特别适合需要动态、可交互天空背景的中大型项目比如飞行游戏、开放世界RPG、或者任何希望提升天空视觉品质的应用。2. 核心原理视差遮蔽映射POM深度拆解要理解POM我们需要先了解它的“前辈们”。视差效果的本质是欺骗人眼根据表面法线和视线方向对纹理坐标进行偏移让平坦的表面看起来有凹凸。2.1 从简单视差到陡峭视差最基础的视差映射Parallax Mapping算法很简单根据高度图Height Map一张表示表面凹凸程度的灰度图和视线方向对纹理坐标进行一次性的偏移。这能产生不错的静态凹凸感但当视角变得陡峭比如平视一个凹凸表面时效果会严重失真甚至出现拉伸因为它没有处理光线被“高处”遮挡的情况。于是有了陡峭视差映射Steep Parallax Mapping。它把高度范围比如0到1等分成若干层从最高处开始沿着视线方向一步步“深入”表面。每深入一层就采样一次高度图比较当前采样深度和实际视线深度。当视线深度首次大于采样高度时就认为找到了表面的交点。这个方法解决了陡峭视角的问题但结果精度受分层数限制。分层少了会有明显的“阶梯”状瑕疵分层多了性能开销就上去了。2.2 POM的核心线性插值与光线步进视差遮蔽映射POM是陡峭视差映射的优化版。它同样采用分层步进但在找到交点附近的两个采样层一层在表面上一层在表面下后并不简单地取其中一个而是利用这两个点的深度信息进行线性插值从而估算出更精确的交点位置。这个过程可以类比为更精细的“探针”光线步进从视线起点出发沿着视线方向按照固定的深度步长层前进。深度测试每一步都采样高度图得到该点表面的理论高度。比较当前步进深度和该点高度。发现穿越当某一步的步进深度首次大于该点的高度值说明光线在这一步“穿透”了表面。那么上一步i-1的点就在表面之上当前步i的点就在表面之下。线性插值利用上一步的深度depth_{i-1}和高度height_{i-1}以及当前步的深度depth_i和高度height_i通过插值公式计算出更精确的交点t。t (depth_{i-1} - height_{i-1}) / ((depth_{i-1} - height_{i-1}) - (depth_i - height_i))这个t是一个介于0到1之间的值代表了交点在上一步和当前步之间的精确位置。计算最终偏移用这个插值后的t值去计算最终的纹理坐标偏移量这个偏移量会比简单的分层法精确得多。注意POM虽然更精确但计算量也更大因为它不仅需要多次纹理采样步进还需要进行插值计算。在Shader中步进次数_ParallaxSteps是一个关键的性能与质量权衡参数。2.3 为何POM适合云层云层是一种半透明、体积感强、边界柔和的自然现象。POM的特性与之完美契合体积感通过高度图模拟云层的上下起伏POM能产生光线在云团中“穿梭”的错觉而非简单的表面凹凸。柔和遮挡POM产生的遮挡边缘可以通过插值变得相对平滑配合云纹理本身的柔和过渡能模拟出云朵边缘的羽化效果。动态响应随着摄像机视线移动纹理偏移量实时变化云层会展现出“横看成岭侧成峰”的动态立体感。3. 效果实现从Shader编写到场景整合接下来我们一步步拆解在Unity中实现这个效果的全过程。我将使用Unity的URPUniversal Render Pipeline作为示例因为它是当前和未来的主流。核心在于编写一个自定义的Unlit Shader Graph或者手写HLSL代码。3.1 资源准备纹理与材质你需要准备两张核心纹理云层颜色纹理Albedo一张包含云层颜色和形状的RGBA纹理。Alpha通道通常用来控制云的不透明度/密度。建议使用无缝平铺Tiling的纹理。云层高度图Height Map一张灰度图白色区域代表云层“高”或“厚”的地方黑色区域代表“低”或“薄”甚至无云的地方。这张图的质量直接决定了体积感的真实性。通常可以由颜色纹理的灰度版本加工得到。在Unity中创建材质时我们还需要一些控制参数_ParallaxStrength视差强度控制高度图对纹理坐标偏移的影响程度。值越大云层的“起伏”感越强。_ParallaxSteps视差步数控制光线步进的层数。值越高精度越高性能消耗越大。对于云层15-30步通常能在质量和性能间取得良好平衡。_BaseTiling基础平铺控制云层纹理在模型上的重复次数用于调整云朵的尺度。_HeightRemap高度重映射一个Vector2Min, Max用于调整高度图的对比度让薄的区域更薄厚的区域更厚。3.2 Shader实现核心构建POM节点网络如果你使用Shader Graph需要构建一个处理视差偏移的子图Subgraph。关键步骤如下计算视差偏移方向在Fragment Shader阶段获取表面法线Normal、视线方向View Dir和切线空间矩阵。将视线方向转换到切线空间。视差偏移的方向是切线空间下视线方向的XY分量忽略Z并对其进行归一化和缩放。实现步进与插值循环这是最复杂的部分。你需要使用一个For Loop节点来模拟光线步进。循环次数由_ParallaxSteps控制。在循环内计算当前步进深度currentDepth 1.0 - (i / steps)。计算当前步进的UV偏移量currentOffset parallaxDir * currentDepth * _ParallaxStrength。用基础UV加上currentOffset去采样高度图得到当前点的表面高度sampledHeight。比较currentDepth和sampledHeight。当currentDepth sampledHeight时记录下上一个步进的信息prevDepth,prevOffset和当前步进的信息currDepth,currOffset。线性插值退出循环后利用记录的上一步和当前步信息按照前面提到的插值公式计算插值因子t然后混合prevOffset和currOffset得到最终的精确UV偏移量parallaxUVOffset。应用偏移并采样将parallaxUVOffset应用到基础UV上得到最终的UV坐标用这个UV去采样颜色纹理、法线纹理如果需要等。实操心得在Shader Graph中实现循环和条件判断不如代码直观。一个常见的技巧是将步进和插值逻辑封装在一个自定义HLSL函数节点中然后在Shader Graph里调用这样既能利用Graph的直观性又能保持代码的灵活性。对于性能敏感的项目直接手写HLSL Shader通常是更优选择。3.3 场景搭建与参数调优Shader写好并赋给材质后将其应用到一个巨大的球体Sky Dome或平面上作为天空。调参是关键这直接决定了效果是“仙境”还是“故障”_ParallaxStrength从小值如0.05开始慢慢增加。过大会导致UV扭曲严重云朵形状破碎。对于覆盖整个天空的大球体这个值通常很小0.02-0.1。_ParallaxSteps在编辑器里动态调整观察远处云层的锯齿状瑕疵“阶梯”效应何时消失。找到一个刚好消除瑕疵的最小值。移动平台可能从10步开始PC可以尝试20-30步。_BaseTiling调整云朵的大小。值越小云朵越大、越稀疏值越大云朵越小、越密集。结合一张好的无缝纹理可以创造出非常自然的大范围云海。光照与融合云是自发光的通常使用Unlit Shader或为其添加简单的边缘光。为了更好的天空融合可以采样一个天空盒颜色作为底色然后根据云层密度颜色纹理的Alpha或高度值进行混合。还可以在云层边缘高度较低的区域混合进一些天空的颜色模拟半透效果。4. 性能优化与进阶技巧POM是一种屏幕空间效果其性能消耗主要在于Fragment Shader中的循环纹理采样。在移动端或需要大量云层渲染的场景中优化至关重要。4.1 多级细节LOD与动态步进最直接的优化是为POM Shader设置不同的LOD级别或者根据摄像机距离动态调整_ParallaxSteps。实现思路在Shader中根据像素到摄像机的距离或者根据屏幕空间导数ddx/ddy估算的像素覆盖率来动态减少步进次数。远处的云朵或只占几个像素的云朵完全可以用更少的步数甚至退回到简单的视差映射而不被察觉。// 示例根据深度粗略调整步数需在CPU端计算并传入参数 float adaptiveSteps lerp(_MinSteps, _MaxSteps, saturate(depth / _FadeDistance));4.2 利用深度缓冲进行早期跳出这是一个高级优化技巧。在步进循环中我们可以将当前步进点的深度转换到屏幕空间与已经渲染好的场景深度缓冲Depth Buffer进行比较。原理如果当前POM计算的点其深度值已经比深度缓冲中对应位置的值更远即被其他物体挡住了那么无论这个点后面还有什么细节最终都不会被看到。此时可以立即跳出循环节省后续计算。注意在URP中访问深度纹理需要在Renderer Asset中开启“Depth Texture”选项并在Shader中声明TEXTURE2D(_CameraDepthTexture)和SAMPLER(sampler_CameraDepthTexture)。此技巧实现较复杂但对包含复杂前景的场景优化效果显著。4.3 艺术化控制与动态效果基础的POM云是静态的。要让它“活”起来需要引入动态变化。UV动画对输入Shader的基础UV加上基于时间的偏移可以让云层缓慢飘动。使用两个不同速度和方向的UV层进行混合可以创造出更复杂、不重复的流动效果。float2 uv1 baseUV float2(_Time.y * _Speed1, 0); float2 uv2 baseUV * 1.5 float2(0, _Time.y * _Speed2); float4 cloudColor lerp(tex2D(_CloudTex, uv1), tex2D(_CloudTex, uv2), _BlendFactor);高度图动态同样可以对高度图的UV进行动画或者使用程序化噪声随时间改变高度值这样云层的形状和厚度也会发生变化模拟云卷云舒。与天气系统联动将_ParallaxStrength、云层密度、颜色等参数暴露给脚本。在晴天时减弱视差强度、提高亮度在暴雨前增强视差感、使用深灰色调让云层效果与游戏逻辑完美结合。5. 常见问题与排查实录在实际开发中你几乎一定会遇到下面这些问题。5.1 云层边缘闪烁或撕裂现象摄像机移动时云朵边缘出现快速闪烁或锯齿状撕裂。原因这是POM的经典问题称为“纹理游泳”Texture Swimming。根本原因是步进精度不足和纹理过滤的冲突。解决方案增加_ParallaxSteps这是最直接的方法但耗性能。软化高度图对高度图进行更强的高斯模糊减少尖锐的高度变化。尖锐的边缘在POM中更容易产生采样不稳定。使用导数偏移在计算初始UV偏移前使用ddx和ddy函数估算屏幕空间的UV变化率并施加一个微小的偏移这有助于稳定采样。这是一些高级POM实现中的技巧。启用Mipmaps确保你的云层纹理和高度图启用了Mipmap并且Shader中使用了三线性或各向异性过滤。这能有效减少远处闪烁。5.2 效果在边缘或特定角度消失现象当摄像机看向天空球边缘时云层效果突然消失或变得很奇怪。原因视线方向与表面法线接近90度时切线空间下的计算可能出现问题或者UV偏移过大导致采样超出了纹理的合理范围。解决方案钳制偏移量对计算出的parallaxUVOffset的幅度进行限制确保它不会大得离谱。衰减因子根据视角与法线的夹角dot(N, V)来计算一个衰减因子。当夹角很小平视时应用完整的POM效果当夹角接近90度看边缘时逐渐减弱POM强度直至为0平滑地过渡到无效果状态。float parallaxStrength _ParallaxStrength * saturate(dot(N, V) - 0.2); // 示例衰减5.3 性能开销过大现象游戏帧率明显下降GPU Profiler显示Fragment Shader耗时很高。原因_ParallaxSteps设置过高或者使用了高分辨率纹理且云层覆盖屏幕大部分区域。解决方案实施4.1和4.2的优化方案动态步进和深度提前跳出是最有效的。降低纹理分辨率云层纹理通常不需要4K特别是作为背景时。尝试使用2K甚至1K配合良好的滤波视觉损失很小。分块渲染如果天空球巨大考虑将其分成多个Mesh只为摄像机近处的区块启用完整的POM远处的区块使用简化Shader或静态天空盒。5.4 与后期效果如体积光的兼容性问题现象添加了屏幕空间体积光God Rays后云层看起来不参与光线遮挡或产生错误的光照。原因POM只是在着色阶段模拟了凹凸并没有真正改变几何体的深度信息或世界位置。屏幕空间后期效果依赖于深度纹理和法线纹理而POM修改的只是当前着色点的颜色和法线如果计算了。解决方案输出自定义深度/法线在POM Shader中除了计算颜色还需要将经过POM偏移后计算出的世界空间位置和修正后的法线输出到GBuffer延迟渲染或自定义渲染纹理前向渲染。这样后续的屏幕空间效果才能获取到正确的几何信息。使用基于世界空间的方案考虑将POM的核心计算从切线空间转移到世界空间。虽然计算更复杂但得到的世界位置和法线信息直接就是正确的更容易与引擎的其他部分集成。这通常意味着你需要将高度图与世界空间的位移联系起来而不仅仅是UV偏移。实现一个高质量的POM云效果是一个在艺术表现和技术实现之间不断权衡的过程。它不像拖入一个粒子系统那么简单但带来的视觉提升和沉浸感是质的飞跃。从一张高度图开始通过调整那些看似微小的参数看着平坦的纹理逐渐演化成翻腾的云海这个过程本身就充满了乐趣和成就感。记住所有参数都没有绝对的最佳值反复测试用你的眼睛而不是数字去找到最适合你项目场景的那个“甜蜜点”。