最近很火的Remy大家有没有体验,平面的2D图片已经不能满足用户,未来可能会更多的相机支持拍摄3D照片。今天来了解一下鸿蒙的3D图形展示。我找了个汽车的3D模型资源,看一下展示效果。由于能力有限,本文只实现修改相机旋转角度。

演示.gif
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<T> | 结点的子结点 |
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) 度

坐标系.png
实现源码
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)
})
}
}
}
}
}