☰
Three.js 官方接入 Gaussian Splatting,Web 端实时渲染更简单
2026/10/27 19:27:21 网站建设 项目流程

在网页里还原一个真实场景,过去既昂贵又麻烦;现在,Three.js 正在把这条路的门槛再降一截。

3D Gaussian Splatting(3DGS)是这两年在三维重建和渲染领域讨论度最高的方向之一。它把真实场景的训练结果变成一堆带颜色、透明度和形状的高斯点,在运行时用类似“高效光栅化”的方式渲染成照片级别的画面。相比 NeRF,它训练更快、渲染能到实时;相比传统倾斜摄影建模,它绕开了网格重建和贴图烘焙的复杂流程。但 Web 端落地一直有个现实问题:谁来维护 loader、谁来渲染这堆点。

Three.js 的官方仓库近期加入了GaussianSplattingLoader和相关示例,意味着主流的 Web 3D 引擎开始官方承接这项技术。这篇文章会先把 3DGS 和 Three.js 的原生支持讲清楚,说明它为什么值得关注;然后给出一套可复制的环境准备和最小示例,再扩展到多场景切换、WebGPU 方向、常见问题与工程实践。读完以后,你可以用官方 loader 把一个.splat场景跑起来,再逐步接入自己的业务项目。

1. 为什么这条消息值得关注

先做一个判断:Three.js 官方支持 Gaussian Splatting,真正降低的是 Web 端接入 3DGS 的工程成本,而不是算法成本。

3DGS 的核心训练算法在 SIGGRAPH 2023 提出后,社区很快就有了 Web 端渲染方案。比如 Antimatter15 的 gaussian-splatting 仓库,比如 gsplat.js,再比如各种依赖 Three.js 的社区 hack。这些方案能跑,但你一旦想在真实项目里长期使用,就要面对几个问题:

  • .ply、.splat、.ksplat格式谁来解析?
  • 深度排序在 WebGL 里怎么做近似,在 WebGPU 里怎么做精确?
  • 官方修复管线升级时,第三方库还在不在维护?
  • 出了渲染 Bug,issue 应该提给谁?

过去这些问题都悬在“社区造轮子”上空。Three.js 把GaussianSplattingLoader放进官方 examples 目录后,情况发生变化:格式解析、基础渲染、WebGPU 示例都有了官方维护的参考实现。

对业务开发者的直接收益是:

  • 不需要从零理解 3DGS 的底层数据格式,也能加载渲染;
  • 可以和现有 Three.js 场景、OrbitControls、交互逻辑无缝整合;
  • 官方示例同时覆盖 WebGL 与 WebGPU,方便评估不同渲染路径;
  • 后续升级 Three.js 版本时,至少有一条平滑迁移路径。

所以,如果你在做 Web 端数字孪生、云展厅、在线设计工具,或者想把实景建模结果放进 Cesium 这类地理平台,这条消息值得停下来看。

2. 3D Gaussian Splatting 是什么:从“带形状的点云”说起

2.1 一句话理解 3DGS

你可以把 3DGS 想象成一种“带形状、带颜色的点云”。

普通点云是一堆没有体积的坐标点,渲染出来就像星空,很难表达光滑表面。3DGS 的每个基本单元不是一个点,而是一个三维高斯分布,说人话就是一个小椭球。每个小椭球有自己的:

  • 空间位置;
  • 三个轴方向的尺度;
  • 旋转方向;
  • 不透明度;
  • RGB 颜色。

渲染时,摄像机把分布在场景里的这成千上万个半透明小椭球投影到屏幕上,再做一次从近到远的排序和颜色叠加,最终得到接近照片的画面。

这也是它和“光场”“体素”这些概念不一样的地方:最终画面不是插值出来的网格,而是大量高斯核累加出来的像素结果。

2.2 与点云、NeRF 的对比

维度点云NeRF3D Gaussian Splatting
基本单元无体积的点隐式神经场有形状的高斯点
训练速度不需要训练慢,小时级起步较快
渲染速度快但效果单薄慢,逐像素采样光栅化,可实时
场景编辑直接改点难度高显式点集,相对方便
Web 端接入简单复杂官方 loader 出现后明显简化

