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

Unity Shader模拟心电图波形:从PQRST合成到动态滚动实现

发布时间:2026/9/26 16:47:32

资讯中心
01
ARTICLE

Unity Shader模拟心电图波形:从PQRST合成到动态滚动实现

Unity Shader模拟心电图波形:从PQRST合成到动态滚动实现
简介这是一份Unity心电图线波模拟效果项目demo面向Unity初中级开发者适合在游戏UI、医疗模拟、数据可视化等场景中展示动态波形。压缩包共6688个文件包含1066个C#逻辑脚本、5个预制体、4个场景、材质与PNG贴图等Unity工程典型资源整包约39.19MB。已有330人学习下载。Demo支持通过键盘Q/W键交互实时调整波线速度、颜色与振幅内置可复用的线波绘制组件可直接挂载到任意游戏对象上使用。工程覆盖图形Mesh渲染、Input.GetKeyDown输入监听、Time.deltaTime驱动动画、Material.color动态改色、Transform.localScale振幅控制、模块化脚本组件设计等核心知识点同时提供完整目录结构便于定位各功能模块。作者实测运行稳定适合作为心电图模拟、信号波形类特效的参考模板方便二次开发与效果扩展。1. 用Shader写“会跑的心电图”比逐帧动画曲线省心十倍模拟心电图的Unity线波效果最笨的做法是拿一条AnimationCurve切成几十个关键帧手动拉动上下振幅再补一个位移动画。我第一版demo就是这么干的做完发现波形像被击穿的示波器想调波线速度得去改关键帧时间轴想换颜色还得新建材质完全没办法把参数暴露给策划或美术。换成Shader方案之后问题一下简单了波形是“算”出来的不是画出来的。所谓模拟心电图上下振幅本质就是把心电波形写成一个周期函数然后让屏幕上横向的每一个像素都去采样它波线速度控制采样点沿时间轴跑多快颜色和振幅就是两个可调属性。这个方案亲测好用适合做监控大屏、医疗HMI、数字孪生里的动态背景也很适合当作Unity Shader的入门案例。读完这篇你能得到一个可以直接丢进项目的可调demo以及一堆我不太想再踩第二次的坑。2. 心电波形不是正弦波先拆PQRST再决定用Shader还是LineRenderer2.1 一条让人信服的心电波形至少要包含P、QRS和T三个成分如果只是做“会上下动的波浪线”直接用正弦波就够了。但你要的是“模拟心电图”那么人眼第一眼看的不是平滑度而是那个又窄又高的R波尖峰。心电图上一个小P波、一个窄而高的大R波R波前后各带一个向下的小槽Q和S最后跟一个缓一点的T波。少了R波尖峰整条线看起来就是普通水波谈不上心电图。把这些形态落成数学函数最常见的做法不是去拼贝塞尔曲线而是用几个高斯脉冲叠加。高斯函数的优点是没有if分支、没有跳变在GPU上计算非常便宜改位置、改宽度、改方向都很直观。下面是一个简化但足够用的单个心跳周期表达式其中t是归一化时间范围0到1p 0.22 * exp(-pow((t - 0.20) * 25.0, 2.0)) q -0.14 * exp(-pow((t - 0.30) * 40.0, 2.0)) r 1.00 * exp(-pow((t - 0.35) * 35.0, 2.0)) s -0.16 * exp(-pow((t - 0.42) * 40.0, 2.0)) u 0.24 * exp(-pow((t - 0.56) * 10.0, 2.0)) ecg p q r s u这里的0.20、0.35、0.56是各个波在周期内的位置。后面的25、40、35是高斯核宽度乘数数值越大波越窄正负号代表朝上还是朝下。R波峰值是1.0所以它在整条波形里最突出。到了后面实际写Shader时这段函数会被原样搬进Fragment里乘以一个振幅系数就形成了“上下振幅”的概念。有一点容易搞混心电图的“振幅”并不是正弦函数的那个A而是这条周期函数整体乘以的缩放系数。因为R波本身是1如果振幅设为0.15那么R波最高点相对基线只抬升0.15个屏幕高度S波最深的地方可能只下降0.03。这样视觉上不会太过夸张也符合医疗设备那种克制的画风。2.2 滚动式扫描每个像素都是一根“采样探针”监护仪上的心电图不是整条线上下跳而是波形从右往左或者从左往右连续滚动。用Shader实现滚动思路和逐帧移动LineRenderer完全不同每个屏幕像素都独立计算自己在“时间轴”上对应的波形值。拿RawImage或Quad的UV坐标来说uv.x天生就是0到1。把它当成横向时间轴再叠加一个运行时间偏移就形成了滚动。具体写法通常是t frac(uv.x * _Tiling _Time.y * _WaveSpeed) y 0.5 ecgShape(t) * _Amplitudefrac取余数是为了让时间永远落在0到1之间循环这样一屏可以显示多个心跳周期。_Tiling决定同一时间屏幕上能看到几个周期_WaveSpeed决定波形向哪个方向滚、滚多快_Amplitude决定振幅_Color决定颜色。这就是标题里“波线速度、颜色和振幅都可以自定义”的实现原理。这个方案的另一个好处是波形是逐像素算出来的不管屏幕多大、周期数多少波形永远不会出现顶点不足导致的折线感。滚动过程中也不会有任何GameObject或Transform参与CPU开销几乎为零很适合放在UI里当背景。2.3 Shader和LineRenderer两条路径怎么选做这种效果还有另一条常见路线用LineRenderer每帧更新顶点位置。两条路我都走过选型核心就一句话看你的数据源是程序生成的还是真实采集的。Shader路线适合程序化模拟。比如做UI背景、Demo展示、宣传片效果只需要一个波形函数不需要交互也不需要接外部数据。它的优点是运行开销极低参数在Inspector里直接调改速度、改颜色、改振幅都像调音量一样简单。LineRenderer路线适合接真实数据。比如你手上有医疗设备的采样点或者想把某段心电数据文件里的波形画出来那就应该让C#逐帧把数据点填入LineRenderer的顶点数组。这样波形不再是数学函数的产物而是真实可操作的点集方便做碰撞、做点击、做局部放大。两种路线并不冲突。Shader方案可以当作最终渲染层LineRenderer方案负责数据预处理甚至可以把LineRenderer的顶点数据烘焙成Texture再交给Shader采样。但作为一开始的demo优先把Shader跑通因为调参效率最高。3. 用Unlit Shader跑通最小可调心电图demo把速度、颜色、振幅都留在Inspector里3.1 先搭一个RawImage承载材质不买插件不写自定义渲染管线最简单的承载方式不是建Camera、不是做Plane而是在UI里放一个RawImage。为什么是RawImage而不是Image因为RawImage的UV天然是0到1的矩形Shader可以直接拿uv.x当时间轴如果你用Image还要处理图集和Sprite的UV裁切凭空多一些麻烦。在Canvas下创建RawImage把它的Texture留空新建一个Material把下面这个Shader指定给Material再把Material拖到RawImage的Material槽位上。需要注意RawImage组件自身还有一个Color属性CanvasRenderer会把顶点颜色和Shader输出的颜色相乘所以记得把RawImage.color设为纯白否则你看到的结果总是偏暗或偏灰。这一步排错很常见后面避坑章会专门说。3.2 Shader核心代码从PQRST叠加到线宽的完整Pass下面这段是内置渲染管线下的完整Shader。它可以直接放在Unity里跑不需要Shader Graph也不需要额外插件。核心思路就是前面说的每个像素横向坐标对应一个心跳周期时间算出波形值再把当前像素的uv.y和波形值的距离换算成亮度。Shader Custom/ECGLine { Properties { _Color(线波颜色, Color) (0.12, 0.92, 0.32, 1) _Amplitude(上下振幅, Range(0.01, 0.5)) 0.15 _WaveSpeed(波线速度, Range(-5, 5)) 1.5 _Tiling(周期数, Range(1, 12)) 2 _LineWidth(线宽, Range(0.001, 0.05)) 0.015 _Glow(辉光, Range(0, 2)) 0.4 } SubShader { Tags { Queue Transparent RenderType Transparent IgnoreProjector True } Blend SrcAlpha OneMinusSrcAlpha Cull Off ZWrite Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; fixed4 _Color; float _Amplitude; float _WaveSpeed; float _Tiling; float _LineWidth; float _Glow; float ecgShape(float t) { float p 0.22 * exp(-pow((t - 0.20) * 25.0, 2.0)); float q -0.14 * exp(-pow((t - 0.30) * 40.0, 2.0)); float r 1.00 * exp(-pow((t - 0.35) * 35.0, 2.0)); float s -0.16 * exp(-pow((t - 0.42) * 40.0, 2.0)); float u 0.24 * exp(-pow((t - 0.56) * 10.0, 2.0)); return p q r s u; } v2f vert(appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv v.uv; return o; } fixed4 frag(v2f i) : SV_Target { // 横向坐标映射到心跳周期_Time.y 是运行秒数 float t frac(i.uv.x * _Tiling _Time.y * _WaveSpeed); // 0.5 是基线把波形抬到 RawImage 中间 float y 0.5 ecgShape(t) * _Amplitude; // 当前像素离波形越近line 值越接近 1 float dist abs(i.uv.y - y); float line 1.0 - smoothstep(_LineWidth, _LineWidth * 3.0, dist); // 辉光部分用 line*line 做二次衰减让线条边缘有光晕 fixed4 col _Color * (line * 1.5 _Glow * line * line); col.a * line; return col; } ENDCG } } }逻辑说明并不复杂frac(i.uv.x * _Tiling _Time.y * _WaveSpeed)把横向位置和运行时间合并成一个周期相位ecgShape(t)算出该相位下的波形高度_Amplitude控制高度缩放0.5 让波形围绕RawImage中线上下浮动。接着用smoothstep把“像素到波形的距离”映射成亮度距离小于线宽时为1距离再大就平滑衰减为0这样线条边缘不会有硬锯齿。参数说明方面_Tiling越大同一时间屏幕上出现的周期越多看起来心跳越密集_WaveSpeed正负会影响滚动方向当你发现波形“逆行”时把它取反即可_LineWidth的单位是UV长度RawImage高度如果是200像素那0.01大约对应2像素属于一个比较合理的起调值_Glow只增加视觉光晕不影响波形形态。3.3 一个20行的C#脚本让参数在Inspector里实时生效Shader写好了接下来要让它“亲测好用”。给RawImage挂一个控制脚本把Shader里的属性暴露成Inspector上的滑条这样你或策划就能在编辑界面直接看到变化。下面这段脚本用了ExecuteInEditMode所以即使在编辑状态改参数Material也会立刻同步。using UnityEngine; [ExecuteInEditMode] public class ECGLineController : MonoBehaviour { public Material lineMaterial; [Range(0.01f, 0.5f)] public float amplitude 0.15f; [Range(-5f, 5f)] public float waveSpeed 1.5f; [Range(1f, 12f)] public float tiling 2f; [Range(0.001f, 0.05f)] public float lineWidth 0.015f; [Range(0f, 2f)] public float glow 0.4f; public Color lineColor new Color(0.12f, 0.92f, 0.32f, 1f); private void OnValidate() { if (lineMaterial null) { return; } lineMaterial.SetColor(_Color, lineColor); lineMaterial.SetFloat(_Amplitude, amplitude); lineMaterial.SetFloat(_WaveSpeed, waveSpeed); lineMaterial.SetFloat(_Tiling, tiling); lineMaterial.SetFloat(_LineWidth, lineWidth); lineMaterial.SetFloat(_Glow, glow); } }这段脚本有一个实用习惯只在OnValidate里写赋值逻辑表示编辑Inspector时同步一次。如果游戏运行中不需要动态改参数那没必要放Update循环里反复Set省一点CPU。假如后续出现“血氧饱和度骤降要报警”那种逻辑再把Apply()抽出来在具体事件里调用就行。3.4 参数设定参考振幅不是越大越好线速度不是越快越真实下面是这个demo里最常用的几组参数范围。初次上手可以直接把RawImage宽度设为600、高度设为200然后按这个表试。参数推荐范围效果说明主要坑Amplitude0.10 - 0.25控制R波最高点到中线的距离超过0.35容易顶到边缘WaveSpeed-3 - 3绝对值越大滚动越快超过5会肉眼眩晕且容易闪Tiling1 - 4一屏显示几个QRS波群超过6时波形挤在一起R波被压扁LineWidth0.01 - 0.03线条粗细小于0.005在真机上会闪烁Glow0.2 - 0.8辉光强度过高会让波形糊成一团调试小技巧把LineWidth暂时调到0.05让波形变成一条很宽的带状肉眼就能看清整体形态确认R波位置和T波比例没问题之后再调回0.015左右。这个过程比对着细线猜形态要快得多。4. 接真实数据时改用LineRenderer先算顶点再让线自己走4.1 从心跳周期到顶点数组的C#实现Shader方案虽然轻巧但它只适合“纯模拟”。如果后面要把真实心电数据文件或串口数据画到屏幕上Shader里那套数学函数就不好维护了。这时候我建议换成LineRenderer用C#直接控制顶点。下面是个最简实现它同样能显示可自定义的振幅和滚动速度而且每个顶点都能被业务代码访问。using UnityEngine; public class ECGLineRendererDemo : MonoBehaviour { public LineRenderer lineRenderer; [Range(2, 512)] public int segments 256; [Range(0.1f, 25f)] public float scrollSpeed 4f; [Range(0.01f, 1f)] public float amplitude 0.3f; private Vector3[] points; private static float EvalEcgShape(float t) { float p 0.22f * Mathf.Exp(-Mathf.Pow((t - 0.20f) * 25f, 2f)); float q -0.14f * Mathf.Exp(-Mathf.Pow((t - 0.30f) * 40f, 2f)); float r 1.00f * Mathf.Exp(-Mathf.Pow((t - 0.35f) * 35f, 2f)); float s -0.16f * Mathf.Exp(-Mathf.Pow((t - 0.42f) * 40f, 2f)); float u 0.24f * Mathf.Exp(-Mathf.Pow((t - 0.56f) * 10f, 2f)); return p q r s u; } private void Start() { lineRenderer.positionCount segments; points new Vector3[segments]; } private void Update() { if (points null) { points new Vector3[segments]; lineRenderer.positionCount segments; } for (int i 0; i segments; i) { float phase (float)i / (segments - 1); float t Mathf.Repeat(phase Time.time * scrollSpeed / 10f, 1f); float y EvalEcgShape(t) * amplitude; points[i] new Vector3(phase * 10f, y - amplitude * 0.2f, 0f); } lineRenderer.SetPositions(points); } }这段代码里segments控制顶点数量256个顶点已经能把波形画得很顺滑。scrollSpeed对应Shader方案里的波线速度amplitude对应上下振幅。Mathf.Repeat和Shader里的frac作用一样让相位永远在0到1之间循环。phase * 10f是为了把归一化的横向坐标展开到LineRenderer自己的世界坐标范围具体数值取决于你的场景尺寸。需要注意LineRenderer的宽度默认是世界坐标单位在UI或近景下要调得很小才有细线效果我一般会把lineRenderer.widthMultiplier设为0.02到0.05并关掉useWorldSpace让它跟随挂载物体的局部坐标。4.2 用AnimationCurve包络给振幅加上“一次心跳”的变化真实心电图的振幅不是每次完全一样会受呼吸影响出现细微强弱变化。用Shader硬算这种变化也能做但成本不低。LineRenderer方案则简单得多乘一条AnimationCurve即可。常见做法是在Inspector上放一个AnimationCurve字段默认值设为0时刻0.60.3时刻1.01.0时刻0.7左右代表一次心率周期内“呼吸起伏”对振幅的影响。然后在Update里计算float envelope beatEnvelope.Evaluate(Time.time % 1f); float y EvalEcgShape(t) * amplitude * envelope;Time.time % 1f会每秒循环一次所以这条包络线每秒都对波形施加一次强弱变化。如果你想让包络和滚动速度同步可以用Time.time * scrollSpeed而不是Time.time来驱动Evaluate这样节奏更符合逻辑。这个做法适合美术或医疗顾问直接拖曲线比让他们改数学参数更友好。4.3 什么时候果断换回Shader方案LineRenderer不是万能的。当顶点数量超过1024或者屏幕分辨率高、滚动速度快你会发现CPU更新顶点的开销会明显抬升。最直观的翻车现象是在手机上波形出现横向撕裂因为上一帧和下一帧的顶点位置相差太大LineRenderer线性插值来不及平滑。遇到这种场景采样的数据点不需要逐帧更新可以先烘焙成Texture再交给Shader采样或者干脆把LineRenderer只当作数据层把顶点转成曲线贴图最终绘制还是走Shader。判断标准很简单如果你的波形是数学函数生成的就用Shader如果你手里已经有真实采样点但数量少于几百个用LineRenderer。数量更多或需要滚动超长时间段就把采样点烘焙成Texture曲线再Shader采样。这个顺序能避开大多数性能问题。5. 心电图demo常见问题排查与避坑5个必须收藏的踩坑记录5.1 R波峰值一高就被“铲平”振幅不是越大越好看现象把Amplitude从0.15拉到0.35波形顶部像被剪刀平切了一样R波最大尖峰变成一条平顶亮带怎么调线宽都没用。原因心电图函数里R波峰值是1.0乘上0.35后波形上边已经超过RawImage的UV范围0到1等于撞到了图片边界。一部分像素的uv.y落在波形上方距离直线不再随振幅增加而减小视觉上就是平顶。解决把振幅上限从0.5改成0.3并把RawImage高度留出上下各10%的安全边距。另一个常见补救是先把R波系数从1.0降到0.7再乘振幅这样即使振幅调到0.3最高也只到0.71不会顶边。不要用clamp(0.05, 0.95)硬截因为截出来的平顶会让你误以为波形形态就是这样。5.2 RawImage上的颜色又灰又淡怎么都调不鲜艳现象材质里明明把_Color设成了亮绿色RawImage显示出来却是灰绿色透明度也不对背景颜色都透出来了。原因RawImage组件自带Color属性CanvasRenderer会把顶点颜色和Shader输出颜色相乘。如果RawImage.color不是纯白结果就会被整体压暗另外Shader里col.a * line之后波形外围的alpha降成0此时RawImage所在的Canvas背景如果是灰色会透过半透明区域直接影响观感。解决第一步把RawImage.color设为(1,1,1,1)第二步把Canvas的弱背景色改成深色比如(0.03, 0.06, 0.05)让绿色波形有对比度。如果还觉得暗就把_Glow调到0.8让辉光部分把中心亮线带亮一点。注意不要为了提亮而把Shader改成不透明混合否则波形线条会变成一条很难看的矩形色块。5.3 放进ScrollView或Mask里波形不按遮罩裁切现象把RawImage放进带Mask的ScrollView波形却跑到遮罩外面去了或者被遮罩区域完全挡死和别的UI元素显示顺序错乱。原因uGUI的Mask依赖Stencil模板测试来实现裁剪而自定义Unlit Shader里没有写任何Stencil指令等于没有参与模板测试遮罩自然拦不住它。解决在SubShader的Pass里补上Stencil代码Stencil { Ref 1 Comp Equal Pass Keep }Ref 1是uGUI Mask组件最常用的模板参考值但不保证你的项目一定用它。最靠谱的办法是打开FrameDebugger找到正在生效的Mask材质看它的Stencil Ref是多少再把它填进你的Shader。写完后波形就能被Mask裁剪了而且不影响RawImage自身颜色。5.4 手机真机上波形闪烁、毛边严重PC上却一切正常现象Editor里看得很平滑的细线打包到Android手机上以后边缘出现锯齿波形滚动时整条线一抖一抖的像刷新率跟不上。原因你的Canvas可能处于低分辨率缩放模式RawImage高度只有150像素_LineWidth0.008折算下来不到1.5像素。线宽低于屏幕物理像素时就成了典型的欠采样问题smoothstep提供的过渡带在低分辨率下也没有足够像素来表现。解决把_LineWidth提到0.02并把smoothstep的软边范围从_LineWidth * 3.0改成_LineWidth * 6.0给边缘更多过渡像素。如果改了还是闪就把这个RawImage单独渲染到一张两倍分辨率的RenderTexture再让RawImage显示RT相当于做了一次超采样抗锯齿。我的经验是先把线宽提上来大多数项目都能撑住没必要一上来就上RT。5.5 URP项目里Shader变粉红色控制台报错现象项目用的是URP渲染管线把这个Shader赋给RawImage后界面显示洋红色Console报“shader error”或者“material doesnt support this pass”。原因我上面给的Shader是内置管线的CGPROGRAM写法依赖UnityCG.cginc和UnityObjectToClipPos。虽然这些函数在URP下大部分还兼容但新版本URP会按UniversalPipeline标签和Shader关键字做材质校验只要项目里打开了SRP Batcher或默认设置了URP Material这个Shader就容易被判定为不支持。解决如果项目不想为这一个效果切回内置管线最简单的办法是把它改成HLSL写法把UnityCG.cginc换成Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl顶点函数里调用TransformObjectToHClip(v.vertex.xyz)。另一个更省事的替代方案是直接用Shader Graph复现这个波形公式把Fraction、Add、Exp这些节点按顺序连起来效果几乎一样。无论选哪条改完之后都记得去Player Settings里把URP的Shader质量设置重新提交一次避免老缓存继续报粉红。6. 进阶把demo封装成可复用组件再用“呼吸包络”验证效果6.1 把Shader参数收敛到同一个MonoBehaviour里当项目里不只有一个心电图背景而是有监护仪、床头屏、巡逻大厅好几处地方都要用这个效果时不要让策划到处找Material。做一个ECGLineConfig把所有参数打包成一个ScriptableObject会比重度依赖Inspector里的魔改参数更稳。脚本本身不需要多复杂无非是把材质属性同步逻辑抽成一个Apply方法由外部统一调用。public void Apply(Material target) { target.SetColor(_Color, lineColor); target.SetFloat(_Amplitude, amplitude); target.SetFloat(_WaveSpeed, waveSpeed); target.SetFloat(_Tiling, tiling); target.SetFloat(_LineWidth, lineWidth); target.SetFloat(_Glow, glow); }这样同一个配置可以同时推给几个RawImage切换页面风格时不用逐个材质去翻。我习惯在做这一类UI演示效果时把所有参数都收进一个可序列化配置里不然过一个月回来自己都忘了哪个材质是哪一层调出来的。6.2 给波形加“呼吸性心率变异”更接近真实监护仪恒定速度滚动看起来机械稍微让它“呼吸”一下会自然很多。做法很简单在C#里用一个缓慢的正弦波去扰动相位累加速度。WaveSpeed作为基础速度每帧累加到一个phaseOffset但累加的速度本身每秒变化几次。float breathing 1f 0.12f * Mathf.Sin(Time.time * 0.6f); phaseOffset Time.deltaTime * waveSpeed * breathing; lineMaterial.SetFloat(_WaveOffset, phaseOffset % 1f);对应Shader里把_Time.y * _WaveSpeed整体替换成_WaveOffset这样滚动方向和速度就完全交给脚本控制。你仍然可以在面板上调速度但最终的滚动节奏会因为呼吸包络而略有起伏。这里唯一要注意的是不要让负速度出现否则相位回退时% 1f会把负值转回正数导致波形突然回跳一帧。6.3 验证波形是否合格的三个动作验证一个模拟心电图demo我习惯做三件事。第一把Amplitude从最小值拖到最大值观察R波尖峰是否始终在RawImage范围内有没有平顶和裁切同时把WaveSpeed从-5拖到5看波形滚动是否方向正确、顺滑连续。第二在C#里打印EvalEcgShape函数的最大值和最小值确认数学函数本身没有超出预期范围这一步能防止你在调参时把R波峰值改烂了。第三用FrameDebugger看一眼这个RawImage的DrawCall和Overdraw如果波形区域占屏幕比例过高就用RenderTexture缩小绘制分辨率保证低端设备不卡。做完这三步这个demo基本就合格了。我自己每个新项目做这类线波效果时都保留一个最小可复现的RawImage场景把Shader和组件都留好。下次再遇到“模拟心电图”需求直接复制一份改参数就行不用再从零开始调关键帧了。希望这些步骤和坑能帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

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

03
WHY YAOTU

想打造同款高转化官网?

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

◈

场景化定制

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

◐

营销型架构

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

▲

全周期服务

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

免费获取你的建站方案

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