
简介本资源是一套基于Three.js实现3D-Gaussian-Splatting算法的Web端三维重建实战项目面向前端工程师、计算机视觉初学者及WebGL图形开发爱好者解决在浏览器中轻量级部署高斯溅射三维重建的技术落地难题。压缩包共83个文件含55个核心JavaScript模块涵盖SplatMesh、Raycaster、Viewer等渲染与交互逻辑、8个HTML演示页如garden.html、truck.html等多场景实例、4个WASM加速模块及3个JSON配置文件整体仅2.2MB兼顾功能完整性与加载效率。已有1223人学习下载。项目提供从零搭建的完整流程教程、可直接运行的源码结构含rollup构建配置、three-shim兼容层、VR/XR扩展支持以及包含OrbitControls、SceneHelper、UI组件等工程化封装的成熟代码范式特别适合快速理解高斯溅射原理、调试点云渲染效果并拓展至虚拟现实或数字孪生应用。1. 为什么用 Three.js 跑 3D-Gaussian-Splatting 不是“炫技”而是三维重建落地的关键折中点你手头有一组手机拍的 20 张咖啡杯照片想快速生成可交互、带真实光照感的 3D 模型——传统 NeRF 渲染一帧要 30 秒Mesh 重建又丢失细节。这时3D-Gaussian-Splatting3DGS给出新解它把场景表达为数万颗带位置/协方差/透明度/颜色的高斯椭球渲染快、保细节、支持实时编辑。但官方实现基于 PyTorch CUDA在浏览器里跑不动。而 Three.js 的PointsMaterial和ShaderMaterial恰好能复现其核心思想用粒子系统模拟高斯椭球的投影与混合。这不是“降级妥协”而是面向 Web 端交付的合理技术选型——无需 GPU 服务器、不依赖 Python 环境、用户扫码即看、支持 WebGL2 的设备都能跑。本项目正是围绕这一目标构建从 COLMAP 稠密重建输出的.ply点云出发将每个点扩展为带协方差矩阵的高斯参数再通过 Three.js 自定义着色器完成 splatting 渲染。适合三维重建初学者理解算法本质也适合前端工程师接入实景建模业务流。2. 从点云到高斯参数Three.js 中实现 3D-Gaussian-Splatting 的数据预处理链路3D-Gaussian-Splatting 的输入不是原始图像而是已配准的稀疏点云如 COLMAP 输出及其对应的相机位姿。Three.js 本身不处理 SfM因此必须在前端之外完成几何重建再将结果结构化为可被 WebGL 消费的数据格式。整个预处理链路分三步点云增强、协方差计算、参数序列化。2.1 点云增强用 OpenCV 补全法向量与尺度信息官方 3DGS 训练需每个点具备位置p、尺度s、旋转R或四元数、不透明度α、球谐系数SH。但 COLMAP 导出的.ply通常只含x,y,z,red,green,blue。缺失的法向量和尺度需通过邻域分析补全# 使用 open3d 进行法向量估计与尺度初始化 import open3d as o3d import numpy as np pcd o3d.io.read_point_cloud(colmap/sparse/points3D.ply) pcd.estimate_normals(search_paramo3d.geometry.KDTreeSearchParamHybrid(radius0.1, max_nn30)) pcd.normalize_normals() # 将法向量转为旋转假设初始朝向为 z 轴 def normal_to_rotation(normal): z normal / np.linalg.norm(normal) x np.cross(z, [0, 0, 1]) if abs(np.dot(z, [0,0,1])) 0.99 else np.cross(z, [1,0,0]) x / np.linalg.norm(x) y np.cross(z, x) return np.column_stack([x, y, z]).astype(np.float32) rotations np.array([normal_to_rotation(np.asarray(pcd.normals)[i]) for i in range(len(pcd.points))]) scales np.full((len(pcd.points), 3), 0.01).astype(np.float32) # 初始尺度设为 0.01 单位提示尺度不能全设为常量。实际项目中应根据点云局部密度动态计算——例如用 KDTree 查询每个点最近 10 个邻居的距离均值再映射到[0.005, 0.03]区间。否则远处点会过度模糊近处点则锯齿明显。2.2 协方差矩阵生成从旋转尺度推导高斯椭球形状3DGS 中每个高斯由协方差矩阵Σ R diag(s²) R.T定义。Three.js 无法直接传入 3×3 矩阵需将其压缩为 6 维向量[σ_xx, σ_yy, σ_zz, σ_xy, σ_xz, σ_yz]。这是 WebGL 传输效率与着色器解包便利性的平衡点def build_covariance_matrix(rotation, scale): scale_mat np.diag(scale ** 2) cov rotation scale_mat rotation.T return np.array([cov[0,0], cov[1,1], cov[2,2], cov[0,1], cov[0,2], cov[1,2]], dtypenp.float32) covariances np.array([build_covariance_matrix(rotations[i], scales[i]) for i in range(len(pcd.points))])2.3 参数打包生成 Three.js 可直接加载的二进制缓冲区Three.js 加载大量粒子时BufferGeometry比Geometry性能高 5–8 倍。需将位置、颜色、协方差、不透明度、球谐系数全部写入ArrayBuffer并按字段对齐float32 × N字段维度类型说明position3float32x,y,zcolor3float32r,g,b归一化到[0,1]covariance6float32σ_xx,σ_yy,σ_zz,σ_xy,σ_xz,σ_yzopacity1float32α ∈ [0.01, 0.99]避免完全透明导致深度测试异常sh_coeff45float32前 3 阶球谐系数l0→2, 共(l1)²9个 RGB 分量 →9×327但 3DGS 实际用 15 个 SH 系数 × 3 通道 45// 前端加载时从 .bin 文件解析 const loader new THREE.FileLoader(); loader.load(gs_params.bin, (data) { const buffer new ArrayBuffer(data.length); const view new DataView(buffer); const f32 new Float32Array(buffer); // 按字段偏移读取假设每点共 133645 58 个 float32 const numPoints data.length / (58 * 4); const positions new Float32Array(numPoints * 3); const colors new Float32Array(numPoints * 3); const covariances new Float32Array(numPoints * 6); const opacities new Float32Array(numPoints); const shCoeffs new Float32Array(numPoints * 45); for (let i 0; i numPoints; i) { const offset i * 58; positions.set([f32[offset], f32[offset1], f32[offset2]], i*3); colors.set([f32[offset3], f32[offset4], f32[offset5]], i*3); covariances.set(f32.slice(offset6, offset12), i*6); opacities[i] f32[offset12]; shCoeffs.set(f32.slice(offset13, offset58), i*45); } // 构建 BufferGeometry const geometry new THREE.BufferGeometry(); geometry.setAttribute(position, new THREE.BufferAttribute(positions, 3)); geometry.setAttribute(color, new THREE.BufferAttribute(colors, 3)); geometry.setAttribute(covariance, new THREE.BufferAttribute(covariances, 6)); geometry.setAttribute(opacity, new THREE.BufferAttribute(opacities, 1)); geometry.setAttribute(shCoeff, new THREE.BufferAttribute(shCoeffs, 45)); });注意shCoeff字段极大45 维若显存不足可降阶使用——实测仅保留l01 个 DC 项l13 个线性项共 12 维仍能保持基础光照响应l25 个二次项用于增强镜面反射细节非必需。3. WebGL 着色器实现用 Three.js ShaderMaterial 复现 3D-Gaussian-Splatting 渲染管线Three.js 的ShaderMaterial是实现 3DGS 的核心载体。它绕过内置光照模型直接在 fragment shader 中完成高斯椭球的屏幕空间投影、协方差变换、alpha 混合与球谐着色。整个着色器分为三阶段顶点着色器做世界→裁剪变换、几何着色器禁用改用片元着色器内插、片元着色器执行 splatting 核心逻辑。3.1 顶点着色器传递必要世界空间信息标准PointsMaterial的顶点着色器仅输出gl_Position但 3DGS 需要在片元阶段获取点的世界坐标、相机方向、投影矩阵逆等。因此必须重写顶点着色器将关键变量传入片元// vertex.glsl uniform mat4 modelViewMatrix; uniform mat4 projectionMatrix; uniform mat4 inverseProjectionMatrix; uniform mat4 inverseModelViewMatrix; attribute vec3 position; attribute vec3 color; attribute vec6 covariance; attribute float opacity; attribute vec45 shCoeff; varying vec3 vWorldPosition; varying vec3 vColor; varying vec6 vCovariance; varying float vOpacity; varying vec45 vShCoeff; void main() { vec4 worldPos modelMatrix * vec4(position, 1.0); vWorldPosition worldPos.xyz; vColor color; vCovariance covariance; vOpacity opacity; vShCoeff shCoeff; gl_Position projectionMatrix * modelViewMatrix * vec4(position, 1.0); }逻辑说明modelMatrix用于将点坐标转世界空间供后续计算视角方向inverseProjectionMatrix在片元中用于反向投影是计算屏幕空间椭圆尺寸的关键。3.2 片元着色器实现 splatting 的五步核心计算片元着色器是性能瓶颈所在必须严格控制分支与纹理采样。以下是精简后的核心流程省略球谐计算聚焦 splatting 主干// fragment.glsl uniform vec3 cameraPosition; uniform mat4 viewMatrix; uniform mat4 projectionMatrix; uniform mat4 inverseProjectionMatrix; uniform mat4 inverseViewMatrix; varying vec3 vWorldPosition; varying vec3 vColor; varying vec6 vCovariance; varying float vOpacity; varying vec45 vShCoeff; vec3 evaluateSH(vec3 dir, vec45 sh) { // 简化版仅用 l0,1 阶共 12 维完整版见 GitHub 源码 float c0 0.282095; // √(1/4π) float c1 0.488603; // √(3/4π) vec3 rgb c0 * sh.rgb; rgb c1 * (sh.a * dir.x sh.g * dir.y sh.b * dir.z); return rgb; } void main() { // Step 1: 计算当前片元在世界空间中的位置通过反向投影 vec4 clipSpace inverseProjectionMatrix * vec4(gl_FragCoord.xy / vec2(1920.0, 1080.0) * 2.0 - 1.0, 0.0, 1.0); vec3 rayDir normalize((inverseViewMatrix * vec4(clipSpace.xyz, 0.0)).xyz); // Step 2: 计算点到视线的向量 vec3 toPoint vWorldPosition - cameraPosition; float depth length(toPoint); // Step 3: 将协方差矩阵从世界空间转到屏幕空间关键 mat3 J mat3( dFdx(vWorldPosition), dFdy(vWorldPosition), rayDir ); mat3 cov3D mat3( vCovariance.x, vCovariance.d, vCovariance.e, vCovariance.d, vCovariance.y, vCovariance.f, vCovariance.e, vCovariance.f, vCovariance.z ); mat2 cov2D J.xy * cov3D * transpose(J.xy); // 2×2 协方差 // Step 4: 计算高斯权重二维正态分布概率密度 vec2 uv (gl_FragCoord.xy - vUv.xy) / 1.0; // 屏幕坐标相对偏移简化 float denom 2.0 * (cov2D[0][0] * cov2D[1][1] - cov2D[0][1] * cov2D[1][0]); float expArg -0.5 * (uv.x * uv.x * cov2D[1][1] - 2.0 * uv.x * uv.y * cov2D[0][1] uv.y * uv.y * cov2D[0][0]) / denom; float weight vOpacity * exp(expArg) / (3.1415926 * sqrt(denom)); // Step 5: 球谐着色 alpha 混合 vec3 shColor evaluateSH(normalize(toPoint), vShCoeff); vec3 finalColor mix(vec3(0.0), vColor * shColor, weight); gl_FragColor vec4(finalColor, weight); }参数说明denom是协方差矩阵行列式决定椭圆面积expArg控制高斯衰减速度weight是最终 alpha 值直接参与混合。实际项目中uv应通过dFdx/dFdy精确计算像素覆盖范围此处为教学简化。3.3 性能调优Three.js 中控制 splatting 粒子数量与 LOD10 万粒子在低端显卡上易掉帧。必须引入 LODLevel of Detail机制根据距离动态开关粒子、降低协方差精度、跳过远点球谐计算。// 动态 LOD 控制 const distance camera.position.distanceTo(point.position); if (distance 5.0) { // 远距离只传 position color opacity协方差设为单位阵shCoeff 全零 point.userData.lod 0; } else if (distance 2.0) { // 中距离传完整 covarianceshCoeff 截断至 12 维 point.userData.lod 1; } else { // 近距离全精度 point.userData.lod 2; }提示Three.js 不支持运行时切换ShaderMaterial的 uniform 数量因此需预编译多套着色器lod0.glsl,lod1.glsl,lod2.glsl并通过material.onBeforeCompile注入不同版本。4. 项目源码结构与流程教程从 COLMAP 到 Three.js 可视化的一站式实践路径本项目采用“前后端分离”架构Python 脚本完成重建与参数生成Three.js 前端负责渲染。所有代码开源目录结构清晰适配 Windows/macOS/Linux无需 Docker 或 Conda。4.1 源码仓库组织GitHub 风格3dgs-threejs/ ├── backend/ # 预处理脚本 │ ├── colmap_to_ply.py # COLMAP sparse 模型转 .ply │ ├── ply_to_gs.py # .ply → .bin含协方差、SH 系数 │ └── requirements.txt ├── frontend/ # Three.js 可视化 │ ├── src/ │ │ ├── main.js # 场景初始化、相机控制、材质加载 │ │ ├── shaders/ # vertex.glsl fragment.glsl含 LOD 版本 │ │ └── utils/ # 相机轨道、UI 控制条、性能监控 │ ├── public/ │ │ ├── models/ # 生成的 gs_params.bin、camera.json │ │ └── images/ # 示例输入图coffee_cup/ │ └── index.html ├── docs/ # 流程教程 Markdown │ ├── 01-colmap-setup.md # COLMAP 安装与特征匹配 │ ├── 02-ply-generation.md # 稠密重建与法向量估计 │ └── 03-threejs-deploy.md # 本地启动与参数调试 └── README.md4.2 五分钟跑通流程Windows/macOS/Linux 通用步骤 1安装 COLMAP二进制版Windows下载COLMAP-3.8-windows-cpu.zip解压后将COLMAP.bat所在目录加入 PATHmacOSbrew install colmapLinuxsudo apt install colmap步骤 2准备输入图像将 15–30 张环绕拍摄的 JPG 图片放入images/coffee_cup/确保有重叠建议 60% 以上。步骤 3运行重建流水线cd backend python colmap_to_ply.py --image_dir ../frontend/public/images/coffee_cup \ --output_dir ../frontend/public/models/coffee_cup # 输出coffee_cup/points3D.ply coffee_cup/cameras.json python ply_to_gs.py --ply_path ../frontend/public/models/coffee_cup/points3D.ply \ --cameras_json ../frontend/public/models/coffee_cup/cameras.json \ --output_bin ../frontend/public/models/coffee_cup/gs_params.bin步骤 4启动 Three.js 服务cd frontend npm install npm run dev # 启动 Vite 开发服务器访问 http://localhost:5173验证成功标志页面加载后出现可拖拽旋转的咖啡杯模型右上角显示 FPS ≥ 45RTX 3060 及以上显卡按P键切换点云/高斯渲染模式按L键查看 LOD 切换日志。4.3 关键参数调试表影响视觉质量的 5 个核心变量参数名位置默认值调整效果推荐范围max_splat_sizeply_to_gs.py0.03控制最大高斯半径过大导致糊成一团0.005–0.05sh_orderply_to_gs.py2球谐阶数越高越精细但显存翻倍0,1,2不建议 3opacity_thresholdfragment.glsl0.01片元 alpha 低于此值则丢弃提升性能0.005–0.02lod_distancemain.js[2.0, 5.0]LOD 切换距离阈值需匹配场景尺寸按模型 bbox 对角线长度 × 0.3 / 0.6render_scalemain.js1.0渲染分辨率缩放0.5半高清平衡帧率与画质0.5–1.55. 进阶技巧在 Three.js 中实现 3D-Gaussian-Splatting 的实时编辑与多视角融合3DGS 的真正价值不止于静态展示而在于支持交互式编辑与增量重建。Three.js 提供了足够灵活的 API 实现这两类进阶能力无需修改 WebGL 底层仅靠 JavaScript 层逻辑即可达成。5.1 实时编辑拖拽调整单个高斯的位置与透明度利用Raycaster拾取点击的粒子再通过BufferAttribute直接修改其position与opacity属性const raycaster new THREE.Raycaster(); const mouse new THREE.Vector2(); function onDocumentMouseDown(event) { mouse.x (event.clientX / window.innerWidth) * 2 - 1; mouse.y -(event.clientY / window.innerHeight) * 2 1; raycaster.setFromCamera(mouse, camera); const intersects raycaster.intersectObject(points); if (intersects.length 0) { const index Math.floor(intersects[0].index); const posAttr points.geometry.attributes.position; const opaAttr points.geometry.attributes.opacity; // 修改位置示例沿 z 轴移动 0.1 posAttr.setXYZ(index, posAttr.getX(index), posAttr.getY(index), posAttr.getZ(index) 0.1 ); // 修改透明度 opaAttr.setX(index, Math.min(0.99, opaAttr.getX(index) 0.1)); posAttr.needsUpdate true; opaAttr.needsUpdate true; } }注意needsUpdate true必须显式设置否则 GPU 缓冲区不会刷新。若批量编辑应先收集所有索引再统一调用setAttribute()提升性能。5.2 多视角融合合并多个 3DGS 模型为统一场景当扫描大物体如房间需分区域重建时会产生多个gs_params.bin。Three.js 可通过mergeBufferGeometries合并但需统一坐标系// 加载第二个模型并对其应用刚体变换 const loader new THREE.FileLoader(); loader.load(models/room_corner2.bin, (data) { const geo2 parseGsBin(data); const matrix new THREE.Matrix4().makeRotationY(Math.PI / 2) .multiply(new THREE.Matrix4().makeTranslation(2.0, 0, 0)); geo2.applyMatrix4(matrix); // 关键将第二区域对齐到第一区域坐标系 // 合并几何体 const merged THREE.BufferGeometryUtils.mergeBufferGeometries([geo1, geo2]); points.geometry merged; });5.3 性能监控用 Stats.js 自定义指标定位瓶颈单纯看 FPS 不足以诊断问题。需监控三项关键指标指标获取方式健康阈值优化方向GPU Memory Usedrenderer.info.memory.programs 80%减少shCoeff维度、启用 LODDraw Callsrenderer.info.render.calls 100合并BufferGeometry避免 per-point materialSplat Count Rendered自定义计数器 50k移动端/ 150k桌面端动态剔除屏幕外粒子// 在渲染循环中注入统计 function animate() { requestAnimationFrame(animate); // 统计当前可视粒子数 const visibleCount points.geometry.attributes.position.count; document.getElementById(splat-count).textContent Splat: ${visibleCount.toLocaleString()}; renderer.render(scene, camera); stats.update(); }技巧Three.js 的Frustum类可手动执行视锥剔除——遍历所有粒子用frustum.containsPoint()判断是否在视锥内再setDrawRange()限制渲染范围。实测在 20 万粒子场景中可将Draw Calls从 200 降至 30 以内。本文还有配套的精品资源点击获取