从开发者的角度看,3DGS 的最大优势是“显式”和“实时”。显式意味着拿到点集就能编辑、裁剪、分层;实时意味着可以放进交互页面,让用户转动视角,而不是播放一段预渲染视频。

3. Three.js“原生支持”到底支持了什么

3.1 官方 addons 中的 Loader 与示例

从 Three.js 官方仓库看,这套支持主要体现在 examples 目录下的几个文件里:

  • loaders/GaussianSplattingLoader.js:负责加载和解析 3DGS 数据;
  • 配套的 WebGL 示例、WebGPU 示例;
  • 一些底层辅助类和材质逻辑。

GaussianSplattingLoader目前的主用输入是.splat文件。这是一种为 Web 端准备的二进制格式,比原生 PLY 更轻,加载速度也更快。你不需要知道它每一字节的含义,也可以直接使用,但理解基础结构对排查问题有帮助:

  • 每个高斯点会记录位置、缩放、旋转、颜色和不透明度;
  • 数据按二进制紧凑排列,加载后由 Three.js 侧的材质和渲染逻辑解释。

3.2 “原生”的边界:别误会成核心 API

这里要明确边界:目前它是examples / addons 级别,不是THREE.GaussianSplatting这种在核心库里到处可用的 API。

这意味着:

  • 官方会维护它,加入它,更新它;
  • 但它仍然有 example 代码的“试运行”属性,API 可能随版本调整;
  • 生产环境使用前,需要锁版本、做回归,不能认为“官方支持”就等于“绝对稳定”。

这个判断非常重要。很多项目拿到“原生支持”的消息后会直接升级依赖,如果遇到 API 变化,反而容易被坑。稳妥的做法是:用一个固定版本先跑通,再评估是否升级。

4. 环境准备与前置条件

这一节给出一套通用环境要求。不同 Three.js 版本的具体路径可能有差异,本文以0.170.0为例,重点展示原理和可复制的步骤。

4.1 浏览器要求

  • WebGL 路径:需要支持 WebGL2 的现代浏览器,Chrome、Edge、Firefox、Safari 均可;
  • WebGPU 路径:建议使用较新的 Chrome 或 Edge,并确认浏览器已启用 WebGPU;
  • 移动端:不建议在低端手机上直接加载大场景,后面会讲原因。

4.2 Three.js 版本与模块方式

建议直接使用支持GaussianSplattingLoader的近期版本。本文代码使用 npm 生态中的three@0.170.0,并通过importmap从 CDN 加载。

如果你已经在项目里使用 npm 管理依赖,也可以先安装:

npm install three@0.170.0

然后根据构建工具配置调整 import 路径。核心代码逻辑是通用的。

4.3 本地静态服务器

.splat文件加载本质上是 fetch 文件,所以不要用file://直接打开 HTML,否则会遇到跨域和模块加载问题。推荐方式:

  • VS Code 的 Live Server 插件;
  • Python:python -m http.server 8080;
  • Node:npx serve .

4.4 数据文件

准备一个.splat文件放到assets目录。获取方式一般有:

  • 使用 Three.js 官方示例中引用的数据文件;
  • 使用社区开源 3DGS 项目提供的.splat样例;
  • 使用自己的 3DGS 重建结果,通过社区转换工具从 PLY 转成.splat。

社区转换工具形态各异,有的提供 Python 脚本,有的提供在线转换服务,命令请以你选择的仓库 README 为准,本文不限定具体工具。

5. 最小可运行示例:用 WebGL 渲染 .splat 场景

5.1 项目目录

先规划一个最小目录:

gaussian-splatting-demo/ ├── assets/ │ └── scene.splat └── index.html

asset这个名字可以随意,关键是路径要和代码里的loader.load保持一致。

