Unity Shader实现雷达扫描效果:从原理到实战优化

📅 发布时间:2026/7/31 9:37:08
Unity Shader实现雷达扫描效果:从原理到实战优化
1. 项目概述从“雷达扫描”到视觉叙事在游戏开发、数字孪生或者交互式数据可视化项目中我们常常需要一种直观、动态的方式来展示空间信息或状态变化。一个经典的解决方案就是“雷达效果”。它不仅仅是屏幕上划过的一道扇形光波更是一种强有力的视觉语言能够引导玩家或用户的注意力揭示隐藏信息或者营造紧张、探索的氛围。无论是《红色警戒》中的基地扫描还是《Apex英雄》中的侦察技能亦或是智慧城市大屏中实时更新的监控范围雷达效果都扮演着关键角色。这次我们就来深入聊聊如何在Unity中从零开始实现一个既炫酷又实用的雷达效果。这个效果的核心绝不仅仅是画一个扇形那么简单。它涉及到Shader编程、数学计算、性能优化以及如何与游戏逻辑深度结合。我会带你拆解每一个环节分享我踩过的坑和总结的技巧目标是让你不仅能复现一个基础效果更能理解其背后的原理从而灵活应用到你的项目中。无论你是刚接触Shader的新手还是想优化现有效果的开发者相信都能从中找到有用的东西。2. 核心思路与方案选型不止于“画个圈”在动手写代码之前我们先明确目标我们需要一个怎样的雷达效果通常一个完整的雷达效果包含几个关键视觉元素一个动态旋转的扫描扇形、逐渐淡出的扫描波涟漪、以及可能存在的目标点标记。其背后的逻辑是在一个平面上通常是地面或特定UI层模拟雷达波从中心点向外周期性扫描波阵面经过的区域会高亮显示并可能触发一些事件如显示敌人。实现方案主要有三种各有优劣方案一纯Shader实现推荐用于复杂、高性能场景这是最灵活、性能最优的方案。核心思想是使用一个片元着色器Fragment Shader根据当前像素在世界或局部空间中的位置计算其与雷达中心、扫描方向和时间的相对关系动态决定该像素的颜色和透明度。优点完全在GPU运行性能极高可以实现非常复杂和流畅的视觉效果如扭曲、噪声干扰、多层波纹与场景渲染管线深度集成。缺点需要一定的Shader编程基础调试相对不便。适用场景需要大量雷达同时存在、效果要求炫酷如科幻游戏、或需要与场景光照、阴影交互的3D地面雷达。方案二使用粒子系统Particle System模拟波通过控制粒子发射器的形状扇形、速度、生命周期和颜色渐变来模拟扫描波扩散的效果。优点无需编写Shader利用Unity编辑器即可快速搭建和调整直观易懂可以方便地添加纹理、颜色变化。缺点性能开销相对较大尤其是需要精细效果时难以实现“波阵面经过时高亮地面”这种与场景几何体精确交互的效果效果风格受粒子系统限制。适用场景2D游戏、UI上的雷达提示、或者作为Shader方案的补充如添加一些飞散的粒子光点。方案三使用LineRenderer或动态生成网格通过代码动态生成一个扇形的网格或者用LineRenderer画线然后通过修改顶点颜色或材质属性来实现扫描动画。优点实现简单完全在C#中控制逻辑清晰。缺点性能较差每帧更新网格或顶点视觉效果比较“生硬”难以实现平滑的渐变和高级特效。适用场景快速原型验证或者对性能、效果要求极低的场合。我的选择与理由 对于追求效果和性能的通用情况我强烈推荐方案一纯Shader实现。它虽然入门门槛稍高但一次投入长期受益。接下来我们将以这个方案为主线详细拆解实现过程。我会尽量用通俗的方式解释Shader中的数学确保你能跟上。注意在开始前请确保你的Unity项目包含了可编程渲染管线URP或HDRP的支持或者你了解Built-in管线下的Shader编写差异。本文将以URP管线为例进行说明因为这是Unity现在的推荐方向。3. Shader核心原理深度拆解要实现雷达扫描我们的Shader需要解决几个核心问题如何定义扇形区域如何让扇形动起来如何实现波阵面的渐变内实外虚和涟漪效果这一切都始于一段关键的数学计算。我们首先在Shader中定义雷达的属性// 在Properties块中定义方便在材质面板调节 _RadarCenter(“雷达中心”, Vector) (0, 0, 0, 0) _RadarRadius(“扫描半径”, Float) 10 _ScanWidth(“扫描角度”, Range(1, 180)) 90 _ScanSpeed(“扫描速度”, Float) 180 // 度/秒 _ScanFrontWidth(“波前宽度”, Range(0.01, 1)) 0.1 _WaveColor(“波阵面颜色”, Color) (0, 1, 0, 1) _FadeColor(“衰减区颜色”, Color) (0, 0.5, 0, 0.5)现在进入片元着色器的核心逻辑。对于屏幕上每一个像素片元我们都需要判断它是否在“正在扫描的扇形区域”内。第一步计算相对位置和角度。// 假设雷达在XZ平面上地面_RadarCenter是世界坐标 float3 worldPos GetWorldPosition(input); // 获取当前片元的世界坐标 float2 dir worldPos.xz - _RadarCenter.xz; // 计算相对于雷达中心的水平方向向量 float distance length(dir); // 计算距离 float angle atan2(dir.x, dir.y); // 计算方向向量的角度弧度制atan2返回值范围是[-π, π] // 将角度归一化到[0, 2π]区间方便计算 angle fmod(angle 2 * PI, 2 * PI);第二步计算当前扫描线的角度。扫描线是随时间旋转的我们需要一个基于时间变化的当前角度。float _CurrentScanAngle _Time.y * _ScanSpeed * (PI / 180); // 将速度转换为弧度/秒再乘以时间 _CurrentScanAngle fmod(_CurrentScanAngle, 2 * PI); // 取模让角度在[0, 2π]循环第三步判断像素是否在扫描扇形内。这是最关键的一步。我们不能简单地判断角度是否等于_CurrentScanAngle那样只有一条线。我们需要一个宽度。// 将扫描角度宽度也转换为弧度 float scanWidthRad _ScanWidth * (PI / 180); // 计算当前像素角度与扫描线角度的差值并处理360度循环的边界情况 float angleDiff abs(angle - _CurrentScanAngle); angleDiff min(angleDiff, 2 * PI - angleDiff); // 处理循环例如359度和1度其实只差2度 // 判断条件1. 距离小于半径2. 角度差小于扫描宽度的一半 bool isInScanSector (distance _RadarRadius) (angleDiff scanWidthRad * 0.5);如果isInScanSector为真说明这个像素在扫描扇形的“几何范围”内。但这还不够我们需要让效果有渐变。第四步计算波阵面强度与涟漪。我们希望在扫描波的前沿最亮然后向后逐渐衰减。if (isInScanSector) { // 计算“前沿”因子越靠近扫描方向前沿值越接近1 // angleDiff越小表示越靠近中心线。用1减去归一化的角度差得到前沿强度。 float frontFactor 1.0 - saturate(angleDiff / (scanWidthRad * 0.5)); // 计算“距离”因子越靠近雷达中心波的能量越强或者我们更希望波阵面处的强度高。 // 这里我们计算像素在扇形内的“径向位置”让波阵面靠近外圈更亮。 // 一种方法是计算像素相对于扫描方向的“进度”。 float radialProgress distance / _RadarRadius; // 单纯的距离比 // 更高级的做法结合时间模拟一个从中心向外移动的波前 float waveFront _RadarRadius * fmod(_Time.y * _WaveSpeed, 1.0); // 假设_WaveSpeed是波速 float distanceToWaveFront abs(distance - waveFront); float waveFactor saturate(1.0 - distanceToWaveFront / _ScanFrontWidth); // 综合因子 float finalStrength frontFactor * waveFactor; // 混合颜色 float4 finalColor lerp(_FadeColor, _WaveColor, finalStrength); // 还需要根据距离和角度计算一个整体的透明度衰减让扇形边缘淡出 float distanceFade 1.0 - saturate(distance / _RadarRadius); float edgeFade 1.0 - saturate(angleDiff / (scanWidthRad * 0.5)); // 扇形两侧边缘淡出 finalColor.a * distanceFade * edgeFade; return finalColor; } else { // 不在扇形内返回透明或背景色 return float4(0,0,0,0); }以上代码勾勒出了最核心的逻辑。实际编写时我们还需要将其嵌入到完整的URP Shader Graph节点中或者写成HLSL代码。使用Shader Graph可以通过节点连线更直观地构建这个逻辑特别是处理角度计算和条件判断。4. 完整实现步骤与材质配置理解了原理我们开始动手。这里我给出使用URP Shader Graph和编写Custom Shader两种路径的详细步骤。4.1 使用URP Shader Graph实现可视化推荐新手创建Shader Graph在Project窗口中右键 - Create - Shader - Universal Render Pipeline - Shader Graph。命名为“RadarScan”。设置Graph设置在Graph Inspector中将“Graph Type”设置为“Sub Graph”或“Master Stack”确保支持自定义光照模式。我们这里可能需要“Unlit”模式因为雷达通常是自发光效果。构建核心节点网络获取世界位置添加一个Position节点设置为World空间。计算水平方向与距离用Split节点分离出世界位置的X和Z分量与Vector2类型的_RadarCenter属性相减得到dir向量。用Length节点计算其长度得到distance。计算角度使用Arctangent2节点输入dir.x, dir.y计算角度。注意处理角度范围使用Fraction和乘法、加法节点将其归一化到[0, 1]对应[0, 2π]。生成当前扫描角度添加一个Time节点获取_Time.y乘以_ScanSpeed需要转换为弧度因子如* 0.01745再用Fraction节点取小数部分得到[0,1)循环的值再乘以2π。角度差与扇形判断计算当前像素角度与扫描角度的差值。处理循环差值是个难点在Shader Graph中可以用条件节点计算差值diff1 abs(A-B)差值diff2 1.0 - diff1因为归一化到1代表2π然后用Min节点取两者最小值。再将此差值除以扫描宽度归一化后的与1比较。距离判断比较distance和_RadarRadius。综合判断将角度判断和距离判断的输出用And逻辑节点连接输出即为是否在扇形内。强度计算基于角度差和距离计算frontFactor和waveFactor。这里可以引入一个_WaveOffset属性用Time驱动模拟向外移动的波前。颜色混合与输出用Lerp节点混合_FadeColor和_WaveColor用计算出的综合强度作为插值系数。最后连接到Master Stack的Color和Alpha端口。记得用distanceFade和edgeFade来调制最终的Alpha。创建材质并应用将生成的RadarScan.shadergraph文件拖到场景中的一个平面Plane或自定义网格上生成材质。在材质面板调整_RadarCenter,_Radius等参数你就能看到扫描效果了。实操心得在Shader Graph中调试时可以经常使用Custom Function节点或者将中间计算结果如角度、距离直接输出到颜色上方便可视化查看数值范围是否正确。例如将角度除以2π后输出到Color的R通道你就能看到一个彩色的角度图。4.2 编写Custom HLSL Shader更灵活性能控制更细对于更复杂的效果或者需要深度优化的项目直接编写Shader代码是更好的选择。以下是一个URP下Unlit Shader的简化框架Shader “Custom/RadarScan” { Properties { _BaseColor (“Base Color”, Color) (1,1,1,1) _RadarCenter (“Radar Center”, Vector) (0,0,0,0) _RadarRadius (“Radius”, Float) 5 _ScanWidth (“Scan Width (Deg)”, Range(5, 180)) 90 _ScanSpeed (“Scan Speed (Deg/sec)”, Float) 180 _WaveColor (“Wave Color”, Color) (0,1,0,1) _WaveWidth (“Wave Width”, Range(0.01, 0.5)) 0.1 } SubShader { Tags { “RenderType”“Transparent” “Queue”“Transparent” “RenderPipeline”“UniversalPipeline”} Blend SrcAlpha OneMinusSrcAlpha ZWrite Off Pass { HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include “Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl” struct Attributes { float4 positionOS : POSITION; }; struct Varyings { float4 positionHCS : SV_POSITION; float3 positionWS : TEXCOORD0; }; Varyings vert (Attributes IN) { Varyings OUT; VertexPositionInputs vertexInput GetVertexPositionInputs(IN.positionOS.xyz); OUT.positionHCS vertexInput.positionCS; OUT.positionWS vertexInput.positionWS; return OUT; } // 声明属性变量 float4 _RadarCenter; float _RadarRadius; float _ScanWidth; float _ScanSpeed; float4 _WaveColor; float _WaveWidth; float4 frag (Varyings IN) : SV_Target { // 将属性转换为方便计算的变量 float scanWidthRad radians(_ScanWidth); float currentAngle fmod(_Time.y * radians(_ScanSpeed), TWO_PI); float2 dir IN.positionWS.xz - _RadarCenter.xz; float distance length(dir); float angle atan2(dir.x, dir.y); // 注意坐标系可能需要调整 angle fmod(angle TWO_PI, TWO_PI); float angleDiff abs(angle - currentAngle); angleDiff min(angleDiff, TWO_PI - angleDiff); float inSector (distance _RadarRadius) (angleDiff scanWidthRad * 0.5); if (inSector) { // 计算波前强度 float waveFront _RadarRadius * fmod(_Time.y * 2.0, 1.0); // 示例波速 float distToFront abs(distance - waveFront); float waveFactor 1.0 - saturate(distToFront / (_WaveWidth * _RadarRadius)); // 边缘淡化 float distanceFade 1.0 - smoothstep(_RadarRadius * 0.7, _RadarRadius, distance); float angleFade 1.0 - smoothstep(scanWidthRad * 0.4, scanWidthRad * 0.5, angleDiff); float4 col _WaveColor; col.a * waveFactor * distanceFade * angleFade; return col; } return float4(0,0,0,0); } ENDHLSL } } }将这个Shader保存为.shader文件赋给材质即可。代码提供了更清晰的控制流你可以轻松地添加噪声纹理干扰、多重波纹、深度检测等高级功能。4.3 材质与场景配置技巧渲染队列与混合模式雷达通常是半透明叠加效果需要设置正确的Tags。“Queue”“Transparent”确保它在不透明物体之后渲染。Blend SrcAlpha OneMinusSrcAlpha是标准的Alpha混合模式。ZTest与ZWrite通常设置ZWrite Off防止雷达效果写入深度缓冲区避免遮挡后续物体。ZTest LEqual默认或ZTest Always取决于你是否希望雷达被场景物体遮挡。应用到什么物体上最常见的是应用到一个覆盖地面的低面数网格如圆形或方形平面。你也可以将其应用到Shader Ball一个始终面向相机的广告牌网格作为角色头上的扫描指示器。多雷达管理如果场景需要多个雷达你有两种选择一是为每个雷达创建单独的材质实例通过脚本修改每个材质的_RadarCenter等属性二是编写更复杂的Shader支持多个雷达数据如使用数组但这会显著增加Shader复杂度。通常材质实例的方式更简单可控。5. 性能优化与高级效果拓展基础效果实现后我们关注如何让它更快、更炫。5.1 性能优化要点减少Overdraw雷达Shader通常是全屏透明的即使像素最终输出Alpha为0也可能经过了所有计算。可以在片元着色器最开头进行快速剔除。例如如果像素距离雷达中心远超_RadarRadius可以直接clip(-1)或返回透明。这能节省大量GPU计算。float distance length(worldPos.xz - _RadarCenter.xz); if (distance _RadarRadius * 1.05) return float4(0,0,0,0); // 额外增加5%缓冲区域简化计算在Shader中atan2、sin、cos等都是相对耗时的操作。如果扫描范围是固定的比如总是90度可以尝试用向量点积和叉积来判断点是否在扇形内避免使用反三角函数。但这种方法对于动态角度的通用扇形判断会更复杂。使用LOD细节层次对于远处的雷达可以使用一个简化版本的Shader减少波纹数量、降低计算精度。批处理与GPU Instancing如果场景中有大量完全相同的雷达仅位置不同确保Shader启用了GPU Instancing并尽可能合并绘制调用。5.2 高级视觉效果实现多层涟漪单一的扫描波可能显得单调。我们可以模拟多个同心圆状的涟漪从中心向外扩散。在Shader中这可以通过多个waveFront计算来实现每个波有自己的速度和相位偏移。float waveFactor 0; for (int i 0; i _WaveCount; i) { float phase _Time.y * _WaveSpeed i * _WaveInterval; float waveFront _RadarRadius * fmod(phase, 1.0); float distToFront abs(distance - waveFront); waveFactor saturate(1.0 - distToFront / _WaveWidth); } waveFactor saturate(waveFactor); // 防止叠加过亮噪声干扰模拟雷达信号受到干扰的效果。采样一张噪声纹理Noise Texture根据时间和位置偏移UV将噪声值添加到角度计算或强度计算中可以制造出扫描线抖动、信号断续的效果。float noise tex2D(_NoiseTex, worldPos.xz * _NoiseScale _Time.x).r; // 干扰角度使扇形边缘不规则 float perturbedAngleDiff angleDiff (noise - 0.5) * _PerturbStrength;深度检测与遮挡让雷达波只显示在地面或特定物体上遇到墙壁或障碍物时被遮挡。这需要深度纹理Depth Texture的支持。在URP中你需要启用_CameraDepthTexture在Shader中采样深度比较当前像素的深度与雷达波“理论”深度通常是地面高度如果当前场景深度更小物体更近则减弱或取消雷达波的显示。与游戏逻辑交互这是雷达效果的灵魂。通常我们会在C#脚本中管理一个潜在目标列表。在Update中对于每个目标计算其位置是否在当前扫描扇形区域内使用和Shader中类似的数学。如果是则触发事件比如显示目标图标、播放音效、或将其状态设为“已探测”。Shader本身不处理游戏逻辑它只负责视觉表现。逻辑和渲染分离是良好的设计。6. 常见问题排查与实战心得即使按照步骤操作你也可能会遇到一些问题。这里记录了一些典型坑点和解决方案。问题1雷达效果不显示或全黑。检查材质Shader确认材质球是否正确选择了你创建的Radar Scan Shader。检查渲染队列和混合确保Shader的Queue是Transparent并且Blend模式正确。不透明物体会挡住它。检查中心点和半径_RadarCenter是世界坐标。如果你的雷达网格在(0,0,0)但中心点设在了别处效果可能出现在你看不到的地方。可以尝试在Shader中直接将中间变量如distance输出为颜色来调试。检查平面朝向默认的Unity Plane平面朝向是Y轴向上。如果你的雷达是平铺在地面上的那么Shader中计算worldPos.xz是正确的。如果你的平面是竖着的可能需要调整计算如用worldPos.xy。问题2扫描边缘有锯齿Aliasing。原因在扇形边界和波前沿颜色强度变化剧烈像素采样不足导致锯齿。解决方案使用smoothstep代替硬性saturate或条件判断。smoothstep会在边界处产生平滑过渡能有效抗锯齿。// 将 bool isInScanSector (angleDiff width) 改为 float sectorSmooth 1.0 - smoothstep(width*0.9, width, angleDiff); // sectorSmooth在扇形内为1边界处平滑过渡到0。开启抗锯齿在项目质量设置或相机上开启MSAA。增加渲染分辨率对于UI或全屏效果可以考虑使用Render Texture以更高分辨率渲染再缩放到屏幕。问题3多个雷达重叠时渲染顺序错乱。原因半透明物体的渲染顺序依赖其到相机的距离但多个雷达平面可能在同一深度导致排序错误。解决方案尽量使用ZWrite Off避免深度写入干扰。手动控制渲染队列通过脚本动态修改不同雷达材质的renderQueue确保后渲染的雷达在正确的上层。但这比较繁琐。使用Stencil Buffer模板缓冲区这是一个高级技巧。可以为雷达区域写入一个模板值然后让后续的雷达效果只在模板值匹配的区域绘制可以精确控制叠加。但这需要较深的图形学知识。问题4在移动设备上性能不佳。简化Shader移除或简化循环如多层涟漪、复杂的噪声计算。降低精度将部分float计算改为half。减少波纹数量动态调整_WaveCount根据设备性能或距离降低。使用LOD如前所述为不同距离的雷达使用不同复杂度的Shader。我的实战心得参数动画化不要只让扫描角度动起来。尝试让_RadarRadius、_ScanWidth甚至_WaveColor也随着时间或游戏状态变化能让效果更加生动。例如当雷达发现目标时让波纹颜色瞬间变红并闪烁。结合后处理雷达效果可以和后处理Post Processing完美结合。例如使用Bloom泛光让波阵面发出辉光或者使用Vignette暗角来强调雷达屏幕的边界感。音频同步视觉和听觉同步能极大提升沉浸感。为扫描循环、发现目标等事件配上合适的音效并确保音效的播放节奏与Shader中的扫描周期同步。从参考中学习多玩一些带有优秀雷达效果的游戏用录屏软件慢放观察。注意它的波纹细节、颜色变化、遇到障碍物的反应等。理解其设计意图比单纯复制效果更重要。实现一个炫酷的雷达效果是一个融合了数学、图形学和游戏设计的趣味挑战。从最基础的扇形判断到加入动态波纹、噪声干扰再到与游戏逻辑深度结合每一步的深入都能带来表现力的提升。希望这篇长文能为你提供一个坚实的起点和清晰的路线图。剩下的就是发挥你的创意去打造属于你自己的视觉盛宴了。如果在实现过程中遇到任何具体问题不妨回头再看看核心的数学计算和Shader调试方法那往往是解决问题的关键。