系列番外·空间计算篇。终章发布后,有读者留言:“现在都在聊Vision Pro和空间计算,鸿蒙在这方面有布局吗?能不能把我们这个电商Demo弄成3D的?” 答案是肯定的。HarmonyOS NEXT的ArkUI 3D和Spatial Computing Kit正是为此而生。今天作为系列的第一个番外篇,我们跳出手机屏幕的限制,将电商Demo升级为3D沉浸式购物空间。用户不再只是“刷列表”,而是可以走进虚拟展厅,“拿起”商品查看细节,甚至把家具“摆放”在自己家里。全程基于API23,解决3D模型加载、手势交互、虚实锚定三大核心技术点。这是通往下一代操作系统的入场券。
一、前言:为什么电商是空间计算的“杀手级场景”?
回顾我们之前的系列,所有的交互都被局限在6英寸的矩形屏幕里。无论是滑动列表还是点击按钮,本质上都是在操作二维平面上的像素。
空间计算(Spatial Computing)打破了这个界限:
从“看图片”到“看展品”:以前的详情页是几张平面图,现在是一个可以720°旋转的3D模型。
从“想象摆放”到“真实预览”:买沙发不用凭空想象,直接把虚拟沙发“放”在客厅里,看看尺寸和风格是否合适。
从“手指点击”到“自然交互”:用手捏合来缩放商品,用手抛动作来切换视角,就像在现实世界一样自然。
HarmOS 6.1提供了低门槛的3D开发能力。我们不需要学习OpenGL或Unity复杂的Shader语言,直接用熟悉的ArkTS和类Compose的声明式语法就能构建3D场景。今天,我们就来给电商Demo加一个“3D展厅”页面。
二、核心概念辨析(2D UI vs 3D Scene)
很多前端开发者对3D有畏难情绪,其实只要理清这几个核心概念,上手非常快:
概念 | 2D UI (传统) | 3D Scene (空间计算) | 类比说明 |
|---|---|---|---|
坐标系 | X(横), Y(纵) | X(横), Y(纵),Z(深) | 从画纸变成了雕塑台 |
基本单元 |
|
| 积木块变成了3D模型 |
位置 |
|
| Z轴负值表示远离镜头 |
旋转 |
|
| 指定绕哪个轴旋转 |
相机 | 屏幕即是相机 |
| 导演的眼睛,决定看哪里 |
💡核心认知:ArkUI 3D的本质是把3D场景当作一个特殊的UI组件来渲染。我们可以在3D场景里嵌套2D的按钮(Overlay),也可以在2D页面里嵌入3D视口。这种混合渲染能力是鸿蒙的独特优势。
三、代码实现:构建3D商品展厅
3.1 环境准备与资源引入
模型准备:3D开发离不开模型文件。常用的格式是
.glb或.gltf。我们可以从Sketchfab等网站下载免费的电商产品模型(如椅子、包包),放入entry/src/main/resources/rawfile/models/目录。权限配置:空间计算可能需要相机权限(用于虚实融合),在
module.json5中添加:{ "module": { "requestPermissions": [ { "name": "ohos.permission.CAMERA", "reason": "AR空间锚定需要" } ] } }
3.2 构建3D场景组件
创建entry/src/main/ets/pages/SpatialShowroom.ets。这是我们的核心3D页面。
import { scene } from '@kit.ArkGraphics3D' import { abilityAccessCtrl } from '@kit.AbilityKit' @Entry @Component struct SpatialShowroom { private scene: scene.Scene = null! private modelNode: scene.Node3D = null! @State rotationY: number = 0 // 控制模型Y轴旋转 @State scale: number = 1.0 // 控制模型缩放 aboutToAppear(): void { // 请求相机权限(用于AR模式) this.requestCameraPermission() } async requestCameraPermission(): Promise<void> { let atManager = abilityAccessCtrl.createAtManager() try { await atManager.requestPermissionsFromUser(getContext(this), ['ohos.permission.CAMERA']) } catch (err) { console.error('相机权限申请失败:', err) } } /** * 加载3D场景 */ onSceneCreated(scene: scene.Scene): void { this.scene = scene console.log('3D场景加载成功') // 1. 设置环境光(让模型更清晰) const light = scene.getNode('EnvLight') as scene.Light if (light) { light.intensity = 20000 } // 2. 加载商品模型(例如一把椅子) scene.loadModel({ path: 'models/chair.glb', // rawfile目录下的路径 onLoaded: (node: scene.Node3D) => { this.modelNode = node console.log('模型加载成功:', node.name) // 调整模型位置和大小 node.position = { x: 0, y: -1, z: -5 } // 稍微下移,后退 node.scale = { x: 0.5, y: 0.5, z: 0.5 } // 缩小一半 }, onError: (err: Error) => { console.error('模型加载失败:', err) } }) } build() { Stack() { // 3D场景视口(铺满全屏) SceneView({ sceneOptions: { environment: scene.EnvironmentPreset.STUDIO // 使用工作室光照环境 }, onSceneCreated: (scene: scene.Scene) => this.onSceneCreated(scene) }) .width('100%') .height('100%') // 叠加在3D场景上的2D UI(操作面板) Column() { // 标题 Text('3D商品展厅') .fontSize(24) .fontColor(Color.White) .fontWeight(FontWeight.Bold) .shadow({ radius: 10, color: Color.Black }) .margin({ top: 40 }) Blank() // 底部操作栏 Row() { // 旋转按钮 Button('↻') .onClick(() => { this.rotationY += 45 if (this.modelNode) { this.modelNode.rotation = { x: 0, y: 1, z: 0, angle: this.rotationY } } }) .width(60) .height(60) .borderRadius(30) .backgroundColor('#0A59F7') // 缩放控制 Slider({ value: this.scale, min: 0.5, max: 2.0, step: 0.1 }) .width(150) .onChange((value: number) => { this.scale = value if (this.modelNode) { this.modelNode.scale = { x: value, y: value, z: value } } }) // AR放置按钮(进阶功能) Button('AR') .onClick(() => { this.enterARMode() }) .width(60) .height(60) .borderRadius(30) .backgroundColor('#4CAF50') } .width('90%') .justifyContent(FlexAlign.SpaceBetween) .margin({ bottom: 40 }) } .width('100%') .height('100%') } .width('100%') .height('100%') .backgroundColor(Color.Black) } /** * 进入AR放置模式(虚实融合) */ enterARMode(): void { if (!this.scene) return // 切换到AR环境(需要设备支持,如Mate 60 Pro+) this.scene.switchEnvironment(scene.EnvironmentPreset.AR) // 提示用户点击地面放置模型 promptAction.showToast({ message: '请点击地面放置商品' }) // 监听点击事件,在点击位置放置模型 this.scene.on('touch', (hitResult: scene.HitResult) => { if (hitResult.type === scene.HitType.PLANE) { if (this.modelNode) { // 将模型移动到点击的平面位置 this.modelNode.position = hitResult.point promptAction.showToast({ message: '商品已放置' }) } } }) } }四、踩坑记录(官方文档没写的5个细节)
模型格式陷阱:一定要用
.glb格式,不要用.obj或.fbx。.glb是二进制格式,体积小,加载快,且支持PBR材质(金属、粗糙度),鸿蒙对其支持最好。.gltf是JSON格式,加载慢,不推荐。坐标系手性:鸿蒙3D使用的是右手坐标系(Y轴向上),而Unity等引擎常用左手坐标系。导入模型后如果发现上下颠倒,记得在Blender等建模软件中调整坐标轴方向,或者在代码里旋转90度。
性能瓶颈:3D渲染非常耗电耗性能。不要在列表页一次性加载多个复杂3D模型。建议只在详情页进入全屏3D模式。同时,务必给模型做LOD(多层次细节)处理,远看用低模,近看用高模。
AR模式的设备限制:目前的
EnvironmentPreset.AR主要依赖华为自研的Cyberverse技术,并非所有鸿蒙设备都支持(主要是旗舰机如Mate 60系列、Pura 70系列)。赶稿时如果设备不支持,可以用下面的浏览器模拟方案。触摸事件穿透:3D场景的触摸事件默认会吃掉所有点击,导致后面的2D按钮点不了。需要在
SceneView上设置.hitTestBehavior(HitTestMode.None)或者在3D场景里手动转发事件给UI层。