讲师:天瀑——易知微高级开发工程师
在数字孪生应用日益普及的今天,不同行业、不同项目对三维可视化呈现的需求千差万别。通用组件虽然能满足大部分常规场景,但面对温度阵列展示、有限元结构建模、热力图渲染或体渲染烟雾等特殊需求时,我们必须拥有自主扩展的能力。这正是EasyTwin平台赋予开发者的重要价值——通过定制化3D组件开发,打造真正差异化的孪生体验。本文将基于EasyTwin平台的组件开发实践,从平台简介、环境配置、架构剖析、蓝图交互到进阶案例,全方位梳理整个开发流程,帮助读者建立起从入门到精通的系统认知。
一、EasyTwin平台简介:低代码数字孪生的核心能力
EasyTwin是一个低代码数字孪生开发平台,采用B/S架构,旨在让开发者乃至设计师都能以极低的门槛构建三维交互场景。它的核心设计理念是“重塑开发体验”,将传统的“代码拼装”模式转变为“一站式平台”服务。
在场景构建层面,EasyTwin支持直接拖拽FBX或GLB格式的模型文件进入场景进行实时预览,无需编写任何加载代码。
渲染方面,平台内置了完善的天空系统、光照环境及后期特效,这些效果全部通过零代码的配置面板即可完成调整,即便没有图形学背景的用户也能快速获得精美的视觉输出。
平台最引人注目的功能当属“蓝图交互系统”,它实现了EasyTwin与EasyV之间的双向通信。简单来说,用户可以通过EasyV前端的图表、按钮、下拉菜单等控件,去控制EasyTwin三维场景中模型的运动、动画的启停、材质的切换甚至相机视角的定位。这种数据驱动与可视化联动的机制,让数字孪生不再只是静态的模型展示,而是真正可交互、可响应的智能系统。
之所以选择基于EasyTwin引擎进行开发,是因为它内置了多项关键能力:资源管理器可以轻松读取模型、贴图、HDR等资源,无需手动管理加载队列;高效的渲染系统借助实例化技术,能够稳定渲染数百万个粒子而保持流畅帧率;蓝图化的交互系统让设计师也能通过连线方式完成场景联动,极大降低了协作门槛;同时,平台提供了大量精美的正式组件,覆盖了绝大多数通用需求。然而,在实际项目中,客户往往有独特的定制化诉求,比如温度场阵列的可视化、ANSYS导出的结构网格建模、烟雾体渲染等,这些通用组件无法覆盖,因此自定义组件开发便成了项目落地的关键环节。
二、环境配置及开发前准备
在正式开始编码之前,我们需要正确配置本地开发环境。
1.首先,确保Node.js版本高于18,因为整个工具链依赖pnpm进行包管理。如果本地有多个Node版本共存,建议安装nvm进行版本切换,避免冲突。安装pnpm的方法参考官方文档即可。
教程可见:nvm安装教程-CSDN博客
安装完成后,我们通过一条命令创建monorepo仓库:
pnpm create easytwin-component <仓库名>2.这条命令会在当前路径下生成一个同名文件夹,内部已经包含了基础的仓库结构和配置文件,并且会自动从npm查询并写入easytwin-runtime和runtime-frontend的最新版本。由于网络原因,这一步可能需要多次尝试才能成功。仓库创建完成后,进入该目录并执行pnpm i安装所有依赖项,这一步至关重要,因为后续的所有脚手架命令和工具包都依赖这些本地模块。
3.依赖安装完毕后,我们就可以创建第一个组件了。使用pnpm easytwin-cli create <组件名>命令,例如pnpm easytwin-cli create ExampleAaa,会在packages目录下生成一个以easytwin-components-example-aaa命名的文件夹,这就是我们组件的全部源码存放位置。每个组件独立成包,互不干扰,便于管理和版本控制。
在发布组件之前,必须配置AccessKey,这是上传到线上环境的身份凭证。获取方式是在EasyTwin的场景编辑页中,点击“调试工具”并选择“获取Access Key”,确保当前用户角色拥有相应权限。复制该Key后,通过命令行执行pnpm easytwin-cli key -s '<粘贴的key>' -m '<环境>',其中环境可选dev、test、prod或pre。需要注意的是,Windows系统下使用PowerShell粘贴时可能出现数据不正确的问题,此时建议手动将Key写入.vscode目录下对应的环境文件中,如access_key.test.json。不同环境的Key彼此独立,务必要在对应环境下获取。
最后,为了验证整个链路是否通畅,我们可以在package.json中找到upload命令并执行。但要注意,默认的upload命令指向正式组件,我们需要在命令后追加--source officialCustom标识,让平台识别这是定制组件而非内置组件。这样,首次上传成功后就完成了环境的全部配置,接下来便可以专注于组件逻辑的开发了。
三、组件架构深度剖析:生命周期、配置面板与核心管理器
运行时组件
一个标准的自定义组件目录下,核心文件集中在core文件夹中。其中,ExampleComponent.ts是主逻辑类,initState.ts负责为配置项提供默认值,config.ts声明了配置面板上的UI结构,setting.ts定义组件名称、分类、版本和唯一类型标识。每次发布新版本时,setting.ts中的version字段必须递增,否则服务器会拒绝上传。同时,type字段必须全局唯一,否则会覆盖已有组件。
组件的生命周期函数是驱动一切行为的基础。引擎为每个组件提供了一系列按特定时机调用的钩子,开发者可以根据需求选择合适的钩子实现业务逻辑。具体包括:
- onInitialize:在帧循环前调用,无论组件是否处于激活状态。这是绑定蓝图事件、初始化资源、加载模型的最佳入口,因为此时可以安全访问到组件的根节点
sceneObject(Three.js的Object3D对象)以及引擎的camera、scene、renderer等核心对象。 - onAwake:同样在帧循环前调用,但仅在组件
active为true时执行,若初次为false则延迟到第一次激活时调用,且整个生命周期只会调用一次。 - onStart:在帧循环最开始调用,也仅执行一次,适合放置那些只需运行一次的初始化代码。
- onUpdate:每帧调用一次,晚于
onStart,通常在此处理持续更新的逻辑,比如动画插值、Uniforms参数变化等。参数dt为与上一帧的时间间隔(秒),可用于实现帧率无关的运动。 - onFixedUpdate:以固定间隔(目前设定为60FPS)调用,适合处理物理模拟或需要稳定步长的计算,如粒子系统的状态迭代。
- onEnable:当组件变为可见时调用(即
activeInScene为true)。 - onDisable:当组件变为隐藏时调用(
activeInScene为false)。 - onDestroy:组件被销毁时调用,在此必须手动释放所有监听器、几何体、材质等资源,防止内存泄漏。
组件提供了两个重要的活动状态属性:active表示本地的激活状态,与世界大纲中的显隐图标直接对应;而activeInScene表示全局激活状态,它受父子层级影响——只有所有父级和自身active均为true时,activeInScene才为true,并且其变化会自动触发onEnable或onDisable回调。
组件配置面板
配置面板的渲染完全由config.ts驱动。在该文件中,StateSchemaFunc函数返回一个数组,数组的每一项都是一个UI组件的描述对象。引擎内置了丰富的UI类型,如inputNumber(数字输入框)、inputVector3(三维向量输入)、select(下拉选择框)、colorPicker(颜色选择器)等,这些类型均来自UIGenerator模块,该模块位于packages/easytwin-runtime-frontend/src/UIGenerator/uicomponents,提供了完善的TypeScript注释,开发者可以在编写schema时直接查看类型提示。如果需要更自由的布局,也可以使用custom类型嵌入自己编写的Vue或React组件,但此时数据的双向绑定需要自行实现。
Manager:引擎实现了一些提供给组件使用的Manager
除了生命周期和配置,引擎还提供了多个实用的Manager,方便组件与平台核心能力交互。最常用的是ResourceManager,通过this.engine.getManager(ResourceManager)即可获取,它提供了loadModelAsync(加载模型)、loadTextureAsync(加载贴图)、loadHDRTextureAsync(加载HDR环境贴图)等异步方法,极大简化了资源加载流程。
另一个是InputManager,用于管理键盘和鼠标操作,例如检测空格键是否被按下,适用于需要用户交互的场景。这些Manager的抽象让开发者无需关心底层实现细节,专注于业务逻辑本身。
值得特别说明的是数字要素组件,它与普通组件在架构上有明显区别。数字要素的core目录下除了component外,还有一个childComponent,这就是点击“新增子要素”时所实例化的模板。数字要素本身不保存子元素的状态,而是通过virtualChildren数组管理所有子实例,并在onRenderState中遍历每个子要素调用其refresh方法完成更新。这种设计使得数字要素非常适合动态增删子对象的场景,比如批量创建信息面板、动态生成标签阵列等。
四、蓝图交互系统:连接三维场景与前端应用
蓝图是EasyTwin与EasyV进行通信的桥梁,它让组件能够以可视化方式暴露自己的“事件”和“动作”,从而与EV端的图表、按钮等控件联动,形成完整的交互链路。在蓝图上,一个组件节点可以拥有两类端口:
- 动作(Action):代表组件可以接收的外部输入,在蓝图上表现为左侧的输入引脚。其他节点可以通过连线触发该动作,组件内部则通过监听对应的事件名称来执行逻辑。
- 事件(Event):代表组件在某个时刻主动向外发出通知,在蓝图上表现为右侧的输出引脚。当组件内部调用发送方法后,后续节点会依次被执行。
要为一个组件添加蓝图能力,首先需要在组件根目录下新建blueprint.json文件,其结构为一个包含name、events和actions三个字段的对象。其中name与组件的settings.name保持一致,便于识别;events和actions均为数组,每个元素包含name(显示名称)和value(内部标识符)。
例如,定义一个“隐藏测试”动作,其value为hideTest,那么在组件的onInitialize中,我们就可以调用this.addEasyvEventListener('hideTest', callback)来注册监听器,当蓝图触发该动作时,回调函数便会执行。
发送事件同样简单,使用this.emitEasyvFunc(eventName, data)方法即可。data是一个对象,可以包含任意字段,蓝图会将它们传递给下游节点。为了保证易用性,引擎还支持在fields字段中为每个参数添加详细说明。fields是一个数组,每一项包括id(字段名)、desc(字段描述)和type(数据类型),这些信息会显示在EasyV蓝图面板的小问号提示中,极大方便了团队协作——使用组件的同事无需翻看源码即可知道每个参数的含义和格式。
在实现蓝图交互时,有一个容易被忽视但至关重要的细节:内存管理。每一个addEventListener注册的监听器都会占用内存,如果组件被频繁创建销毁,或者场景中有多个实例,必须要在onDestroy生命周期中调用this.removeEasyvEventListener(value)移除对应的监听器。同时,如果监听器中引用了外部对象(如场景中的网格),也要一并释放其几何体和材质。只有做到“有始有终”,才能确保长时间运行时不会出现内存泄漏导致页面卡顿甚至崩溃。
蓝图不仅让组件具备了可编排的能力,更重要的是它打通了三维世界与前端数据看板的界限。比如,我们在EV端放置一个按钮,用户点击按钮后,蓝图将“点击”事件连接到三维组件的“旋转”动作,即可实现按钮控制模型旋转;反之,当模型被鼠标悬停时,组件触发“悬停”事件,蓝图再连接到一个弹出面板,显示模型的详细信息。这种双向互动正是数字孪生应用最具魅力的地方。
五、从零到一开发实战:亲手实现一个可交互组件
理论铺垫之后,我们通过一个完整的开发实例来串联所有知识点。假设我们已经按照第二部分配置好了环境,并创建了一个名为ExampleAaa的组件。首次上传成功后,在场景的自定义组件列表中将其拖入场景,会发现它默认显示一个立方体(Box),配置面板中有“网格类型”的下拉选项,但切换并没有实际效果,因为对应的逻辑尚未实现。
第一步,我们来实现网格类型的切换功能。打开core/ExampleComponent.ts,找到onRenderState方法,该方法接收的state参数即为当前配置项的状态对象。在config.ts中查看meshType字段的定义,发现其uiType为select,可选值为cube和sphere。因此在onRenderState中,我们只需解构出meshType,然后判断其值:若为sphere,则删除当前的立方体,新建一个球体并赋值给this.sceneObject;若为cube则做相反操作。注意,不能简单地将新物体直接添加到this.scene,因为this.scene是引擎的全局场景,而每个组件有自己的根节点this.sceneObject,应当操作后者以避免影响其他组件。同时,每次新建物体前,务必对旧物体的几何体和材质调用dispose()方法,这是Three.js内存管理的基本准则。
第二步,我们新增一个控制物体缩放的配置项。在config.ts的StateSchemaFunc数组中追加一项,类型为inputNumber,fieldName设为scale,label设为“缩放”。然后,在ExampleComponentStateConfig接口中补充scale?: number,再在initState.ts中为其赋默认值(如1.0)。最后回到onRenderState,在更新逻辑中读取state.scale,并应用到this.sceneObject.scale.setScalar(scale)。重新发布(记得更新version),刷新场景后即可在配置面板中看到新的“缩放”输入框,调整数值时立方体或球体的大小会实时变化。
第三步,我们加上拖拽移动功能。默认情况下,自定义组件在场景中是无法拖拽的,因为引擎的变换控制器(TransformControls)需要监听拖拽结束事件。在onRenderState中,我们找到onTransformControlDragEnd回调,当拖拽结束时,newState中包含了最新的position和rotation,我们需要将这些值同步到state中,并调用this.setState(newState)触发更新。这样,用户拖拽组件后,配置面板中的位置和旋转数值也会同步刷新,并且再次发布后拖拽功能就永久生效了。
最后,我们为这个组件添加一个蓝图动作,比如“隐藏/显示”切换。在blueprint.json的actions中添加一项{ "name": "切换显隐", "value": "toggleVisible" },然后在onInitialize中注册监听器:this.addEasyvEventListener('toggleVisible', () => { this.sceneObject.visible = !this.sceneObject.visible; })。这里要注意,visible是Three.js原生的属性,但引擎推荐使用this.active或this.sceneObject.active来控制显隐,因为后者会触发生命周期钩子,保持状态一致性。不过简单演示中直接操作visible也可行。发布后,在EasyV的蓝图中拖出该组件的节点,从按钮的“点击”事件连线到“切换显隐”动作,保存并预览,点击按钮就能看到三维组件的显隐切换了。
通过这个完整的从零到一流程,我们不仅掌握了组件开发的基本范式,还熟悉了配置项扩展、状态更新、拖拽交互和蓝图联动的全部环节。这些步骤在后续更复杂的组件开发中会反复出现,形成肌肉记忆后,开发效率将大幅提升。
六、进阶案例:后处理、文字贴图与自定义材质的实战技巧
掌握了基础开发之后,我们来看看几个来自实际项目的高阶需求,它们展示了如何利用引擎的底层机制实现更具视觉冲击力和实用价值的效果。
案例一:双屏渲染后处理
在某项目中,客户希望同时展示宏观地形与局部细节——上半屏固定显示某个俯瞰视角,下半屏跟随相机自由漫游。这本质上是后处理管线的定制。我们通过继承Three.js的Pass类,编写了一个HalfScreenPass,在render方法中通过scissor和viewport裁剪渲染区域,将场景分别渲染到屏幕的上下两部分。在组件的onInitialize中,通过this.engine.postProcessingComponent获取后处理组件实例,再拿到其内部的effectComposer,然后移除默认的RenderPass,插入我们的HalfScreenPass,并保留泛光(Bloom)和外描边(Outline)等后续Pass。为了增强灵活性,我们还通过蓝图暴露了“启用/关闭双屏”和“调节分屏比例”的动作,用户可以在EV端动态控制这些参数。这个案例展示了如何将Three.js的后处理生态无缝集成到EasyTwin中,关键在于正确操作effectComposer的Pass队列,以及利用this.hook机制将配置项的更新拆分为独立监听,避免频繁重建渲染管线,从而保障性能。
案例二:Canvas生成的文字标识
在测绘类场景中,我们经常需要在地面或墙面上标注距离或编号。使用Three.js的Sprite虽然简单,但永远面向相机的特性在某些设计风格下显得不够自然。我们的方案是创建一个平面网格(PlaneGeometry),然后使用Canvas绘制文字,生成纹理并作为网格材质的map。在更新文字内容时,重新绘制Canvas并标记texture.needsUpdate = true,即可实现动态刷新。这个方法的优势在于文字完全固定在世界坐标系中,可以随场景旋转俯仰,更具真实感。我们在组件中封装了一个CanvasTextMesh类,支持设置字体大小、颜色、背景透明等参数,并提供了便捷的updateText(text)方法,极大简化了标签类组件的开发。
案例三:基于ShaderMaterial的体渲染烟雾
工业仿真中经常需要展示粉尘扩散、气流流动等效果,这些往往需要体渲染技术。在EasyTwin中,我们直接使用Three.js的ShaderMaterial编写自定义着色器,将体数据(如噪声纹理)传入片元着色器,通过光线步进(Ray Marching)算法生成半透明的烟雾体积。在顶点着色器中,我们传递模型的UV坐标和世界位置,片元着色器中则根据时间uniform和噪声采样计算密度场,最终与场景颜色混合。每帧在onUpdate中更新timeuniform,烟雾便会缓缓流动。为了提升性能,我们还将计算部分移到GPU,通过GPUComputationRenderer实现粒子位置的迭代,这个工具类同样从Three.js官方示例中移植而来,只需要注意版本匹配即可。这个案例充分体现了自定义材质和着色器编程在视觉差异化上的巨大潜力。
这些进阶案例并非孤立的炫技,而是切实解决项目中的真实痛点。它们的共同思路是:深入理解EasyTwin对Three.js的封装边界,在需要时大胆穿透抽象层,直接操作底层对象(如effectComposer、shaderMaterial、canvas等),同时利用平台提供的生命周期和管理器来确保集成后的稳定性和性能。
七、总结与建议
定制化3D组件开发,本质上是基于EasyTwin平台强大的底层能力和灵活的扩展机制,将业务需求转化为可视、可交互的三维功能模块。从环境配置到架构理解,从蓝图联调到进阶渲染,每一步都有章可循。回顾整个知识体系,有几个要点值得反复强调:
- 版本管理是生命线,每次发布前务必递增
version,且type不可重复。 - 配置项的声明必须与TypeScript类型和初始值三者同步,否则编辑器会报错,影响开发效率。
- 内存释放不可忽视,每一个
addEventListener、每一个new Geometry和new Material,都应当在onDestroy中清理干净。 - 蓝图参数的
fields说明一定要写详细,这不仅是文档,更是团队协作的润滑剂。 - 当内置UI类型不足时,大胆使用
custom并嵌入自定义组件,但要注意数据流的一致性。
最后,希望本文能帮助开发者建立起对EasyTwin组件开发的系统认知,并在实际项目中灵活运用这些技术,打造出真正差异化、高价值的数字孪生应用。随着平台能力的持续迭代,定制化开发的想象空间也将不断扩大,期待大家在实践中创造出更多精彩的作品。