系列番外·空间计算篇 。终章发布后,有读者留言:"现在都在聊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(深)** | 从画纸变成了雕塑台 |
| 基本单元 | Column, Text, Image |
Scene, Node3D, Mesh |
积木块变成了3D模型 |
| 位置 | position({x:0, y:0}) |
position({x:0, y:0, **z:-5**}) |
Z轴负值表示远离镜头 |
| 旋转 | rotate({angle: 90}) |
rotation({x:0, y:1, z:0, angle:90}) |
指定绕哪个轴旋转 |
| 相机 | 屏幕即是相机 | Camera组件 |
导演的眼睛,决定看哪里 |
💡 核心认知: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层。