5.2 完整 HTML 代码

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <title>Three.js Gaussian Splatting 最小示例</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <style> html, body { margin: 0; padding: 0; overflow: hidden; background: #111; } canvas { display: block; } #info { position: fixed; top: 12px; left: 12px; color: #eee; font: 14px/1.8 system-ui, sans-serif; background: rgba(0, 0, 0, 0.45); padding: 8px 12px; border-radius: 6px; pointer-events: none; } </style> </head> <body> <div id="info">拖动鼠标旋转视角,滚轮缩放</div> <script type="importmap"> { "imports": { "three": "https://unpkg.com/three@0.170.0/build/three.module.js", "three/addons/": "https://unpkg.com/three@0.170.0/examples/jsm/" } } </script> <script type="module"> import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { GaussianSplattingLoader } from 'three/addons/loaders/GaussianSplattingLoader.js'; // 场景、相机、渲染器 const scene = new THREE.Scene(); scene.background = new THREE.Color(0x111111); const camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.01, 100 ); camera.position.set(2, 1.5, 2); const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); document.body.appendChild(renderer.domElement); // 轨道控制器 const controls = new OrbitControls(camera, renderer.domElement); controls.target.set(0, 0, 0); controls.update(); // 加载 Gaussian Splatting 场景 const loader = new GaussianSplattingLoader(); loader.load( 'assets/scene.splat', (splat) => { scene.add(splat); }, (progress) => { console.log( '加载进度:', ((progress.loaded / progress.total) * 100).toFixed(1) + '%' ); }, (error) => { console.error('加载失败:', error); } ); // 渲染循环 function animate() { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); } animate(); // 窗口缩放处理 window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); </script> </body> </html>

5.3 代码拆解

这里真正容易踩坑的点是importmap。Three.js 从很早开始就推荐通过 ES module 引入,而importmap能让浏览器把three和three/addons/映射到 CDN 地址。这样你在业务代码里写import { OrbitControls } from 'three/addons/controls/OrbitControls.js',实际加载的是远程模块。如果 CDN 不稳定,可以把文件下载到本地后改映射路径。

GaussianSplattingLoader的用法和 Three.js 其他 Loader 基本一致:

  • 第一个参数是.splat文件地址;
  • 第二个参数是加载成功回调,拿到一个splat节点后加入场景;
  • 第三个参数是进度回调;
  • 第四个参数是错误回调。

加载成功后,splat是一个可以被相机渲染的对象。不需要再手动加灯,因为 Gaussian Splatting 场景本身已经包含训练时的光照信息。这一点和传统网格加载完全不同。

5.4 运行方式

在项目目录启动静态服务器:

python -m http.server 8080

然后打开:

http://localhost:8080

预期现象是:页面先显示深色背景,然后.splat场景逐渐出现,按住鼠标左键拖动可以旋转视角,滚动滚轮可以缩放。

如果加载成功后画面上空无一物,先看控制台有没有报错,再确认相机的near/far和position是否落在场景数据范围内。.splat文件可能来自不同尺度,单位不一定是“米”,所以相机位置要调到数据附近。

6. 扩展:加载多个场景与相机动效

实际业务里往往不是一个场景加载完就结束。云展厅可能要切换房间,家装效果可能要切换户型,对比展示要切换多套重建结果。这里需要一个简单的“场景管理”思路。

核心做法是维护一个Map,把每个加载完的splat节点存下来,再通过控制visible属性切换显示。这样做不会重复请求资源,也不会因为重复scene.add导致渲染隐患。

6.1 代码示例:多场景切换

import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { GaussianSplattingLoader } from 'three/addons/loaders/GaussianSplattingLoader.js'; const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.01, 100); camera.position.set(2, 1.5, 2); const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); document.body.appendChild(renderer.domElement); const controls = new OrbitControls(camera, renderer.domElement); // 管理多个 splat 节点 const splatScenes = new Map(); const loader = new GaussianSplattingLoader(); function loadSplat(url, key) { loader.load( url, (splat) => { splat.visible = false; splatScenes.set(key, splat); scene.add(splat); console.log(`[${key}] 加载完成`); }, (progress) => { console.log(`[${key}] 加载中:`, progress.loaded, '/', progress.total); }, (err) => { console.error(`[${key}] 加载失败`, err); } ); } function showSplat(key) { splatScenes.forEach((splat, k) => { splat.visible = k === key; }); } loadSplat('assets/scene_a.splat', 'room'); loadSplat('assets/scene_b.splat', 'garden'); // 3 秒后切换到第二个场景,仅用于演示 setTimeout(() => { showSplat('garden'); }, 3000); function animate() { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); } animate();

