最近很火的Remy大家有没有体验,平面的2D图片已经不能满足用户,未来可能会更多的相机支持拍摄3D照片。今天来了解一下鸿蒙的3D图形展示。我找了个汽车的3D模型资源,看一下展示效果。由于能力有限,本文只实现修改相机旋转角度。
![]()
ArkGraphics 3D(方舟3D图形)基于轻量级的3D引擎以及渲染管线为开发者提供基础3D场景绘制能力,供开发者便捷、高效地构建3D场景并完成渲染。
一个3D场景通常由光源、相机、模型三个关键部分组成。
光源:为整个3D场景提供光照,使得3D场景中的模型变得可见。与真实物理场景一致,没有光源场景将变得一片漆黑,得到的渲染结果也就是全黑色。
相机:为3D场景提供一个观察者。3D渲染本质上是从一个角度观察3D场景并投影到2D图片上。没有相机就没有3D场景的观察者,也就不会得到渲染结果。
模型:3D场景中的模型用于描述对象的形状、结构和外观,一般具有网格、材质、纹理、动画等属性。一些常见的3D模型格式有OBJ、FBX、glTF等。
模型加载后,可以通过ArkUI的Component3D渲染组件呈现给用户。
Component3D(sceneOptions?: SceneOptions)
Component3D组件配置选项 SceneOptions
| 名称 | 说明 |
|---|
| scene | 3D模型资源文件 |
| modelType | 3D场景显示合成方式 |
设置场景 Scene
属性
| 名称 | 说明 |
|---|
| environment | 环境对象 |
| animations | 动画数组 |
| root | 3D场景树根结点 |
方法
| 名称 | 说明 |
|---|
| load | 待加载的模型文件资源路径 |
| getNodeByPath | 通过路径获取结点 |
| getResourceFactory | 获取场景资源工厂对象 |
| destroy | 销毁场景 |
| importNode | 从其他场景导入结点 |
| importScene | 导入其他场景 |
| renderFrame | 控制渲染帧率 |
| createComponent | 在指定节点上创建新的组件 |
| getComponent | 获取对应的组件实例 |
| getDefaultRenderContext | 当前对象关联的渲染上下文 |
创建3D场景资源 SceneResourceFactory
| 名称 | 说明 |
|---|
| createCamera | 根据结点参数创建相机 |
| createLight | 根据结点参数和灯光类型创建灯光 |
| createNode | 创建结点 |
| createMaterial | 根据场景资源参数和材质类型创建材质 |
| createEnvironment | 根据场景资源参数创建环境 |
| createGeometry | 根据场景结点参数和网格数据创建几何对象 |
| createEffect | 根据特效参数创建特效对象 |
相机类型,Camera继承自Node
Node属性
| 名称 | 类型 | 说明 |
|---|
| position | Position3 | 结点位置 |
| rotation | Quaternion | 结点旋转角度 |
| scale | Scale3 | 结点缩放 |
| visible | boolean | 结点是否可见 |
| nodeType | NodeType | 结点类型 |
| layerMask | LayerMask | 结点的图层掩码 |
| path | string | 结点路径 |
| parent | Node | 结点的父结点 |
| children | Container | 结点的子结点 |
Camera属性
| 名称 | 说明 |
|---|
| fov | 视场,取值在0到π弧度之间 |
| nearPlane | 近平面,取值大于0 |
| farPlane | 远平面,取值大于nearPlane |
| enabled | 是否使能相机 |
| postProcess | 后处理设置 |
| effects | 应用于相机输出的后处理特效 |
| clearColor | 将渲染目标(render target)清空后的特定颜色 |
| renderingPipeline | 控制渲染管线 |
3D空间中旋转的数学结构 Quaternion(四元数)
用于表示3D空间中旋转的数学结构。与传统的欧拉角相比,四元数在数值稳定性和避免万向节锁方面具有优势。
四元数的形式是 (x, y, z, w),由1 个实部(w)+ 3 个虚部(x/y/z) 组成,核心对应 3D 旋转的两个关键信息:
x/y/z:表示旋转轴的方向(比如绕 Y 轴旋转时,x=0、z=0,y≠0);
w:表示绕这个轴旋转的角度(具体是 w = cos(θ/2),θ 是旋转的总角度,单位弧度)
旋转 = 绕 (x,y,z) 这个方向的轴,旋转 2×arccos(w) 度
![]()
实现源码
import { Scene, Camera, Node, SceneResourceFactory, Quaternion } from '@kit.ArkGraphics3D'; @Entry @ComponentV2 struct GSNodeTest { @Local sceneOpt: SceneOptions | null = null; @Local scene: Scene | null = null; @Local cam: Camera | null = null; @Local node: Node | null | undefined = null; @Local cameraZ: number = 10 @Local rotationX: number = 0 @Local rotationY: number = 0 @Local rotationZ: number = 0 aboutToAppear(): void { this.init(); } init(): void { if (this.scene == null) { // 加载场景资源,支持.gltf和.glb格式,路径和文件名可根据项目实际资源自定义 Scene.load($rawfile("glbs/car.glb")) .then(async (result: Scene) => { this.scene = result; let rf: SceneResourceFactory = this.scene.getResourceFactory(); // 创建相机 this.cam = await rf.createCamera({ "name": "Camera" }); // 设置合适的相机参数 this.cam.enabled = true; // 设置相机的位置 this.cam.position.z = this.cameraZ; this.sceneOpt = { scene: this.scene, modelType: ModelType.SURFACE } as SceneOptions; this.node = this.scene.root!.children.get(0) }).catch((error: Error) => { console.error('Scene load failed:', error); }); } } eulerToQuaternion(xDeg:number, yDeg:number, zDeg:number):Quaternion { // 步骤1:角度转弧度(Math.cos/sin要求弧度制) const xRad = xDeg * Math.PI / 180; // 绕X轴旋转弧度 const yRad = yDeg * Math.PI / 180; // 绕Y轴旋转弧度 const zRad = zDeg * Math.PI / 180; // 绕Z轴旋转弧度 // 步骤2:计算半角的正弦/余弦(简化公式) const cx = Math.cos(xRad / 2); const sx = Math.sin(xRad / 2); const cy = Math.cos(yRad / 2); const sy = Math.sin(yRad / 2); const cz = Math.cos(zRad / 2); const sz = Math.sin(zRad / 2); // 步骤3:按XYZ旋转顺序计算四元数(标准欧拉角转四元数公式) const w = cx * cy * cz + sx * sy * sz; const x = sx * cy * cz - cx * sy * sz; const y = cx * sy * cz + sx * cy * sz; const z = cx * cy * sz - sx * sy * cz; return { x, y, z, w }; } build() { Column() { Row() { Column() { if (this.sceneOpt) { // 通过Component3D呈现3D场景 Component3D(this.sceneOpt) } else { Text("Loading···") } }.width('100%') }.height('60%') Column() { Row({ space: 10 }) { Text('相机高度:' + this.cameraZ) Slider({ value: this.cameraZ, min: 1, max: 30, style: SliderStyle.OutSet }).width('50%') .onChange((value: number) => { this.cameraZ = value; this.cam!.position.z = value }) } Row({ space: 10 }) { Text('X轴旋转:' + this.rotationX) Slider({ value: this.rotationX, min: 0, max: 360, style: SliderStyle.OutSet }).width('50%') .onChange((value: number) => { this.rotationX = value; this.node!.rotation = this.eulerToQuaternion(this.rotationX,this.rotationY,this.rotationZ) }) } Row({ space: 10 }) { Text('Y轴旋转:' + this.rotationY) Slider({ value: this.rotationY, min: 0, max: 360, style: SliderStyle.OutSet }).width('50%') .onChange((value: number) => { this.rotationY = value; this.node!.rotation = this.eulerToQuaternion(this.rotationX,this.rotationY,this.rotationZ) }) } Row({ space: 10 }) { Text('Z轴旋转:' + this.rotationZ) Slider({ value: this.rotationZ, min: 0, max: 360, style: SliderStyle.OutSet }).width('50%') .onChange((value: number) => { this.rotationZ = value; this.node!.rotation = this.eulerToQuaternion(this.rotationX,this.rotationY,this.rotationZ) }) } } } } }