Vue.js - 加载并展示3DGS高斯泼溅模型教程2(使用Spark库)
作者:hangge | 2026-06-01 10:25
前文我演示了如何使用 GaussianSplats3D 库在 Vue.js 项目中加载和渲染 3D Gaussian Splatting(3DGS)模型,实现了基本的高斯泼溅效果与点云可视化(点击查看)。本文接着介绍另一种 Web 项目的 3DGS 渲染方案: Spark 库。Spark 是专为 THREE.js 设计的 3DGS 扩展,提供了 SplatMesh 和 SparkRenderer 等 API,可以轻松将高斯泼溅模型集成到 Three.js 场景中,并与其他网格对象、光照和交互控件无缝结合。
二、使用 Spark 库加载 3DGS 模型
1,Spark 库介绍
(1)Spark 是一个面向 Web 的 3D Gaussian Splatting(3DGS)渲染库 / 扩展,专为与 Three.js 无缝集成而设计。它把高斯泼溅(splat)作为一等对象(SplatMesh)引入 Three.js 场景,并通过 SparkRenderer、SparkViewpoint 等组件把高效的 splat 渲染管线挂接到 Three 的渲染流程中,从而可以在同一场景内同时渲染 splat 与常规模型、UI 层与后处理。
- GitHub 主页:https://github.com/sparkjsdev/spark
- 官方在线演示:https://sparkjs.dev/examples
(2)Spark 主要特性如下:
- Three.js 原生集成:SplatMesh 派生自 THREE.Object3D,可像普通 Mesh 一样放入场景、平移旋转、与其他对象混合渲染。
- 丰富的加载格式支持:自动识别并加载常见 splat/3DGS 格式(如 .ply、.spz,以及其它压缩/二进制格式),提供便捷的一行加载 API(new SplatMesh({ url }))。
- 高内存/缓存效率布局:支持 PackedSplats(16 bytes/ splat 等高效内存布局)以提升缓存与显存利用效率,利于大规模 splat 集合。
- 多视点 & 多场景渲染:内建对多 viewpoint / 并行渲染的支持,便于实现例如小地图、多相机截图或多视点比较。
- 编辑与程序化生成:提供 splat 编辑管线(RGBA / XYZ SDF edits)与 Procedural Splats API,可运行时对 splat 做形状/颜色/位移修改,适合交互化编辑或可视化标注
2,安装配置
(1)进入 Vue 项目目录后执行如下命令安装 Spark 库。
npm install @sparkjsdev/spark
(2)我们将需要加载的 ply、splat 或者 ksplat 格式的 3DGS 文件放置项目的 public/models/3dgs 目录下。

