欢迎光临
我们一直在努力

Spark 2.0:面向 Web 的 3DGS 可视化与大场景渲染平台详解

很多人第一次看到 Spark 2.0,会把它理解成一个“3DGS 重建工具”。

但更准确地说,Spark 2.0 的重点不是从图片重建 Gaussian Splat,而是如何把已有的 3DGS 资产在浏览器里高质量、低延迟、可交互地渲染出来。

它解决的问题很具体:3DGS 资产动辄几百万到几千万个 splat,传统浏览器渲染器要么加载慢,要么内存爆,要么多个 splat 对象之间排序不正确。Spark 2.0 通过 THREE.js + WebGL2、全局 splat 排序、LoD Splat Tree、RAD 流式格式和 GPU 虚拟页表,把 3DGS 从“单个模型查看器”推进到“可组合的大型 Web 3D 世界渲染系统”。


1. Spark 2.0 是什么?

Spark 是 World Labs 开源的 3D Gaussian Splatting 渲染器,面向 Web 端,和 THREE.js 深度集成。你可以把它理解为:

SparkRenderer 负责把 Gaussian Splats 插入 THREE.js 渲染管线;SplatMesh 则像 THREE.Mesh 一样,代表一个可移动、可旋转、可动画的 3DGS 对象。

它支持常见 3DGS 文件格式,包括 .ply、.spz、.splat、.ksplat、.sog/.zip,以及 Spark 2.0 新增的 .rad 流式 LoD 格式。

