做水面反射的时候,我第一次栽在UE4的反射系统上。屏幕空间反射(SSR)在起伏的水面上闪烁得厉害,反射探针又只能反映一个静态的近似环境,怎么调都透着一股“塑料感”。后来把目光转到UE4的平面反射(Planar Reflection)上,才意识到自己之前压根没搞清楚这个功能的原理和成本。这篇内容围绕平面反射做了一次完整实操记录:它到底解决什么问题、UE4底层是怎么把场景“翻到镜子另一侧”再渲染一遍的、从创建组件到材质接线怎么一步步落地、性能账怎么算划算,以及在项目里实际踩过的几个坑。适合正在做水面、镜面地板、车漆这类需要精确反射表现的朋友,也适合那些打开Planar Reflection又关掉、觉得“太贵了”的开发者——看完你会明白它贵在哪里,以及什么情况下这笔开销是值得的。
1. 平面反射解决的痛点:SSR和反射探针为什么都顶不上
1.1 屏幕空间反射的先天短板
先得承认,SSR(屏幕空间反射)确实是很方便的方案,因为它几乎不需要额外的场景渲染,只是在主画面已经生成的颜色缓冲和深度缓冲上做光线步进。但它有几个非常要命的限制。第一个是屏幕外的东西永远反射不到——角色站在画面边缘的时候,水面里就没有他的倒影;镜头稍微仰一点,岸边建筑的上半部分直接消失。第二个是反射射线会被前景遮挡,比如人走到墙后面,水面里那块区域就什么都没有。还有一种情况是反射射线根本找不到交点,于是水面远处出现大片黑洞,只能靠环境探针兜底。
我做某个镜面展厅的Demo时,大厅里铺了一层浅水,SSR当时的表现真的可以用“惨不忍睹”形容:镜头推近,水面倒影只有相机正前方两三米的范围,再远的地方要么断成碎块,要么直接黑掉。原因是SSR的采样步数和搜索半径是有限的,屏幕空间上的反射信息天然受制于当前视角,远距离反射在射线步进中早就衰减没了。还有个隐藏问题是性能,把SSR质量拉高之后,像素着色器的开销会成倍上涨,最后结果依然是闪烁的。对需要长时间盯着水面看的场景来说,这种方案很难撑起画面可信度。
1.2 反射探针的天花板
反射探针(Reflection Capture)是另一条路,UE4里通常以SphereReflectionCapture或者BoxReflectionCapture的形式出现。它做的事情是把周围环境预烘焙成一张CubeMap,然后在像素着色器里根据视向量采样。这个方案的优点是成本极低,甚至不参与运行时渲染,但代价是它本质是一张低频环境贴图。分辨率上不去,近处物体的细节在反射里基本糊成一团;更麻烦的是,CubeMap没有场景深度,也没有遮挡关系,反射的物体明明离镜面只有几米,反射结果却像贴在无限远的地方,透视关系一对比就很假。
我见过很多项目想用反射探针硬撑镜面地板,结果就是地板“亮”但是“空”,反射里只有天花板和远处窗户的大色块,站在地板中央的玩家角色完全没有倒影。你当然可以把角色也烘焙进探针里,但那是纯静态方案,角色一走动穿帮立刻暴露。反射探针适合作为粗糙金属、旧瓷砖、远处环境的补充反射,但要承担主要反射对象,它既没有精度也没有视差。
1.3 什么场景才必须上平面反射
根据我的经验,平面反射真正有不可替代优势的场景有三类。第一是平整水面,比如湖泊、泳池、雨后的广场地面积水,需要反射出岸边建筑和人物的清晰倒影。第二是室内装饰性镜面,地板、大理石墙面、镜面玻璃,反射物体离平面近,透视关系强烈。第三是车漆类高光反射表面,尤其汽车展示Demo里车盖和侧裙需要反射环境,不能接受一张CubeMap在那里充当倒影。
这三类需求的共同点是反射平面明确、反射内容离平面近、对清晰度和透视准确性要求高。平面反射的思路是“把场景精确地翻印一遍”,正好全覆盖。不过也别误会,平面反射并不是所有反射需求的万能解,它有自己的适用边界:反射平面必须是平整的,曲面反射它做不了;场景里如果有大量需要递归反射的镜中镜,原生方案也支撑不住。选型的时候先想清楚,你要的到底是“一个清晰的倒影”还是“一个模糊的环境补光”,前者平面反射,后者探针就够了。
2. 平面反射的工作原理:把整个世界“翻”到镜子下面
2.1 反射矩阵到底做了什么
平面反射的实现思路其实一句话就能讲完:以反射平面为对称轴,把当前相机完全镜像到平面另一侧,用这个“镜像相机”渲染一次场景到一张RenderTarget,再把这张RT作为纹理贴到反射平面上。难点在于这个镜像变换不是简单的Scale翻转。设反射平面上一点为原点,法线为n,那么世界点P的反射点P'是:
P' = P - 2 * dot(n, P) * n
如果平面本身到原点有距离d,公式里还要再减去2 * d * n。UE4里这件事已经被封装好了:用FPlane(0,0,1,0)定义水平面,随后FMatrix::MirrorByPlane(Plane)就能生成对应的镜像矩阵。引擎在渲染PlanarReflection时,会拿这个矩阵重新生成ReflectionViewMatrix,然后跑一遍常规的场景渲染管线。所以平面反射本质上是“镜面相机渲染的世界”,而不是一个简单的屏幕后处理。
很多刚接触的人会混淆平面反射和“把材质UV翻转”,觉得一个翻转纹理就能模拟反射,那只是最粗糙的假反射。平面反射的每一帧都是实时生成的,角色走过去、灯光变化、世界里的门打开,反射画面都会跟着变,这才是它和烘焙探针最大的分水岭。
2.2 RenderTarget和斜裁剪矩阵的配合
镜像相机生成之后,要把场景渲染到一张RenderTarget上。这里有个关键问题:如果不做处理,反射相机的水下视角会把水面以下的物体也渲染出来,你会看到水面反射里出现泥巴、水底石头,甚至半透明水草交错的穿帮画面。解决办法是使用斜裁剪矩阵(Oblique Near Clip Plane),把相机的近裁剪面“压”到反射平面上。
UE4的PlanarReflection内部会动态计算这个裁剪矩阵,你在组件上看到的ReflectionPlane参数就是用来定义这个裁剪平面的。水面以下的东西会在这个矩阵作用下被裁剪掉,既避免了穿帮,也省掉了一部分像素填充。从这个角度看,平面反射并不是“把整个场景完整再画一遍”,而是“把镜像视角里、反射平面以上的可见内容再画一遍”,裁剪做得越准,开销就越可控。
2.3 为什么平面反射“贵”?因为它是第二次完整渲染
很多朋友第一次开PlanarReflection,帧率肉眼可见地掉了一截,然后说“这功能好垃圾”。其实不是功能垃圾,而是它在物理上就多渲染了一遍场景。主相机要渲染的所有不透明物体、半透明物体、阴影、后处理,反射相机也得来一遍,只是换了个视角。场景里的三角形越多、动态光源越多、RT分辨率越高,这笔二次渲染开销就越可观。
理解了这一点,后面的性能优化思路就清晰了:要么降低反射相机渲染的负载,比如限制视锥、剔除远处物体;要么降低反射渲染的频率和分辨率,比如隔帧捕捉、降低ScreenPercentage。如果环境完全静态,你甚至可以让反射相机只渲染一次,把结果当成一张大贴图用,那样开销就只剩下采样了。平面反射贵,但它贵得有道理,因为它是反射方案里少有的“真实时、真遮挡、真透视”方案。
3. 从创建组件到材质接线:落地一份可用平面反射的全流程
3.1 两种实现方式怎么选
UE4里实现平面反射主要有两条路。第一条是直接用PlanarReflection Actor,它自带PlanarReflectionComponent,你把Actor放在反射平面上,设置好法线方向即可,引擎会自动处理镜像矩阵、裁剪矩阵和RT创建,省心。第二条是用SceneCaptureComponent2D自己拼,自由度更高,可以控制RT格式、更新频率、CaptureSource,适合需要自定义多张反射纹理或特殊效果需求的项目。
常规水面和镜面我推荐前者,代码量少、配置简单。但如果你的场景需要多个反射平面互相独立,或者要对反射结果做额外后处理(比如给反射单独加模糊、做渐变淡出),那SceneCapture更能满足你。我之前在一个项目里同时用了三个SceneCapture分别捕捉三个朝向的镜面,再把三张RT输入到UI材质里做电子看板效果,这种情况下PlanarReflection Actor反而绑手绑脚。先评估场景复杂度,再决定走哪条路,别一上来就照着教程抄。
3.2 Actor摆放和关键参数
把PlanarReflectionActor放到你想做反射的平面位置。举个例子,水面世界坐标Z=50,那么Actor的Z就放在50。调整Actor的Rotation,让它的本地Z轴朝向平面法线方向,水平面法线朝上,那就是Rotation的Pitch设为0,水平放平就行。如果平面是斜的,比如镜面墙,需要让Actor的Z轴垂直墙面。
选中组件后,有几个参数我每次都会先改一遍:
- Capture Every Frame:是否每帧捕捉。环境不变化或者变化慢时,关掉它,配合关卡蓝图里的事件触发Capture,性能能省下一大部分。
- Screen Percentage:反射RT的渲染分辨率百分比。默认100%,水面我一般降到50%到60%,镜面可以拉到75%左右,高速移动中肉眼基本分不出来。
- Max View Distance Override:反射可见最大距离。设置成你需要的反射范围,别让反射相机渲染整个大世界。一个室内大厅限制256米就能显著降低DrawCall。
- Normal Distortion Strength:法线扭曲强度,主要给水面用。控制反射纹理采样的UV扰动幅度,别开太大,不然远处倒影会糊成色块。
CaptureSource默认选FinalColor(LDR),如果你希望反射里保留更亮的高光细节,可以选SceneColor(HDR),但HDR RT的处理和颜色校正也要跟上,否则反射部分容易出现过曝。
3.3 材质端接入
反射RT本身是一张普通纹理,材质里关键是怎么得到正确的采样UV。最容易踩的误区是直接把RT当平面贴图用,用常规UV坐标去采样,结果反射内容是斜的或者错位的。正确思路是:反射相机渲染出的RT,对应的是反射视角的屏幕空间,所以材质里应该基于屏幕坐标生成UV。
在实际操作里,如果你用的是PlanarReflection Actor,引擎会直接把反射结果合并到场景颜色里,水面材质不需要自己算UV,这也是它比SceneCapture省心的地方。此时水面材质要做的主要是两层混合:一个是反射层的采样,一个是折射层的采样。如果反射Actor的RT没有出现在材质里,先检查水面的材质混合模式是否被设成了半透明,以及是否开启了Render in Main Pass相关选项。
如果你走的是SceneCapture路线,做法可以这样:在蓝图里把SceneCapture的TextureTarget赋给一个材质参数(Texture Parameter),然后在材质里用ScreenPosition节点生成UV,对RT采样。水面材质通常是半透明的,反射层通过Lerp混合进BaseColor或者Emissive通道。要特别注意,反射图是HDR还是LDR会影响你后续的颜色调整,尤其当你打算在反射结果上叠加高光时。
3.4 反射平面与半透明的关联
如果反射平面本身是不透明的,比如镜面,直接把采样结果接到Emissive或BaseColor上就行。如果反射平面是水面这种需要半透明的材质,就要考虑折射和反射的混合顺序。水面材质里通常的做法是:反射RGB乘上一个反射强度,叠加到色调上,再与折射采样做混合。这个顺序会直接影响水面的通透度。
平面反射的画面是一张完整带颜色的图,如果材质混合模式设置不当,反射里的Bloom会和主画面的Bloom重叠,看起来整体过曝。我通常把水面材质设为默认Lit、不透明模式,用OpacityMask控制可见区域,尽量避免用半透明模式去采样反射RT。半透明模式下,反射物体与水面本身的深度排序会变得很微妙,容易出现反射内容时有时无的问题。如果一定要半透明,也要把Refraction相关的参数一起理顺,否则水面会比较“脏”。
4. 性能账本:一份平面反射的帧数代价与五条优化手段
4.1 开销拆解
平面反射的GPU开销主要由四部分构成。第一是几何阶段的DrawCall,反射相机视角下所有可见物体的顶点处理和提交,场景越复杂这笔开销越高。第二是分辨率决定的像素着色开销,RT越大,需要着色处理的数量越多。第三是阴影开销,反射相机视角下动态光源的阴影要重新生成,方向光的级联阴影在反射相机里会再次计算,代价很大。第四是RT本身的内存带宽,写回那张反射纹理、再被主画面采样,都会消耗带宽。
CPU端也不是完全没压力,DrawCall提交和裁剪计算同样会反映到帧耗时上。遇到帧率下降,先别急着关反射,用ProfileGPU定位反射相机对应的pass到底花了多少时间,再做针对性调整。我遇到过一种情况:反射分辨率只开了50%,但反射相机视锥范围太宽,把整个城市模型的远景网格都拉进来了,DrawCall直接翻倍,瓶颈根本不在像素上。
4.2 五种能立刻着手的降本手段
第一个手段是降低Screen Percentage,这是性价比最高的一刀。从100%降到50%,像素量直接变四分之一,很多场景下画质差异会被水面的法线扰动掩盖掉。第二个是关掉逐帧捕捉,环境不变时,Capture Every Frame设成false,只在关键事件发生时手动触发一次捕捉。静态场景里,这一刀几乎可以省掉全部反射开销。
第三个是限制Max View Distance Override,提前规划反射可见范围,室内场景设个256到512,室外水面也尽量不要无限拉。这个参数直接影响反射相机的视锥裁剪结果,把它压下来,反射相机需要渲染的物体数量会肉眼可见地减少。第四个是隐藏不必要的反射物体,通过PrimitiveComponent的可见性标志,让一些次要物体不进反射。注意这里的坑很多,别一个不小心把主画面的物体也藏掉了,建议用一个专用的Actor分组管理。第五个是善用LOD和简化几何,反射相机渲染时,把较远的细节用更低级的LOD替代,或者让背面和远处的网格直接不进视锥。
4.3 三种反射方案的性能对照
| 方案 | 额外渲染 | 反射精度 | 遮挡关系 | 动态表现 | 典型帧数代价 |
|---|---|---|---|---|---|
| SSR | 无 | 屏幕空间像素级,但有断裂 | 有(基于深度) | 好 | 像素着色器开销,通常10%-25% |
| Reflection Capture | 无(烘焙时) | 低,低频环境近似 | 无 | 静态 | 几乎免费 |
| Planar Reflection | 完整二次渲染 | 极高,精确镜像 | 有(二次渲染天然遮挡) | 好 | 视场景复杂度,30%-80% |
别被80%吓住,我说的是极端情况。实际项目里水面不会让PlanarReflection反射整个城市,裁剪得当的情况下35%以内是常态。还有一个容易被忽略的省钱点:镜头转背对水面时,反射相机的视锥里没有可见物体,那一帧的反射开销会降到接近零。平面反射是“视角依赖”的功能,它自带这种自适应省钱的特性,这也是它和固定开销的反射探针最大的区别。
5. 踩坑记录:反射全黑、物体被裁、镜中镜与半透明排序
5.1 反射全黑和“只见一次”排查路线
我自己遇到最多的问题是反射全黑,或者在特定视角才出现。排查链路通常是这样:先确认PlanarReflectionActor的位置是否正确、法线是否朝外;再看CaptureSource是不是被改成SceneColor但RT没有设成HDR;然后在材质里临时涂一个纯色来确认RT到底有没有内容;最后看材质用的UV对不对。这几步按顺序走完,大多数全黑问题都能定位。
另一种常见情况是反射只工作一帧,随后全黑。这多半是Capture Every Frame设置为false,但引擎没有收到Capture事件。解决办法是把更新回调绑定到Actor的ReflectionUpdate事件上,或者干脆在需要的时候手动调用一次Capture。很多教程只教你勾选参数,不教你生命周期管理,这类“跑一帧就变黑”的问题才会反复出现。
5.2 水面下的物体被错误反射
如果玩家操控角色穿过水面,或者相机离水面平面过近,镜像相机可能落到平面另一侧,导致水面以下的物体被带进反射画面。理论上斜裁剪矩阵能切掉平面以下的物体,但遇到半透明物体、Decal和带自定义深度Pass的东西,裁剪就不一定生效了。我给水面加过一遍自定义Stencil标记,把不需要在反射中出现的物体用Stencil排除,或者直接把水下地形组件设为对反射相机不可见。
这里要记住一个原则:反射相机和主相机共用同一套可见性系统,任何在反射视角下可见的东西都可能出现在反射画面里。所以设计场景时,反射平面下方的物体必须提前规划,要么物理上放在层的可见性之外,要么在材质层面做排除。等穿帮了再加修补,成本反而更高。
5.3 镜中镜无限递归的问题
平面反射的画面里如果再放一面镜子,就会出现递归问题。UE4默认不会递归渲染反射,反射相机的场景里即使有另一个PlanarReflectionActor,它的反射内容也不会被二次反射出来。表现就是镜子里的镜子黑了一块,或者显示不到下一层。引擎设置了递归深度限制,指望靠RT套娃是不现实的。
做双镜互相对照的效果,一般需要额外烘一张环境图,或者手动画一个远程环境的假反射。别幻想原生插件能递归渲染,那是另一个量级的功能。如果必须表现镜中镜,我建议在视觉上降低第二层反射的精度,用模糊加暗处理,观众通常不会太在意第二层反射是否完全正确,但会立刻注意到第一层反射缺失。
5.4 半透明物体的缺席和排序错乱
水面反射最典型的问题是:透明材质、粒子、交错的半透明叶片,在反射画面里要么消失,要么显示在错误的排序位置。原因并不复杂,半透明物体在主相机那边是单独排序的,但反射相机渲染时,这些物体的透明混合方式和主相机视角下的混合结果不一样,于是出现重影或穿透感。
我的处理办法是,把水面需要的倒影物体都做成不透明模式,或者使用Opaque加OpacityMask。反射专用的简化模型单独放到反射相机可见层,避开半透明排序问题。如果一定要让透明物体也进反射,那就至少把它的Blend Mode调成与反射一致的参数,并且接受可能的排序瑕疵。千万不要又想让粒子有漂亮的Alpha渐变,又想它在反射里完美无缺,那是违背渲染规律的。
5.5 抗锯齿与法线扰动的小坑
最后补两个平时容易漏掉的坑。第一个是反射RT通常没有抗锯齿,分辨率一旦降得狠,反射边缘会出现明显的锯齿和闪烁,尤其是渲染大树和建筑边缘时。这时候要么把Screen Percentage稍微拉高一点,要么在反射RT后面挂一个TAA类的后处理材质,画面会立刻干净很多。第二个是水面法线扰动的强度不是越大越好,NormalDistortionStrength过大会让反射被“扯碎”,反射RT分辨率低的时候,像素级别的扰动会产生明显的波纹噪点。
我通常把扰动强度保持在0.1到0.3之间,并且把水面分成近、中、远三层的UV采样,分别设置不同的扰动量。近距离扰动强一点,水面波光粼粼;远距离扰动弱一点,倒影保持稳定。这样视觉上看水面既灵动又不至于糊成一片。
最后分享一点个人经验。我后来再做水面项目时,并没有把平面反射当作唯一方案,而是做成一个分层策略:近距离用一个256乘256的平面反射RT负责清晰倒影,中距离用SSR补充屏幕内容,远处交给反射探针兜底。三层叠加起来,水面在任何距离都不会出现“一块有倒影、一块没倒影”的断裂感。如果你正在做一个需要长时间盯着水面看的场景,建议先按前面说的性能账本估一遍,再决定要不要打开平面反射。它确实贵,但对值得的地方,这笔开支能够换回真实可信的画面表现。