Unity红外热成像仿真:从顶点着色到后处理渲染全流程解析 📅 2026/7/22 11:08:08 1. 项目概述一个能“看见”温度的Unity工程最近在做一个模拟训练的项目客户提了个挺有意思的需求能不能在虚拟场景里模拟出红外热成像的效果比如让操作员在夜间或者烟雾环境下通过“热像仪”视角去识别目标。这需求一听就很有挑战性也很有价值。红外热成像的原理是把物体表面的温度分布转换成不同亮度或颜色的可视图像核心就是一张灰度图或者伪彩色图。在Unity里实现这个难点不在于渲染一个“看起来像”的滤镜而在于如何根据场景中物体的“温度”属性动态生成这张图。我在网上搜了一圈免费的、能直接跑起来看效果的完整示例工程真不多。大部分都是讲Shader原理的代码片段或者需要配合复杂的第三方插件。功夫不负有心人还真让我找到了一个宝藏级的免费示例工程。它没有花里胡哨的界面就是一个干净的Unity项目但完整地演示了从“赋予物体温度”到“实时渲染热成像效果”的全流程。这个工程的价值在于它把红外热成像在游戏或仿真中的应用逻辑给跑通了你拿过来改改参数、换换模型就能快速集成到自己的项目里省去了从零研究Shader和渲染管线的时间。这个工程特别适合这几类朋友一是正在开发军事模拟、安防演练、工业检测这类严肃仿真应用的朋友二是对Unity高级渲染技术特别是后处理Post-Processing和自定义着色器Shader感兴趣想找个实际案例深入学习的朋友三是需要快速验证某个创意原型比如做一个“幽灵视觉”或者“能量视觉”的游戏玩法的独立开发者。接下来我就结合这个工程把红外热成像效果在Unity里的实现思路、核心技术和实操细节掰开揉碎了讲清楚。2. 核心思路与方案选型如何让虚拟物体“有温度”要实现热成像第一步不是写Shader而是要想清楚数据从哪里来。在真实世界热像仪接收的是红外辐射。在Unity的虚拟世界里我们需要给物体定义一个模拟的“温度”属性并让这个属性能够被我们的渲染系统读取。2.1 温度数据的来源与存储这个示例工程采用了最直观且灵活的一种方案利用顶点颜色Vertex Color的R通道来存储温度信息。为什么是顶点颜色因为它本身就是网格数据的一部分可以被Shader直接访问不需要额外的纹理采样效率很高。而且通过脚本或者建模软件我们可以很方便地为模型的每个顶点绘制不同的颜色值从而模拟出温度分布。注意顶点颜色的精度是有限的通常每个通道是0-255的整数在Shader中会被归一化为0-1的浮点数。对于温度梯度变化非常平滑细腻的物体比如一块被均匀加热的金属板用顶点颜色可能会产生色阶断层。但对于大多数仿真场景人体、车辆、建筑其温度变化本身就是块状的这个精度完全够用。当然还有别的方案。比如可以单独用一张纹理一张贴图来存储温度信息这张图叫做“温度图”或“热辐射图”。这样精度更高可以表现更细腻的温度变化但代价是多了一次纹理采样并且需要额外的美术资源制作这张图。示例工程选择顶点颜色体现了“快速原型”的思想用最小的开销和最简单的流程先实现核心功能。2.2 渲染管线与后处理的选择有了温度数据怎么把它变成屏幕上的灰度图呢这里涉及到渲染流程的选择。示例工程是基于Unity内置的渲染管线Built-in Render Pipeline实现的这保证了最大的兼容性无论你的项目是老项目还是新项目都能轻松导入。它的核心思路是使用后处理Post-Processing技术。具体流程是这样的正常渲染场景先按照普通方式渲染一遍得到一张包含颜色、深度等信息的画面。提取温度信息通过一个自定义的后处理Shader在全屏范围内对每个像素点找到它对应的场景物体表面的那个点然后读取该点顶点颜色中的R值即温度。映射为灰度将读取到的温度值0-1通过一个映射函数转换成最终的灰度值或者伪彩色输出到屏幕。这个方案的优势是非侵入式。你不需要修改场景中每个物件的材质球只需要在摄像机上加一个后处理脚本和效果就能让整个场景拥有热成像视角。切换视角就像开关一个滤镜一样简单。3. 工程核心组件拆解与实操下载并打开这个示例工程后你会看到场景结构很清晰。我们重点看几个核心部分。3.1 温度着色器ThermalShader.shader这是整个工程的灵魂一个Surface Shader。它本身并不复杂关键的一行代码是// 在surf函数中从顶点输入中获取温度值 float temperature IN.color.r; // 假设温度存储在顶点颜色的红色通道这个Shader被应用在一个叫“ThermalMaterial”的材质球上。它的作用是在渲染物体时将顶点颜色中的温度信息传递出去供后处理阶段使用。这里有个关键技巧为了让后处理能读到这些信息我们需要确保场景在渲染时温度数据被写入到一张中间纹理里。示例工程通常通过设置摄像机的某个渲染目标或者使用OnRenderImage方法配合特定的Shader Pass来实现这一步。实操心得在你自己创建温度材质时记得将材质的Shader选为“Custom/Thermal”或者示例工程中的对应名称。然后将这个材质赋给场景中需要具有热成像效果的物体。如果物体没有顶点颜色信息它就会显示为默认色通常是低温的黑色。3.2 后处理脚本ThermalPostEffect.cs这个C#脚本挂载在主摄像机上它负责调用后处理Shader。它的核心工作是在OnRenderImage函数中获取当前渲染好的源图像source。创建一个临时的RenderTexture作为目标destination。使用Graphics.Blit函数将源图像、温度信息可能来自另一个渲染纹理以及一些控制参数如温度范围、对比度传递给一个名叫“ThermalPostProcess”的Shader进行全屏处理。将处理结果输出到屏幕。脚本里通常会有一些可调节的参数比如MinTemperature/MaxTemperature定义温度显示范围。低于最小值的显示为最暗冷高于最大值的显示为最亮热。Contrast对比度让温度差异更明显。UseColorGradient是否使用伪彩色代替灰度。3.3 后处理着色器ThermalPostProcess.shader这个Shader是实际进行“热成像转换”的地方。它是一个Image Effect Shader。其核心片段着色器代码逻辑如下// 假设 _ThermalTex 是一张包含了场景温度信息的纹理 fixed4 thermalData tex2D(_ThermalTex, i.uv); float temp thermalData.r; // 提取温度值 // 将温度值映射到0-1区间基于Min/Max Temperature参数 float remappedTemp saturate((temp - _MinTemp) / (_MaxTemp - _MinTemp)); // 灰度输出 fixed4 grayColor fixed4(remappedTemp, remappedTemp, remappedTemp, 1.0); // 或者使用伪彩色梯度纹理Ramp Texture进行着色 fixed4 colorOutput tex2D(_ColorRampTex, float2(remappedTemp, 0.5)); return colorOutput;参数计算过程解析remappedTemp的计算是关键。saturate函数将结果钳制在0到1之间。假设_MinTemp设为0.3_MaxTemp设为0.8。当一个点的温度temp为0.5时计算(0.5-0.3)/(0.8-0.3)0.4这意味着该点在当前显示的温度区间里处于40%的位置会显示为中灰色。如果temp低于0.3结果为负被saturate截断为0显示为黑色最冷高于0.8则结果为1显示为白色最热。通过调整_MinTemp和_MaxTemp你可以实现“温度窗”的效果只关注特定温度区间的物体。3.4 为模型赋予温度顶点颜色绘制这是准备工作的核心。你需要让模型有温度数据。有两种主要方法在3D建模软件中绘制这是最推荐的方法可以获得精确、艺术可控的温度分布。以Blender为例将模型导入Blender进入“顶点绘制”模式。选择红色通道因为我们的Shader用R通道存温度。用画笔工具在模型表面绘制。比如想象发动机部位是热的就涂成亮红色值接近1车身外壳是常温涂成暗红色值比如0.2-0.4阴影或背阴面是冷的涂成黑色值为0。导出模型时务必勾选“导出顶点颜色”选项。在Unity中通过脚本动态生成适合程序化生成温度或者根据游戏逻辑动态变化。获取模型的Mesh组件。访问mesh.colors数组这是一个Color数组对应每个顶点的颜色。根据你的规则比如距离某个热源越近温度值越高计算每个顶点应有的温度值并赋值给Color的r字段。记得调用mesh.RecalculateBounds()等必要函数并考虑性能避免每帧修改。重要提示在Unity中默认的FBX导入器可能不会自动识别顶点颜色。如果导入后模型在Scene视图中看不到顶点色需要在Project面板选中模型文件在Inspector的“Model”分页下找到“Import Colors”选项并勾选。4. 效果优化与高级应用拓展基础效果实现后我们可以让它更真实、更实用。4.1 模拟热传导与动态温度变化静态的温度图看久了会显得假。我们可以用简单的脚本模拟温度的动态变化。思路是在场景中设定几个“热源”比如点光源位置然后每帧根据物体顶点到热源的距离衰减计算温度值并更新到顶点颜色中。// 简化示例非完整代码 void UpdateThermalData() { Mesh mesh GetComponentMeshFilter().mesh; Vector3[] vertices mesh.vertices; Color[] colors mesh.colors; Vector3 worldPos transform.position; for (int i 0; i vertices.Length; i) { Vector3 worldVertexPos worldPos vertices[i]; float temp 0f; foreach (var heatSource in heatSources) // heatSources是热源Transform列表 { float distance Vector3.Distance(worldVertexPos, heatSource.position); // 温度随距离平方衰减简化模型 temp Mathf.Clamp01(1.0f / (distance * distance 1)); } colors[i].r Mathf.Clamp01(temp); // 更新R通道 } mesh.colors colors; }注意每帧更新整个网格的顶点颜色对性能消耗很大只适用于顶点数很少的物体或用于原型验证。生产环境可以考虑使用计算着色器Compute Shader在GPU上并行处理或者将温度数据编码到一张纹理上通过采样纹理来动态更新。4.2 实现伪彩色与等温线纯灰度图虽然专业但伪彩色彩虹色、铁红色等更能直观区分温度层次。在后处理Shader中这通常通过查询一个一维的“颜色梯度纹理”来实现。在Photoshop或其他工具中创建一张长条形的纹理从左到右颜色渐变例如黑-蓝-青-绿-黄-红-白。在Shader中声明一个sampler2D _ColorRampTex并在属性中暴露方便在材质面板调整。在片段着色器中用计算得到的remappedTemp值作为U坐标V坐标取0.5去采样这张梯度纹理。fixed4 finalColor tex2D(_ColorRampTex, float2(remappedTemp, 0.5));等温线效果可以增强科学分析感。可以在Shader中添加一个步骤对remappedTemp进行“阶梯化”处理。例如将连续的0-1值通过floor(remappedTemp * numberOfBands) / numberOfBands这样的计算量化成有限的几个阶梯值然后再去映射颜色就会产生明显的色带分界线。4.3 与真实物理属性挂钩对于仿真度要求更高的项目温度不应该只是画上去的。可以尝试与Unity的物理系统做简单挂钩。例如定义一个ThermalObject脚本挂在物体上包含currentTemperature、thermalMass热质量、emissivity发射率等属性。在Update中模拟与环境的换热currentTemperature (ambientTemperature - currentTemperature) * heatTransferRate * Time.deltaTime;将计算出的currentTemperature通过脚本映射到模型顶点颜色的R通道上或者写入到一张专用的温度纹理中。这样一个靠近热源的物体温度会逐渐升高移开后又会慢慢冷却效果会生动很多。5. 常见问题排查与性能调优实录在实际集成和测试这个示例工程时你可能会遇到以下问题。5.1 效果不显示或显示异常问题现象可能原因排查步骤与解决方案屏幕全黑或全白后处理Shader未正确执行或温度范围设置极端。1. 检查摄像机上的ThermalPostEffect脚本是否启用。2. 检查脚本中引用的ThermalPostProcess材质球是否赋值正确。3. 调整脚本或Shader中的_MinTemp和_MaxTemp参数将其设为一个合理的范围如0.2和0.8。场景无变化还是普通画面温度数据没有传递到后处理阶段。1. 确认场景中的物体是否使用了正确的ThermalMaterial材质。2. 检查用于存储温度信息的中间纹理如_ThermalTex是否被正确渲染。可能需要查看ThermalPostEffect脚本中Graphics.Blit的调用确认源纹理和目标纹理是否正确。3. 在ThermalShader中确保温度数据顶点颜色R通道被输出到正确的渲染目标如G-buffer的某个自定义通道或单独的RenderTexture。模型显示为纯色如全红模型顶点颜色数据可能未导入或全部为同一值。1. 在Unity的Scene视图将着色模式切换到“Shaded Wireframe”或“Vertex Color”查看模型是否有顶点色变化。2. 在Project面板选中模型文件在Import Settings中确认勾选了“Import Colors”。3. 回到3D软件检查顶点绘制确保有变化。边缘闪烁或重影深度纹理Depth Texture未启用或后处理Shader深度采样有误。1. 在主摄像机上勾选“Allow MSAA”并确保“Depth Texture Mode”不是“Disabled”。对于后处理通常需要深度信息来精确定位像素对应的世界位置。2. 检查后处理Shader中关于深度重建世界坐标的代码。示例工程如果涉及此部分代码可能较为复杂确保理解其原理。一个简单的测试是暂时屏蔽掉依赖深度的部分看是否还有闪烁。5.2 性能优化要点后处理效果是每帧对全屏幕像素进行操作Shader复杂度直接影响帧率。简化Shader计算确保后处理Shader中的数学运算尽可能简单。避免在片段着色器中使用循环、复杂的三角函数或多次纹理采样。像温度映射这样的操作一次纹理采样和几次乘加运算就足够了。降低渲染分辨率如果对画面精度要求不高可以考虑让后处理效果在较低的分辨率下渲染。在ThermalPostEffect脚本的OnRenderImage中可以创建一个尺寸为原图一半的RenderTexture作为中间目标处理完成后再上采样到屏幕分辨率。这能显著提升性能。有选择地启用热成像效果通常只在特定视角或模式下需要。不要让它一直运行。可以通过脚本来控制ThermalPostEffect组件的enabled属性或者控制应用热成像材质的摄像机的启用状态。合并后处理效果如果你的项目还有其他后处理效果如Bloom, Color Grading尽量将它们合并到一个Shader中执行减少全屏Blit的次数。多次后处理Pass是性能杀手。5.3 从Built-in管线迁移到URP/HDRP示例工程是基于Built-in管线的。如果你使用的是URP或HDRP需要做适配。URPURP使用Volume系统和Renderer Features。你需要将后处理逻辑改写为一个Scriptable Renderer Feature。在AddRenderPasses方法中创建并配置你的自定义ScriptableRenderPass。在这个Pass里你需要使用CommandBuffer来调度绘制命令核心仍然是调用Blit但API有所不同。温度数据的获取可能需要通过Renderer Features中的DrawRenderers设置特定的RenderState和ShaderPass来将温度信息渲染到一张RTHandle上。HDRPHDRP的后处理框架更复杂通常需要编写自定义的FullScreen Custom Pass。将你的后处理Shader编写为HDRP兼容的HLSL代码并在Custom Pass中执行。HDRP对Shader的输入输出有严格规定需要参考官方文档和示例。迁移过程是学习渲染管线底层的好机会但确实有门槛。如果项目不紧急建议先在Built-in管线下完成原型开发。这个免费的示例工程就像一个功能完整、结构清晰的“骨架”它展示了红外热成像在Unity中从数据到显示的核心链路。你拿到手后可以根据项目需求为这个骨架填充“肌肉”和“皮肤”——比如更逼真的动态温度模型、更丰富的伪彩色方案、与游戏逻辑的深度结合等。它节省的是你最初也是最耗时的探索阶段让你能直接站在一个可行的起点上向更深处进发。