鸿蒙开发-想加载3D场景?Scene场景的创建和模型加载
本文介绍了在 HarmonyOS 上使用 ArkGraphics 3D 模块创建 3D 场景的两种主要方法:直接加载 glTF/glb 模型文件和从零构建空场景。文章首先说明了 3D 场景管理的基本概念,然后详细讲解了如何通过 Scene.load() 方法加载模型资源,包括从应用内部资源和沙盒目录加载的不同方式。接着介绍了如何利用 RenderResourceFactory 创建空场景,并自行添
想在 HarmonyOS 上做个 3D 模型查看器?先搞懂 Scene 场景管理
你有没有想过,在手机上做一个 3D 模型查看器是什么感觉?用户点一下就能旋转、缩放一个 3D 物体,就像在看一个真实的雕塑一样。听起来很高大上,但其实 HarmonyOS 的 ArkGraphics 3D 模块已经帮你把底层渲染都封装好了,你只需要学会"搭场景"就行。
这篇文章就来聊聊最基础的部分——Scene 场景管理。简单说就是:怎么创建一个 3D 场景,怎么把 glTF 模型加载进去,怎么获取渲染上下文来管理资源。
下面是 Scene 场景创建的两种方式对比:
先搞清楚整体架构
在 ArkGraphics 3D 里,所有 3D 内容都是围绕"场景(Scene)"来组织的。你可以把 Scene 想象成一个舞台,上面有各种演员(节点)、道具(资源)、灯光(Light)、摄影机(Camera)。而你要做的第一步,就是把这个舞台搭起来。
核心的模块是 @kit.ArkGraphics3D,你需要从里面导入各种类型:
import { SceneResourceParameters, SceneNodeParameters, RaycastResult, RaycastParameters,
RenderResourceFactory, SceneResourceFactory, SceneComponent, RenderContext, RenderConfiguration,
RenderParameters, Scene } from '@kit.ArkGraphics3D';
别被这一长串吓到,我们一个一个来认识。
第一步:加载一个 glTF 模型
最简单的起步方式就是用 Scene.load() 把一个现成的 3D 模型加载进来。ArkGraphics 3D 支持 .gltf 和 .glb 两种格式,glb 是把所有数据打包成一个文件,用起来更方便。
import { Scene } from '@kit.ArkGraphics3D';
function loadModel(): void {
// 加载场景资源,支持.gltf和.glb格式,路径和文件名可根据项目实际资源自定义
let scene: Promise<Scene> = Scene.load($rawfile("gltf/CubeWithFloor/glTF/AnimatedCube.glb"));
scene.then((result: Scene) => {
console.info("Scene loaded, root node: " + result.root?.name);
});
}
这里 $rawfile() 是 HarmonyOS 提供的 rawfile 资源访问方式,你把 .glb 文件放在项目的 resources/rawfile/ 目录下,就能用相对路径加载。Scene.load() 返回的是一个 Promise,因为加载 3D 模型是个异步操作——模型文件可能很大,需要时间解析。
加载成功后,result 就是一个 Scene 对象了。你可以通过 result.root 拿到整个场景的根节点,通过 result.root?.name 看到根节点叫什么名字。
从应用沙盒目录加载
有时候你的模型文件不是打包在应用里的,而是用户下载的,或者从网络上缓存下来的。这时候你需要用绝对路径加载:
import { common } from '@kit.AbilityKit';
import { fileIo } from '@kit.CoreFileKit';
import { Scene } from '@kit.ArkGraphics3D';
async function loadModelFromAbsolutePath(context: common.UIAbilityContext): Promise<void> {
// 获取应用沙盒目录(Scene.load仅能读取应用自身写入的文件,不能读取hdc/adb push写入的文件)
const appCtx = context.getApplicationContext();
const filesDir = appCtx.filesDir; // /data/storage/el2/base/files
// 从rawfile读取模型内容(实际使用中也可以替换为其他来源的数据)
// 使用.glb文件更易于复制加载;若为.gltf,请将其.bin和贴图文件一并复制到同一目录
const src = 'gltf/CubeWithFloor/glTF/AnimatedCube.glb';
const load_uri = `${filesDir}/AnimatedCube.glb`;
// 写入模型文件到应用沙盒目录,生成可被Scene.load(绝对路径)访问的实际文件
const rawData = await context.resourceManager.getRawFileContent(src);
const file = fileIo.openSync(load_uri, fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC | fileIo.OpenMode.WRITE_ONLY);
fileIo.writeSync(file.fd, rawData.buffer.slice(rawData.byteOffset, rawData.byteOffset + rawData.byteLength));
fileIo.closeSync(file);
// 使用绝对路径加载模型
Scene.load(load_uri).then((scene: Scene) => {
// 加载成功后的逻辑处理
}).catch((error: string) => {
console.error('Scene load failed: ' + error);
});
}
这段代码做了这么几件事:先从 rawfile 读出模型的二进制数据,然后写到应用沙盒目录里,最后用绝对路径去加载。注意有个坑:Scene.load 只能读取应用自己写入的文件,你用 hdc push 进去的文件是读不到的,必须通过代码写入才行。
另外如果你的模型是 .gltf 格式(不是 .glb),记得把 .bin 文件和贴图文件一起复制到同一个目录下,不然模型会加载失败。
用工厂模式创建空场景
除了加载现成的模型,你还可以从零开始创建一个空场景,然后自己往里面添加相机、灯光、节点。这就需要用到 RenderResourceFactory 和 SceneResourceFactory 了。
先来看怎么获取渲染上下文和工厂:
import { Scene, RenderContext, RenderResourceFactory } from '@kit.ArkGraphics3D';
// fromFile=true:从指定glb文件加载场景,fromFile=false:创建一个空场景,此参数是为了示例展示两种常见场景创建方式
function createScenePromise(fromFile: boolean = false): Promise<Scene> {
const renderContext: RenderContext | null = Scene.getDefaultRenderContext();
if (!renderContext) {
return Promise.reject(new Error("RenderContext is null"));
}
const renderResourceFactory: RenderResourceFactory = renderContext.getRenderResourceFactory();
if (fromFile) {
// 创建场景并加载.gltf或.glb文件作为初始内容,路径和名称可根据项目实际资源自定义
return renderResourceFactory.createScene($rawfile("gltf/CubeWithFloor/glTF/AnimatedCube.glb"));
} else {
// 创建空场景
return renderResourceFactory.createScene();
}
}
这里有两个关键角色:
- RenderContext:渲染上下文,你可以理解为 GPU 的"连接通道"。所有在同一个 RenderContext 下创建的场景,可以共享渲染资源(比如纹理、着色器)。
Scene.getDefaultRenderContext()拿到的是默认的全局上下文。 - RenderResourceFactory:渲染资源工厂,通过它你可以创建场景(
createScene)、着色器(createShader)、图像(createImage)、网格(createMesh)等各种资源。
当 createScene() 不传参数时,创建的是一个空场景——一个光秃秃的舞台,什么都没有。你需要自己往里面加东西。
Scene 对象有哪些能力?
Scene 对象提供了丰富的场景管理能力:
加载或创建好 Scene 之后,来看看它能干什么。
属性一览
Scene 对象有几个重要的属性:
- root:场景树的根节点,类型是
Node | null。整个 3D 场景是一棵树,所有东西(相机、灯光、模型)都是这棵树上的节点。 - animations:动画数组,如果 glTF 模型里带有动画数据,会自动解析到这里。
- environment:环境对象,控制场景的背景、光照等。
- renderConfiguration:渲染配置,比如阴影贴图分辨率等。
通过路径查找节点
场景加载后,里面的节点可能很多,你需要一种方式来定位到特定的节点。getNodeByPath 就是干这个的:
import { Scene, Node } from '@kit.ArkGraphics3D';
function getNode(): void {
// 加载场景资源,支持.gltf和.glb格式,路径和文件名可根据项目实际资源自定义
let scene: Promise<Scene> = Scene.load($rawfile("gltf/CubeWithFloor/glTF/AnimatedCube.glb"));
scene.then(async (result: Scene) => {
if (result) {
// 寻找指定路径的节点
let node : Node | null = result.getNodeByPath("rootNode_");
}
});
}
路径用 / 分隔层级,比如 "rootNode_/Unnamed Node 1/AnimatedCube" 就是沿着节点树一层层往下找。你还可以传第二个参数 NodeType 来限定你期望找到的节点类型,如果类型不匹配就返回 null。
获取资源工厂
要往场景里添加新的元素(相机、灯光、材质等),你需要通过 SceneResourceFactory:
import { SceneResourceFactory, Scene } from '@kit.ArkGraphics3D';
function getFactory(): void {
// 加载场景资源,支持.gltf和.glb格式,路径和文件名可根据项目实际资源自定义
let scene: Promise<Scene> = Scene.load($rawfile("gltf/CubeWithFloor/glTF/AnimatedCube.glb"));
scene.then(async (result: Scene) => {
if (result) {
// 获得SceneResourceFactory对象
let sceneFactory: SceneResourceFactory = result.getResourceFactory();
}
});
}
SceneResourceFactory 继承自 RenderResourceFactory,所以它不仅能创建渲染资源(着色器、图像、网格),还能创建场景元素(相机、灯光、节点、材质、环境等)。后面几篇文章会详细讲这些。
按需渲染
默认情况下,3D 场景是每帧都在渲染的。但有些场景你可能不需要这么高的刷新率——比如一个静态的产品展示页面,用户不操作的时候画面根本不变。这时候你可以用 renderFrame 来手动控制渲染:
import { Scene } from '@kit.ArkGraphics3D';
function RenderFrameTest() {
// 加载场景资源,支持.gltf和.glb格式,路径和文件名可根据项目实际资源自定义
Scene.load($rawfile("gltf/DamagedHelmet/glTF/DamagedHelmet.glb"))
.then(async (result: Scene | undefined) => {
if (!result) {
return;
}
console.info("TEST RenderFrameTest");
result.renderFrame({ alwaysRender: true });
});
}
RenderParameters 里有个 alwaysRender 属性:设为 true 就是每帧都渲染(默认行为),设为 false 就是按需渲染——只在你调用 renderFrame() 的时候才画一帧。这对于省电和降低 GPU 负载很有帮助。
从其他场景导入节点
有时候你会遇到这样的需求:我有两个 glTF 模型,想把它们合并到一个场景里。ArkGraphics 3D 提供了 importNode 和 importScene 两个方法来解决这个问题。
import { Scene } from '@kit.ArkGraphics3D';
function ImportNodeTest() {
Scene.load().then(async (result: Scene | undefined) => {
if (!result) {
return;
}
// 加载场景资源,支持.gltf和.glb格式,路径和文件名可根据项目实际资源自定义
Scene.load($rawfile("gltf/AnimatedCube/glTF/AnimatedCube.glb"))
.then(async (extScene: Scene) => {
let extNode = extScene.getNodeByPath("rootNode_/Unnamed Node 1/AnimatedCube");
console.info("TEST ImportNodeTest");
let node = result.importNode("scene", extNode, result.root);
if (node) {
node.position.x = 5;
}
});
});
}
importNode 会把一个节点从源场景"搬"到目标场景里,搬完之后你就可以像操作普通节点一样修改它的位置、旋转、缩放了。如果你想把整个场景都导进来,可以用 importScene:
import { Scene } from '@kit.ArkGraphics3D';
function ImportSceneTest() {
Scene.load().then(async (result: Scene | undefined) => {
if (!result) {
return;
}
// 加载场景资源,支持.gltf和.glb格式,路径和文件名可根据项目实际资源自定义
let content = await result.getResourceFactory().createScene($rawfile("gltf/DamagedHelmet/glTF/DamagedHelmet.glb"))
console.info("TEST ImportSceneTest");
result.importScene("helmet", content, null);
});
}
这个功能在做虚拟展厅之类的应用时特别有用——你可以把每个展品做成独立的 glTF 文件,然后在运行时动态导入到主场景里。
克隆节点
如果你需要在场景里放多个一样的物体(比如一排相同的椅子),不需要加载多次同一个模型,直接克隆节点就行:
import { Scene, Node } from '@kit.ArkGraphics3D';
function CloneNode() {
// 加载场景资源,支持.gltf和.glb格式,路径和文件名可根据项目实际资源自定义
Scene.load($rawfile("gltf/CubeWithFloor/glTF/AnimatedCube.gltf"))
.then(async (result: Scene) => {
let node = result.getNodeByPath("rootNode_/Unnamed Node 1/AnimatedCube") as Node;
let parent = result.root as Node;
let name = "cloneNode_";
let clone = result.cloneNode(node, parent, name);
if (clone) {
console.info("cloneNode success");
} else {
console.error("cloneNode failed");
}
});
}
cloneNode 有三个参数:要克隆的节点、克隆后挂到哪个父节点下、克隆节点的名字。注意克隆只能在同一个场景内进行,不支持跨场景克隆。
销毁场景
当你不再需要某个 3D 场景时(比如用户退出了查看页面),记得调用 destroy() 释放资源。3D 场景占用的内存和 GPU 资源都不少,不释放的话会越积越多:
import { Scene } from '@kit.ArkGraphics3D';
function destroy(): void {
// 加载场景资源,支持.gltf和.glb格式,路径和文件名可根据项目实际资源自定义
let scene: Promise<Scene> = Scene.load($rawfile("gltf/CubeWithFloor/glTF/AnimatedCube.glb"));
scene.then(async (result: Scene) => {
if (result) {
// 销毁scene
result.destroy();
}
});
}
渲染上下文的高级用法
除了默认的渲染上下文,你还可以用它来做一些高级操作。
加载插件
如果你需要扩展 3D 引擎的能力(比如自定义后处理效果),可以通过 loadPlugin 加载插件:
import { Scene, RenderContext } from '@kit.ArkGraphics3D';
function loadPlugin(): Promise<boolean> {
const renderContext: RenderContext | null = Scene.getDefaultRenderContext();
if (!renderContext) {
console.error("RenderContext is null");
return Promise.reject(new Error("RenderContext is null"));
}
return renderContext.loadPlugin("pluginName");
}
注册资源路径
当你使用自定义着色器时,着色器文件内部可能会引用其他资源文件(比如纹理图片)。这时候你需要告诉引擎这些资源在哪里:
import { Scene, RenderContext } from '@kit.ArkGraphics3D';
function registerResourcePath(): void {
// 创建shader资源,路径和文件名可根据项目实际资源自定义
Scene.load($rawfile("shaders/custom_shader/custom_material_sample.shader"))
.then(() => {
const renderContext: RenderContext | null = Scene.getDefaultRenderContext();
if (!renderContext) {
console.error("RenderContext is null");
return false;
}
// 注册路径检索名"myproto"及其对应的资产路径目录"OhosRawFile://shaders/custom_shader/"
// 当shader内部通过检索名引用关联文件,如路径为"myproto://textures/base.png",
// 系统会将"myproto://"替换为"OhosRawFile://shaders/custom_shader/",
// 最终从"OhosRawFile://shaders/custom_shader/textures/base.png"加载关联文件
return renderContext.registerResourcePath("myproto", "OhosRawFile://shaders/custom_shader/");
})
.then(result => {
if (result) {
console.info("resource path registration success");
} else {
console.error("resource path registration failed");
}
});
}
这里 registerResourcePath 的第一个参数是"检索名"(就像一个协议前缀),第二个参数是实际的文件目录。当着色器内部写着 myproto://textures/base.png 时,引擎会自动把 myproto:// 替换成 OhosRawFile://shaders/custom_shader/,最终从正确的位置加载文件。
小结
Scene 是 ArkGraphics 3D 的入口和核心。掌握了 Scene 的创建和加载,你就搭好了 3D 开发的第一块积木。关键要点:
Scene.load()是最常用的方式,直接加载 glTF/glb 模型- 通过
getDefaultRenderContext()和getResourceFactory()可以获取工厂来创建各种资源 - 场景是一棵节点树,用
getNodeByPath定位节点 - 别忘了用完之后
destroy()释放资源
下一篇文章我们来看看这棵节点树上的节点们——怎么移动、旋转、缩放它们,怎么组织父子关系。
更多推荐

所有评论(0)