☰
Unity LineRenderer实现心电图波形模拟:参数可调、支持UI与3D场景
2026/10/6 8:40:09 网站建设 项目流程

简介:Unity线波模拟心电图上下振幅项目demo,面向Unity初学者与游戏特效开发者,提供可视化ECG线波效果,波线速度、颜色与振幅均可自定义调整,适合医疗模拟、数据可视化或交互式视觉场景。包体共6688个文件,压缩包约39.19MB,以C#脚本与Unity资源为主,涵盖1066个cs脚本、1828个meta资源配置、575个png贴图、31个asset及5个prefab预制体等,覆盖项目脚本、场景配置与图形资源,可直接导入Unity工程运行调试。已有331人学习浏览。按键交互与动态参数调整机制,为理解Unity网格渲染、材质颜色控制与时间动画等核心编程思路提供了直观参考案例。上手门槛低,实测运行稳定,既适合拿来即用,也便于二次开发与学习拆解。

1. Unity 线波模拟心电图 Demo:振幅、速度、颜色全可调,拖进场景就能跑

做医疗设备界面、数据可视化 HUD,或者游戏里的生命体征面板时,最烦的就是「要一条心电图那样的波形线」。自己写吧,三角波、正弦波调半天不像;用插件吧,一堆依赖。这个 Unity 线波模拟心电图效果的 demo 属于打开就能用的那一类:波线速度、上下振幅、颜色全部在 Inspector 里暴露,改完立即生效,亲测 Unity 2018 以上的工程里都能跑。适合 UI 面板、2D 场景、3D 大屏,甚至数字孪生看板里做装饰性生命体征显示。下面按拆项目的习惯,从原理讲到踩坑,把这条线怎么被画出来的讲透,看完你也能自己改出想要的心电波形。

2. 原理与选型:为什么用 LineRenderer 逐帧改点,而不是 Shader 采样

2.1 波形生成的核心逻辑:phase 累加与顶点重排

任何一个波形动画,本质上都是「让时间参与点的坐标计算」。波形线不是一整张贴图在滚动,而是一排采样点在横轴上固定、纵轴随某个函数变化。心电图里的波不是纯正弦,它由 P 波、QRS 波群、T 波组成,每个部分频率和振幅都不同。所以GetWaveHeight不能只写一个Mathf.Sin,得用多正弦叠加,或者用分段函数去逼近。demo 默认是几个不同频率、不同振幅的正弦叠在一块,P 波的小起伏、QRS 的尖峰、T 波的宽缓靠不同频率分量自然凑出来,视觉上已经接近监护仪。

