传统 3D 模型用三角网格(Mesh)描述表面,而 3D Gaussian Splatting(3DGS) 用大量高斯椭球体「泼溅」出场景,能在浏览器里呈现照片级真实感,且文件往往比同等质量的 Mesh 更紧凑。
本项目是一个 最小可运行的 3D 入门 Demo:
- 技术栈:Vite 6 + TypeScript +
@manycore/aholo-viewer - 目标:从远程加载
.sog格式的小熊模型,在 500×500 画布中实时渲染 - 代码量:约 80 行 TypeScript,无框架依赖
整体流程可以概括为:
创建容器 → 初始化 Viewer → 加载 .sog 模型 → 解析为 Splat → 加入场景 → 配置相机与渲染管线 → 渲染循环
运行效果:

二、技术选型
| 组件 | 选择 | 作用 |
|---|---|---|
| 渲染引擎 | @manycore/aholo-viewer |
支持 Mesh 与 3DGS 的 Web 渲染器 |
| 构建工具 | Vite 6 | 开发服务器、TypeScript 转译 |
| 语言 | TypeScript | 类型提示与工程化 |
| 模型格式 | .sog |
Aholo 优化的 3DGS 格式,平衡性能与画质 |
Aholo Viewer 的核心概念(来自官方文档):
- Scene:场景树,通过
add()挂载对象 - Splat:3DGS 的可渲染对象,不能直接
new,需通过 Loader 创建 - PerspectiveCamera:透视相机,模拟人眼「近大远小」
- Pipeline Config:背景、Splatting、TAA 等渲染管线配置
三、项目结构
3D-learn/
└── project/
└── aholo-first/
├── index.html # 页面入口
├── index.ts # 核心 3D 逻辑
├── vite.config.ts # Vite 配置(排除预构建)
├── package.json # 依赖与 Rollup WASM 覆盖
└── tsconfig.json
依赖极简:运行时只有 @manycore/aholo-viewer,开发时加 Vite 和 TypeScript。
四、核心实现拆解
4.1 创建渲染容器
javascript
const container = document.createElement('div');
container.style.width = '500px';
container.style.height = '500px';
document.body.appendChild(container);
Viewer 需要挂载到一个 DOM 容器;宽高决定画布尺寸。
4.2 初始化 Viewer 与相机
javascript
const viewer = createViewer('example-viewer', container, {});
const camera = new PerspectiveCamera(60, 1, 0.1, 2000);
createViewer:创建渲染实例,绑定容器PerspectiveCamera(fov, aspect, near, far):60° 视场角,近平面 0.1,远平面 2000
4.3 加载并解析 3DGS 模型
javascript
const SPLAT_URL = 'https://holo-cos.aholo3d.cn/aholo-opensource/gs_file/bear/bear.3d71a266.sog';
const resp = await fetch(SPLAT_URL);
const buffer = await resp.arrayBuffer();
const data = await SplatLoader.parseSplatData(
SplatLoader.SplatFileType.SOG,
new Uint8Array(buffer),
SplatLoader.SplatPackType.Compressed,
);
const splat = await SplatUtils.createSplat(data);
三步链路:
- fetch 拉取远程
.sog二进制 - parseSplatData 解析(SOG 格式 + Compressed 打包),内部会用到 Web Worker(
splat-worker.js) - createSplat 生成可渲染的
Splat对象
4.4 相机坐标系:一个容易踩坑的细节
javascript
// 模型使用 OpenCV 坐标系,-Y 向上
camera.up.set(0, -1, 0);
camera.position.set(-1.5, -0.5, 0);
camera.lookAt(new Vector3(0, 0, 0));
3DGS 模型常用 OpenCV 坐标(Y 轴向下),而 WebGL 默认 Y 轴向上。这里把 camera.up 设为 (0, -1, 0),并调整位置与朝向,才能正确看到小熊。
4.5 组装场景与渲染管线
javascript
viewer.getScene().add(splat);
viewer.setCamera(camera);
setViewerConfig(viewer, {
pipeline: {
Background: { /* 黑色背景,关闭地面 */ },
Splatting: { /* 开启高斯泼溅,预计算、合成等 */ },
TAA: { enabled: false },
},
});
- Background:纯黑背景,关闭地面
- Splatting:核心 3DGS 渲染,含预计算、模糊、合成等
- TAA:时间抗锯齿,Demo 中关闭以降低开销
4.6 渲染循环
javascript
function render() {
viewer.render();
}
viewer.requestRenderHandler = function () {
requestAnimationFrame(render);
};
requestAnimationFrame(render);
按需渲染:Viewer 需要更新时通过 requestRenderHandler 触发下一帧,避免空转。
五、踩过的两个坑
5.1 Rollup 原生模块被 Windows 策略拦截
现象:
Error: Cannot find module @rollup/rollup-win32-x64-msvc
Application Control policy has blocked this file
原因: Vite 依赖 Rollup 的 .node 原生二进制,部分 Windows 环境会拦截。
解法: 在 package.json 中强制使用 WASM 版 Rollup:
javascript
"overrides": {
"vite": {
"rollup": "npm:@rollup/wasm-node@^4.34.9"
}
}
5.2 splat-worker.js 路径失效
现象:
The file does not exist at ".../node_modules/.vite/deps/splat-worker.js"
原因: Aholo Viewer 用 new URL("./splat-worker.js", import.meta.url) 加载 Worker。Vite 预构建会把包打进 .vite/deps/,import.meta.url 变了,相对路径就找不到 Worker。
解法: 在 vite.config.ts 中排除预构建:
javascript
export default defineConfig({
optimizeDeps: {
exclude: ['@manycore/aholo-viewer'],
},
});
修改后需清除 node_modules/.vite 缓存再启动。
六、运行方式
cd 3D-learn/project/aholo-first
npm install
npm run dev
浏览器访问 http://localhost:5173/,即可看到 3D 高斯泼溅小熊。