这个例子没有引入 UI 组件,而是用setTimeout模拟切换动作。你在真实项目里可以把它接到按钮、下拉框或者路由事件里。

需要注意:visible = false只是不渲染,内存仍被占用。如果场景很多且内存吃紧,应该考虑按需加载、卸载,而不是一股脑全部加载。

6.2 相机动效

如果想做“自动漫游”,最简单的方案是每帧修改相机位置和目标点,让 OrbitControls 跟随:

function updateCameraByTime(time) { const radius = 3; const angle = time * 0.2; camera.position.x = Math.cos(angle) * radius; camera.position.z = Math.sin(angle) * radius; camera.position.y = 1.2; controls.target.set(0, 0.5, 0); controls.update(); } function animate() { requestAnimationFrame(animate); updateCameraByTime(performance.now() * 0.001); renderer.render(scene, camera); } animate();

这里要注意用户交互和自动漫游的优先级。更稳妥的做法是:用户按下鼠标或触摸屏幕时暂停自动漫游,等交互结束后再决定是否恢复。

7. WebGPU 方向:性能潜力和注意事项

7.1 为什么 3DGS 需要 WebGPU

3DGS 渲染的关键步骤之一是每一帧都要对高斯点做从近到远排序。只有排序正确,半透明高斯点叠加出来的画面才不会闪烁、穿帮。

在 WebGL 里,排序通常需要 CPU 参与,或者借助不太优雅的近似方案。高斯点数量一旦上百万,CPU 排序会成为性能瓶颈。

WebGPU 有 compute shader,可以在 GPU 上并行完成排序和坐标变换,这也是 Three.js 官方提供webgpu_gaussian_splatting示例的原因。从 Web 渲染技术栈的演进看,3DGS 是一个很适合把 WebGPU 推向真实应用的场景,因为数据量大、计算密集、效果又直观。

7.2 用 WebGPU 渲染器替换 WebGL

在官方支持 WebGPU 的 Three.js 版本中,你可以把渲染器从WebGLRenderer换成WebGPURenderer。最小改动示例:

import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { WebGPURenderer } from 'three/addons/renderers/webgpu/WebGPURenderer.js'; import { GaussianSplattingLoader } from 'three/addons/loaders/GaussianSplattingLoader.js'; async function init() { const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.01, 100); camera.position.set(2, 1.5, 2); // WebGPU 渲染器,注意新版需要先初始化 const renderer = new WebGPURenderer(); await renderer.init(); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); document.body.appendChild(renderer.domElement); const controls = new OrbitControls(camera, renderer.domElement); const loader = new GaussianSplattingLoader(); loader.load('assets/scene.splat', (splat) => { scene.add(splat); }); function animate() { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); } animate(); } init();

这段代码最大的区别是await renderer.init()。WebGPU 渲染器需要异步获取 GPU device,所以初始化变成了 async。不同 Three.js 版本的 API 可能调整,请以官方示例为准。

需要提醒的是:不要以为切到 WebGPU 就万事大吉。如果浏览器不支持 WebGPU,页面会直接黑屏或报错,所以要加能力检测,并准备回退到 WebGL 的逻辑。性能优化的优先级依然是“减少高斯点数量、控制像素比例、避免同时加载多套场景”。

8. 常见问题与排查思路

