最近在做 Three.js 场景时,尝试把高斯泼溅模型接入到普通的 WebGL 三维场景中。相比传统的 glTF、OBJ、FBX 这类网格模型,高斯泼溅更适合表达真实扫描场景:它不是由三角面组成,而是由大量带颜色、透明度、方向和尺度的 3D Gaussian 点元组成。
本文会基于一个完整的 Demo,介绍如何使用 Three.js + SparkJS 加载 .ksplat 文件,并把它纳入 Three.js 的相机、控制器、环境贴图、坐标轴、变换控件和包围盒调试体系中。

如果你更关注快速落地,而不是从零开发完整的三维编辑、拖拽、场景管理和发布能力,也可以看看低代码可视化三维搭建平台 Meteor3D。它提供拖拉拽式的三维场景搭建能力,用户可以快速基于高斯泼溅模型组织真实场景,再结合其他三维资产、标注和交互能力完成可视化应用搭建。

最终效果包括:
- 使用 SparkJS 加载并渲染
.ksplat高斯泼溅模型 - 通过 OrbitControls 旋转、平移、缩放观察模型
- 通过 TransformControls 拖拽移动或旋转模型
- 加载 HDR 作为背景和环境光照
- 显示模型包围盒,辅助定位高斯模型
- 支持 UI 输入动态缩放高斯模型
1. 高斯泼溅和传统模型有什么不同
传统 Three.js 模型通常是网格结构:
- 顶点
- 三角面
- 法线
- UV
- 材质贴图
高斯泼溅模型则不是这样。它更像是一批空间中的"椭球点云",每个点元都携带颜色、透明度、旋转和缩放信息。渲染时,这些高斯点会根据相机视角投影到屏幕上,经过排序、混合之后形成连续的真实感画面。
这也意味着,高斯泼溅模型在 Three.js 中不能简单地当作普通 Mesh 来处理。我们需要一个专门的渲染器来完成高斯点的排序、投影和混合,这里用到的是 SparkJS。
2. 依赖引入
Demo 使用了 importmap 来管理浏览器端 ES Module:
html
<script type="importmap">
{
"imports": {
"three": "./three184/build/three.module.js",
"three/addons/": "./three184/examples/jsm/",
"@sparkjsdev/spark": "https://sparkjs.dev/releases/spark/2.1.0/spark.module.js"
}
}
</script>
这里有三类依赖:
three:Three.js 核心库three/addons/:OrbitControls、TransformControls、RGBELoader 等扩展模块@sparkjsdev/spark:高斯泼溅渲染相关能力
进入业务代码后,核心 import 如下:
js
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { TransformControls } from 'three/addons/controls/TransformControls.js';
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
import { SparkRenderer, SplatMesh } from '@sparkjsdev/spark';
其中最关键的是 SparkRenderer 和 SplatMesh。
SparkRenderer:负责把 SparkJS 的高斯泼溅渲染流程接入 Three.js rendererSplatMesh:表示一个高斯泼溅模型对象,可以添加到 Three.js 场景树中
3. 创建 Three.js 基础场景
基础场景部分和普通 Three.js 项目基本一致:
js
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
box.clientWidth / box.clientHeight,
0.1,
100000
);
camera.position.set(5, 5, 5);
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
logarithmicDepthBuffer: true
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(box.clientWidth, box.clientHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;
box.appendChild(renderer.domElement);
这里有一个值得注意的配置:
js
logarithmicDepthBuffer: true
高斯泼溅场景有时会和大尺度三维场景结合,比如 GIS、建筑扫描、城市级数据等。开启对数深度缓冲可以缓解远近裁剪范围过大时的深度精度问题,减少闪烁和穿插。
相机远裁剪面设置到了 100000,也是为了给后续接入更大场景预留空间。
4. OrbitControls:让用户自由观察
观察控制器直接挂到相机和 canvas 上:
js
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
开启 enableDamping 后,相机控制会带一点惯性,交互体验更自然。不过要注意,开启 damping 后必须在每一帧调用:
js
controls.update();
所以动画循环中会写成:
js
renderer.setAnimationLoop(() => {
controls.update();
renderer.render(scene, camera);
});
5. 接入 SparkRenderer
高斯泼溅真正接入 Three.js 的关键代码是这两行:
js
const spark = new SparkRenderer({ renderer });
scene.add(spark);
SparkRenderer 接收已有的 Three.js WebGLRenderer,然后作为一个对象添加到 Three.js 场景里。这样做的好处是,高斯泼溅不需要脱离 Three.js 单独维护一套渲染上下文,而是可以和原来的场景、相机、控制器一起工作。
这也是 Three.js 集成高斯泼溅时比较舒服的一点:我们仍然可以使用熟悉的场景树组织方式。
6. 加载 .ksplat 高斯模型
模型路径定义如下:
js
const splatUrl = './assets/deskFlower.ksplat';
然后创建 SplatMesh:
js
const deskFlower = new SplatMesh({
url: splatUrl
});
为了方便整体控制,Demo 没有直接把 deskFlower 加到场景,而是先创建了一个 Group:
js
const splatGroup = new THREE.Group();
splatGroup.position.set(2, 2, -3);
scene.add(splatGroup);
deskFlower.quaternion.set(0, 0, -1, 0);
splatGroup.add(deskFlower);
这个 Group 非常重要。后面 TransformControls 实际 attach 的是 splatGroup,而不是直接 attach deskFlower。
这样做有几个好处:
- 可以把高斯模型的内部姿态修正和外部场景变换分开
- 方便统一移动、旋转、缩放整个高斯模型
- 后续如果一个高斯对象外面还要挂包围盒、标注、辅助点,也更容易组织
这里的:
js
deskFlower.quaternion.set(0, 0, -1, 0);
是对模型朝向做一次修正。不同来源的高斯数据坐标系可能不一样,比如 Z-up、Y-up、左右手坐标系差异等,实际接入时经常需要做一次旋转校正。
7. 等待模型初始化完成
SplatMesh 的加载是异步的。只有等 initialized 完成后,才能安全获取包围盒、绑定变换控件等:
js
deskFlower.initialized.then(() => {
const boundingBox = deskFlower.getBoundingBox(false);
centerPivotOnBoundingBox(splatGroup, deskFlower, boundingBox);
const boundingBoxHelper = createBoundingBoxHelper(boundingBox, 0xffff00);
deskFlower.add(boundingBoxHelper);
transformControls.attach(splatGroup);
statusEl.innerText = '加载完成';
statusEl.style.color = '#00ff00';
}).catch((err) => {
statusEl.innerText = '加载失败 (请检查控制台)';
statusEl.style.color = '#ff0000';
console.error('加载出错:', err);
alert('加载失败,请确认 .ksplat 路径正确,并使用本地服务器访问页面。');
});
这里做了几件事:
- 获取高斯模型包围盒
- 根据包围盒重新调整 pivot
- 创建黄色线框包围盒
- 把 TransformControls 绑定到
splatGroup - 更新加载状态
如果加载失败,页面会提示检查 .ksplat 路径,并提醒使用本地服务器访问。因为 ES Module、HDR、模型文件加载都涉及浏览器安全策略,直接双击 HTML 文件经常会遇到跨域或路径问题。
8. 为什么要重新调整模型 pivot
这是整个 Demo 中比较关键的细节。
很多扫描模型的原点并不在物体中心。如果直接旋转,会出现"绕远处某个点转圈"的现象;如果用 TransformControls 操作,也会感觉坐标轴位置不对。
Demo 通过包围盒中心重新修正 Group 和 Mesh 的相对位置:
js
function centerPivotOnBoundingBox(group, mesh, box) {
const localCenter = box.getCenter(new THREE.Vector3());
const groupCenterOffset = localCenter.clone().applyQuaternion(mesh.quaternion);
group.position.add(groupCenterOffset);
mesh.position.sub(groupCenterOffset);
splatBasePosition.copy(mesh.position);
}
这段逻辑的思路是:
- 先拿到模型本地包围盒中心
- 根据模型自身 quaternion 把中心偏移转换到当前方向下
- 把 Group 移动到这个中心位置
- 再把 Mesh 反向移动相同偏移
这样从视觉结果看,模型位置没有发生突变;但从场景层级看,splatGroup 的原点已经被移动到了模型中心附近。
最终效果就是:
- TransformControls 的坐标轴出现在模型中心
- 旋转模型时围绕自身中心旋转
- 后续缩放时也更符合直觉
9. 绘制高斯模型包围盒
由于高斯泼溅本身不是传统三角网格,调试时很难一眼看出它的空间范围。Demo 使用 LineSegments 手动创建了一个包围盒:
js
function createBoundingBoxHelper(box, color) {
const { min, max } = box;
const vertices = [
min.x, min.y, min.z, max.x, min.y, min.z,
max.x, min.y, min.z, max.x, max.y, min.z,
max.x, max.y, min.z, min.x, max.y, min.z,
min.x, max.y, min.z, min.x, min.y, min.z,
min.x, min.y, max.z, max.x, min.y, max.z,
max.x, min.y, max.z, max.x, max.y, max.z,
max.x, max.y, max.z, min.x, max.y, max.z,
min.x, max.y, max.z, min.x, min.y, max.z,
min.x, min.y, min.z, min.x, min.y, max.z,
max.x, min.y, min.z, max.x, min.y, max.z,
max.x, max.y, min.z, max.x, max.y, max.z,
min.x, max.y, min.z, min.x, max.y, max.z
];
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
const material = new THREE.LineBasicMaterial({
color,
depthTest: false,
transparent: true,
opacity: 0.9
});
const helper = new THREE.LineSegments(geometry, material);
helper.renderOrder = 10;
return helper;
}
这里没有直接使用 Box3Helper,而是手写顶点。这样可以更灵活地控制材质参数,比如:
depthTest: false:让线框不被模型遮挡transparent: true:支持透明度renderOrder = 10:提高辅助线的渲染顺序
对于高斯泼溅这种半透明、混合渲染比较复杂的对象,调试辅助线最好显式控制渲染顺序和深度测试。
10. TransformControls:拖拽移动和旋转
Demo 中加入了 Three.js 自带的 TransformControls:
js
const transformControls = new TransformControls(camera, renderer.domElement);
transformControls.setMode('translate');
transformControls.size = 1.5;
scene.add(transformControls.getHelper());
并且监听拖拽状态:
js
transformControls.addEventListener('dragging-changed', (event) => {
controls.enabled = !event.value;
});
这一步很重要。如果不处理,拖拽坐标轴时 OrbitControls 也会响应鼠标事件,导致模型和相机同时动,交互会很混乱。
这里的逻辑是:
- 当 TransformControls 正在拖拽时,禁用 OrbitControls
- 当拖拽结束时,恢复 OrbitControls
再配合键盘快捷键切换模式:
js
window.addEventListener('keydown', (event) => {
if (event.key.toLowerCase() === 'w') transformControls.setMode('translate');
if (event.key.toLowerCase() === 'e') transformControls.setMode('rotate');
});
这样就可以用 W 切换移动,用 E 切换旋转,和很多三维编辑器里的习惯比较接近。
11. 缩放高斯模型
页面上提供了一个简单的 number input:
html
<input id="scaleInput" type="number" min="0.1" max="10" step="0.1" value="1">
输入变化时调用:
js
scaleInput.addEventListener('input', () => {
const scale = Number(scaleInput.value);
if (!Number.isFinite(scale) || scale <= 0) return;
setSplatScale(scale);
});
缩放函数如下:
js
function setSplatScale(scale) {
deskFlower.scale.setScalar(scale);
deskFlower.position.copy(splatBasePosition).multiplyScalar(scale);
}
这里除了设置 scale,还同步调整了 position。原因是前面为了把 pivot 移到包围盒中心,对 deskFlower.position 做过反向偏移。如果只缩放模型,不缩放这个偏移量,模型会相对 Group 原点发生不符合预期的偏移。
所以这里保存了一个 splatBasePosition,每次缩放时都基于原始偏移重新计算位置。
12. HDR 环境背景
Demo 使用 RGBELoader 加载 HDR:
js
function loadHDR() {
const rgbeLoader = new RGBELoader();
rgbeLoader.load('./assets/day.hdr', (texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.background = texture;
scene.environment = texture;
}, undefined, (err) => {
console.warn('HDR 加载失败:', err);
});
}
这里同时设置了:
js
scene.background = texture;
scene.environment = texture;
background 负责显示背景,environment 负责给 Three.js PBR 材质提供环境反射。对于当前高斯泼溅模型来说,HDR 主要是提供更接近真实环境的空间氛围;如果场景中还有 glTF 模型、金属材质、玻璃材质,那么 environment 的效果会更加明显。
13. 窗口自适应
最后是标准的 resize 处理:
js
window.addEventListener('resize', () => {
camera.aspect = box.clientWidth / box.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(box.clientWidth, box.clientHeight);
});
页面容器使用 100vw 和 100vh,所以窗口大小变化时,需要同步更新相机宽高比和 renderer 尺寸,否则画面会被拉伸。
14. 实践中的几个注意点
必须使用本地服务器
不要直接双击打开 HTML 文件。建议用任意静态服务器启动项目,例如:
bash
npx serve .
或者使用 VS Code / Cursor 插件里的 Live Server。
原因是:
- ES Module import 有浏览器安全限制
.ksplat模型需要通过 HTTP 请求加载.hdr文件同样需要通过 HTTP 请求加载
注意模型坐标系
不同工具导出的高斯模型坐标系可能不同。接入后如果发现模型倒了、左右反了、朝向不对,可以先从这几个地方排查:
quaternionrotationscale- 数据导出时的 up axis
当前 Demo 使用:
js
deskFlower.quaternion.set(0, 0, -1, 0);
就是在处理模型朝向问题。
高斯模型适合放进 Group 管理
不要急着直接操作 SplatMesh。更推荐外面套一层 THREE.Group:
js
const splatGroup = new THREE.Group();
splatGroup.add(deskFlower);
scene.add(splatGroup);
这样后续做拖拽、吸附、标注、坐标转换、批量显示隐藏都会更方便。
包围盒对调试非常有用
高斯泼溅看起来像一团真实影像,但它的空间边界并不直观。加一个包围盒可以快速判断:
- 模型是否加载到预期位置
- 模型是否被错误缩放
- pivot 是否在中心附近
- TransformControls 是否绑定到了正确对象
15. 总结
这份 Demo 的重点不只是"把 .ksplat 显示出来",而是把高斯泼溅真正接入到 Three.js 的交互体系里。
核心链路可以概括为:
- 创建 Three.js 场景、相机和 WebGLRenderer
- 创建
SparkRenderer并加入场景 - 使用
SplatMesh加载.ksplat文件 - 用
THREE.Group包裹高斯模型,方便整体控制 - 等待
initialized后获取包围盒 - 根据包围盒中心修正 pivot
- 添加 TransformControls、OrbitControls 和辅助线框
- 在动画循环中正常渲染 Three.js 场景
高斯泼溅很适合用于真实场景扫描、室内空间展示、文物数字化、街景重建等方向。而 Three.js 的优势在于成熟的场景管理、交互控制和 Web 生态。两者结合之后,可以在浏览器里快速做出既真实又可交互的三维展示系统。
后续还可以继续扩展:
- 多个
.ksplat模型的加载和显隐控制 - 高斯泼溅与 glTF 模型混合展示
- 与 GIS 坐标、3D Tiles、地图底图结合
- 增加点击拾取、标注、测距等业务能力
- 对高斯模型做 LOD、裁剪和性能优化
如果不想从零写完整的三维编辑、拖拽、场景管理和发布能力,也可以尝试低代码可视化三维搭建平台 Meteor3D。它提供拖拉拽式的三维场景搭建能力,用户可以快速基于高斯泼溅模型组织真实场景,再结合其他三维资产、标注和交互能力完成可视化应用搭建。
这也是我认为高斯泼溅值得和 Three.js 一起研究的原因:它负责真实感表达,Three.js 负责三维应用能力,组合起来会非常适合 Web 端沉浸式可视化场景。