Spark 的价值不在于生成 splat,而在于:

  • 在浏览器里渲染已有 3DGS 资产;

  • 支持多个 splat 对象在同一场景中正确混合;

  • 支持大场景的 LoD 渲染与按需加载;

  • 支持与 THREE.js mesh、灯光、相机、控制器、WebXR 等生态结合;

  • 支持动态 splat 修改、颜色编辑、shader 扩展和动画效果。

  • 一个很重要的点是:Spark 2.0 不是 WebGPU 优先,而是选择 WebGL2。这样做的原因很现实:WebGL2 的设备覆盖率更高,桌面、iOS、Android、VR 浏览器都更容易跑起来。Spark 的目标不是只服务高端桌面,而是让大规模 3DGS 可以在真实 Web 分发环境中运行。


    2. 最小使用方式:用 Spark 显示一个 3DGS 模型

    最简单的方式是直接用 CDN + importmap 写一个 index.html。

    <style>
    body {
    margin: 0;
    overflow: hidden;
    }
    </style>

    <script type="importmap">
    {
    "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/",
    "@sparkjsdev/spark": "https://sparkjs.dev/releases/spark/2.1.0/spark.module.js"
    }
    }
    </script>

    <script type="module">
    import * as THREE from "three";
    import { SparkRenderer, SplatMesh } from "@sparkjsdev/spark";
    import { OrbitControls } from "three/addons/controls/OrbitControls.js";

    const scene = new THREE.Scene();

    const camera = new THREE.PerspectiveCamera(
    60,
    window.innerWidth / window.innerHeight,
    0.01,
    1000
    );
    camera.position.set(0, 0, 3);

    const renderer = new THREE.WebGLRenderer({
    antialias: false
    });
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);

    const spark = new SparkRenderer({ renderer });
    scene.add(spark);

    const splat = new SplatMesh({
    url: "./assets/model.spz"
    });

    splat.position.set(0, 0, 0);
    splat.quaternion.set(1, 0, 0, 0);
    scene.add(splat);

    window.addEventListener("resize", () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
    });

    renderer.setAnimationLoop(() => {
    controls.update();
    renderer.render(scene, camera);
    });
    </script>

    核心只有三步:

    const spark = new SparkRenderer({ renderer });
    scene.add(spark);

    const splat = new SplatMesh({ url: "./assets/model.spz" });
    scene.add(splat);

    在 Spark 2.0 中,SparkRenderer 必须显式创建并加入 scene。否则 SplatMesh 不会被渲染出来。


    3. 用 NPM / Vite 部署一个项目

    如果你要做正式项目,建议使用 Vite。

    npm create vite@latest spark-viewer — –template vanilla
    cd spark-viewer
    npm install three @sparkjsdev/spark
    npm install
    npm run dev

    项目结构可以这样放:

    spark-viewer/
    public/
    assets/
    model.spz
    scene-lod.rad
    scene-lod-0.radc
    scene-lod-1.radc
    src/
    main.js
    index.html
    package.json

    src/main.js:

    import * as THREE from "three";
    import { SparkRenderer, SplatMesh } from "@sparkjsdev/spark";
    import { OrbitControls } from "three/addons/controls/OrbitControls.js";

    const scene = new THREE.Scene();

    const camera = new THREE.PerspectiveCamera(
    60,
    window.innerWidth / window.innerHeight,
    0.01,
    1000
    );
    camera.position.set(0, 0, 3);

    const renderer = new THREE.WebGLRenderer({
    antialias: false
    });
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);

    const spark = new SparkRenderer({
    renderer,
    lodSplatScale: 1.0,
    maxStdDev: Math.sqrt(8)
    });
    scene.add(spark);

    const splat = new SplatMesh({
    url: "/assets/model.spz"
    });
    scene.add(splat);

    renderer.setAnimationLoop(() => {
    controls.update();
    renderer.render(scene, camera);
    });

    构建部署:

    npm run build

    然后把 dist/ 部署到任意静态服务器即可,例如 Nginx、Vercel、Netlify、Cloudflare Pages、S3 + CloudFront。

    如果使用 .rad + .radc 分块流式加载,要注意:

  • .rad 和所有 .radc 文件必须一起上传;

  • 路径必须保持一致;

  • CDN 不能错误地改写或压缩二进制文件;

  • 服务器最好支持 HTTP Range 或至少稳定支持并发 chunk 请求;

  • 如果跨域加载,需要正确配置 CORS。


  • 4. 如何启用 Spark 2.0 的 LoD 渲染

    Spark 2.0 的大场景能力来自 LoD。

    如果你只有一个普通 .spz 或 .ply 文件,可以在浏览器里临时生成 LoD:

    const splat = new SplatMesh({
    url: "./model.spz",
    lod: true
    });
    scene.add(splat);

    这会在后台 Web Worker 中运行快速 LoD 构建。小模型可以接受,但大模型不推荐在线构建,因为首次加载会慢。

    更推荐的做法是:离线预处理成 .rad。

    git clone https://github.com/sparkjsdev/spark.git
    cd spark

    npm install
    npm run build:wasm
    npm run build

    npm run build-lod — ./my-splats.ply –quality

    输出通常是:

    my-splats-lod.rad

    然后在代码中加载:

    const splat = new SplatMesh({
    url: "/assets/my-splats-lod.rad"
    });
    scene.add(splat);

    如果要启用分块流式加载,构建时使用:

    npm run build-lod — ./my-splats.ply –quality –rad-chunked

    输出类似:

    my-splats-lod.rad
    my-splats-lod-0.radc
    my-splats-lod-1.radc
    my-splats-lod-2.radc

    加载时:

    const splat = new SplatMesh({
    url: "/assets/my-splats-lod.rad",
    paged: true
    });
    scene.add(splat);

    这样浏览器不会一次性下载所有 splat 数据,而是先显示一个粗略版本,再根据相机视角不断拉取细节 chunk。


    5. Spark 的渲染管线:为什么多个 Splat 能正确混合?

    普通透明物体渲染有一个经典问题:透明对象必须按从远到近排序,否则混合结果会错。3DGS 更严重,因为一个模型可能有几百万个半透明 splat。

    Spark 的基本渲染流程是三步:

  • 收集场景中所有 SplatMesh 的 splat;

  • 把它们变换到同一个坐标空间,生成一个全局 splat 列表;

  • 按当前相机视角从后往前排序;

  • 用一次 instanced draw call 渲染所有 splat。

  • 这和“每个模型自己排序、自己画一遍”完全不同。后者在多个 splat 物体相互穿插时会出错,看起来像一个物体被贴在另一个物体上。Spark 通过全局排序,让多个 3DGS 对象真正存在于同一个 3D 空间。

    每个 splat 在 GPU 上通常被画成一个朝向屏幕的 quad,也就是两个三角形。vertex shader 负责把 3D Gaussian 投影成屏幕上的椭圆范围;fragment shader 在椭圆内计算 Gaussian 衰减和 alpha,再通过硬件 blending 混合到 framebuffer。

    所以 Spark 的渲染本质是:

    SplatMesh objects
    -> global splat accumulation
    -> view-dependent sorting
    -> instanced quad rendering
    -> Gaussian fragment evaluation
    -> alpha blending


    6. Spark 2.0 的核心:LoD Splat Tree

    普通 3DGS 文件是一个扁平 splat 集合。比如 1000 万个 splat,就是真的有 1000 万个点状高斯要加载、排序、渲染。

    Spark 2.0 做了一件很关键的事:把 splat 组织成一棵 LoD Splat Tree。

    这棵树的叶子节点是原始高精度 splat,内部节点是更粗的 splat。一个父节点 splat 近似表示它下面一组子 splat 的整体形状、颜色和透明度。树根则是整个对象的极粗略表示。

    渲染时,Spark 不会固定渲染某一层,而是根据相机位置和屏幕尺寸,在树上切一刀:

    远处区域:使用较粗的父节点 splat
    近处区域:展开到更细的子节点 splat
    视野中心:分配更多细节
    视野边缘或背后:分配更少细节

    这就是 continuous LoD。它比传统“低模/中模/高模切换”更平滑,不容易出现突然 popping。

    Spark 的 LoD 遍历大致如下:

    1. 从根节点开始,把根节点放入优先队列;
    2. 计算节点投影到屏幕上的尺寸;
    3. 每次取出屏幕尺寸最大的 splat;
    4. 如果它已经足够小,或者是叶子,就加入渲染集合;
    5. 如果还能继续细分,并且没有超过 splat budget,就替换为它的子节点;
    6. 如果超过预算,就停止展开,把队列中剩余节点作为当前帧渲染集合。

    这个算法的目标不是“显示所有 splat”,而是在固定预算内显示最值得显示的 splat。

    这就是 Spark 2.0 能稳定跑大场景的关键:不管原始场景有 500 万还是 5000 万 splat,每帧真正渲染的 splat 数可以控制在一个固定预算里。


    7. splat budget:怎么调渲染质量和性能

    SparkRenderer 中比较重要的 LoD 参数是:

    const spark = new SparkRenderer({
    renderer,
    lodSplatScale: 1.0,
    lodRenderScale: 1.0,
    coneFov0: 90,
    coneFov: 120,
    coneFoveate: 0.4,
    behindFoveate: 0.2
    });

    常用调法:

    // 更清晰,但更吃性能
    spark.lodSplatScale = 1.5;

    // 更快,但更糊
    spark.lodSplatScale = 0.6;

    // 提高最小屏幕 splat 尺寸,减少浪费在微小 splat 上的预算
    spark.lodRenderScale = 2.0;

    经验上可以这样理解:

    设备建议渲染预算
    Quest 3 约 100 万以内
    Android 手机 约 100 万到 200 万
    iPhone 约 100 万到 300 万
    普通桌面 约 100 万到 500 万
    高端桌面 可能更高,但取决于场景和 GPU

    Spark 还有一个重要优化参数:

    const spark = new SparkRenderer({
    renderer,
    maxStdDev: Math.sqrt(5)
    });

    maxStdDev 控制每个 Gaussian 绘制到多远的标准差范围。默认约为 sqrt(8),质量更完整;调小可以减少 fragment 填充压力,尤其适合 VR 或移动端。

    另外,创建 THREE renderer 时建议:

    const renderer = new THREE.WebGLRenderer({
    antialias: false
    });

    因为 Gaussian Splatting 本身不是靠 MSAA 提升质量,开启 antialias 反而会增加巨大开销。


    8. RAD 文件:为什么 Spark 2.0 不只用 PLY / SPZ?

    传统 .ply 通常是 row-oriented,即一个 splat 的所有属性连续存储。它简单,但未压缩,文件巨大。

    .spz 更紧凑,通常以 column-oriented 方式存储,比如所有 position 放一起、所有 opacity 放一起,并进行量化压缩。但它的问题是:不适合随机访问和渐进式加载。你往往要等整个文件下载完,才能恢复完整 splat 数据。

    Spark 2.0 引入 .rad 格式,核心目标是:

  • 压缩;

  • 可随机访问;

  • 可渐进式流式加载;

  • 可扩展;

  • 适配 LoD Splat Tree。

  • .rad 的结构可以理解为:

    RAD header
    -> JSON metadata
    -> chunk offsets
    -> RADC chunk 0
    -> RADC chunk 1
    -> RADC chunk 2
    -> …

    每个 .radc chunk 通常包含一组 splat 数据,比如 64K splats。Spark 先加载最粗的 chunk,让画面几乎立刻出现;然后根据相机视角继续加载最有价值的细节 chunk。

    这意味着用户不需要等一个几百 MB 甚至 GB 级文件全部下载完。场景可以先以低细节显示出来,再逐步变清晰。


    9. 渐进式 Streaming:画面为什么能“先粗后细”?

    Spark 2.0 的 streaming 不是简单地按文件顺序下载,而是视角相关的优先级下载。

    流程大概是:

    1. 加载 .rad header,知道所有 chunk 的 offset、大小和空间关系;
    2. 先加载 chunk 0,得到全局粗略表示;
    3. LoD 遍历时发现某些子节点所在 chunk 还没加载;
    4. 当前帧先用父节点代替;
    5. 根据遍历访问顺序,为缺失 chunk 排优先级;
    6. Web Worker 并行下载和解码 chunk;
    7. chunk 到达后写入 GPU page;
    8. 下一帧可展开更细节点,画面变清晰。

    这套机制让 Spark 更像一个 3DGS 版的地图瓦片系统:用户看哪里,细节就优先加载哪里。


    10. GPU 虚拟内存:为什么大场景不会一次性爆显存?

    大场景 3DGS 的瓶颈不只是下载,也包括 GPU 显存。即使服务器能提供 20GB 数据,浏览器和移动 GPU 也不可能全部放进去。

    Spark 2.0 借鉴了虚拟内存思想:在 GPU 上分配一个固定大小的 splat page pool。例如桌面默认可以分配较多页,移动端少一些。每页对应固定数量的 splat,例如 64K。

    可以把它理解成:

    虚拟数据:所有 .rad 文件里的所有 chunk
    物理显存:固定数量的 GPU pages
    page table:记录哪个 virtual chunk 当前被放在哪个 GPU page
    eviction:不重要的 chunk 被换出,重要 chunk 被换入

    当相机移动时,Spark 根据 LoD 遍历结果决定哪些 chunk 重要。重要 chunk 不在 GPU 中,就请求下载或解码;GPU page 满了,就用类似 LRU 的策略把低优先级 chunk 换出。

    这就是为什么 Spark 可以浏览远大于 GPU 显存容量的 3DGS 世界。


    11. Tiny-LoD 与 Bhatt-LoD:LoD 树怎么生成?

    Spark 2.0 提供两类 LoD 构建算法。

    Tiny-LoD

    Tiny-LoD 更快,适合浏览器端或快速预览。它把空间划分成网格,把落在同一格内的 splat 合并成更粗的 splat,再逐级向上合并,直到形成根节点。

    它的优点是:

  • 快;

  • 内存占用相对友好;

  • 可以在 Web Worker 里临时生成;

  • 适合交互式 on-demand LoD。

  • 缺点是质量不如更慢的离线方法。

    Bhatt-LoD

    Bhatt-LoD 更适合离线构建。它基于 splat 的形状、颜色和统计相似度来决定哪些 splat 更应该被合并。它通常能生成更高质量的 LoD 树,但速度更慢。

    实际项目建议:

    # 快速测试
    npm run build-lod — ./model.spz –quick

    # 正式上线
    npm run build-lod — ./model.spz –quality –rad-chunked

    如果你做的是产品级 Web 展示、数字孪生、大场景浏览、VR 展示,优先使用 –quality –rad-chunked。


    12. 一个推荐的生产级加载方案

    对于普通小模型:

    const splat = new SplatMesh({
    url: "/assets/object.spz"
    });
    scene.add(splat);

    对于中型模型,需要 LoD 但不一定 streaming:

    npm run build-lod — ./object.spz –quality

    const splat = new SplatMesh({
    url: "/assets/object-lod.rad"
    });
    scene.add(splat);

    对于大型场景或多对象世界:

    npm run build-lod — ./scene/*.spz –quality –rad-chunked

    const spark = new SparkRenderer({
    renderer,
    lodSplatScale: 1.0,
    numLodFetchers: 3,
    maxStdDev: Math.sqrt(8)
    });
    scene.add(spark);

    const cityBlock = new SplatMesh({
    url: "/assets/city-block-lod.rad",
    paged: true
    });
    scene.add(cityBlock);

    const interior = new SplatMesh({
    url: "/assets/interior-lod.rad",
    paged: true
    });
    interior.position.set(20, 0, -10);
    scene.add(interior);

    Spark 会把多个 .rad 对象的 LoD 遍历和 page 管理统一起来,而不是每个对象各自为政。


    13. 部署注意事项

    1. 静态资源路径

    确保 .rad 和 .radc 文件在同一目录,并且命名不要被构建工具 hash 掉,除非你同时改写 Spark 可访问的路径。

    推荐放在:

    public/assets/splats/

    然后用:

    new SplatMesh({
    url: "/assets/splats/scene-lod.rad",
    paged: true
    });

    2. CDN 缓存

    .radc 是稳定二进制 chunk,非常适合 CDN 缓存。可以设置较长缓存:

    Cache-Control: public, max-age=31536000, immutable

    如果文件会更新,建议文件名带版本号:

    scene-v003-lod.rad
    scene-v003-lod-0.radc

    3. CORS

    如果 splat 文件放在另一个域名,需要设置:

    Access-Control-Allow-Origin: *

    或者指定你的前端域名。

    4. MIME 类型

    .rad 和 .radc 可以当作二进制文件服务:

    application/octet-stream

    5. 不要错误二次压缩

    .radc 内部已经有压缩数据。服务器/CDN 不应再以错误方式强制 gzip、br 或转码,避免破坏 range / chunk 请求行为。


    14. 常见问题

    为什么我看不到模型?

    检查:

    const spark = new SparkRenderer({ renderer });
    scene.add(spark);

    Spark 2.0 必须显式加入 SparkRenderer。

    为什么模型方向不对?

    很多 3DGS 数据来自 OpenCV 坐标系,WebGL/THREE.js 坐标可能不同。可以尝试:

    splat.quaternion.set(1, 0, 0, 0);

    或手动调整 rotation。

    为什么移动端很卡?

    先调低:

    spark.lodSplatScale = 0.5;
    spark.maxStdDev = Math.sqrt(5);
    renderer.setPixelRatio(1);

    并确认:

    new THREE.WebGLRenderer({ antialias: false });

    为什么大场景加载慢?

    不要直接加载超大 .ply 或 .spz。应该离线构建:

    npm run build-lod — ./scene.spz –quality –rad-chunked

    然后:

    new SplatMesh({
    url: "/assets/scene-lod.rad",
    paged: true
    });


    15. 总结

    Spark 2.0 的核心不是“更会重建”,而是“更会渲染”。

    它把 3DGS Web 渲染拆成了几个关键系统:

  • SplatMesh:像 THREE.Mesh 一样组织 3DGS 对象;

  • SparkRenderer:接管 splat 收集、排序、渲染;

  • 全局排序:让多个 splat 对象正确透明混合;

  • LoD Splat Tree:让大场景按视角选择合适细节;

  • RAD / RADC:让 3DGS 数据可压缩、可随机访问、可流式加载;

  • SplatPager:用固定 GPU page pool 管理超大虚拟 splat 数据;

  • Web Worker + Wasm:把 LoD、排序、下载、解码放到后台,避免阻塞主线程。

  • 如果你的目标是把 3DGS 资产做成 Web 端可交互展示、数字空间浏览、产品展示、VR 场景或大规模世界可视化,Spark 2.0 是目前非常值得研究的方案。它不是一个单纯 viewer,而是一个面向浏览器的大规模 3DGS 渲染系统。

    赞(0)
    未经允许不得转载:171主机测评 » Spark 2.0:面向 Web 的 3DGS 可视化与大场景渲染平台详解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址