void Update() { phase += Time.deltaTime * speed; // 相位累加,speed 决定流动速度 for (int i = 0; i < pointCount; i++) { float t = (float)i / (pointCount - 1); // 横坐标归一化到 0~1 float x = (t - 0.5f) * lineLength; // 居中显示,避免线偏出视野 float y = GetWaveHeight(t, phase) * amplitude; // 振幅乘在函数外侧 positions[i] = new Vector3(x, y, 0f); } lineRenderer.SetPositions(positions); // 顶点一次性提交 }

这段代码有三个设计点。第一,phase只增不减,波形默认从左到右流动,想反向就把speed设成负值。第二,amplitude乘在波形函数外面,意味着所有波峰等比例缩放,调振幅时波形形状不塌,只是整体长高变矮,符合直觉。第三,用SetPositions一次提交整个数组,比循环调用SetPosition少一截 API 开销,波型点数到 256 时差距能感觉得到。

还有个小细节:长时间挂着不关,phase会一直涨。浮点数的精度在值很大时会下降,导致波形出现肉眼可见的抖动。常见做法是给 phase 做个取模,比如phase %= 10000f,一天跑下来数值也不会溢出精度范围。这个属于不写也行、写了更稳的边际优化,我在正式项目里都会带上。

2.2 为什么是 LineRenderer 而不是 Shader:性能、可读性与移动端友好

另一条常见路线是着色器方案:建一个细长的平面网格,在片元着色器里按 UV 采样高度函数做顶点偏移。这条路能渲染出非常精细的波形纹理,适合「整片心电图屏幕」的效果,但对一个参数要全开放、要随手改的 demo 来说代价很大——要维护 Shader 文件,要处理内置管线与 URP 的兼容差异,参数要从 C# 传到材质属性,调试链路长一大截。资源本身没走这条路,选 LineRenderer 是有道理的。

LineRenderer 方案把这些麻烦全压了下去:状态全部暴露在 Inspector,参数就是 C# 字段,运行时Debug.Log就能看到中间值。性能上,只要管住两点——点数量别超几百、每帧别 new 数组——移动端毫无压力。给个对比表供你选型时判断:

维度LineRenderer 方案Shader 方案
修改成本改 C# 字段,即时生效改材质参数,重启着色器编译
兼容性内置管线/URP/HDRP 通用管线间材质不通用
性能瓶颈顶点数与 GC 分配采样次数与填充率
扩展上限适合线条类波形可做纹理级复杂效果

放到实际项目里,生命体征线、告警波形、音频可视化这类需求,LineRenderer 足够;等哪天要做整屏像素级心电图纹理,再切 Shader 不迟。移动端上尤其要注意:Shader 方案如果没做好纹理采样优化,发热和耗电会比逐帧改 LineRenderer 顶点更难看。

2.3 宽度、颜色与坐标:三个常被忽略的属性

widthMultiplier、colorGradient、useWorldSpace这三个属性 demo 里都暴露了,但很多人只调前两个。useWorldSpace默认是 true,点的坐标按世界坐标算;波形放进 Canvas 做 UI 时如果忘记改成 false,坐标会被父级 Canvas 的 scale 再乘一遍,波形直接飞出屏幕,表现就是「波形烂成一团」。记住一句话:场景显示用 true,UI 显示用 false,没有第三种情况。

颜色渐变是心电图观感的关键。打开 LineRenderer 的 Color 属性,渐变编辑器默认是一条从左到右的色带,起点终点各一个色标。你可以中间加色标,把 QRS 高峰对应的位置调成偏青白的高亮,其余部分保持半透明蓝,播放起来就是荧光屏脉冲感。这个操作在编辑器里两分钟完成,对观感提升比调任何数字都明显。如果线边缘出现奇怪的半透明叠加,检查 LineRenderer 的 textureMode,改成 Stretch 而不是 Tile,否则颜色会被纹理平铺切割。

参数作用建议值
speed波线水平流动速度1.0~3.0
amplitude上下振幅0.5~2.0
pointCount波形采样点数64~256
widthMultiplier线宽0.03~0.06
frequency波形疏密8~20
useWorldSpace坐标空间场景 true,UI false

这张参数表是我反复试出来的起步区间,不保证一次到位,但能让你从零状态快速拉到一条能看的波形,后续微调靠眼睛就行。

3. 场景落地:导入、挂载、坐标与参数面板一次走通

3.1 导入方式与工程目录说明

资源解压后是标准 Unity 工程结构,Assets 下有 Scripts、Prefabs、Materials 三个目录,没有第三方包依赖。两种接入方式看现状:空工程直接用 Hub 的 Add 按钮选到工程根目录打开;已有业务工程,把三个目录拷进自己工程的 Assets 即可,脚本清一色 C#,编译通过就能用,不用改任何 Player Settings。Prefabs 目录里已经放好一个组装好的波形物体,嫌手动搭麻烦的,直接从 Project 窗口拖进场景,比手动挂组件快得多。

提示:别把脚本拖进 Editor 目录下。Editor 下的脚本只在编辑器环境生效,发布到 Android 或 WebGL 后波形会静默消失,而且不报错,这种问题最难定位。

版本上,demo 面向 Unity 2018 LTS 到 2022 LTS 都验证过。如果用新版 Unity 且工程开了 Input System 的新输入模式,注意脚本里如果用了老Input接口,需要在 Player Settings 里把 Active Input Handling 设为 Both,否则运行时日志会刷输入异常。波形本身不受影响,但这条日志会干扰你排查其他问题,建议直接改掉。

3.2 最小场景搭建:空物体、挂脚本、拉引用

新建场景后四步走:创建空物体 WaveRoot,挂 LineWaveController;创建子物体 WaveLine,挂 LineRenderer;把 WaveLine 拖到脚本的 lineRenderer 字段;给 LineRenderer 指定材质。默认的 Sprite-Default 能跑,追求干净线条就新建一个 Unlit/Color 材质。

脚本里建议做两层兜底,这样无论先建物体还是先挂脚本,引用都不会断:

public LineRenderer lineRenderer; void Reset() { // 挂脚本瞬间自动寻找子物体上的 LineRenderer lineRenderer = GetComponentInChildren<LineRenderer>(); } void Awake() { if (lineRenderer == null) lineRenderer = GetComponentInChildren<LineRenderer>(); }

Reset()是编辑器回调,组件被挂上时自动执行一次;Awake是运行时兜底。两个都写,团队里新手复制物体时把引用复制丢了,运行时也能自己找回来。材质这一步容易出问题:LineRenderer 用默认粒子材质时,线条边缘会有光晕和透明混合。建议建一个材质,Shader 选 Unlit/Color,颜色设白,这样波形颜色就完全由 colorGradient 控制,不会混进材质本身的颜色。

3.3 相机、坐标系与 UI 场景的适配

波形默认在 X-Y 平面,Z 为 0,正交相机下只要在视野内就显示。透视相机要留意两点:WaveRoot 要放在相机前方,别让波线被视锥裁剪;距离相机太近线宽会显得很粗,太远又细到看不见。建议先用正交相机调参,最后切回透视相机摆位置,中间不要反复切,容易把视野判断搞乱。

做 UI 场景时,常见做法是把 WaveRoot 放到 Canvas 下,LineRenderer 的 useWorldSpace 设 false,波形坐标变成相对父物体的局部坐标。此时注意 Canvas 的 Render Mode——Screen Space - Overlay 模式下 Canvas 的缩放不是 1:1,波形长度要按参考分辨率等比换算。参考分辨率是 1920×1080,你想要的波形宽度是 400 像素,就把 lineLength 按400f / canvasScaler.referenceResolution.x * 2f量级换算,具体比例看你的缩放模式,没有标准公式,屏幕实测最快。

3.4 参数面板:波形怎么在整个生命周期里被控制

Inspector 上的字段设计是 demo 做得顺手的地方。振幅和线宽做成滑条,速度做输入框,Play 模式下直接拖、直接改,下一帧立即生效,不需要停掉重跑。滑条适合连续调整找手感,输入框适合定精确值,两者混用比全用滑条高效。

建议把参数在 Inspector 里分好组,接手的人不懵。一般这样分:波形形态一组(frequency、wavePerBeat、pointCount);运动一组(speed、direction);外观一组(amplitude、widthMultiplier、colorGradient)。分组只是[Header]特性的事,两分钟加上,省掉后续大量沟通成本。验证接入是否成功也有两个很快的办法:Play 模式下看波形是否在动,以及切到 Scene 视图选中 WaveLine 看 LineRenderer 的顶点预览是否跟手。两条都过,说明这条链路是通的。

4. 三种自定义玩法:Inspector 直调、数据驱动、替换波形函数

4.1 Inspector 直调:一分钟调出监护仪风格

如果只是要一屏「像那么回事」的心电波形,最快的路径是纯编辑器操作:colorGradient 起点设半透明红、终点设近全透明红;speed 拉到 1.5,amplitude 拉到 1.8,widthMultiplier 设 0.04;frequency 调到 12 左右让 QRS 疏密接近真实心率。播放后就是典型床边监护仪的观感,整个过程两分钟,不写代码,适合给客户演示、给策划过目。如果你要的颜色是团队规范里的品牌色,直接在渐变编辑器里输入十六进制值就行,别靠肉眼拖。

4.2 数据驱动:把波形接到游戏状态或业务数据上

参数全暴露在 Inspector 方便是方便,但产品里不能指望运行时有人手拖。更合理的接法是暴露语义化接口,内部把业务量换算成波形参数。比如心率联动:

public void SetHeartRate(float bpm) { // 每分钟心跳数 → 每秒相位增量 speed = bpm / 60f * wavePerBeat; }

wavePerBeat表示一个完整心跳周期占几个归一化横轴单位,取值 0.8~1.2。bpm 60 时 speed 约等于 1.0,波形平稳缓慢;bpm 拉到 140,相位累加速度翻倍多,视觉上立刻变急促。这个接口在游戏 HUD 和数字孪生大屏的告警联动里都实用。注意接口内部加 clamp,比如Mathf.Clamp(bpm, 30f, 220f),防止异常值把波形刷成满屏高频振动。不加这一行,数据源出一次脏值,画面直接崩给你看。

4.3 替换波形函数:从叠加正弦到分段心电形态

想要更接近真实心电,把波形函数从多正弦叠加换成时间分段函数。一段心跳周期大致划分:P 波小幅圆峰 → QRS 波群陡峭尖峰 → T 波宽缓波 → 静息段。代码如下:

float GetWaveHeight(float t, float phase) { float tp = (t * wavePerBeat + phase) % 1f; // 映射到一个心跳周期内 if (tp < 0.12f) return Mathf.Sin(tp / 0.12f * Mathf.PI) * 0.3f; // P 波:小幅圆峰 if (tp < 0.28f) return QRS(tp); // QRS:陡峭波群 if (tp < 0.5f) return Mathf.Cos((tp - 0.28f) / 0.22f * Mathf.PI) * 0.25f; // T 波:缓波 return 0f; // 静息段 }

tp用% 1f取余落入 0~1,配合流动中的phase,波就横轴循环滚动;0.12、0.28、0.5 分别是 P 波结束、QRS 结束、T 波结束的阈值,改这三个数就能改变波形节奏。QRS()可以继续细分成两段半正弦,一段模拟 Q 波下冲、一段模拟 R 波上冲,视觉上跟单峰的差距肉眼可辨。替换函数的好处是接口不变,相位累加、振幅缩放、颜色渐变全部复用,把上面这段粘进脚本,其余逻辑一行不用改。

如果团队里有美术想自己调波形形状,还有一个偷懒的办法:用 AnimationCurve 当波形模板。Inspector 上画一条多段曲线,运行时按时间采样曲线高度,效果跟分段函数一样,但编辑门槛更低——不需要写代码,谁都能在曲线编辑器里拖出一个心电形状。一帧采样几百个点,AnimationCurve 的Evaluate开销可以忽略。

5. 避坑记录:心电图波形常见的五类翻车与排查

5.1 波形方向反了,R 峰往左跑

现象:播放后波形整体从右往左流动,和监护仪从左到右的习惯相反。原因:phase 累加方向与横坐标映射方向叠加后出现视觉反向,属于方向约定不一致,不报错、不影响画面质量,但是联调最后才容易被注意到。解决:把phase += Time.deltaTime * speed改成phase -=,或者把归一化横坐标t换成1f - t。两处只改一个,都改等于没改,方向又翻回去。改完立刻确认一次,别等过一天再回来看。

5.2 振幅拉大后波形变锯齿

现象:amplitude 调高之后,波峰不再顺滑,出现折线感。原因:pointCount 固定时,振幅放大意味着相邻采样点的 Y 值落差跟着变大,LineRenderer 的直线段连接就把棱角暴露了。解决:优先把 pointCount 提到 256;想波形更稀一点就同步降低 frequency。更省事的是做振幅与频率联动,振幅改变时执行frequency = baseFrequency * (1f + amplitude * 0.3f),从源头避开「波形密且尖」的组合。注意给 baseFrequency 留一个合理基线,不然频率会被带偏到不可控范围。

5.3 波形发虚、边缘模糊

现象:Scene 视图里线条干净,游戏视图或真机上边缘发虚、有锯齿。原因:LineRenderer 默认材质带纹理采样,线宽又细,边缘锯齿被纹理放大;相机侧不开 MSAA 或抗锯齿被后处理覆盖时问题更明显。解决:材质换成 Unlit/Color 关闭纹理干扰;线宽不小于 0.03;相机开 MSAA 2x 或 4x。别指望后期全屏抗锯齿给 LineRenderer 兜底,线和多边形几何的走样来源不同,后处理救不了它。排查顺序固定:先材质、再线宽、最后相机 AA,按这个顺序不会来回白折腾。

5.4 长时间运行内存缓步上涨

现象:运行几分钟,Profiler 的 GC Alloc 曲线缓慢向上,波形本身没异常。原因:典型写法问题——在 Update 里new Vector3[pointCount],或每帧往List<Vector3>里 Add,每次分配都在堆上产生垃圾。解决:数组在 Awake 里一次性分配,Update 只改数组内容;pointCount 需要动态变化时,改完调一次lineRenderer.positionCount = pointCount触发重建,继续复用数组。用 Memory Profiler 录制十秒,按 Allocated 排序,分配热点一眼看见。这条对同时挂多条波形的场景尤其重要,一条线不漏,十条线叠加就是肉眼可见的卡顿。

5.5 改了 amplitude 波形纹丝不动

现象:Inspector 上拖滑条,播放中的波形没有任何反应。原因:场景里存在两个同名的波形物体,Inspector 改的是 A 上的字段,波形渲染用的是 B 上的组件;或者 LineRenderer 被 Animator、Timeline 之类的动画系统驱动,Inspector 改的值被动画每帧覆盖。解决:暂停播放,确认场景层级里选中的 WaveRoot 就是正在渲染波形的那个;再看波形物体上有没有挂 Animator 或 Timeline。按这两条顺序排查,基本十分钟内定位。这是所有问题里最费时间的,因为它不报错、无日志,只能靠检查层级关系。

6. 让波形更有「心电图味」:平滑插值、基线漂移与亮度渐变

真实心电图的质感不是一条光滑正弦,而是带轻微毛刺、基线缓慢漂移、QRS 尖锐和 T 波圆润并存的复合形态。想让 demo 从「实验效果」变成「能放进产品」,我一般加三样东西。

第一,低频噪声叠加模拟基线漂移。Mathf.PerlinNoise(t * 0.3f, phase * 0.1f)返回 0~1 的值,减去 0.5 后乘以 0.05 量级的系数加到 Y 坐标上,整条线就有电极贴在皮肤上的细微晃动感。系数控制在 0.03~0.08,再大就变成随机噪声了。

y += (Mathf.PerlinNoise(t * 0.3f, phase * 0.1f) - 0.5f) * 0.05f;

第二,点数量提到 256,并在波形函数返回前对相邻采样做一次 SmoothStep 插值,抹平陡峭段的棱角;第三,在渐变编辑器里给 QRS 区间对应的顶点位置加高亮色标,模拟荧光屏的脉冲亮度。从那次以后,我每次拿到这类波形 demo 都强制走一遍:先开 Play 跑三十秒看内存曲线,再关掉确认波形没有残留亮度,然后才谈观感怎么调。这套顺序帮我挡掉了不少「Demo 里好好的,一进项目就完蛋」的坑。这个 demo 的骨架是稳的,接数据、换波形函数、嵌 UI 都不会碰到底层问题,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询