ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Unity URP红外热成像效果实现:分层渲染与特效覆盖技术详解

Unity URP红外热成像效果实现:分层渲染与特效覆盖技术详解 1. 项目概述在URP管线中实现一个“红外窗口”在游戏开发或者一些需要特殊视觉表现的交互应用中模拟红外热成像效果是一个挺有意思的需求。想象一下你的游戏里有一个“热成像仪”道具或者一个UI界面上的监控画面需要显示红外视角。这个需求的核心并不是让整个游戏世界都变成红外色而是要实现一个分层的、局部的效果——即只有特定摄像机比如一个虚拟的“红外摄像机”看到的内容经过特殊着色处理后显示在屏幕的某个区域比如一个UI的RawImage上而游戏世界的主视觉保持不变。这就是标题“Unity urp实现红外效果分层渲染特效覆盖”所指向的核心场景。它不是一个全局的后处理而是一个基于特定摄像机渲染结果、进行二次着色、并叠加到UI层的复合技术方案。在URPUniversal Render Pipeline管线中实现它会涉及到URP的多摄像机渲染流程、Render Texture的管理、自定义Shader的编写以及如何让这个“红外窗口”与UI和其他特效和谐共处。最近在社区里看到不少朋友在问URP下特效叠加、分层渲染的问题这个红外效果的实现恰好是一个很好的综合案例能串起好几个关键知识点。2. 核心思路与方案选型实现这个“红外窗口”效果本质上是一个渲染目标Render Target的切换与再处理过程。我们不能简单地在主摄像机上挂一个全屏后处理Shader因为那样会改变所有物体的颜色。我们需要的是一个独立的、可控的渲染流程。2.1 分层渲染的核心多摄像机与Render Texture最直接也最有效的思路是使用第二台摄像机。我们让一台普通的摄像机我们称之为“红外摄像机”去拍摄我们想要以红外效果显示的那些物体。但这台摄像机的输出我们不直接显示到屏幕上而是渲染到一张Render Texture上。这张Render Texture就是“红外摄像机”看到的原始画面。为什么选择这个方案隔离性红外摄像机的渲染包括其Culling Mask、Layer、后期效果完全独立于主摄像机互不干扰。你可以精细控制哪些物体出现在红外视野里比如只渲染特定的“Heat”层。灵活性Render Texture作为一张纹理我们可以把它赋值给一个UI RawImage轻松地在屏幕任意位置、任意大小创建一个“窗口”。你也可以把它作为材质贴图用在3D物体上比如做成一个监控屏幕。性能可控红外摄像机的分辨率可以独立设置对应Render Texture的大小。如果“红外窗口”很小你可以用较低的分辨率来渲染节省性能。2.2 红外着色在Shader中处理Render Texture得到了红外摄像机渲染的原始彩色图像Render Texture后下一步就是把它“染”成红外色。这个步骤需要在Shader中完成。我们通常会创建一个专门用于屏幕后处理的Shader一个Image Effect Shader或者直接写一个Unlit Shader给UI RawImage使用。红外热成像的典型颜色映射是从“冷”到“热”通常表现为从深蓝/紫色到红色/黄色/白色的渐变。在Shader里我们的核心操作是采样Render Texture得到原始像素的RGB颜色。计算亮度Luminance。因为红外热成像感知的是物体的热辐射强度而不是其原本的颜色所以我们需要将彩色信息转换为灰度亮度信息。一个常见的公式是float luminance dot(color.rgb, float3(0.299, 0.587, 0.114));。根据亮度值进行颜色重映射。这里就是艺术发挥的地方了。我们可以使用一个渐变纹理Gradient Ramp Texture或者一个颜色插值函数将计算出的亮度值0到1之间映射到我们想要的红外色带上。例如低亮度冷映射到深蓝色中等亮度映射到红色高亮度热映射到亮黄色或白色。2.3 URP下的实现路径在URP中我们需要按照它的管线规则来组织上述步骤创建红外摄像机它是一个Camera组件但它的Render Type需要设置为Base吗这里有个关键点我们不希望它渲染整个管线的所有Pass比如阴影、不透明、透明物体分开渲染我们通常只关心它的最终颜色输出。更常见的做法是将其Render Type设置为Overlay但这需要配合URP的摄像机堆叠Camera Stacking。对于独立的“画中画”效果更清晰的方案是让它作为一个完全独立的摄像机输出到Render Texture。配置摄像机的Output将红外摄像机的Output Target从默认的Screen改为我们创建的Render Texture。创建并应用着色材质编写一个Shader实现上述的颜色转换逻辑。创建一个Material使用这个Shader然后将红外摄像机输出的Render Texture作为这个Material的输入纹理通常是_MainTex。在UI中显示创建一个UIRawImage将其Texture设置为上一步中那个应用了红外着色Material的Render Texture吗不这里有个中间步骤。RawImage直接显示的是纹理。我们需要的是将经过着色Material处理后的结果显示出来。有两种方式方式一推荐使用Command Buffer或URP的Renderer Feature在红外摄像机渲染完成后立即用我们的着色Material对Render Texture进行一次Blit操作即全屏绘制将处理结果写回同一张或另一张Render Texture。然后UI RawImage显示这张处理后的Render Texture。方式二写一个特殊的UI Shader给RawImage在这个Shader里完成“采样原始Render Texture - 计算亮度 - 颜色映射”的全过程。这样RawImage的材质就直接是红外着色材质它采样的是红外摄像机输出的原始Render Texture。方案取舍方式一Renderer Feature Blit更符合URP的渲染流程将后处理逻辑放在渲染管线内更专业也便于管理多个后处理效果。方式二自定义UI Shader更直接但将渲染逻辑混入了UI系统对于复杂的后处理链可能不够灵活。本文将重点介绍方式一因为它更具普适性也是URP下的标准做法。3. 详细实现步骤拆解下面我们一步步拆解在URP中实现该效果的具体操作。3.1 项目准备与基础设置首先确保你的项目使用的是URP。在Package Manager中安装或更新Universal RP包。创建一个URP Asset通常命名为UniversalRP-HighFidelity或类似并分配给项目的Graphics Settings。创建层级Layer为了精确控制哪些物体能被红外摄像机看到我们创建一个新的Layer例如命名为Infrared。将你希望显示在红外视野中的物体如角色、发热的机器等的Layer设置为Infrared。创建Render Texture在Project窗口中右键 - Create - Render Texture。命名为RT_InfraredRaw。根据你的“红外窗口”大小设置其尺寸例如512x512。如果窗口需要抗锯齿可以设置Anti-Aliasing为2x或4x。注意如果红外摄像机会渲染透明物体需要确保Depth Buffer选项不是No depth buffer。3.2 配置红外摄像机在场景中创建一个新的GameObject命名为Camera_Infrared并添加Camera组件。基本参数Clear Flags: 通常设为Solid Color并选择一个中性偏暗的颜色作为背景如深灰色因为纯黑可能在后续着色中难以区分。Culling Mask: 这是关键只勾选我们刚才创建的Infrared层。这样这台摄像机就只会渲染我们指定层的物体。Projection: 根据你的需求选择透视或正交。Target Texture: 拖入我们创建的RT_InfraredRaw。设置了这个之后这台摄像机的渲染结果就不会输出到屏幕而是全部画到这张Render Texture上。在URP Asset中排除此摄像机重要我们不希望URP的全局后处理如Bloom, Tonemapping影响到红外摄像机的渲染结果因为红外着色需要基于原始颜色进行计算。在URP Asset的Renderer列表中找到你正在使用的Renderer如Universal Renderer Data在其Inspector中找到Renderer Features列表下方或摄像机设置中通常有办法通过Camera的Rendering部分或使用Render Type为Overlay并管理堆叠来隔离。更简单直接的方法是为红外摄像机单独分配一个不包含任何后处理Renderer Feature的URP Renderer。复制一份你的URP Renderer Data命名为Renderer_Infrared。在Renderer_Infrared中移除所有的Renderer Features如Bloom等。在红外摄像机的Camera组件上找到Renderer下拉菜单将其从Use Graphics Settings改为Renderer_Infrared。注意这一步是避免干扰的关键。如果红外摄像机的渲染结果已经经过了色调映射、泛光等处理其颜色和亮度信息就不“原始”了会导致后续红外着色计算不准确效果怪异。3.3 编写红外着色Shader在Project中创建Shader/Unlit类型的Shader命名为InfraredEffect。我们将编写一个简单的图像处理Shader。Shader Hidden/InfraredEffect { Properties { _MainTex (Texture, 2D) white {} _RampTex (Color Ramp, 2D) white {} // 用于颜色映射的渐变纹理 _Intensity (Effect Intensity, Range(0, 2)) 1.0 } SubShader { Cull Off ZWrite Off ZTest Always // 标准后处理Shader设置 Pass { HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionHCS : SV_POSITION; float2 uv : TEXCOORD0; }; TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); TEXTURE2D(_RampTex); SAMPLER(sampler_RampTex); float _Intensity; Varyings Vert(Attributes input) { Varyings output; output.positionHCS TransformObjectToHClip(input.positionOS.xyz); output.uv input.uv; return output; } half4 Frag(Varyings input) : SV_Target { // 1. 采样原始图像 half4 col SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv); // 2. 计算亮度灰度值 // 使用标准亮度权重公式 half luminance dot(col.rgb, half3(0.299h, 0.587h, 0.114h)); // 可选对亮度进行一些非线性调整增强对比度 // luminance pow(luminance, _Contrast); // 3. 使用亮度采样颜色渐变纹理 // 渐变纹理通常是一维纹理1xN像素水平方向从0冷到1热 half3 infraredColor SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, float2(luminance, 0.5)).rgb; // 4. 混合原始颜色和红外颜色根据强度参数 // 这里也可以选择完全替换即直接返回 infraredColor half3 finalColor lerp(col.rgb, infraredColor, _Intensity); // 保持原始Alpha通道如果重要的话 return half4(finalColor, col.a); } ENDHLSL } } }Shader要点解析Cull Off ZWrite Off ZTest Always这是全屏后处理Shader的标准配置意味着这个Shader不参与深度剔除和写入总是绘制。亮度计算dot(col.rgb, half3(0.299h, 0.587h, 0.114h))是ITU-R BT.601标准下的亮度公式能较好地反映人眼对RGB不同通道的敏感度。渐变纹理_RampTex这是艺术控制的核心。你可以在Photoshop或其他工具中创建一张细长的水平渐变图从左到右定义从“冷”到“热”的颜色变化。在Unity中导入时确保将其Wrap Mode设置为ClampFilter Mode设置为Bilinear以获得平滑过渡。3.4 创建Renderer Feature进行后处理Blit这是连接红外摄像机渲染结果和着色Shader的关键桥梁。我们需要创建一个URP的Renderer Feature在红外摄像机渲染完成后立即对它的Render Texture应用我们的红外着色。创建Renderer Feature脚本在Project中创建C#脚本命名为InfraredRendererFeature。using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public class InfraredRendererFeature : ScriptableRendererFeature { [System.Serializable] public class Settings { public RenderPassEvent renderPassEvent RenderPassEvent.AfterRenderingOpaques; public Material infraredMaterial; // 引用我们创建的红外着色材质 public string textureName _InfraredTex; // 用于传递RenderTexture的Shader属性名 } public Settings settings new Settings(); private InfraredRenderPass _renderPass; // 用于传递RenderTexture的标识符 private static readonly int InfraredTextureId Shader.PropertyToID(_InfraredTex); public override void Create() { if (settings.infraredMaterial null) { Debug.LogWarning(InfraredRendererFeature: No infrared material assigned.); return; } _renderPass new InfraredRenderPass(settings.renderPassEvent, settings.infraredMaterial, settings.textureName); } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { // 确保只有红外摄像机使用这个Renderer时才添加这个Pass // 这里可以通过摄像机标签或名称进行过滤简单起见我们先为所有摄像机添加但通过材质判断 if (settings.infraredMaterial ! null) { // 将当前摄像机的targetTexture即RT_InfraredRaw传递给Pass var cameraTarget renderer.cameraColorTarget; _renderPass.Setup(cameraTarget); renderer.EnqueuePass(_renderPass); } } protected override void Dispose(bool disposing) { _renderPass?.Dispose(); } // 自定义的Render Pass class InfraredRenderPass : ScriptableRenderPass { private Material _material; private string _textureName; private RenderTargetIdentifier _source; private RenderTargetHandle _tempTexture; public InfraredRenderPass(RenderPassEvent renderPassEvent, Material material, string textureName) { this.renderPassEvent renderPassEvent; _material material; _textureName textureName; _tempTexture.Init(_TempInfraredTexture); } public void Setup(RenderTargetIdentifier source) { _source source; } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { if (_material null) { Debug.LogError(InfraredRenderPass: Material is missing.); return; } CommandBuffer cmd CommandBufferPool.Get(Infrared Effect); // 创建一个临时的RenderTexture用于存储处理结果 RenderTextureDescriptor descriptor renderingData.cameraData.cameraTargetDescriptor; descriptor.depthBufferBits 0; // 后处理通常不需要深度 cmd.GetTemporaryRT(_tempTexture.id, descriptor, FilterMode.Bilinear); // 核心使用红外材质进行Blit操作 // 从 _source (RT_InfraredRaw) Blit 到 _tempTexture过程中应用_material Blit(cmd, _source, _tempTexture.Identifier(), _material); // 再将处理后的结果Blit回 _source Blit(cmd, _tempTexture.Identifier(), _source); // 释放临时RT cmd.ReleaseTemporaryRT(_tempTexture.id); context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } public void Dispose() { // 清理资源 } } }配置Renderer Feature创建一个Material使用我们刚才写的Hidden/InfraredEffectShader命名为Mat_Infrared。为这个Material赋值一张你制作好的颜色渐变纹理_RampTex。回到之前为红外摄像机创建的Renderer_InfraredAsset。在它的Renderer Features列表里点击加号添加我们刚创建的InfraredRendererFeature。将Mat_Infrared材质拖拽到Feature的Infrared Material槽中。此时流程已经串联起来红外摄像机将场景渲染到RT_InfraredRaw-InfraredRendererFeature捕获这张纹理 - 用Mat_Infrared材质对其进行全屏Blit处理 - 处理结果写回RT_InfraredRaw。现在RT_InfraredRaw存储的就是经过红外着色后的图像了。3.5 在UI中创建红外显示窗口最后一步将处理好的红外图像显示在UI上。在Canvas下创建一个RawImageGameObject命名为UI_InfraredView。调整其Rect Transform到你希望显示红外窗口的位置和大小。将RT_InfraredRaw这张Render Texture直接拖拽到RawImage组件的Texture属性上。大功告成运行游戏你应该能看到在UI的指定位置显示着一个只有Infrared层物体、且呈现红外热感颜色的窗口而游戏的主画面保持正常颜色。4. 关键细节、优化与问题排查实现基本功能后我们还需要关注一些细节和潜在问题让效果更完善、性能更优。4.1 处理透明物体与深度如果你的红外视野需要包含半透明物体比如火焰特效需要确保红外摄像机的Render Type如果使用Base它会按照URP的渲染顺序先不透明后透明正确渲染。我们的Renderer Feature在AfterRenderingOpaques事件执行此时不透明物体已渲染但透明物体还未渲染。为了让透明物体也被处理需要将renderPassEvent设置为AfterRenderingTransparents。但这样又可能错过了一些在更后阶段渲染的内容。更稳健的做法让红外摄像机使用一个独立的、完整的渲染流程。即不依赖主渲染器的Renderer Feature而是通过脚本在摄像机渲染完成后手动调用Graphics.Blit。这给了我们更精确的控制时机。不过在URP中更现代的方式是使用RenderPipelineManager.endCameraRendering事件回调。// 示例使用事件在摄像机渲染后立即处理 using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public class InfraredPostProcess : MonoBehaviour { public Camera infraredCamera; public Material infraredMaterial; private RenderTexture _outputTexture; void OnEnable() { RenderPipelineManager.endCameraRendering OnEndCameraRendering; if (infraredCamera.targetTexture ! null) { _outputTexture infraredCamera.targetTexture; } } void OnDisable() { RenderPipelineManager.endCameraRendering - OnEndCameraRendering; } void OnEndCameraRendering(ScriptableRenderContext context, Camera camera) { if (camera infraredCamera infraredMaterial ! null _outputTexture ! null) { // 使用临时RT进行Blit避免读写冲突 RenderTexture tempRT RenderTexture.GetTemporary(_outputTexture.width, _outputTexture.height, 0, _outputTexture.format); Graphics.Blit(_outputTexture, tempRT, infraredMaterial); Graphics.Blit(tempRT, _outputTexture); RenderTexture.ReleaseTemporary(tempRT); } } }将这段脚本挂到红外摄像机上并赋值材质。这样可以确保在摄像机所有渲染包括透明物体完成之后再应用红外效果。4.2 性能优化要点Render Texture分辨率这是最大的性能杠杆。你的UI窗口多大Render Texture就设多大没必要用全屏分辨率。通过RenderTextureDescriptor创建时或直接设置width/height。摄像机的裁剪Culling确保红外摄像机的Culling Mask尽可能精确只渲染必要的层。调整其Far Clip Plane到刚好能看见所有目标物体的距离。Shader复杂度我们的红外着色Shader非常简单一次纹理采样一次点积一次纹理采样混合性能开销很小。但如果你的颜色映射算法非常复杂比如多个噪声纹理叠加模拟热流就需要关注Shader的指令数。避免每帧创建RTRenderTexture.GetTemporary和ReleaseTemporary是配对使用的它们使用一个内部池管理RT效率很高。避免在Update中频繁new RenderTexture()。4.3 常见问题与排查技巧问题1红外窗口一片黑或显示不正确。检查步骤确认红外摄像机的Target Texture已正确赋值RT_InfraredRaw。确认有物体的Layer在红外摄像机的Culling Mask中并且这些物体在摄像机视野内。在Game视图右上角将Display下拉菜单从Game切换到RT_InfraredRaw可以直接查看这张Render Texture的内容。如果这里显示正常问题出在UI或后续处理如果这里也不正常问题出在摄像机渲染阶段。检查InfraredRendererFeature是否被正确添加到红外摄像机使用的Renderer中并且材质已赋值。检查红外着色材质使用的渐变纹理是否有效Shader编译是否有错误材质球是否显示粉色。问题2红外窗口有奇怪的边框或颜色污染。可能原因Blit操作时临时RT的初始值问题或者混合模式不对。解决在CommandBuffer.Blit或Graphics.Blit时确保源和目标清晰。在我们的Renderer Feature中我们先用材质Blit到临时RT再Blit回去是标准做法。也可以尝试在Shader中对UV采样使用CLAMP模式避免边缘采样到纹理之外。问题3UI上的红外窗口和3D场景中的其他特效叠加顺序不对。分析这是“特效覆盖”要处理的核心。UICanvas默认位于Overlay层会渲染在最上层。如果你希望某些全屏后处理特效如Bloom也影响红外窗口或者希望红外窗口能被某些场景元素遮挡就需要更精细的排序。解决这涉及到渲染队列Render Queue和Sorting Layer/Order。对于UI可以调整Canvas的Sort Order。更复杂的方案可能需要将红外窗口也通过一个世界空间的摄像机渲染到另一个RT然后作为3D物体如一个Quad贴图放入场景参与正常的3D深度排序。但这超出了基础“UI窗口”的范畴。问题4移动端性能开销大。优化方向降低RT分辨率。考虑是否每帧都需要更新如果红外视野内容变化不频繁可以改为按需渲染如将红外摄像机的Render设置为手动调用camera.Render()。简化红外着色Shader减少纹理采样和复杂计算。5. 效果扩展与进阶思路基础的红外效果实现后你可以在此基础上进行丰富的扩展使其更逼真或更风格化。动态热源在Shader中除了基于静态亮度的颜色映射还可以引入动态因素。例如在物体上添加一个“热强度”的顶点颜色或纹理通道如UV2的R通道在Shader中采样这个值来影响最终的亮度或颜色映射的偏移模拟物体发热和冷却的过程。热扭曲效果真实的热空气会上浮并导致光线扭曲。可以在红外着色Shader的UV采样阶段加入基于时间或噪声纹理的轻微扰动模拟这种视觉扭曲感。// 在Frag函数中采样前扰动UV float2 distortedUV input.uv float2(sin(_Time.y * 10 input.uv.y * 20) * 0.005, 0); half4 col SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, distortedUV);与全局后处理特效的交互如果你希望主摄像机的某些后处理如运动模糊、镜头污渍也影响红外窗口实现起来会复杂很多。一种思路是将红外摄像机的渲染结果在最终合成到UI之前先与主摄像机的某个中间渲染结果通过额外的摄像机或Render Feature获取进行混合。这需要深入理解URP的渲染管线图Frame Debugger是你的好朋友。多光谱切换将颜色渐变纹理做成可切换的就能轻松实现“普通模式”、“热成像模式”、“夜视模式”等不同观察模式的切换。只需要在运行时动态更换Material中的_RampTex即可。实现这个红外窗口效果的过程是一次对URP渲染管线、多摄像机渲染、Render Texture、自定义Shader和Renderer Feature的综合性实践。它看起来是一个特效但背后串联的是现代游戏渲染中非常核心的资源管理和渲染流程控制思想。理解了这个案例再去看其他类似的“画中画”、“监控屏幕”、“武器瞄准镜”效果你会发现其底层逻辑都是相通的。
返回列表