Three.js的3D高斯散射渲染
这是一个基于Three.js的3D高斯散射实时辐射场渲染的渲染器实现。原项目基于CUDA并需要在本地机器上运行,而我想构建一个可通过网络访问的查看器。
3D场景以类似点云的格式存储,可以实时查看、导航和交互。这个渲染器可以处理INRIA项目生成的.ply
文件、标准的.splat
文件,或者我自己的自定义.ksplat
文件,后者是原始.ply
文件的精简压缩版本。
当我开始时,已经有了基于网络的查看器 -- antimatter15的WebGL查看器和cvlab-epfl的WebGPU查看器 -- 但还没有Three.js版本。我以这些版本为起点进行了初步实现,但目前这个项目包含的都是我自己的代码。
亮点
- 渲染完全通过Three.js完成
- 代码组织为现代ES模块
- 内置查看器是自包含的,因此加载和查看场景只需很少的代码
- 查看器可以导入
.ply
文件、.splat
文件或我的自定义压缩.ksplat
文件 - 用户可以将
.ply
或.splat
文件转换为.ksplat
文件格式 - 允许与散射点一起渲染Three.js场景或对象组
- 内置WebXR支持
- 支持1级和2级球谐函数以实现视角相关效果
- 注重优化:
- 在排序和渲染之前使用自定义八叉树对散射点进行剔除
- WASM散射点排序:使用WASM SIMD指令在C++中实现
- 部分GPU加速的散射点排序:使用变换反馈预计算散射点距离
已知问题
- 散射点排序在CPU上运行 – 最好能找到基于GPU的方法
- 移动或旋转太快时会出现伪影(由于基于CPU的散射点排序)
- 移动设备上性能不佳
- 自定义
.ksplat
文件格式仍需改进,尤其是压缩方面 - 默认的基于整数的散射点排序对大型场景效果不佳。在这种情况下,可以将查看器参数
integerBasedSort
设为false
,强制使用较慢的基于浮点数的排序
限制
目前可渲染的散射点数量有限制,主要取决于所需的球谐函数程度。这些限制是:
球谐函数程度 | 最大散射点数 |
---|---|
0 | 约 16,000,000 |
1 | 约 11,000,000 |
2 | 约 8,000,000 |
未来的工作将包括优化散射点数据在数据纹理中的打包方式,这将有助于提高这些限制。
未来工作
这仍然是一个进行中的工作!还有几件事需要完成:
- 改进散射点数据在数据纹理中的打包方式
- 继续优化基于CPU的散射点排序 - 也许可以尝试某种增量排序?
- 支持非常大的场景(流式传输分段和LOD)
在线演示
https://projects.markkellogg.org/threejs/demo_gaussian_splats_3d.php
控制
鼠标
- 左键单击设置焦点
- 左键单击并拖动可围绕焦点旋转
- 右键单击并拖动可平移相机和焦点
键盘
-
C
切换网格光标,显示鼠标投影射线与散射点网格的交点 -
I
切换显示调试信息的信息面板:- 相机位置
- 相机焦点/观察点
- 相机上向量
- 网格光标位置
- 当前FPS
- 渲染器窗口大小
- 渲染的散射点与总散射点的比例
- 上次散射点排序持续时间
-
U
切换显示相机控制方向的调试对象。它包括一个代表相机轨道轴的绿色箭头和一个代表相机仰角为0时平面的白色方块。 -
左箭头
逆时针旋转相机的上向量 -
右箭头
顺时针旋转相机的上向量 -
P
切换点云模式,每个散射点渲染为填充圆 -
=
增加散射点比例 -
-
减小散射点比例 -
O
切换正交模式
从源代码构建并在本地运行
导航到代码目录并运行
npm install
接下来运行构建。对于Linux和Mac OS系统,运行:
npm run build
对于Windows,我添加了一个Windows兼容版本的构建命令:
npm run build-windows
要在本地查看演示场景,运行
npm run demo
演示将在本地http://127.0.0.1:8080/index.html上可访问。你需要下载演示场景的数据并将它们解压到
<代码目录>/build/demo/assets/data
演示场景数据可在此处获取:https://projects.markkellogg.org/downloads/gaussian_splat_data.zip
作为NPM包安装
如果你不想从源代码构建库,它也可以作为NPM包使用。NPM包不包含源代码仓库中可用的源代码或演示。要安装,运行以下命令:
npm install @mkkellogg/gaussian-splats-3d
基本用法
要运行内置查看器:
import * as GaussianSplats3D from '@mkkellogg/gaussian-splats-3d';
const viewer = new GaussianSplats3D.Viewer({
'cameraUp': [0, -1, -0.6],
'initialCameraPosition': [-1, -4, 6],
'initialCameraLookAt': [0, 4, 0]
});
viewer.addSplatScene('<.ply、.ksplat或.splat文件的路径>', {
'splatAlphaRemovalThreshold': 5,
'showLoadingUI': true,
'position': [0, 1, 0],
'rotation': [0, 0, 0, 1],
'scale': [1.5, 1.5, 1.5]
})
.then(() => {
viewer.start();
});
查看器参数
参数 | 用途 |
---|---|
cameraUp | 查看场景的自然"上"向量(仅在使用轨道控制且查看器使用自己的相机时有效)。作为相机将围绕其旋转的轴,用于确定场景相对于相机的方向。 |
initialCameraPosition | 相机的初始位置(仅在查看器使用自己的相机时使用)。 |
initialCameraLookAt | 相机的初始焦点和相机轨道的中心(仅在查看器使用自己的相机时使用)。 |
addSplatScene()
的参数
参数 | 用途 |
---|---|
format | 强制加载器在加载散射点场景时假定指定的文件格式。这在从没有文件扩展名的URL加载时很有用。有效值在SceneFormat 枚举中定义:Ply 、Splat 和KSplat 。 |
splatAlphaRemovalThreshold | 告诉addSplatScene() 忽略任何alpha值小于指定值的散射点(有效范围:0 - 255)。默认为1 。 |
showLoadingUI | 在场景加载时显示加载旋转器和/或加载进度条。默认为true 。 |
position | 场景的位置,作为相对于其默认位置的偏移。默认为[0, 0, 0] 。 |
rotation | 场景的旋转,表示为四元数,默认为[0, 0, 0, 1] (单位四元数)。 |
scale | 场景的缩放,默认为[1, 1, 1] 。 |
progressiveLoad | 渐进加载场景的散射点数据,并允许在加载散射点时渲染和查看场景。此选项仅对addSplatScene() 有效,不适用于addSplatScenes() 。 |
Viewer
还可以使用addSplatScenes()
函数同时加载多个场景:
import * as GaussianSplats3D from '@mkkellogg/gaussian-splats-3d';
viewer.addSplatScenes([{
'path': '<第一个.ply、.ksplat或.splat文件的路径>',
'splatAlphaRemovalThreshold': 20
},
{
'path': '<第二个.ply、.ksplat或.splat文件的路径>',
'rotation': [-0.14724434, -0.0761755, 0.1410657, 0.976020],
'scale': [1.5, 1.5, 1.5],
'position': [-3, -2, -3.2]
}
])
.then(() => {
viewer.start();
});
addSplatScene()
和addSplatScenes()
方法将接受原始的.ply
文件、标准的.splat
文件和我的自定义.ksplat
文件。
集成Three.js场景
如果你希望渲染由查看器处理,可以将你自己的Three.js场景集成到查看器中。只需将Three.js场景对象作为threeScene
参数传递给构造函数:
import * as GaussianSplats3D from '@mkkellogg/gaussian-splats-3d';
import * as THREE from 'three';
const threeScene = new THREE.Scene();
const boxColor = 0xBBBBBB;
const boxGeometry = new THREE.BoxGeometry(2, 2, 2);
const boxMesh = new THREE.Mesh(boxGeometry, new THREE.MeshBasicMaterial({'color': boxColor}));
boxMesh.position.set(3, 2, 2);
threeScene.add(boxMesh);
const viewer = new GaussianSplats3D.Viewer({
'threeScene': threeScene,
});
viewer.addSplatScene('<.ply、.ksplat或.splat文件的路径>')
.then(() => {
viewer.start();
});
目前这只适用于写入深度缓冲区的物体(例如标准不透明物体)。支持透明物体将更具挑战性:)
viewer还支持"即插即用"模式。DropInViewer
类封装了Viewer
,可以像任何其他可渲染对象一样添加到Three.js场景中:
import * as GaussianSplats3D from '@mkkellogg/gaussian-splats-3d';
import * as THREE from 'three';
const threeScene = new THREE.Scene();
const viewer = new GaussianSplats3D.DropInViewer({
'gpuAcceleratedSort': true
});
viewer.addSplatScenes([{
'path': '<.ply、.ksplat或.splat文件的路径>'
'splatAlphaRemovalThreshold': 5
},
{
'path': '<.ply、.ksplat或.splat文件的路径>',
'rotation': [0, -0.857, -0.514495, 6.123233995736766e-17],
'scale': [1.5, 1.5, 1.5],
'position': [0, -2, -1.2]
}
]);
threeScene.add(viewer);
高级选项
viewer通过构造函数参数允许各种级别的自定义。你可以通过为selfDrivenMode
参数传递false
来控制何时调用其update()
和render()
方法,然后在你认为合适的时候/地方调用这些方法。你还可以使用自己的相机控制器,以及自己的Three.js Renderer
或Camera
实例。以下示例展示了所有这些选项:
import * as GaussianSplats3D from '@mkkellogg/gaussian-splats-3d';
import * as THREE from 'three';
const renderWidth = 800;
const renderHeight = 600;
const rootElement = document.createElement('div');
rootElement.style.width = renderWidth + 'px';
rootElement.style.height = renderHeight + 'px';
document.body.appendChild(rootElement);
const renderer = new THREE.WebGLRenderer({
antialias: false
});
renderer.setSize(renderWidth, renderHeight);
rootElement.appendChild(renderer.domElement);
const camera = new THREE.PerspectiveCamera(65, renderWidth / renderHeight, 0.1, 500);
camera.position.copy(new THREE.Vector3().fromArray([-1, -4, 6]));
camera.up = new THREE.Vector3().fromArray([0, -1, -0.6]).normalize();
camera.lookAt(new THREE.Vector3().fromArray([0, 4, -0]));
const viewer = new GaussianSplats3D.Viewer({
'selfDrivenMode': false,
'renderer': renderer,
'camera': camera,
'useBuiltInControls': false,
'ignoreDevicePixelRatio': false,
'gpuAcceleratedSort': true,
`enableSIMDInSort`: true,
'sharedMemoryForWorkers': true,
'integerBasedSort': true,
'halfPrecisionCovariancesOnGPU': true,
'dynamicScene': false,
'webXRMode': GaussianSplats3D.WebXRMode.None,
'renderMode': GaussianSplats3D.RenderMode.OnChange,
'sceneRevealMode': GaussianSplats3D.SceneRevealMode.Instant,
'antialiased': false,
'focalAdjustment': 1.0,
'logLevel': GaussianSplats3D.LogLevel.None,
'sphericalHarmonicsDegree': 0,
`enableOptionalEffects`: false,
`plyInMemoryCompressionLevel`: 2
`freeIntermediateSplatData`: false
});
viewer.addSplatScene('<.ply、.ksplat或.splat文件的路径>')
.then(() => {
requestAnimationFrame(update);
});
由于selfDrivenMode
为false,开发者需要自行调用Viewer
类的update()
和render()
方法:
function update() {
requestAnimationFrame(update);
viewer.update();
viewer.render();
}
高级Viewer
参数
参数 | 用途 |
---|---|
selfDrivenMode | 如果为false ,告诉viewer你将手动调用其update() 和render() 方法。默认为true 。 |
renderer | 向viewer传递Three.js Renderer 的实例,否则它将创建自己的实例。默认为undefined 。 |
camera | 向viewer传递Three.js Camera 的实例,否则它将创建自己的实例。默认为undefined 。 |
useBuiltInControls | 告诉viewer使用自己的相机控制器。默认为true 。 |
ignoreDevicePixelRatio | 告诉viewer假装设备像素比为1,这可以提高设备像素比较大的设备上的性能,但会略微降低视觉质量。默认为false 。 |
gpuAcceleratedSort | 告诉viewer使用部分GPU加速的方法来排序splats。目前这意味着在GPU上进行splat与相机距离的预计算。建议仅在sharedMemoryForWorkers 也为true 时将此设置为true 。在移动设备上默认为false ,其他设备上默认为true 。 |
enableSIMDInSort | 启用SIMD WebAssembly指令用于splat排序。默认为true 。 |
sharedMemoryForWorkers | 告诉viewer通过SharedArrayBuffer 使用共享内存来与排序web worker进行数据传输。如果设置为false ,建议也将gpuAcceleratedSort 设置为false 。默认为true 。 |
integerBasedSort | 告诉排序web worker使用相关数据的整数版本来计算splats与相机的距离。由于整数运算比浮点运算更快,这减少了排序时间。但在较大的场景中可能导致整数溢出,因此只应用于小型场景。默认为true 。 |
halfPrecisionCovariancesOnGPU | 告诉viewer在纹理中存储splat协方差数据时使用16位浮点值,而不是32位。默认为false 。 |
dynamicScene | 告诉viewer不要进行任何依赖于场景静态的优化。此外,从viewer的splat网格中检索的所有splat数据默认情况下不会应用其各自的场景变换。 |
webXRMode | 告诉viewer是否启用内置的Web VR或Web AR。有效值在WebXRMode 枚举中定义:None 、VR 和AR 。默认为None 。 |
renderMode | 控制viewer何时渲染场景。有效值在RenderMode 枚举中定义:Always 、OnChange 和Never 。默认为Always 。 |
sceneRevealMode | 控制场景加载时使用的淡入效果。有效值在SceneRevealMode 枚举中定义:Default 、Gradual 和Instant 。Default 为渐进加载的场景提供一个漂亮的缓慢淡入效果,为非渐进加载的场景提供快速淡入效果。Gradual 将强制所有场景使用缓慢淡入效果。Instant 将强制所有加载的场景数据立即可见。 |
antialiased | 当为true时,将在渲染过程中执行额外步骤,以解决由于在与训练时明显不同的分辨率下渲染高斯体而导致的伪影。这只适用于使用包含此补偿计算的过程训练的模型。更多详情:https://github.com/nerfstudio-project/gsplat/pull/117, https://github.com/graphdeco-inria/gaussian-splatting/issues/294#issuecomment-1772688093 |
focalAdjustment | 用于调整焦距相关计算的非科学参数。对于具有非常小的高斯体和细节的场景,增加此值可以帮助提高视觉质量。默认值为1.0。 |
logLevel | 控制台日志的详细程度。默认为GaussianSplats3D.LogLevel.None 。 |
sphericalHarmonicsDegree | 在渲染splats时使用的球谐函数程度(假设splat场景中存在数据)。有效值为0、1或2。默认值为0。 |
enableOptionalEffects | 当为true时,允许在渲染过程中使用额外的属性和特性来实现诸如不透明度调整等效果。出于性能考虑,默认为false 。这些属性与由dynamicScene 参数启用的变换属性(缩放、旋转、位置)是分开的。 |
plyInMemoryCompressionLevel | 加载.ply 文件进行直接渲染(不导出为.ksplat )时的压缩级别。有效值与.ksplat 压缩级别相同(0、1或2)。默认为2。 |
freeIntermediateSplatData | 当为true时,将释放解压splat缓冲区后用于填充数据纹理的中间splat数据。这将减少内存使用,但如果需要修改该数据,则需要从splat缓冲区重新填充。默认为false 。 |
splatRenderMode | 确定要启用哪种splat渲染模式。有效值在SplatRenderMode 枚举中定义:ThreeD 和TwoD 。ThreeD 是原始/传统模式,TwoD 是这里描述的新模式:https://surfsplatting.github.io/ |
### 创建KSPLAT文件 要将`.ply`或`.splat`文件转换为精简压缩的`.ksplat`格式,有几种选择。最简单的方法是使用[http://127.0.0.1:8080/index.html](http://127.0.0.1:8080/index.html)主演示页面中的用户界面。如果你想以编程方式进行转换,可以在浏览器中运行以下代码:
import * as GaussianSplats3D from '@mkkellogg/gaussian-splats-3d';
const compressionLevel = 1;
const splatAlphaRemovalThreshold = 5; // 255中的5
const sphericalHarmonicsDegree = 1;
GaussianSplats3D.PlyLoader.loadFromURL('<.ply或.splat文件路径>',
compressionLevel,
splatAlphaRemovalThreshold,
sphericalHarmonicsDegree)
.then((splatBuffer) => {
GaussianSplats3D.KSplatLoader.downloadFile(splatBuffer, 'converted_file.ksplat');
});
上述两种方法都会提示你的浏览器自动开始下载转换后的.ksplat
文件。
第三种选择是使用附带的nodejs脚本:
node util/create-ksplat.js [.PLY或.SPLAT文件路径] [输出文件] [压缩级别 = 0] [alpha移除阈值 = 1]
目前支持的compressionLevel
值为0
、1
或2
。0
表示不压缩,1
表示将缩放、旋转、位置和球谐系数值从32位压缩到16位。2
与1
类似,但球谐系数压缩到8位。
CORS问题和SharedArrayBuffer
默认情况下,Viewer
类使用共享内存(通过由SharedArrayBuffer
支持的类型化数组)与排序splat的Web Worker通信。这种机制存在潜在的安全问题,详见:https://web.dev/articles/cross-origin-isolation-guide。可以通过在`Viewer`构造函数中为`sharedMemoryForWorkers`参数传递`false`来禁用共享内存,但如果你想保持启用,则需要在加载应用程序时服务器发送的响应中包含几个额外的CORS HTTP头。如果没有设置这些头,你可能会在调试控制台中看到类似以下的错误:
"DOMException: Failed to execute 'postMessage' on 'DedicatedWorkerGlobalScope': SharedArrayBuffer transfer requires self.crossOriginIsolated."
对于本地演示,我创建了一个简单的HTTP服务器(util/server.js)来设置这些头:
response.setHeader("Cross-Origin-Opener-Policy", "same-origin");
response.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
Apache中的CORS
对于Apache,你可以编辑.htaccess
文件以允许CORS,添加以下行:
Header add Cross-Origin-Opener-Policy "same-origin"
Header add Cross-Origin-Embedder-Policy "require-corp"
此外,你可能需要要求安全连接到你的服务器,将所有通过http://
的访问重定向到https://
。在Apache中,可以通过在.htaccess
文件中添加以下行来实现:
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R,L]
Vite中的CORS
对于Vite,一个流行的选择是通过npm
安装vite-plugin-cross-origin-isolation插件,然后在vite.config.js
文件中添加以下内容:
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
{
name: "configure-response-headers",
configureServer: (server) => {
server.middlewares.use((_req, res, next) => {
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
next();
});
},
},
],
});
在issue #41中还提到了其他配置Vite来处理这个问题的方法。