3,样例代码
(1)下面代码我使用 Spark 库加载并渲染一个 ply 格式的 3D Gaussian Splat 模型。
<template>
<div class="splat-container" ref="container">
<canvas ref="glCanvas"></canvas>
<div v-if="loading" class="loading-overlay">加载中…</div>
<div v-if="error" class="error-overlay">{{ error }}</div>
</div>
</template>
<script>
// Vue2 单文件组件
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { SparkRenderer, SplatMesh } from '@sparkjsdev/spark'
export default {
name: 'SplatViewer',
props: {
modelUrl: { type: String, default: '/models/3dgs/demo.splat' }, // 3DGS 模型路径
backgroundColor: { type: Number, default: 0x0a0a0a }, // 场景背景色
fov: { type: Number, default: 60 } // 摄像机视角
},
data() {
return {
loading: true, // 加载状态
error: null, // 错误信息
// runtime 引用
threeRenderer: null,
scene: null,
camera: null,
controls: null,
spark: null,
splatMesh: null,
rafId: null
}
},
mounted() {
// 初始化 Three.js 场景、渲染器、相机、控制器
this.initThree()
// 加载 3DGS splat 模型
this.loadSplat(this.modelUrl)
// 监听窗口尺寸变化,保持自适应
window.addEventListener('resize', this.onResize)
},
beforeDestroy() {
// 移除监听和释放资源
window.removeEventListener('resize', this.onResize)
this.cleanup()
},
methods: {
/**
* 初始化 Three.js 渲染器、场景、相机、轨道控制器
* 并创建 SparkRenderer 注入场景
*/
initThree() {
const canvas = this.$refs.glCanvas
// 创建 Three.js 渲染器并绑定已有 canvas
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true, // 开启抗锯齿
alpha: false, // 不透明背景
powerPreference: 'high-performance' // 高性能模式
})
renderer.setPixelRatio(window.devicePixelRatio || 1)
renderer.setSize(this.$refs.container.clientWidth, this.$refs.container.clientHeight,
false)
renderer.outputEncoding = THREE.sRGBEncoding // 颜色空间编码
// 创建场景
const scene = new THREE.Scene()
scene.background = new THREE.Color(this.backgroundColor)
// 创建透视相机
const aspect = this.$refs.container.clientWidth / this.$refs.container.clientHeight
const camera = new THREE.PerspectiveCamera(this.fov, aspect, 0.01, 1000)
camera.position.set(0, 0, 3) // 拉远初始位置
// 初始化轨道控制器,用于鼠标交互
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.08
controls.minDistance = 0.1
controls.maxDistance = 200
// 创建 SparkRenderer 并挂入场景
const spark = new SparkRenderer({ renderer })
scene.add(spark) // SparkRenderer 本质是 THREE.Object3D
// 添加基础环境光,方便与普通 Mesh 混合渲染
const amb = new THREE.AmbientLight(0xffffff, 0.4)
scene.add(amb)
// 保存引用
this.threeRenderer = renderer
this.scene = scene
this.camera = camera
this.controls = controls
this.spark = spark
// 启动渲染循环
renderer.setAnimationLoop(this.renderLoop)
// 初次设置 canvas 尺寸
this.onResize()
},
/**
* 加载 3DGS 模型并添加到场景
* @param {string} url 模型文件路径
*/
async loadSplat(url) {
this.loading = true
this.error = null
try {
// 创建 SplatMesh 实例,Spark 会自动解析格式
const splat = new SplatMesh({ url })
// 等待加载完成
if (splat.loaded) {
await splat.loaded
} else if (typeof splat.on === 'function') {
// 支持事件监听版本
await new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error('加载超时')), 30000)
splat.on('loaded', () => { clearTimeout(t); resolve() })
splat.on('error', (e) => { clearTimeout(t); reject(e) })
})
} else {
// 保险等待,确保对象构造完成
await new Promise(r => setTimeout(r, 50))
}
// 设置位置并加入场景
splat.position.set(0, 0, 0)
this.scene.add(splat)
this.splatMesh = splat
// 调整相机位置,确保模型可见
this.camera.position.set(0, 0, 3)
this.controls.update()
this.loading = false
} catch (err) {
console.error('加载 splat 出错', err)
this.error = '模型加载失败:' + (err && err.message ? err.message : String(err))
this.loading = false
}
},
/**
* 渲染循环,每帧调用
* @param {number} time 渲染时间戳
*/
renderLoop(time) {
if (!this.threeRenderer) return
// 更新轨道控制器动画阻尼
if (this.controls) this.controls.update()
// 执行场景渲染,SparkRenderer 已自动注入渲染管线
this.threeRenderer.render(this.scene, this.camera)
},
/**
* 窗口大小变化时调整渲染器和相机
*/
onResize() {
const container = this.$refs.container
if (!container || !this.threeRenderer) return
const w = container.clientWidth
const h = container.clientHeight
this.threeRenderer.setPixelRatio(window.devicePixelRatio || 1)
this.threeRenderer.setSize(w, h, false)
if (this.camera) {
this.camera.aspect = w / h
this.camera.updateProjectionMatrix()
}
},
/**
* 清理和销毁 Three.js 与 Spark 资源
* 防止显存泄漏
*/
cleanup() {
try {
// 停止渲染循环
if (this.threeRenderer) {
this.threeRenderer.setAnimationLoop(null)
}
// 移除并释放 SplatMesh
if (this.splatMesh) {
this.scene.remove(this.splatMesh)
if (typeof this.splatMesh.dispose === 'function') this.splatMesh.dispose()
this.splatMesh = null
}
// 移除并释放 SparkRenderer
if (this.spark) {
if (this.scene) this.scene.remove(this.spark)
if (typeof this.spark.dispose === 'function') this.spark.dispose()
this.spark = null
}
// 销毁轨道控制器
if (this.controls) {
this.controls.dispose()
this.controls = null
}
// 销毁 Three.js 渲染器
if (this.threeRenderer) {
this.threeRenderer.forceContextLoss && this.threeRenderer.forceContextLoss()
this.threeRenderer.domElement && this.threeRenderer.domElement.remove()
this.threeRenderer = null
}
// 清空场景与相机引用
this.scene = null
this.camera = null
} catch (e) {
console.warn('cleanup 时出错:', e)
}
}
}
}
</script>
<style scoped>
.splat-container {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
.loading-overlay,
.error-overlay {
position: absolute;
left: 10px;
top: 10px;
padding: 6px 10px;
background: rgba(0, 0, 0, 0.6);
color: #fff;
border-radius: 4px;
}
.error-overlay {
background: rgba(150, 20, 20, 0.9);
}
</style>
(2)页面加载后可以看到 3DGS 模型已经被加载并渲染出来了。

(3)按住鼠标左键拖动可以将视图绕点云旋转,使用滚轮可以缩放视图,按住右键拖动则可平移视角。

附:GaussianSplats3D 与 GaussianSplats3DSpark 对比
1,技术对比
(1)GaussianSplats3D:
- 与 Three.js 关系方面,可以选择使用 Three.js 做场景管理,但核心渲染是自研 WebGL/WebGPU Shader,直接处理 GPU 数据。
- 性能方面,Web Worker + SharedArrayBuffer 做 CPU-GPU 异步,支持 WebGPU 加速。
(2)Spark:
- 与 Three.js 关系方面,完全依赖 Three.js,SplatMesh 是 Object3D,SparkRenderer 注入渲染循环和管线,所有渲染都走 Three.js renderer。
- 性能方面,主要依赖 GPU 渲染和 Three.js 的性能优化,内部也有 PackedSplats 内存布局优化,但没有直接使用 Web Worker 或 WebGPU。
2,使用建议
(1)GaussianSplats3D 更适合 独立渲染大规模 splat、高性能优化。
(2)Spark 更适合 工程集成、与普通 Mesh 混合、后处理和 UI 叠加
全部评论(0)