【open harmony/harmonyos】ArkTS 实现 3D 透视投影让普通组件拥有空间感前言 在 HarmonyOS / OpenHarmony 应用中如果想做 3D 效果很多人第一反应可能是使用 3D 引擎。但如果需求只是“让节点有空间感”其实不一定要上复杂引擎。对于轻量级知识图谱、星图、关系网络这类场景可以用 ArkTS 数学计算 ArkUI 普通组件实现一个简化版 3D 透视效果。这篇文章会结合我的项目星图 Xingtu分享如何用 ArkTS 实现 3D 坐标旋转、透视投影、节点缩放、深度排序让普通 ArkUI 组件呈现出空间层次。✨一、目标效果项目中希望实现这样一个星图界面节点分布在 3D 空间里拖动时节点会围绕视角旋转近处节点更大、更亮远处节点更小、更淡近处节点覆盖远处节点整体看起来像一个可探索的数据空间重点是这些节点本质上仍然是 ArkUI 组件不是复杂 3D 模型。二、定义 3D 坐标和投影结果首先定义一个三维坐标类型exportinterfaceVec3 {x:number;y:number;z:number; }节点保存在真实的 3D 坐标中exportinterfaceXingtuNode{ id:string; title:string; note:string; tags:string[]; position: Vec3; }经过投影后节点会变成屏幕上的二维位置exportinterfaceProjectedNode {id:string;title:string;note:string;tags:string[];screenX:number;screenY:number;scale:number;opacity:number;depth:number; }这里的ProjectedNode是 UI 真正需要的数据screenX屏幕横坐标screenY屏幕纵坐标scale节点大小比例opacity节点透明度depth节点深度三、相机状态设计 为了让用户可以旋转和缩放星图需要一个相机状态。exportinterfaceCameraState { yaw:number; pitch:number; distance:number;scale:number; }项目中的默认相机如下exportfunctiondefaultCamera(): CameraState {return{yaw: -18, pitch: -10, distance:620, scale:1}; }每个字段的含义yaw水平旋转角度pitch垂直旋转角度distance相机和节点空间的距离scale整体缩放比例有了相机状态就可以通过改变相机而不是直接改变所有节点位置来实现视角变化。四、坐标旋转计算3D 投影的第一步是根据相机角度旋转节点。constDEGREE Math.PI /180;exportfunctionrotatePoint(point: Vec3, yaw: number, pitch: number): Vec3 {constyawRad: number yaw * DEGREE;constpitchRad: number pitch * DEGREE;constyawX: number point.x * Math.cos(yawRad) point.z * Math.sin(yawRad);constyawZ: number -point.x * Math.sin(yawRad) point.z * Math.cos(yawRad);constpitchY: number point.y * Math.cos(pitchRad) - yawZ * Math.sin(pitchRad);constpitchZ: number point.y * Math.sin(pitchRad) yawZ * Math.cos(pitchRad);return{ x: yawX, y: pitchY, z: pitchZ }; }这段逻辑分两步根据yaw做水平旋转根据pitch做上下旋转用户拖动屏幕时实际更新的是yaw和pitch然后所有节点重新计算屏幕位置。五、透视投影核心算法 坐标旋转后需要把 3D 坐标投影到 2D 屏幕。constCAMERA_FOCAL 560; export functionprojectNode( node: XingtuNode,camera: CameraState, viewport: ViewportSize ): ProjectedNode {constrotated: Vec3 rotatePoint(node.position,camera.yaw,camera.pitch);constdepth: number camera.distance- rotated.z;constperspective: number CAMERA_FOCAL / Math.max(220, depth);consthalfWidth: number viewport.width/2;consthalfHeight: number viewport.height/2;return{ id: node.id, title: node.title, note: node.note, tags: node.tags,screenX: halfWidth rotated.x*perspective*camera.scale,screenY: halfHeight rotated.y*perspective*camera.scale,scale:perspective*camera.scale, opacity: Math.max(0.28, Math.min(1,0.2perspective*0.35)), depth }; }这里最关键的是perspectiveconstperspective: number CAMERA_FOCAL / Math.max(220, depth);depth越小说明节点越靠近用户perspective越大节点显示就越大。depth越大说明节点越远perspective越小节点显示就越小。这就是透视感的来源。六、让远近影响透明度除了大小透明度也可以用来强化空间感。opacity: Math.max(0.28, Math.min(1,0.2 perspective *0.35))这段代码限制了透明度范围最低不低于0.28最高不超过1近处节点更亮远处节点更淡如果只改变大小不改变透明度空间感会弱一些。大小 透明度一起变化效果会更自然。七、节点深度排序在 3D 空间中近处节点应该盖住远处节点。项目中在 Store 里对投影节点做了排序projectedNodes(viewport: ViewportSize): ProjectedNode[] {returnthis.nodes .map((node: XingtuNode)projectNode(node, this.camera, viewport)) .sort((left: ProjectedNode, right: ProjectedNode)right.depth - left.depth); }排序后远处节点先渲染近处节点后渲染。在Stack里后面的组件会覆盖前面的组件所以这样就能模拟基本的深度遮挡。八、渲染节点组件投影完成后节点组件只关心自己的屏幕位置、大小和透明度。privatenodeSize(): number {returnMath.max(30, Math.min(108,58*this.node.scale)); }privatenodePosX(): number {returnthis.node.screenX -this.nodeSize() /2; }privatenodePosY(): number {returnthis.node.screenY -this.nodeSize() /2; }渲染时Stack() {} .width(this.nodeSize()) .height(this.nodeSize()) .borderRadius(this.nodeSize() /2) .backgroundColor(this.selected ? XingtuTheme.primaryAction : XingtuTheme.accent) .opacity(this.selected ?0.98:this.node.opacity *0.82) .shadow({ radius:this.selected ?30:12this.node.scale *5, color:this.selected ? XingtuTheme.harmonyLightShadow :#3493C5FD, offsetX:0, offsetY:this.selected ?0:4})这里节点本质上就是一个带圆角和阴影的 ArkUI 组件但因为它的位置、大小、透明度都来自 3D 投影所以看起来就有了空间感。九、监听视口尺寸变化投影计算需要知道当前屏幕宽高。项目中使用onAreaChange获取视口尺寸.onAreaChange((_,area) { this.viewportWidth Number(area.width); this.viewportHeight Number(area.height); })然后在计算投影时传入this.store.projectedNodes({ width:this.viewportWidth, height:this.viewportHeight });这样不同设备尺寸下星图都可以以屏幕中心为基准进行布局。十、拖动时更新相机当用户单指拖动时更新相机角度this.store.updateCamera(deltaX *0.42, deltaY *0.28);this.refreshScene();Store 中的实现updateCamera(deltaYaw: number, deltaPitch: number): void {this.camera { yaw:this.camera.yaw deltaYaw, pitch: clampPitch(this.camera.pitch deltaPitch), distance:this.camera.distance, scale:this.camera.scale }; }垂直角度需要限制exportfunctionclampPitch(nextPitch:number):number{returnMath.max(-80,Math.min(80, nextPitch)); }如果不限制pitch用户可能把场景翻转到不舒服的角度。十一、缩放时更新 scale双指缩放最终修改的是camera.scale。updateScale(nextScale: number): void {this.camera { yaw: this.camera.yaw, pitch: this.camera.pitch, distance: this.camera.distance, scale: Math.max(0.6, Math.min(2.2, nextScale)) }; }这里限制缩放范围在0.6到2.2之间。适当限制交互范围可以避免用户把节点放大到失控或者缩小到完全看不清。十二、总结 这篇文章分享了如何在 HarmonyOS / OpenHarmony 中用 ArkTS 和 ArkUI 普通组件实现轻量级 3D 透视投影。核心步骤是用Vec3保存节点三维坐标用CameraState保存相机旋转和缩放用rotatePoint计算旋转后的坐标用projectNode把 3D 坐标转换成屏幕坐标用scale、opacity表现远近关系用depth排序模拟遮挡关系用 ArkUI 组件渲染节点这种方案不适合重型 3D 游戏但非常适合知识星图、关系网络、AI 概念图、灵感空间等轻量级场景。不用复杂引擎也能让普通组件拥有空间感。✨