问题现象可能原因排查方式解决方案
页面黑屏,控制台没有报错相机位置不在场景数据范围打印 splat 节点的包围盒或 center调整相机位置和 target
加载请求跨域失败数据文件在不同域名且未配置 CORS查看 Network 面板的 fetch 状态配置服务端 CORS,或使用同源静态服务器
file://打开时报模块加载错误ES Module 不允许文件协议改用http://localhost访问使用 Live Server / http.server
加载很慢,或页面卡顿.splat文件过大、像素比过高查看文件大小和 GPU 占用降采样、压缩数据、限制 pixelRatio 为 2
画面闪烁或局部出现错误颜色高斯点排序不正确或数据损坏改用 WebGPU 示例对比检查.splat来源,使用转换工具重建
某些浏览器不渲染不支持 WebGL2 或 WebGPU在chrome://gpu查看特性升级浏览器,或提供 WebGL 回退
多个场景切换后内存居高不下visible=false不释放 GPU 资源用 Memory 面板观察增长主动移除节点并释放 geometry/material

这几点每一条都是实际接入时容易遇到的情况。排查时不要先怀疑 Three.js 本身,第一步应该看数据文件能否正常加载,第二步看相机视野是否覆盖场景,第三步再看渲染管线差异。

9. 工程实践建议

9.1 数据准备

3DGS 的数据质量直接决定体验。

  • 使用训练质量可靠的重建结果,不要拿未收敛的点集直接上线;
  • .splat文件体积通常不小,一个场景几十 MB 很正常,要设计合理的加载策略;
  • 如果场景过大,需要做降采样,减少高斯点数量;
  • 对数据做统一坐标处理,否则在数字孪生、Cesium 叠加场景里会出现偏移。

后面这一点需要格外注意:Gaussian Splatting 的坐标单位可能和你的业务坐标单位不同。埋进 Cesium 或 GIS 平台之前,必须做坐标对齐,否则会出现“模型在但位置不对”的诡异问题。

9.2 服务端配置

  • 确保静态服务器支持.splat文件访问,必要时设置正确的 MIME 类型;
  • 为跨域加载配置 CORS;
  • 大文件建议走 CDN,并开启 HTTP 缓存,避免每次进入页面都重新下载;
  • 如果文件超过 50MB,需要考虑按需加载、渐近加载或 LOD 方案。

9.3 渲染与性能

  • 设置renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)),防止高分屏把 GPU 压力放大好几倍;
  • 在 SPA 中,页面切走时要暂停渲染循环;
  • 监控 FPS 和 draw call;
  • 生产环境不要用开发版 CDN,脚本路径要锁版本。

9.4 兼容性策略

WebGPU 是很好的方向,但用户不一定能用。稳妥做法是:

  1. 先检测 WebGPU 是否可用;
  2. 可用则使用 WebGPU 渲染器;
  3. 不可用则回退到 WebGL 渲染器;
  4. 两者都不支持时显示友好提示。

这种渐进增强策略适合大多数面向真实用户的 Web 3D 项目。

10. 总结与后续学习方向

Three.js 将 Gaussian Splatting 纳入官方维护范围,是一个信号:3DGS 正在从研究圈的演示项目,变成 Web 前端可以认真对待的渲染数据形态。它不像传统网格那样需要繁琐的建模和烘焙,也不像 NeRF 那样对 Web 端渲染要求苛刻,而是在“真实感”和“实时性”之间取了很实用的平衡。

你现在可以做的下一步很明确:

  1. 把官方示例或本文的index.html跑起来,确认.splat场景能正常加载;
  2. 准备自己的数据,通过社区工具从 PLY 转到.splat;
  3. 把 loader 接入现有 Three.js 业务,处理加载状态、错误回退和性能监控;
  4. 如果场景大、用户设备好,再研究 WebGPU 渲染路径和 LOD 方案。

学习 3DGS 时,不要只停留在“能加载”这一步。建议画出渲染管线里的关键节点:数据解析、坐标变换、高斯点排序、光栅化、颜色合成。每一步都直接影响画面质量和帧率,也是后续做性能优化时才真正用得上的知识。

另外,如果你正在做 Cesium 与 Three.js 共享 GL 上下文这类深度集成项目,3DGS 可以作为真实场景图层的关键补充。社区后续大概率会围绕“3DGS 与 GIS 坐标对齐”“3DGS 在低端设备上的 LOD”这些方向继续产出方案,值得保持关注。

先让官方示例跑起来,再考虑好不好用。动手永远比争论技术路线更快得到答案。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询