返回 导航

Vue.js

hangge.com

Vue.js - 加载并展示3DGS高斯泼溅模型教程2(使用Spark库)

作者:hangge | 2026-06-01 10:25
   前文我演示了如何使用 GaussianSplats3D 库在 Vue.js 项目中加载和渲染 3D Gaussian Splatting3DGS)模型,实现了基本的高斯泼溅效果与点云可视化(点击查看)。本文接着介绍另一种 Web 项目的 3DGS 渲染方案: Spark 库。Spark 是专为 THREE.js 设计的 3DGS 扩展,提供了 SplatMeshSparkRendererAPI,可以轻松将高斯泼溅模型集成到 Three.js 场景中,并与其他网格对象、光照和交互控件无缝结合。

二、使用 Spark 库加载 3DGS 模型

1,Spark 库介绍

(1)Spark 是一个面向 Web3D Gaussian Splatting3DGS)渲染库 / 扩展,专为与 Three.js 无缝集成而设计。它把高斯泼溅(splat)作为一等对象(SplatMesh)引入 Three.js 场景,并通过 SparkRendererSparkViewpoint 等组件把高效的 splat 渲染管线挂接到 Three 的渲染流程中,从而可以在同一场景内同时渲染 splat 与常规模型、UI 层与后处理。

(2)Spark 主要特性如下:
  • Three.js 原生集成:SplatMesh 派生自 THREE.Object3D,可像普通 Mesh 一样放入场景、平移旋转、与其他对象混合渲染。
  • 丰富的加载格式支持:自动识别并加载常见 splat/3DGS 格式(如 .ply.spz,以及其它压缩/二进制格式),提供便捷的一行加载 APInew SplatMesh({ url }))。
  • 高内存/缓存效率布局:支持 PackedSplats16 bytes/ splat 等高效内存布局)以提升缓存与显存利用效率,利于大规模 splat 集合。
  • 多视点 & 多场景渲染:内建对多 viewpoint / 并行渲染的支持,便于实现例如小地图、多相机截图或多视点比较。
  • 编辑与程序化生成:提供 splat 编辑管线(RGBA / XYZ SDF edits)与 Procedural Splats API,可运行时对 splat 做形状/颜色/位移修改,适合交互化编辑或可视化标注

2,安装配置

(1)进入 Vue 项目目录后执行如下命令安装 Spark 库。
npm install @sparkjsdev/spark

(2)我们将需要加载的 plysplat 或者 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 + SharedArrayBufferCPU-GPU 异步,支持 WebGPU 加速。
(2)Spark
  • Three.js 关系方面,完全依赖 Three.jsSplatMeshObject3DSparkRenderer 注入渲染循环和管线,所有渲染都走 Three.js renderer
  • 性能方面,主要依赖 GPU 渲染和 Three.js 的性能优化,内部也有 PackedSplats 内存布局优化,但没有直接使用 Web WorkerWebGPU

2,使用建议

(1)GaussianSplats3D 更适合 独立渲染大规模 splat、高性能优化。
(2)Spark 更适合 工程集成、与普通 Mesh 混合、后处理和 UI 叠加
评论

全部评论(0)

回到顶部