鸿蒙开发-想用高斯泼洒渲染3D场景?3DGS模型加载和特效
文章摘要 HarmonyOS SpatialReconKit 的 spatialRender 模块支持高效渲染 3DGS(3D Gaussian Splatting)模型,实现逼真的 3D 场景展示。使用流程包括:加载 GSPlugin 插件、导入 3D 场景、加载 3DGS 模型文件,并支持多种特效(如复古、漫画、黑白点阵等)。关键步骤为获取渲染上下文后调用 loadGSNode() 加载模型,
想在 HarmonyOS 里展示 3D 扫描结果?用 SpatialReconKit 的 3DGS 渲染就对了
你有没有想过,用手机扫描一个房间或者一个物体之后,能直接在 APP 里以 3D 的方式展示出来,而且还能加上复古、漫画这种酷炫的滤镜效果?这就是 HarmonyOS SpatialReconKit 中 spatialRender 模块干的事情。
简单说,spatialRender 模块就是用来渲染 3DGS(3D Gaussian Splatting)数据的。3DGS 是一种比较新的 3D 场景表示技术,它用一堆高斯点来表示 3D 场景,渲染速度快、效果好。SpatialReconKit 把这套技术封装好了,你只需要几行代码就能把 3DGS 模型加载到场景里,再加上各种视觉特效。
3DGS 渲染的整体流程
使用 spatialRender 模块进行 3DGS 渲染的完整流程如下:
先把插件加载上
在做任何 3DGS 相关的操作之前,你必须先加载 GSPlugin 插件。这一步特别重要,不加载的话后面所有的接口调用都会出问题,文档里也明确说了"调用 GSPlugin 接口前,必须先加载对应的插件 ID,否则会出现未定义的行为"。
import { spatialRender } from '@kit.SpatialReconKit';
import { Scene } from '@kit.ArkGraphics3D';
// 获取默认渲染上下文
let renderContext = Scene.getDefaultRenderContext();
if (renderContext != null) {
// 加载空间重建插件GSPlugin
renderContext.loadPlugin(spatialRender.GSPlugin.PLUGIN_ID);
}
这段代码做了两件事:第一步,通过 Scene.getDefaultRenderContext() 获取默认的渲染上下文(RenderContext),你可以把它理解为渲染的"画布管理器",所有的渲染操作都要通过它来进行;第二步,调用 renderContext.loadPlugin() 并传入 spatialRender.GSPlugin.PLUGIN_ID 这个常量来加载插件。PLUGIN_ID 的具体值是 1450021d-c57f-d9ff-7770-c24fb3f3321c,不过你不需要记这个,直接用常量就行。
为什么要单独加载插件?因为 3DGS 渲染是 SpatialReconKit 提供的扩展能力,不是 ArkGraphics 3D 的内置功能。通过插件机制,系统可以按需加载,避免不必要的资源开销。
加载一个 3DGS 模型到场景里
插件加载好之后,就可以把 3DGS 模型加载进来了。核心方法是 GSPlugin.loadGSNode(),它是一个异步方法,返回一个 Promise,resolve 之后得到一个 GSNode 对象。
import { Scene, RenderContext } from '@kit.ArkGraphics3D';
import { spatialRender } from '@kit.SpatialReconKit';
// 获取默认的渲染上下文
let renderContext: RenderContext | null = Scene.getDefaultRenderContext();
if (renderContext != null) {
renderContext.loadPlugin(spatialRender.GSPlugin.PLUGIN_ID);
// 加载场景
Scene.load().then(async (scene: Scene) => {
let uri = "OhosRawFile://assets/gltf/model.glb";
let offset = 0;
// 通过GSPlugin.loadGSNode获取GSNode实例
let gsNode: spatialRender.GSNode = await spatialRender.GSPlugin.loadGSNode(scene, { uri, offset }, scene.root);
// 设置节点位置
gsNode.position = { x: 3, y: 0, z: 0 };
// 设置节点缩放
gsNode.scale = { x: 1.5, y: 1.5, z: 1.5 };
// 设置节点可见性
gsNode.visible = true;
});
}
我们一步步来看。首先通过 Scene.load() 加载一个 3D 场景,这一步也是异步的。场景加载完成后,在回调里调用 spatialRender.GSPlugin.loadGSNode(),它接收三个参数:
scene:当前的场景对象,告诉系统要把模型加载到哪个场景里{ uri, offset }:一个 GSImportSettings 对象,uri是 3DGS 模型文件的路径,offset是数据在文件中的偏移量(通常设为 0)scene.root:父节点,表示要把这个模型挂载到场景的根节点下。如果不传这个参数,模型也会默认挂到根节点
loadGSNode 返回的 GSNode 继承自 Node(ArkGraphics 3D 的基础节点类),所以你可以像操作普通 3D 节点一样操作它——设置位置(position)、缩放(scale)、可见性(visible)等等。
GSImportSettings:告诉系统去哪里找模型
你注意到 loadGSNode 的第二个参数 { uri, offset } 了吗?它其实是一个 GSImportSettings 类型的对象,专门用来配置 3DGS 模型的加载参数。
import { Scene, RenderContext } from '@kit.ArkGraphics3D';
import { spatialRender } from '@kit.SpatialReconKit';
// 获取默认的渲染上下文
let renderContext: RenderContext | null = Scene.getDefaultRenderContext();
if (renderContext != null) {
renderContext.loadPlugin(spatialRender.GSPlugin.PLUGIN_ID);
// 加载场景
let scene = Scene.load().then(async (scene: Scene) => {
// 设置3DGS模型文件的URI路径,根据实际情况修改
let uri = "OhosRawFile://assets/gltf/doll.glb";
// 偏移量参数(用于模型的位移调整)
let offset = 0;
// 配置导入参数
let setting: spatialRender.GSImportSettings = { uri: uri, offset : offset};
// 通过GSPlugin加载指定的3DGS节点,并添加到场景根节点下
let gsNodeext: spatialRender.GSNode = await spatialRender.GSPlugin.loadGSNode(scene, setting, scene.root);
});
}
GSImportSettings 有两个属性:
uri(必填):3DGS 模型文件的路径。传空字符串会导致加载失败,所以一定要确保路径正确offset(可选,默认 0):数据在文件中的偏移量。一般情况下直接用 0 就行,除非你的模型数据嵌在某个大文件的特定位置
这里用了一个更清晰的写法,先创建 setting 对象再传入,效果和直接写 { uri, offset } 是一样的,只是可读性更好一些。
给 3DGS 场景加上复古效果
模型加载好了,光是原样展示可能不够酷。SpatialReconKit 内置了好几种视觉特效,先来看看复古效果(RetroEffect)。这个效果会模拟老式显像管电视的显示特征,带颜色抖动、下采样和屏幕曲率。
import { Scene, RenderContext, RenderingPipelineType, Effect } from '@kit.ArkGraphics3D';
import { spatialRender } from '@kit.SpatialReconKit';
// 获取默认渲染上下文
let renderContext: RenderContext | null = Scene.getDefaultRenderContext();
if (renderContext != null) {
// 加载空间重建插件GSPlugin
renderContext.loadPlugin(spatialRender.GSPlugin.PLUGIN_ID);
// 加载场景
Scene.load().then(async (scene: Scene) => {
// 获取场景的资源工厂
let rf = scene.getResourceFactory();
// 创建复古效果实例(使用GSPlugin预定义的效果ID)
let effect : Effect =
await rf.createEffect({ effectId: spatialRender.GSPlugin.RETRO_EFFECT_ID });
// 获取复古效果参数(颜色数量)的当前值
let colNum = effect.getPropertyValue(spatialRender.RetroEffectParams.COLOR_NUM);
// 设置复古效果参数(颜色数量)为4
let res = effect.setPropertyValue(spatialRender.RetroEffectParams.COLOR_NUM, 4);
// 创建摄像机
let camera = await rf.createCamera({ name: "gsCam", path: "//gsCam" }, { renderingPipeline: RenderingPipelineType.FORWARD });
// 将效果添加到摄像机的效果链中
camera.effects.append(effect)
});
}
这段代码做了好几件事,我们拆开来看:
- 获取资源工厂:
scene.getResourceFactory()返回一个资源工厂对象,所有的效果、摄像机等资源都通过它来创建 - 创建效果实例:调用
rf.createEffect()并传入spatialRender.GSPlugin.RETRO_EFFECT_ID作为效果 ID。这个 ID 是预定义的,你不需要记具体的值 - 调整效果参数:通过
getPropertyValue()和setPropertyValue()可以读取和修改效果的属性。这里把颜色数量(COLOR_NUM)设为 4,值越小复古风格越重 - 创建摄像机:摄像机决定了你从什么角度观察场景。这里创建了一个前向渲染管线的摄像机
- 把效果挂到摄像机上:
camera.effects.append(effect)将复古效果添加到摄像机的效果链中,这样通过这个摄像机看到的画面就会带有复古效果
复古效果有 4 个可调参数:
colorNum:颜色抖动用的颜色数量,值越大图像质量越高,值越小复古感越强,默认 8pixelSize:下采样程度,越大颗粒感越重,默认 4(设为 1 就不下采样)blendEnabled:是否把处理后的图和原图融合,设为 true 可以保持亮度和色彩,默认 truecurve:显像管屏幕的曲率,值越大弯曲越明显,默认 0.25
漫画效果:让你的 3D 场景变成漫画风
除了复古效果,还有漫画效果(ComicEffect)。它会检测图像中的轮廓线,然后用指定颜色的线条画出来,看起来就像漫画一样。
import { Scene, RenderContext, RenderingPipelineType, Effect } from '@kit.ArkGraphics3D';
import { spatialRender } from '@kit.SpatialReconKit';
// 获取默认渲染上下文
let renderContext: RenderContext | null = Scene.getDefaultRenderContext();
if (renderContext != null) {
// 加载空间重建插件GSPlugin
renderContext.loadPlugin(spatialRender.GSPlugin.PLUGIN_ID);
// 加载场景
Scene.load().then(async (scene: Scene) => {
// 获取场景的资源工厂
let rf = scene.getResourceFactory();
// 创建漫画效果实例(使用GSPlugin预定义的漫画效果ID)
let effect : Effect =
await rf.createEffect({ effectId: spatialRender.GSPlugin.COMIC_EFFECT_ID });
// 获取漫画效果参数(线条阈值)的当前值
let threshold = effect.getPropertyValue(spatialRender.ComicEffectParams.LINE_THRESHOLD);
// 设置漫画效果参数(线条阈值)为0.5
let res = effect.setPropertyValue(spatialRender.ComicEffectParams.LINE_THRESHOLD, 0.5);
// 创建摄像机
let camera = await rf.createCamera({ name: "gsCam", path: "//gsCam" }, { renderingPipeline: RenderingPipelineType.FORWARD });
// 将漫画效果添加到摄像机的效果链中
camera.effects.append(effect)
});
}
和复古效果的套路一模一样,只是效果 ID 换成了 COMIC_EFFECT_ID,参数换成了 ComicEffectParams。
漫画效果有两个参数:
lineThreshold:判定像素为轮廓线的阈值。图像梯度(你可以理解为相邻像素之间的明暗差异)大于这个值的像素就会被判定为轮廓线。值越小,画出来的线条越多;值越大,只保留最明显的轮廓。取值范围 [0, 1],默认 0.2lineColor:轮廓线的颜色,类型是 Color
如果你想要更强的漫画感,可以把 lineThreshold 调大一点,比如 0.5,这样只有最明显的轮廓线会被画出来,画面更简洁。
黑白点阵效果:ObraDinnEffect
这个效果的名字来源于游戏《Return of the Obra Dinn》,它会把画面处理成黑白点阵风格,很有艺术感。
import { Scene, RenderContext, RenderingPipelineType, Effect } from '@kit.ArkGraphics3D';
import { spatialRender } from '@kit.SpatialReconKit';
// 获取默认渲染上下文
let renderContext: RenderContext | null = Scene.getDefaultRenderContext();
if (renderContext != null) {
// 加载空间重建插件GSPlugin
renderContext.loadPlugin(spatialRender.GSPlugin.PLUGIN_ID);
// 加载场景
Scene.load().then(async (scene: Scene) => {
// 获取场景的资源工厂
let rf = scene.getResourceFactory();
// 创建黑白bit效果实例(使用GSPlugin预定义的效果ID)
let effect : Effect =
await rf.createEffect({ effectId: spatialRender.GSPlugin.OBRA_DINN_EFFECT_ID });
// 获取黑白bit效果参数(噪声强度)的当前值
let noiseStrength = effect.getPropertyValue(spatialRender.ObraDinnEffectParams.NOISE_STRENGTH);
// 设置黑白bit效果参数(噪声强度)为0.5
let res = effect.setPropertyValue(spatialRender.ObraDinnEffectParams.NOISE_STRENGTH, 0.5);
// 创建摄像机
let camera = await rf.createCamera({ name: "gsCam", path: "//gsCam" }, { renderingPipeline: RenderingPipelineType.FORWARD });
// 将黑白bit效果添加到摄像机的效果链中
camera.effects.append(effect)
});
}
ObraDinnEffect 有 4 个参数:
noiseStrength:噪声强度,用来决定哪些像素参与颜色抖动。加大噪声可以让边缘更模糊,起到平滑效果。取值范围 [0, 1],默认 0.3threshold:前景/后景的分界阈值。值越高,画面越偏向后景颜色;值越低,画面越偏向前景颜色。取值范围 [0, 1],默认 0.4foregroundColor:前景颜色backgroundColor:后景颜色
你可以通过调整前景色和后景色来实现不同的配色方案,不一定是纯黑白,也可以是深蓝配浅黄这种。
颜色编辑效果:精细调色
如果你不想用那种风格化很强的效果,只是想微调一下画面的色彩,ColorEditingEffect 就是你需要的。它提供了曝光、对比度、色温、色调、饱和度、自然饱和度这些专业调色参数。
import { Scene, RenderContext, RenderingPipelineType, Effect } from '@kit.ArkGraphics3D';
import { spatialRender } from '@kit.SpatialReconKit';
// 获取默认渲染上下文
let renderContext: RenderContext | null = Scene.getDefaultRenderContext();
if (renderContext != null) {
// 加载空间重建插件GSPlugin
renderContext.loadPlugin(spatialRender.GSPlugin.PLUGIN_ID);
// 加载场景
Scene.load().then(async (scene: Scene) => {
// 获取场景的资源工厂
let rf = scene.getResourceFactory();
// 创建颜色编辑效果实例(使用GSPlugin预定义的效果ID)
let effect : Effect =
await rf.createEffect({ effectId: spatialRender.GSPlugin.COLOR_EDITING_EFFECT_ID });
// 获取颜色编辑效果参数(曝光度)的当前值
let exposure = effect.getPropertyValue(spatialRender.ColorEditingEffectParams.EXPOSURE);
// 设置颜色编辑效果参数(曝光度)为0.5
let res = effect.setPropertyValue(spatialRender.ColorEditingEffectParams.EXPOSURE, 0.5);
// 创建摄像机
let camera = await rf.createCamera({ name: "gsCam", path: "//gsCam" }, { renderingPipeline: RenderingPipelineType.FORWARD });
// 将颜色编辑效果添加到摄像机的效果链中
camera.effects.append(effect)
});
}
ColorEditingEffect 的参数挺丰富的:
exposure:曝光度,推荐范围 [-5, 5],默认 0.0contrast:对比度,推荐范围 [0, 2],默认 1.0temperature:色温,负值偏冷(蓝),正值偏暖(黄),推荐范围 [-2, 2],默认 0.0tint:色调,推荐范围 [-1, 1],默认 0.0saturation:饱和度,推荐范围 [0, 2],默认 1.0vibrance:自然饱和度,推荐范围 [-1, 1],默认 0.0
和 Photoshop 里的调色面板差不多,你可以根据需要精细调整。
用强类型获取效果对象
前面的例子里,我们创建效果的时候用的是 Effect 类型。但如果你需要直接访问效果的属性(而不是通过 setPropertyValue),可以用 as 关键字把效果转成具体的类型,比如 RetroEffect、ComicEffect、ObraDinnEffect、ColorEditingEffect。
import { Scene, RenderContext, RenderingPipelineType } from '@kit.ArkGraphics3D';
import { spatialRender } from '@kit.SpatialReconKit';
// 获取默认渲染上下文
let renderContext: RenderContext | null = Scene.getDefaultRenderContext();
if (renderContext != null) {
// 加载空间重建插件GSPlugin
renderContext.loadPlugin(spatialRender.GSPlugin.PLUGIN_ID);
// 加载场景
Scene.load().then(async (scene: Scene) => {
// 获取场景的资源工厂
let rf = scene.getResourceFactory();
// 创建复古效果实例(类型为RetroEffect)
let effect : spatialRender.RetroEffect =
await rf.createEffect({ effectId: spatialRender.GSPlugin.RETRO_EFFECT_ID }) as spatialRender.RetroEffect;
// 创建摄像机
let camera = await rf.createCamera({ name: "gsCam", path: "//gsCam" }, { renderingPipeline: RenderingPipelineType.FORWARD });
// 将效果添加到摄像机的效果链中
camera.effects.append(effect)
});
}
关键区别在 as spatialRender.RetroEffect 这个类型断言。转成强类型之后,你就可以直接用 effect.colorNum、effect.pixelSize 这样的属性访问方式,不用再通过 getPropertyValue / setPropertyValue 来读写了。代码更简洁,也有更好的类型提示。
特效参数调节指南
不同特效有不同的可调参数,下面是各特效的参数对比:
把这些串起来
回顾一下完整的流程:加载插件 → 加载场景 → 加载 3DGS 模型 → 创建效果 → 把效果挂到摄像机。每一步都不能少,顺序也不能乱。
如果你要做一个空间扫描结果展示的 APP,基本流程就是:用户扫描完一个空间后,系统生成 3DGS 模型文件,你的 APP 加载这个文件并渲染出来,再根据用户的选择加上不同的视觉效果。spatialRender 模块把这些都封装好了,你不需要关心底层的渲染细节,只需要调用对应的 API 就行。
更多推荐



所有评论(0)