实时渲染进阶:全内反射原理与在Unity/UE5中的实现方案 1. 项目概述从“镜面”到“内里”的渲染认知升级干了这么多年实时渲染我发现一个挺有意思的现象大家一提到反射脑子里蹦出来的第一个词就是“镜面反射”。无论是水面、金属还是光滑的地板镜面反射几乎是所有渲染工程师和美术同学的第一反应。这当然没错镜面反射是构成真实感世界的基石。但如果你只盯着镜面可能会错过场景中许多更微妙、更“高级”的真实感细节。今天我想聊的就是这个常被忽略但一旦用对就能让画面质感瞬间提升一个档次的物理现象——全内反射。简单来说全内反射是光线从光密介质比如水、玻璃射向光疏介质比如空气时当入射角大于某个临界角光线会全部被反射回原介质的现象。你肯定见过游泳池的水下靠近水面的池壁看起来异常明亮像自带发光一样或者透过玻璃杯看后面的物体杯壁边缘会有一条亮闪闪的“银边”。这些都不是光源直接照射的结果而是全内反射在“作祟”。在实时渲染领域尤其是追求电影级画质的3A大作或高保真模拟应用中正确地模拟全内反射能极大地增强透明/半透明材质如水体、水晶、宝石、酒液的体积感、通透感和物理可信度。这篇文章就是给那些已经玩转了SSR、平面反射、CubeMap的开发者们提供一个渲染细节上的新思路。我会拆解全内反射的核心原理聊聊它在实时管线中面临的挑战并分别给出在UnityURP/HDRP和Unreal Engine 5中切实可行的实现思路与代码片段。无论你是技术美术、图形程序员还是对渲染效果有极致追求的游戏开发者相信都能从中找到可以直接“抄作业”的干货。2. 全内反射的核心原理与实时渲染挑战2.1 物理现象拆解为什么光会“拐不了弯”要实现它首先得吃透原理。全内反射的发生根植于斯涅尔定律。当光线从折射率高的介质n1射向折射率低的介质n2时折射角会大于入射角。随着入射角增大折射角会趋近于90度。当入射角增大到使折射角等于90度时这个特定的入射角就是临界角。一旦入射角超过临界角折射现象消失所有光线能量都被反射回原介质这就是全内反射。计算公式很简单 临界角 θ_c arcsin(n2 / n1) 其中 n1 n2。例如水对空气的折射率约为1.33那么临界角 θ_c arcsin(1.0 / 1.33) ≈ 48.8度。这意味着在水下所有射向水面且与法线夹角大于48.8度的光线都不会进入空气而是在水内部发生反射。在渲染中这意味着对于像水这样的材质我们不能简单用菲涅尔项混合漫反射和镜面反射就了事。对于超过临界角的观察角度我们应该看到的是来自介质内部其他部分的反射光而不是折射出去的外部环境。这就是池壁发光、玻璃杯边缘有银边的原因——你看到的其实是水底或杯子内部其他部分经过全内反射后进入你眼睛的光线。2.2 实时实现的三大核心挑战理解了原理接下来就得面对实时渲染的残酷现实。离线渲染器可以轻松进行物理精确的模拟但实时帧率下我们必须做出巧妙的权衡。挑战一反射源的确定与采样。镜面反射对着环境贴图或屏幕抓取就行。但全内反射的反射源位于介质内部。这意味着我们需要知道光线在介质内部经过一次全内反射后击中了内部的哪个点。这本质上是一个在介质内部进行的二次光线追踪问题。直接进行精确的体光线步进在实时场景中开销巨大。挑战二能量守恒与菲涅尔的融合。全内反射不是独立存在的。它和普通的菲涅尔反射是同一物理过程的不同表现。当入射角小于临界角时是菲涅尔反射折射大于临界角时是全内反射可以理解为反射率为1的特殊菲涅尔情况。在Shader中我们必须用一个统一的公式平滑地处理这个过渡避免在临界角附近出现突兀的跳变。挑战三性能与质量的平衡。如何以可接受的性能代价模拟出内部反射的效果预计算屏幕空间还是简化模型这是实现方案需要解决的核心问题。我们往往无法追求物理绝对正确而是追求“视觉上合理”且“性能上可行”的近似。注意全内反射的效果强弱与介质本身的吸收系数颜色密切相关。清澈的水和深色的啤酒全内反射的视觉表现天差地别。实现时一定要将反射结果与介质的透射颜色Transmission Color或吸收系数Absorption相乘否则会显得假白、缺乏体积感。3. Unity引擎实现思路与实战解析Unity这边我们分URP通用渲染管线和HDRP高清渲染管线来聊。两者底层架构不同能用的“武器”也不同但核心思路可以互通。3.1 URP管线下的近似方案巧用屏幕空间数据在URP下我们缺乏像HDRP那样的光线追踪硬件加速或完善的体积材质系统。因此思路要更“巧妙”一些核心是利用已有的屏幕空间信息进行近似模拟。方案核心屏幕空间光线步进 深度缓冲查询我们可以在片元着色器中模拟一条从当前像素点出发在介质内部根据全内反射方向步进的光线。利用相机的深度纹理我们可以判断这条光线是否击中了场景中的其他物体即介质内部的其他表面。计算反射方向首先我们需要计算发生全内反射的反射向量。这不能直接用标准的reflect函数因为那是基于表面法线的。全内反射发生在介质内部其反射方向仍然遵循斯涅尔定律的反射部分但计算时使用的是介质界面处的真实法线通常是表面法线和内部折射率。一个实用的近似是当判定发生全内反射时即视角向量与表面法线的夹角大于临界角我们直接使用基于表面法线和视角向量的标准反射向量。虽然物理上不完全精确但视觉上可以接受。屏幕空间步进在Shader中我们从当前屏幕UV坐标出发沿着计算出的反射方向转换到屏幕空间进行步进。每一步我们都用新的屏幕坐标去采样深度纹理获取该点背后的场景深度。命中检测将采样深度与步进光线计算出的预期深度进行比较。如果两者非常接近我们就认为光线“击中”了介质内部的某个表面。这个击中点的颜色就可以通过采样屏幕颜色纹理来获得作为全内反射的颜色源。融合计算最后我们需要一个平滑的权重来混合正常的折射/菲涅尔反射颜色和全内反射颜色。这个权重基于视角与法线夹角和临界角的关系。可以用smoothstep函数在临界角附近创建一个平滑过渡。// Unity URP Shader 代码片段示例 (核心思路) void frag(v2f i, out half4 outColor : SV_Target) { // ... 获取表面法线、视角方向、深度等 ... float NdotV dot(normalWS, viewDirWS); float cosCriticalAngle sqrt(1.0 - (_IOR_Medium / _IOR_Air) * (_IOR_Medium / _IOR_Air)); // 假设从介质到空气 // 判断是否发生全内反射 bool isTIR (1.0 - NdotV * NdotV) (cosCriticalAngle * cosCriticalAngle); // 利用三角恒等式简化计算 float3 reflectionColor tex2D(_MainTex, i.uv).rgb; // 默认镜面反射或环境采样 if (isTIR) { // 计算内部反射方向近似 float3 internalReflectDir reflect(-viewDirWS, normalWS); // 进行屏幕空间光线步进寻找内部击中点 float3 tirColor ScreenSpaceTIRTrace(i.screenPos, internalReflectDir); // 将全内反射颜色与基础反射色混合权重可以基于入射角调整 reflectionColor lerp(reflectionColor, tirColor, _TIRIntensity); } // 将反射结果与折射、漫反射等按照菲涅尔权重最终混合 // ... }URP方案注意事项与避坑指南性能屏幕空间步进的步数和步长直接影响性能。通常步数控制在10-20步并根据距离动态调整步长远处步长大是平衡性能和质量的关键。屏幕边缘失效这是屏幕空间技术的通病。当步进光线指向屏幕外侧时无法获取有效信息。常见的应对策略是给一个回退值比如用环境立方体贴图的采样结果或者简单地淡出效果。深度精度深度比较时要注意精度问题特别是对于陡峭的角度。可以适当增加一个微小的偏差bias来避免自相交光线击中自身所在表面导致的噪点。半透明排序如果介质本身是半透明的全内反射的计算应在半透明混合之前完成并作为材质自身颜色的一部分参与后续混合。处理不当会导致反射内容与背景错误叠加。3.2 HDRP管线下的进阶方案利用光线追踪与体积材质HDRP为我们打开了新世界的大门。如果目标平台支持如DX12 DXR Vulkan Ray Tracing光线追踪是实现全内反射最物理、最优雅的方案。方案核心递归光线追踪或光线追踪反射在HDRP Lit Shader或自定义的Ray Tracing Shader中我们可以配置当光线与表面相交时根据材质属性折射率和入射角动态决定是发射一条反射射线还是折射射线。当计算发现满足全内反射条件时强制发射一条反射射线。这条射线会在HDRP的Ray Tracing系统中继续与场景求交最终带回击中点的光照信息完美模拟内部反射。即使不使用硬件光线追踪HDRP的体积材质系统也是一个强大的替代方案。我们可以将水体等介质定义为一个体积在体积着色器中沿着视线方向进行步进积分。在每一步不仅计算吸收和散射还可以判断当前点是否可能发生全内反射基于当前点法线通常是体积梯度和视线方向。如果可能则近似地采样一个预计算的或简化的内部环境来表示反射光。HDRP实现关键点Ray Tracing Shader Graph使用HDRP的Shader Graph可以接入Ray Tracing节点。在“折射”计算分支后添加一个条件判断如果sin(theta_i) (n2/n1)则将射线类型强制设置为反射并递归追踪。性能考量递归深度是性能杀手。对于全内反射通常设置最大递归深度为2一次反射或折射已经能产生不错的效果。务必在HDRP的Ray Tracing设置中合理配置射线距离和剔除。降噪光线追踪带来的噪声需要时空降噪器处理。确保HDRP的降噪设置如Denoiser已开启并适当配置。实操心得在HDRP中可以先从简单的“屏幕空间反射全内反射角度阈值覆盖”的混合方案开始。即正常使用SSR但在菲涅尔计算中对超过临界角的区域将SSR的混合权重强制设为1并可能用一个色调或亮度系数来模拟内部反射的差异。这是一个性价比极高的起步方案能在视觉上立刻带来提升。4. Unreal Engine 5实现思路与蓝图/材质节点详解UE5的渲染体系同样强大并且通过材质编辑器技术美术可以更直观地搭建效果。实现全内反射我们可以从材质系统本身和渲染特性两方面入手。4.1 材质系统内的模拟反射向量重定向与场景深度采样这是最贴近Unity URP思路的方法完全在材质蓝图内完成。核心节点流程计算临界角与判断使用Custom Node或数学节点组根据介质的折射率IOR计算临界角的余弦值。通过Dot节点计算像素点视图向量与表面法线的点积将其与临界角余弦值比较判断是否发生全内反射。构建内部反射向量如果发生全内反射我们需要一个“内部反射”向量。一个常见技巧是使用ReflectionVector节点基于法线和摄像机向量得到的结果然后利用Transform Vector节点将其从切线空间转换到世界空间。这个向量近似代表了光线在介质内部反射的方向。屏幕空间偏移采样这是模拟内部光线击中的关键。我们将上一步得到的反射向量投影到屏幕空间作为一个UV偏移量。使用SceneTexture: PostProcessInput0节点获取屏幕颜色和SceneDepth节点结合当前像素的屏幕UV加上这个偏移量去采样“可能击中点”的颜色和深度。深度测试与颜色混合将采样点的深度与根据偏移向量和起始点位置计算出的预期深度进行对比。如果两者接近在某个容差范围内则认为命中使用采样到的颜色作为全内反射颜色。否则使用一个回退值如天空球颜色或纯色。动态混合将计算出的全内反射颜色与材质原本的Base Color、Metallic、Roughness等属性通过Lerp节点混合。混合的Alpha通道由全内反射的判断条件和平滑过渡函数如SmoothStep来控制。// UE5 材质函数自定义节点示例伪代码表达逻辑 void TIRSimulation(float3 WorldNormal, float3 WorldView, float IOR, out float3 TIRColor) { float criticalAngle asin(1.0 / IOR); float cosCritical cos(criticalAngle); float NdotV abs(dot(WorldNormal, normalize(WorldView))); // 注意取绝对值处理正反面 if (NdotV cosCritical) // 视角与法线夹角大于临界角余弦值更小 { // 判定为全内反射 float3 ReflectDir reflect(-WorldView, WorldNormal); // 此处应有将ReflectDir转换为屏幕空间偏移并进行步进/采样深度比较的逻辑 // ... TIRColor SampledInternalColor; } else { TIRColor float3(0,0,0); // 不发生TIR贡献为0 } }材质方案避坑点向量空间混乱确保所有向量法线、视角、反射方向在同一个坐标系通常是世界空间下进行运算频繁的坐标转换是错误的主要来源。采样偏移与Mipmap对屏幕纹理进行偏移采样时强烈的偏移可能导致采样到错误的Mipmap级别使反射模糊或出现锯齿。可以考虑使用DDX/DDY节点手动计算梯度或使用SceneTextureLookup节点并指定详细的采样参数。性能屏幕空间深度采样和复杂的每像素计算对带宽敏感。避免在全屏所有物体上使用应通过材质参数或顶点着色器传递的开关将其限制在需要的高光透明物体上。4.2 利用UE5渲染特性光线追踪与Lumen的协同UE5的硬件光线追踪和软件光线追踪Lumen系统为实现物理准确的全内反射提供了终极武器。硬件光线追踪在项目的Render Settings中启用Ray Tracing并在材质的Details面板中勾选Use Ray Traced Reflections和Use Ray Traced Global Illumination。对于透明材质确保Refraction和Refraction Depth设置合理。当光线与表面交互时UE5的Ray Tracing管线会自动处理反射和折射的递归计算其中就包含了根据斯涅尔定律衍生的全内反射。你只需要设置正确的Index of Refraction属性。Lumen软件光线追踪Lumen是UE5的全局光照和反射系统它使用屏幕空间追踪和网格体距离场来模拟光线反弹。对于全内反射Lumen也能提供支持但其效果受限于距离场的精度和屏幕空间的范围。对于大型、连续的透明物体如海洋Lumen可能无法捕捉到远处内部表面的反射。此时可以结合使用Lumen作为主要反射源再叠加上述材质方案来增强近处或特定角度的全内反射细节。UE5方案选择建议移动端/性能敏感项目首选纯材质方案进行近似模拟严格控制采样次数和计算复杂度。高端PC/主机项目毫不犹豫地开启硬件光线追踪这是效果和性能平衡的最佳选择。注意调整射线距离和递归次数。大型开放世界依赖Lumen启用Lumen反射并通过材质的Specular和Roughness属性精细调整反射强度。对于关键的水体材质可以编写一个简单的Custom DepthPass将水体内部的重要物体渲染到自定义深度然后在材质中采样这个深度来辅助全内反射的命中判断作为对Lumen的补充。5. 常见问题、优化技巧与效果调试实录在实际项目里摸爬滚打肯定会遇到各种妖魔鬼怪。这里把我踩过的坑和总结的技巧列出来希望能帮你省点时间。5.1 效果不自然或闪烁Fireflies问题描述全内反射区域出现闪烁的噪点或亮度不稳定的像素。排查与解决深度比较容差这是最常见的原因。在屏幕空间步进中深度比较的容差Tolerance设置太小会导致命中判定极其敏感相邻像素因微小深度差异得到完全不同的结果造成闪烁。适当增大容差或使用双边滤波Bilateral Filter对深度进行比较可以平滑结果。采样噪声如果使用了随机采样或低差异序列来分散步进射线以覆盖更多区域在静态画面下可能看到噪声。解决方法是结合时域抗锯齿。将上一帧的全内反射结果缓存下来与当前帧进行混合如70%当前帧 30%上一帧。在UE5的材质中可以使用PrevFrameBuffer节点在Unity中则需要自己管理一个Render Texture来缓存。法线精度用于计算反射方向的法线贴图如果精度不足或Mipmap导致过度模糊也会引起方向抖动。确保在计算关键向量的地方使用高质量的法线采样。5.2 性能开销过大问题描述开启效果后帧率明显下降。优化策略分步降级不要在所有物体、所有距离上都用全精度计算。可以设计三个等级全精度仅对镜头前最近、最核心的透明物体如主角手中的水晶使用完整的屏幕空间步进。简化版对于中距离物体减少步进次数如从16步减到8步或使用更粗糙的深度图进行采样。关闭对于远距离或屏幕占比很小的物体直接关闭全内反射计算回退到普通的菲涅尔反射。基于距离和角度的剔除全内反射在正面观察时几乎看不见。可以计算视角向量与表面法线的点积当夹角小于一定阈值如30度时逐步淡出或完全跳过计算。同样对于距离摄像机超过一定范围的物体也可以淡出效果。降低采样频率考虑使用半分辨率进行计算。将全内反射的计算在一半宽高的缓冲区中进行然后再上采样并与主画面混合。这对性能提升显著且对视觉效果影响相对较小因为反射细节本身往往是模糊的。5.3 与后期处理效果的冲突问题描述全内反射的颜色或亮度在应用了Color Grading、Bloom、Auto Exposure等后期效果后变得异常。调试技巧线性空间计算确保你的全内反射计算是在线性颜色空间Linear Space中进行的。在Unity中检查Player Settings和Graphics Settings在UE5中确保项目设置中的Color Management正确。错误的空间会导致颜色混合和光照计算出错。Bloom源隔离强烈的全内反射高光可能会触发Bloom导致光晕过曝。可以尝试将全内反射的输出单独写入一个自定义的渲染缓冲区如Unity的CommandBuffer UE5的Render Target在应用Bloom之前再以叠加Additive方式混合到主画面。这样可以对全内反射的Bloom贡献进行单独控制。曝光适应如果使用了自动曝光Auto Exposure全内反射的亮区域可能会拉低整体曝光使场景变暗。考虑将全内反射的计算放在曝光调整之后或者使用眼睛适应Eye Adaptation的中间亮度Middle Grey参数来调节其对曝光的影响。最后调试这类屏幕空间效果一定要善用可视化调试工具。在Unity中可以把中间的计算结果如命中距离、反射权重、步进次数输出到Albedo通道一眼就能看出问题区域。在UE5的材质编辑器中使用Custom Node输出调试值到Emissive Color或者直接使用Preview节点的各种调试视图。亲眼看到数据是如何分布的比凭空想象要高效一百倍。记住最好的效果往往是“看起来对”而不是“算得绝对准”大胆地运用艺术化的调整参数让物理规律为你的艺术表现服务。