尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Unity 用 LineRenderer 实现可调振幅与速度的心电图波形 Demo

发布时间:2026/9/26 11:40:53

资讯中心
01
ARTICLE

Unity 用 LineRenderer 实现可调振幅与速度的心电图波形 Demo

Unity 用 LineRenderer 实现可调振幅与速度的心电图波形 Demo
简介这款Unity心电图线波模拟Demo面向游戏开发者、Unity学习者及需要医疗可视化交互效果的创作者可将真实心电图的上下振幅动态转化为可调节的视觉线波。包体约39.19MB共6688个文件涵盖1066个C#脚本、575张PNG贴图、31个Asset场景资源、5个Prefab预制体以及大量Meta、Json、Dll、MD文档等工程配置与说明文件RAR压缩包内含完整工程目录、脚本组件与资源管理结构。已有330人学习下载项目已实测可用。Demo支持自定义波线速度、颜色与振幅并内置Q/W键触发效果切换的交互逻辑通过阅读源码可掌握Mesh/Renderer绘制线波、Input.GetKeyDown键盘监听、Time.deltaTime动画控制、Material颜色修改及Transform缩放振幅等关键实现。内部包含完整Assets目录、线波测试场景及预制体可直接打开运行并修改参数观察效果同时保留Shader与材质配置便于研究心电图波形的绘制流程与性能优化思路。1. Unity 模拟心电图能自定义振幅和速度的线波 Demo 到底怎么用做医疗仿真训练、数字孪生驾驶舱或者游戏里的生命体征 HUD 时经常需要一条看起来像模像样的心电图线波。最省事的做法是找一段视频素材循环播放但视频方案有个硬伤运行时想改波形颜色、波速、振幅只能重新换素材完全做不到实时交互。这个 Unity 线波 Demo 恰恰是冲这个来的——它用纯代码生成心电图波形速度、颜色、振幅全部暴露在 Inspector 面板上拖进场景改参数就生效亲测确实好用。适合三类人一是在做医疗模拟项目需要心电波形的 Unity 开发者二是游戏 UI 里想加点生命体征风格的动态元素的人三是刚学 Unity 想弄明白 LineRenderer 怎么实现线波效果的新手。2. 心电图波形的生成原理为什么用代码造波而不是贴图2.1 选 LineRenderer 还是 UGUI Image取决于波形要不要跨场景拿到这个需求时第一反应是纠结用 UGUI 的 Image 组件做波形填充还是用 LineRenderer 画线。两种方案的差别非常本质UGUI Image 只能活在 Canvas 坐标系里做 UI 层的心电图很顺手但波形没法出现在 3D 场景的物体表面比如医疗床头的三维监护仪模型。LineRenderer 是渲染管线里的线段组件既能渲染在 3D 世界空间也能配合 Canvas 使用通用性明显更强。这个 Demo 选的就是 LineRenderer因为它能满足要么贴在 3D 屏幕上、要么叠在 UI 上的扩展预期这是贴图方案给不了的灵活性。LineRenderer 的工作方式是把一串 Vector3 顶点按顺序连成线段我们只要每隔一帧更新这些顶点的位置就能得到一条不断滚动的波形。心电图和正弦波最大的区别在于波形形态正弦波是均匀起伏心电图则有一个尖锐的 QRS 波群、一个平缓的 P 波和一个稍宽的 T 波形态靠数学函数叠加出来。代码里用几个高斯函数的加权和来模拟这个特征波峰尖锐程度由高斯函数的衰减系数控制。2.2 P-QRS-T 波的数学建模高斯函数叠加法心电图的单个周期由 P 波、QRS 波群、T 波组成QRS 波群内部又是一个小幅下凹、大幅上凸、再小幅下凹的尖峰组合。用高斯函数的好处是参数语义非常直白每个高斯函数有峰值、中心位置、宽度三个参数正好对应波形的多高、多久出现、多宽。Demo 的做法是这样的把时间轴归一化到 0 到 1 的周期内在这个区间摆上四个高斯波峰再叠加成一个完整周期。// GetECGWaveform: 输入归一化时间(0~1)输出该时刻的归一化波形值 float GetECGWaveform(float t, float heartRate) { // heartRate 影响波形整体节奏但这里先弱化处理只保留形态生成 float pWave 0.08f * Mathf.Exp(-Mathf.Pow((t - 0.20f) / 0.03f, 2f)); float qWave -0.15f * Mathf.Exp(-Mathf.Pow((t - 0.35f) / 0.015f, 2f)); float rWave 1.00f * Mathf.Exp(-Mathf.Pow((t - 0.38f) / 0.01f, 2f)); float sWave -0.12f * Mathf.Exp(-Mathf.Pow((t - 0.41f) / 0.012f, 2f)); float tWave 0.25f * Mathf.Exp(-Mathf.Pow((t - 0.65f) / 0.05f, 2f)); return pWave qWave sWave tWave; }这段代码的逻辑拆开看就是四个高斯函数的求和高斯函数公式是amplitude * exp(-((t - center) / width)^2)center决定波形在周期里的出现时刻width决定波峰宽度amplitude的正负和大小决定上下方向和强度。P 波设定为中心位置 0.20、宽度 0.03 的小正向波Q 波是中心 0.35 的小负向波R 波是中心 0.38、振幅 1.0 的大正向波这是整个心电图最醒目的尖峰S 波是中心 0.41 的小下凹T 波中心在 0.65宽度 0.05比 P 波宽一点。有一个细节需要注意高斯函数的宽度参数越小峰越尖。R 波的 width 是 0.01S 波是 0.012所以 QRS 复合波看起来非常陡而 T 波 width 是 0.05看起来就圆润很多。如果你想把波形改成更像病理性心电图比如 ST 段抬高可以加一个在 0.45 到 0.55 之间持续的正向偏置项这些都是在这个函数基础上做微调就能实现的。2.3 顶点数据的更新策略滚动波形的时间累积方式波形不是静止的它要随时间向左滚动。实现方式有两种一种是修改所有顶点的 x 坐标让整条线左移另一种是保持 x 不动让采样时间递增波形形态向右演化。Demo 用的是后者逻辑上更简单用一个累加的时间变量作为波形的相位原点每个顶点的采样时间等于相位原点加上基于顶点序号的偏移量。这样只要相位原点随时间线性增加波形就自然呈现向左滚动的视觉。// 每帧更新 LineRenderer 顶点位置 private void UpdateLineRenderer() { float phase _timeAccumulator * _waveSpeed; // 相位原点随时间推进 for (int i 0; i _sampleCount; i) { float t (float)i / (_sampleCount - 1); // 顶点在屏幕上的水平位置(0~1) float waveform GetECGWaveform(phase t * _waveLength, _heartRate); float x t * _displayWidth; float y waveform * _amplitude; _lineRenderer.SetPosition(i, new Vector3(x, y, 0f)); } }这个函数是 Demo 的核心循环。phase是当前时间与速度的乘积速度越大相位变化越快波形滚动越快t是 0 到 1 的归一化横坐标乘以_displayWidth后变成实际点位waveform是采样到的波形高度乘上_amplitude后控制整体振幅。_waveLength控制一个周期占多少水平宽度值越大波形在屏幕上越稀疏。这三个参数直接对应波线速度、振幅、波形密度这三个需求点。每帧调用SetPosition更新全部顶点在 PC 上 256 个顶点毫无压力移动端建议把_sampleCount降到 128视觉效果几乎无差但 CPU 开销少一半。这一点放到后面的避坑章节详说。3. 参数化设计速度、颜色、振幅如何在 Inspector 里暴露3.1 组件上的可调参数清单这个 Demo 把常用参数全部暴露在了 Inspector 面板不需要打开代码改数值。我按实际用途把参数分成了三组波形形态组振幅、周期宽度、心率、渲染组颜色渐变、线宽、采样点数、运行组波速、是否自动滚动。这样做的好处是美术调颜色、策划调速度、程序调形态各干各的互不干扰。参数名称类型默认值作用说明waveSpeedfloat1.0波形滚动速度越大滚得越快amplitudefloat1.0波形纵向振幅直接乘到波形值上waveLengthfloat1.0单个心电周期占多少水平宽度heartRatefloat72心率影响波形紧凑度预留扩展sampleCountint256每帧更新的顶点数越高波形越平滑lineWidthfloat0.02波形线宽lineColorGradient默认绿→绿渐变颜色从线头到线尾的颜色过渡颜色这块用 Unity 的 Gradient 类型而不是简单的 Color是因为真实监护仪的波形线通常是有渐变效果的——线尾颜色淡线头颜色亮看起来有尾迹感。Gradient 可以在 Inspector 里可视化编辑多个颜色键比单一 Color 灵活得多而且它是 Unity 原生支持的序列化类型不需要额外写自定义 Inspector。3.2 对外暴露的公共接口让外部系统可以动态改参数Inspector 面板改参数适合编辑器里调效果但运行时如果要从别的脚本动态控制波形比如玩家受伤时心率变快、振幅变大光靠面板是不够的需要一组公共接口。Demo 里做了三个公开方法覆盖最常见的动态控制场景/// summary /// 供外部调用的公共接口动态调整波形表现 /// /summary public void SetAmplitude(float newAmplitude) { _amplitude Mathf.Clamp(newAmplitude, 0.05f, 5f); } public void SetWaveSpeed(float newSpeed) { _waveSpeed newSpeed; } public void SetHeartRate(float newRate) { _heartRate Mathf.Clamp(newRate, 30f, 240f); }接口设计逻辑很简单SetAmplitude加了Mathf.Clamp防止外部传入异常值把波形顶出屏幕SetHeartRate限制在 30 到 240这个范围覆盖了临床上常见的心率区间SetWaveSpeed没有做钳制因为波速是视觉参数允许调得很快甚至反向传负值可以实现波形倒流的效果。实际项目中这些接口通常绑定在游戏事件或者 UI 滑块上。比如做一个玩家受伤的掉血动画可以写一条GetComponentECGLine().SetHeartRate(140)让心率瞬间飙升再配合音效和屏幕特效比纯静态的波形有说服力得多。4. 从零搭建完整 Demo脚本挂载、场景配置与运行验证4.1 创建心电图脚本完整组件代码前面讲了原理和参数设计现在落到能跑的完整代码。新建一个 C# 脚本ECGLineRenderer.cs挂到场景里任意空物体上它会自动创建 LineRenderer 组件并完成初始化。我写的版本里尽量做了防御性处理检查重复组件、初始化 LineRenderer 的材质和宽度、预分配顶点数组避免新手第一次运行就遇到空引用或者不显示波形的尴尬。using UnityEngine; [RequireComponent(typeof(LineRenderer))] public class ECGLineRenderer : MonoBehaviour { [Header(波形形态), SerializeField] private float amplitude 1.0f; [SerializeField] private float waveLength 1.0f; [SerializeField] private float heartRate 72f; [Header(波形速度), SerializeField] private float waveSpeed 1.0f; [Header(渲染参数), SerializeField] private int sampleCount 256; [SerializeField] private float lineWidth 0.02f; [SerializeField] private Gradient lineGradient; private LineRenderer _renderer; private float _timeAccumulator; void Awake() { _renderer GetComponentLineRenderer(); if (lineGradient null || lineGradient.colorKeys.Length 0) { lineGradient new Gradient(); lineGradient.SetKeys( new GradientColorKey[] { new GradientColorKey(Color.green, 0f) }, new GradientAlphaKey[] { new GradientAlphaKey(1f, 0f) }); } _renderer.colorGradient lineGradient; _renderer.startWidth lineWidth; _renderer.endWidth lineWidth; _renderer.positionCount sampleCount; _renderer.useWorldSpace false; } void Update() { _timeAccumulator Time.deltaTime * waveSpeed; UpdateLineRenderer(); } void UpdateLineRenderer() { Vector3[] positions new Vector3[sampleCount]; for (int i 0; i sampleCount; i) { float t (float)i / (sampleCount - 1); float waveform GetECGWaveform(_timeAccumulator t * waveLength, heartRate); positions[i] new Vector3(t * 6.0f, waveform * amplitude, 0f); } _renderer.SetPositions(positions); } float GetECGWaveform(float time, float rate) { // 对时间取模让波形周期化循环 float t time % 1.0f; float p 0.08f * Mathf.Exp(-Mathf.Pow((t - 0.2f) / 0.03f, 2f)); float q -0.15f * Mathf.Exp(-Mathf.Pow((t - 0.35f) / 0.015f, 2f)); float r 1.0f * Mathf.Exp(-Mathf.Pow((t - 0.38f) / 0.01f, 2f)); float s -0.12f * Mathf.Exp(-Mathf.Pow((t - 0.41f) / 0.012f, 2f)); float tv 0.25f * Mathf.Exp(-Mathf.Pow((t - 0.65f) / 0.05f, 2f)); return p q r s tv; } }代码逻辑有几个关键点。Awake里初始化 LineRenderer 时设置useWorldSpace false这个必须做否则顶点坐标会被当成世界坐标波形会跑到离相机十万八千里的地方。Update里用Time.deltaTime * waveSpeed累加时间这样波速调成 0 时波形完全静止调成 2 时滚动速度翻倍而且不受帧率影响。顶点数组中 x 方向的跨度直接写死为 6.0f对应表 3 中waveSpeed和amplitude的调节范围波形水平方向的显示区域刚好在相机可见范围内。4.2 场景搭建与材质配置一步一步跑起来脚本准备好后场景里的配置步骤不多但每一步都有讲究。首先在 Hierarchy 里创建空物体命名ECG_Monitor把脚本拖上去。Unity 会自动添加 LineRenderer 组件不需要手动挂。然后是材质问题。LineRenderer 默认材质是 Default-Line显示效果偏白灰色而且不受光照影响。建议在 Project 窗口创建一个新的 Unlit 材质shader 选择Unlit/Color或UI/Unlit/Detail颜色设成深绿色然后拖到 LineRenderer 的 Material 槽里。如果用了Unlit/Color材质的颜色会覆盖脚本里 Gradient 的效果所以更推荐用Unlit/Color配合 Gradient 时把材质颜色设成白色让渐变完全透出来。最后是相机设置。正交相机的 Size 建议设成 3 到 4这样波形振幅为 1 时屏幕垂直方向能看到完整的上下起伏。透视相机的话把物体放在 z10 的位置调整 Field of View 到 40 左右。波形显示区域默认是 x 从 0 到 6如果想让它居中把脚本里的 x 计算改成(t - 0.5f) * 6.0f即可这样波形中心在原点。运行后你会在 Game 视图看到一条绿色波形从左向右滚动R 波尖峰非常醒目。此时调 Inspector 里的waveSpeed到 3波形滚动明显变快调amplitude到 0.5波形整体变矮。这个反馈是即时的不需要停止运行这是纯代码生成方案对比视频素材的最大优势。4.3 参数调节的推荐范围不同场景下的合理取值不同项目对波形要求的侧重点不一样这里整理一份我实测下来比较靠谱的参数组合。做医疗模拟时波形要给人真实监护仪的信任感振幅 1.2 到 1.5、波速 0.8 到 1.2、采样点 256 最合适做游戏 UI 装饰时波形不需要太精细振幅 0.8、波速 2.0、采样点 128 就够省下的 CPU 留给游戏逻辑。使用场景amplitudewaveSpeedsampleCount备注医疗训练仿真1.2~1.50.8~1.2256波形要平滑R 波要清晰游戏 HUD 装饰0.8~1.02.0~3.0128强调动态感不要求医学级精确数据可视化背景1.0~1.80.5~1.0128振幅大更有视觉冲击力这里补充说明一点heartRate参数在这个 Demo 里只影响波形形态的紧凑度并不会真正改变时间轴节奏因为时间累积用的是Time.deltaTime而不是按心率周期的比例缩放。如果你需要做到心率翻倍时波形滚动也翻倍需要在Update里把TickIncrement改成Time.deltaTime * heartRate / 60f这类形式这个话题放到最后一章进阶里展开。5. 避坑记录波形扭曲、坐标错乱与掉帧的三个常见问题5.1 波形完全看不到或跑到屏幕外坐标空间混淆是主因现象脚本挂好后运行Game 视图里找不到任何波形把物体选中也看不到线。原因LineRenderer 默认useWorldSpace为 true代码里给的顶点坐标是(t * 6.0f, waveform * amplitude, 0)这些坐标在物体没有位移时恰好等于世界坐标看起来没问题但如果物体挂在 Canvas 子节点下或者父物体有缩放、旋转这些局部坐标会被叠加到世界坐标里波形自然跑到屏幕外。解决在 Awake 初始化里强制设置_renderer.useWorldSpace false然后确认物体本身的 Transform 位置在原点附近。如果你把物体放在了某个 UI 面板下面还要检查 Canvas 的 Render ModeScreen Space - Overlay 模式下 LineRenderer 无法正常显示需要切换成 Screen Space - Camera 或者 World Space。5.2 波形看起来一顿一顿的滚动不流畅现象R 波尖峰在滚动时能明显看到跳变尤其在波速调大后更明显像是低帧率动画。原因心电图波形的 R 波非常窄宽度只有 0.01 个周期在时间轴上相当于一个极窄的脉冲。当波形滚动速度较快时_timeAccumulator每次递增的步长变大R 波的采样位置可能一步跳过峰值导致尖峰出现位移感或者忽大忽小。根源是采样点密度跟不上波形的变化速率。解决把sampleCount从 128 提高到 256R 波采样到的位置更连续。如果仍然有跳变不要继续堆高 sampleCount改为限制最大波速——毕竟监护仪的波形滚动速度本身不会快到哪里去。我在代码里给waveSpeed加一个上限判断超过 4 时在 Inspector 里警告提示效果比盲目调参靠谱。5.3 移动端波形显示时 CPU 占用过高现象在 PC 上跑完全没问题打包到手机后波形显示正常但 Profiler 里看到每帧的 CPU 时间多出 2 到 3 毫秒。原因SetPositions每帧要更新全部 256 个顶点并且每次调用都分配了一个新的Vector3[sampleCount]数组在 Update 循环里反复分配内存触发 GC 造成卡顿。这是 Unity 的经典性能坑数组分配比预分配复用开销大得多。另外移动端的 LineRenderer 渲染本身也比 PC 贵一些。解决把顶点数组声明为成员变量在 Awake 里分配一次之后每帧直接往这个数组里写值不要再 new。代码改动只有三行但性能提升明显。private Vector3[] _positions; void Awake() { _positions new Vector3[sampleCount]; } void UpdateLineRenderer() { for (int i 0; i sampleCount; i) { // 直接写 _positions[i]不每次 new 数组 } _renderer.SetPositions(_positions); }翻过这一页之后这个 Demo 基本上就稳定可用了。但如果你只是想白嫖一个现成的心电图波形收工到这里完全足够。不过实际项目里经常会碰到更具体的需求——波形要跟着真实传感器数据变化、场景里要有多个频道同时显示、波形要渲染到 RenderTexture 上供其他 Shader 采样。这些问题等用了几周之后你会自然遇到下面聊的就是这几个方向的改造思路。6. 进阶改造从固定波形到实时数据、多通道与 RenderTexture 输出6.1 对接真实心率数据源用 StreamingData 替换默认波形医疗模拟项目往往需要对接病人的真实心电数据或者从外部设备读取心率信号。做法是让波形函数支持外部注入的数据源而不是只用内置的 P-QRS-T 高斯模型。我一般会在类里加一个public System.Funcfloat, float, float waveformProvider;委托字段运行时的 Update 循环里判断这个委托是否为空空则走内置波形非空则用外部数据源计算波形值。// 外部数据源注入接口 public void SetExternalWaveformProvider(System.Funcfloat, float, float provider) { _externalProvider provider; } float GetWaveformAtTime(float time) { if (_externalProvider ! null) { return _externalProvider(time, _heartRate); } return GetECGWaveform(time % 1f, _heartRate); }真实设备的心电数据通常是采样数组不是连续函数所以外部 provider 里要做插值把收到的数据点按时间戳映射到 0 到 1 的周期内然后用 Lerp 连续化。这里有个经验真实数据里常带基线漂移呼吸造成的低频扰动做显示前最好先跑一遍高通滤波最简单的方案是实时减去 0.5 秒的滑动平均值比硬编码的波形抗干扰能力强很多。6.2 多通道显示用对象池管理多台监护仪医院床位监控大屏通常要在同一个场景里显示 6 到 12 台监护仪每台显示 3 到 5 个通道心率、呼吸、血氧等如果每一路都单独创建 LineRenderer场景组件的数量会非常难看。我的做法是用一个父物体下面挂多个子物体每个子物体各挂一个ECGLineRenderer但共享一个材质实例只是颜色不同。这样每路波形有自己的ampitude和waveSpeed而渲染批次因为是同一个材质合批后性能基本无损。通道数量超过 10 个时把sampleCount统一设成 96 到 128视觉上因为通道波形尺寸小看不出精度差异。实测在低端安卓机上10 路波形 128 采样点CPU 占用能稳定控制在 1 毫秒以内。6.3 把波形输出到 RenderTexture给其他效果当输入源最后一个进阶方向是把波形渲染到 RenderTexture 上这样波形可以作为后续特效的输入纹理或者叠加到自定义 Shader 里做发光效果。做法很简单单独创建一个相机设置为只渲染 ECG 物体所在层RenderTexture 的分辨率设成 512x128把相机的 Target Texture 指过去。之后任何材质都可以用这张纹理采样做成扫光、辉光、故障效果都行。这个技巧特别适合把心跳波形做成整个屏幕的 UI 背景而不是拘泥于一条单调的线。用这个方案时有一个关键的坑心电图物体的 LineRenderer 颜色和材质要注意 alpha 通道纹理格式选 RGBA透明度通道留出来后续 Shader 才能做 alpha 混合不然叠加出来的效果会是一片死绿色。这套 Demo 我已经不止一次在实际项目里用了每次用之前都会强制做一遍三件事确认useWorldSpace false、确认顶点数组只分配一次、确认目标平台的sampleCount适配。这三件事是我最早踩坑踩出来的条件反射现在也成了我检查所有 LineRenderer 类组件的固定流程。希望这篇笔记帮你在做 Unity 波形效果时少走几步弯路把精力省下来去调真正的业务逻辑。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。