最近很火的Remy大家有沒有體驗,平面的2D圖片已經不能滿足用戶,未來可能會更多的相機支持拍攝3D照片。今天來了解一下鴻蒙的3D圖形展示。我找了個汽車的3D模型資源,看一下展示效果。由于能力有限,本文只實現(xiàn)修改相機旋轉角度。

演示.gif
ArkGraphics 3D(方舟3D圖形)基于輕量級的3D引擎以及渲染管線為開發(fā)者提供基礎3D場景繪制能力,供開發(fā)者便捷、高效地構建3D場景并完成渲染。
一個3D場景通常由光源、相機、模型三個關鍵部分組成。
光源:為整個3D場景提供光照,使得3D場景中的模型變得可見。與真實物理場景一致,沒有光源場景將變得一片漆黑,得到的渲染結果也就是全黑色。
相機:為3D場景提供一個觀察者。3D渲染本質上是從一個角度觀察3D場景并投影到2D圖片上。沒有相機就沒有3D場景的觀察者,也就不會得到渲染結果。
模型:3D場景中的模型用于描述對象的形狀、結構和外觀,一般具有網格、材質、紋理、動畫等屬性。一些常見的3D模型格式有OBJ、FBX、glTF等。
模型加載后,可以通過ArkUI的Component3D渲染組件呈現(xiàn)給用戶。
Component3D(sceneOptions?: SceneOptions)
Component3D組件配置選項 SceneOptions
| 名稱 | 說明 |
|---|---|
| scene | 3D模型資源文件 |
| modelType | 3D場景顯示合成方式 |
設置場景 Scene
屬性
| 名稱 | 說明 |
|---|---|
| environment | 環(huán)境對象 |
| animations | 動畫數(shù)組 |
| root | 3D場景樹根結點 |
方法
| 名稱 | 說明 |
|---|---|
| load | 待加載的模型文件資源路徑 |
| getNodeByPath | 通過路徑獲取結點 |
| getResourceFactory | 獲取場景資源工廠對象 |
| destroy | 銷毀場景 |
| importNode | 從其他場景導入結點 |
| importScene | 導入其他場景 |
| renderFrame | 控制渲染幀率 |
| createComponent | 在指定節(jié)點上創(chuàng)建新的組件 |
| getComponent | 獲取對應的組件實例 |
| getDefaultRenderContext | 當前對象關聯(lián)的渲染上下文 |
創(chuàng)建3D場景資源 SceneResourceFactory
| 名稱 | 說明 |
|---|---|
| createCamera | 根據(jù)結點參數(shù)創(chuàng)建相機 |
| createLight | 根據(jù)結點參數(shù)和燈光類型創(chuàng)建燈光 |
| createNode | 創(chuàng)建結點 |
| createMaterial | 根據(jù)場景資源參數(shù)和材質類型創(chuàng)建材質 |
| createEnvironment | 根據(jù)場景資源參數(shù)創(chuàng)建環(huán)境 |
| createGeometry | 根據(jù)場景結點參數(shù)和網格數(shù)據(jù)創(chuàng)建幾何對象 |
| createEffect | 根據(jù)特效參數(shù)創(chuàng)建特效對象 |
相機類型,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空間中旋轉的數(shù)學結構 Quaternion(四元數(shù))
用于表示3D空間中旋轉的數(shù)學結構。與傳統(tǒng)的歐拉角相比,四元數(shù)在數(shù)值穩(wěn)定性和避免萬向節(jié)鎖方面具有優(yōu)勢。
四元數(shù)的形式是 (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
實現(xiàn)源碼
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格式,路徑和文件名可根據(jù)項目實際資源自定義
Scene.load($rawfile("glbs/car.glb"))
.then(async (result: Scene) => {
this.scene = result;
let rf: SceneResourceFactory = this.scene.getResourceFactory();
// 創(chuàng)建相機
this.cam = await rf.createCamera({ "name": "Camera" });
// 設置合適的相機參數(shù)
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旋轉順序計算四元數(shù)(標準歐拉角轉四元數(shù)公式)
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呈現(xiàn)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)
})
}
}
}
}
}