编辑
2025-05-26
cesium
0
请注意,本文编写于 43 天前,最后修改于 43 天前,其中某些信息可能已经过时。

目录

Cesium Gaussian Splat Primitive 渲染流程详细解析
第一部分:模块导入和数据结构分析
导入模块分析(第1-33行)
构造函数详细解析(第36-67行)
坐标系转换矩阵(第58-62行)
第二部分:属性访问器分析
ready属性(第73-77行)
modelMatrix属性(第87-98行)
第三部分:瓦片加载和变换处理
瓦片加载回调(第130-149行)
瓦片变换算法(第192-227行)
位置变换循环(第210-226行)
第四部分:数据合并和优化
点云数据合并(第229-278行)
纹理生成算法(第280-326行)
第五部分:渲染管线设置
渲染状态配置(第328-344行)
着色器宏定义(第346-364行)
着色器属性和Uniform定义(第366-387行)
几何体创建和顶点数组(第420-457行)
第六部分:着色器算法深度解析
顶点着色器 PrimitiveGaussianSplatVS.glsl
片段着色器 PrimitiveGaussianSplatFS.glsl
第七部分:深度排序和渲染循环
主更新循环 update (第481-569行)
总结:完整的渲染流程
1. 数据加载和预处理 (主要在 onTileLoaded 和 update 的瓦片处理阶段)
2. 实时更新和排序 (在每帧的 update 循环中)
3. 构建和提交绘制命令 (在 update 和排序完成回调中)
4. GPU绘制阶段 (由Cesium渲染管线执行命令列表)

Cesium Gaussian Splat Primitive 渲染流程详细解析

本文档详细解析了 cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.js 文件中实现的高斯点云渲染的完整流程,包括数据结构、瓦片处理、渲染管线配置以及顶点和片段着色器算法。

第一部分:模块导入和数据结构分析

导入模块分析(第1-33行)

1:33:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.js
import Frozen from "../Core/Frozen.js"; // 冻结对象工具,防止意外修改 import Matrix4 from "../Core/Matrix4.js"; // 4x4矩阵运算,用于3D变换 import ModelUtility from "./Model/ModelUtility.js"; // 模型工具类,处理3D模型操作 import GaussianSplatSorter from "./GaussianSplatSorter.js"; // 高斯点云排序器,深度排序用 import GaussianSplatTextureGenerator from "./Model/GaussianSplatTextureGenerator.js"; // 纹理生成器 // ... other imports ...

每个导入都有特定用途:

  • Frozen: 创建不可变对象,保证数据安全。
  • Matrix4: 核心的4x4矩阵类,处理所有3D变换(旋转、平移、缩放、投影)。
  • ModelUtility: 提供模型相关工具函数,比如坐标系转换、面剔除等。
  • GaussianSplatSorter: 这是关键组件!用于按深度排序高斯点,确保正确的透明度混合。
  • GaussianSplatTextureGenerator: 将高斯点数据打包成GPU纹理格式,提高渲染效率。
  • 其他导入也为渲染管线提供了必要的工具和状态管理。

构造函数详细解析(第36-67行)

36:67:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.js
function GaussianSplatPrimitive(options) { options = options ?? Frozen.EMPTY_OBJECT; // 如果没有传入参数,使用空对象 // 高斯点云核心数据数组 this._positions = undefined; // Float32Array - 每个点的3D位置 [x,y,z,x,y,z,...] this._rotations = undefined; // Float32Array - 每个点的四元数旋转 [qx,qy,qz,qw,...] this._scales = undefined; // Float32Array - 每个点的3D缩放 [sx,sy,sz,...] this._colors = undefined; // Uint8Array - 每个点的RGBA颜色 [r,g,b,a,...] this._indexes = undefined; // Uint32Array - 排序后的点索引 this._numSplats = 0; // 总点数量 // ... other properties ...

这些数据结构是高斯点云渲染的核心

  • _positions: 每个高斯椭球的中心点在模型空间中的位置。
  • _rotations: 每个椭球的旋转方向,使用四元数表示。
  • _scales: 每个椭球在三个主轴上的缩放因子。
  • _colors: 每个椭球的颜色(RGB)和透明度(Alpha)。
  • _indexes: 经过深度排序后,存储高斯点的渲染顺序索引。
  • _numSplats: 当前加载并准备渲染的高斯点总数。

坐标系转换矩阵(第58-62行)

58:62:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.js
this._axisCorrectionMatrix = ModelUtility.getAxisCorrectionMatrix( Axis.Y, // 源坐标系的上方向 (通常是原始数据格式的上方向) Axis.X, // 目标坐标系的上方向 (Cesium使用X向上) new Matrix4(), );

这是关键的坐标系转换!不同的3D软件和数据格式使用不同的坐标系(例如,Y向上 vs Z向上,左手坐标系 vs 右手坐标系)。

  • ModelUtility.getAxisCorrectionMatrix 根据源和目标坐标系的上方向计算出转换矩阵。
  • 这个矩阵用于确保高斯点云数据能够正确地从其原始坐标系变换到Cesium所使用的坐标系,保证模型方向正确。

第二部分:属性访问器分析

ready属性(第73-77行)

73:77:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.js
ready: { get: function () { return this._ready; // 返回是否准备好渲染 }, }

这个属性是一个标志,指示 GaussianSplatPrimitive 是否已经完成了所有必要的数据加载和预处理(例如,瓦片加载、数据合并、纹理生成),可以开始进行渲染。渲染引擎在绘制前会检查此属性。

modelMatrix属性(第87-98行)

87:98:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.js
modelMatrix: { get: function () { return this.modelMatrix; // 注意:这里getter内部调用了自己,可能是笔误,应该返回 this._modelMatrix }, set: function (modelMatrix) { //>>includeStart('debug', pragmas.debug); Check.typeOf.object("modelMatrix", modelMatrix); // 调试模式下进行类型检查 //>>includeEnd('debug'); this._modelMatrix = Matrix4.clone(modelMatrix, this.modelMatrix); // 深拷贝传入的矩阵并赋值给内部属性 }, }

modelMatrix是3D渲染中最重要的变换矩阵

  • 它定义了高斯点云在世界空间中的位置、旋转和缩放。
  • Getter: 用于获取当前的高斯点云模型变换矩阵。代码中的 return this.modelMatrix; 可能是一个循环引用,正确的应该是 return this._modelMatrix;
  • Setter: 用于设置新的模型变换矩阵。使用 Matrix4.clone 进行深拷贝是为了防止外部修改传入的矩阵对象影响内部状态。

第三部分:瓦片加载和变换处理

瓦片加载回调(第130-149行)

130:149:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.js
GaussianSplatPrimitive.prototype.onTileLoaded = function (tile) { console.log(`Tile loaded: ${tile._contentResource.url}`); // 打印加载的瓦片URL if (tile._spzVisited) { // 检查瓦片是否已经被处理过 return; } if (this._rootTransform === undefined) { // 如果是加载的第一个瓦片,设置其变换为根变换 this._rootTransform = tile.content._tile.computedTransform; } else { // 后续瓦片需要将其数据变换到根瓦片的坐标系下 this.transformTile(tile); } tile._spzVisited = true; // 标记为已访问,防止重复处理 };

这是基于3D Tiles的分层渲染的核心逻辑:

  • 高斯点云数据量通常很大,被分割成多个瓦片(tiles)。
  • onTileLoaded 是当一个瓦片加载完成后触发的回调函数。
  • 第一个加载的瓦片(通常是根瓦片)的变换矩阵被用作整个点云的根变换 (_rootTransform)。
  • 后续加载的瓦片需要调用 transformTile 方法,将其内部的高斯点数据从瓦片本地坐标系变换到这个统一的根坐标系下,以确保所有瓦片的数据都在同一个空间中。

瓦片变换算法(第192-227行)

192:227:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.js
GaussianSplatPrimitive.prototype.transformTile = function (tile) { const transform = tile.computedTransform; // 瓦片在3D Tileset中的计算变换矩阵 const gsplatData = tile.content._gsplatData; // 瓦片内容中的高斯点数据 // 构建完整的变换矩阵:将瓦片本地坐标系变换到世界坐标系(考虑坐标系修正和世界变换) let modelMatrix = Matrix4.multiply( transform, // 瓦片自身的变换 this._axisCorrectionMatrix, // 应用坐标系修正 new Matrix4(), ); modelMatrix = Matrix4.multiplyTransformation( modelMatrix, tile._content._worldTransform, // 应用瓦片内容的额外世界变换 new Matrix4(), ); const inverseRoot = Matrix4.inverse(transform, new Matrix4()); // 计算瓦片自身变换的逆矩阵 // ... position transformation loop ... };

这是将瓦片数据变换到根坐标系的复杂坐标系变换链

  1. 瓦片变换 (transform): 瓦片在整个3D Tileset层级中的相对位置、旋转、缩放。
  2. 坐标系修正 (_axisCorrectionMatrix): 应用之前计算的坐标系修正矩阵,将数据从原始格式的坐标系转换到Cesium使用的坐标系。
  3. 世界变换 (tile._content._worldTransform): 瓦片内容可能还包含一个额外的世界变换矩阵,也需要应用。 通过连续矩阵乘法,得到一个将瓦片本地坐标系中的点变换到世界坐标系的完整 modelMatrix
  4. 根变换的逆矩阵 (inverseRoot): 虽然这里计算的是瓦片自身的逆矩阵,但其目的是将世界空间中的点变换回这个特定瓦片加载时所使用的坐标系(即根瓦片的坐标系,因为瓦片加载时 _rootTransform 被设置为第一个瓦片的 computedTransform)。

位置变换循环(第210-226行)

210:226:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.js
const positions = ModelUtility.getAttributeBySemantic( gsplatData, VertexAttributeSemantic.POSITION, // 获取位置属性,使用语义枚举查找 ).typedArray; // 获取位置数据的 TypedArray for (let i = 0; i < positions.length; i += 3) { // 遍历位置数组,每3个元素代表一个点的(x,y,z)坐标 const wpos = Matrix4.multiplyByPoint( // 将瓦片本地位置变换到世界空间 modelMatrix, new Cartesian3(positions[i], positions[i + 1], positions[i + 2]), // 原始(瓦片本地)位置 new Cartesian3(), // 存储结果的临时变量 ); const position = Matrix4.multiplyByPoint( // 将世界空间位置变换回根瓦片本地坐标系 inverseRoot, // 瓦片自身变换的逆矩阵 (实际上是将点从世界空间变换到根瓦片所在的空间) wpos, // 世界空间位置 new Cartesian3(), // 存储结果的临时变量 ); // 就地更新位置数组,存储变换到根瓦片坐标系后的位置 positions[i] = position.x; positions[i + 1] = position.y; positions[i + 2] = position.z; }

这个循环对每个高斯点的位置进行两次矩阵变换:

  1. 变换到世界空间: worldPosition = modelMatrix * localPosition
  2. 变换到根坐标系: rootLocalPosition = inverseRoot * worldPosition。这里 inverseRoot 是瓦片自身变换的逆矩阵,乘以世界位置相当于将点从世界空间拉回到该瓦片在加载时所处的空间(即根瓦片空间,因为后续所有瓦片都被拉到这里)。
  3. 就地更新数组: 直接修改原始数据,将变换后的位置存储回 positions 数组,节省内存并避免创建新的数组。

第四部分:数据合并和优化

点云数据合并(第229-278行)

229:278:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.js
GaussianSplatPrimitive.prototype.pushSplats = function (attributes) { // 位置数据合并 if (this._positions === undefined) { this._positions = attributes.positions; // 第一次加载瓦片,直接赋值 } else { const newPositions = new Float32Array( // 创建一个更大的数组,足以容纳现有和新数据 this._positions.length + attributes.positions.length, ); newPositions.set(this._positions); // 复制现有数据到新数组开头 newPositions.set(attributes.positions, this._positions.length); // 追加新数据到现有数据之后 this._positions = newPositions; // 替换旧数组为新数组 } // 对 _scales, _rotations, _colors 进行类似合并操作 // ... };

这是高效的数组合并策略

  • 当加载新的瓦片时,需要将新瓦片的高斯点数据合并到 GaussianSplatPrimitive 持有的总数据数组中。
  • 避免频繁的数组重分配和拷贝(虽然这里还是有拷贝,但比单个元素 push 效率高)。
  • 使用 TypedArray (Float32Array, Uint8Array) 提供最佳性能,它们是固定大小的,因此合并时需要创建新的大数组。
  • 对每种属性(位置、缩放、旋转、颜色)都进行相同的合并操作。

纹理生成算法(第280-326行)

280:326:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.js
GaussianSplatPrimitive.generateSplatTexture = function (primitive, frameState) { primitive._gaussianSplatTexturePending = true; // 标记纹理生成正在进行中 // 调用 GaussianSplatTextureGenerator 在 Worker 线程中生成纹理数据 const promise = GaussianSplatTextureGenerator.generateFromAttrs({ attributes: { positions: new Float32Array(primitive._positions), // 复制合并后的位置数据 scales: new Float32Array(primitive._scales), // 复制合并后的缩放数据 rotations: new Float32Array(primitive._rotations), // 复制合并后的旋转数据 colors: new Uint8Array(primitive._colors), // 复制合并后的颜色数据 }, count: primitive._numSplats, // 总点数量 }); if (promise === undefined) { // Worker 可能未准备好或发生错误 primitive._gaussianSplatTexturePending = false; return; } // 异步处理纹理生成结果 promise .then((splatTextureData) => { // 使用生成的数据创建 GPU 纹理 const splatTex = new Texture({ context: frameState.context, // WebGL上下文 source: { width: splatTextureData.width, // 纹理宽度 height: splatTextureData.height, // 纹理高度 arrayBufferView: splatTextureData.data, // 原始纹理数据 }, preMultiplyAlpha: false, // 不进行预乘Alpha,Alpha值将用于高斯衰减计算 skipColorSpaceConversion: true, // 跳过颜色空间转换 pixelFormat: PixelFormat.RGBA_INTEGER, // 像素格式:RGBA整数 pixelDatatype: PixelDatatype.UNSIGNED_INT, // 像素数据类型:32位无符号整数 flipY: false, // 不翻转Y轴 sampler: Sampler.NEAREST, // 采样器:最近邻,用于精确读取数据 }); // 更新 Primitive 状态 primitive._gaussianSplatTexture = splatTex; primitive._hasGaussianSplatTexture = true; primitive._needsGaussianSplatTexture = false; primitive._gaussianSplatTexturePending = false; // 初始化索引数组,初始顺序就是原始顺序 primitive._indexes = new Uint32Array([ ...Array(primitive._numSplats).keys(), // 生成 0 到 _numSplats-1 的序列 ]); }) .catch((error) => { // 处理生成错误 console.error("Error generating Gaussian splat texture:", error); primitive._gaussianSplatTexturePending = false; }); };

纹理生成是关键优化步骤

  • 将所有高斯点的属性数据(位置、缩放、旋转、颜色)打包到一个或多个GPU纹理中。
  • 使用 GaussianSplatTextureGenerator,它通常在 Web Worker 线程中运行,进行耗时的 CPU 计算,避免阻塞主线程。
  • 数据打包: 高斯点的多个属性被编码到一个 RGBA_INTEGER, UNSIGNED_INT 格式的纹理中。例如,位置和协方差(从缩放旋转计算)可能存储在不同的像素中。这种格式允许存储32位整数,可以通过位操作解码出原始的浮点或整数属性。
  • GPU可以通过纹理采样快速访问大量数据,比从传统的 Vertex Buffer 读取更灵活高效,特别是对于每个点属性数量较多且需要随机访问的情况。
  • 异步处理: 纹理生成是一个异步操作,通过 Promise 进行管理。

第五部分:渲染管线设置

渲染状态配置(第328-344行)

328:344:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.js
GaussianSplatPrimitive.buildGSplatDrawCommand = function ( primitive, frameState, ) { // ... (initialization of renderResources, shaderBuilder, renderStateOptions) ... renderStateOptions.cull.enabled = false; // 禁用面剔除 renderStateOptions.depthMask = false; // 禁用深度缓冲写入 renderStateOptions.depthTest.enabled = false; // 禁用深度测试 renderStateOptions.blending = BlendingState.PRE_MULTIPLIED_ALPHA_BLEND; // 设置Alpha混合模式为预乘Alpha混合 renderResources.alphaOptions.pass = Pass.GAUSSIAN_SPLATS; // 指定渲染通道为高斯点云通道 // ... };

这些设置对透明渲染至关重要

  • 禁用面剔除 (cull.enabled = false): 高斯椭球是半透明的,需要从各个角度可见,面剔除会移除背面或正面,导致渲染不完整。
  • 禁用深度缓冲写入 (depthMask = false): 透明物体不应该写入深度缓冲,否则会遮挡后面应该可见的物体。
  • 禁用深度测试 (depthTest.enabled = false): 由于禁用了深度写入,并且高斯点云的透明度混合依赖于从后往前的渲染顺序(通过CPU排序实现),因此通常需要禁用标准的深度测试。渲染顺序由CPU端的排序决定。
  • 预乘Alpha混合 (blending = BlendingState.PRE_MULTIPLIED_ALPHA_BLEND): 高斯点云的透明度渲染需要依赖Alpha混合。预乘Alpha混合是一种常用的透明度混合方式,可以正确处理颜色和透明度。
  • 渲染通道 (Pass.GAUSSIAN_SPLATS): 指定这些绘制命令属于高斯点云渲染通道,有助于渲染管线组织和状态管理。

着色器宏定义(第346-364行)

346:364:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.js
shaderBuilder.addDefine( "HAS_GAUSSIAN_SPLATS", undefined, ShaderDestination.BOTH, // 在顶点和片段着色器中都定义 ); shaderBuilder.addDefine( "HAS_SPLAT_TEXTURE", undefined, ShaderDestination.BOTH, // 在顶点和片段着色器中都定义 ); if (primitive.debugShowBoundingVolume) { // 如果开启调试显示边界体积 shaderBuilder.addDefine( "DEBUG_BOUNDING_VOLUMES", undefined, ShaderDestination.BOTH, // 在顶点和片段着色器中都定义 ); }

这些是用于着色器程序中的条件编译宏

  • HAS_GAUSSIAN_SPLATS: 表示着色器是用于高斯点云渲染的。
  • HAS_SPLAT_TEXTURE: 表示着色器将从高斯点数据纹理中读取属性。这个宏在着色器代码中用于启用相关的纹理采样逻辑。
  • DEBUG_BOUNDING_VOLUMES: 在调试模式下开启,用于在着色器中添加可视化边界体积的代码。 通过宏定义,可以根据需要编译不同的着色器变体,优化性能和功能。ShaderDestination.BOTH 表示这个宏在顶点和片段着色器中都有效。

着色器属性和Uniform定义(第366-387行)

366:387:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.js
shaderBuilder.addAttribute("vec2", "a_screenQuadPosition"); // 顶点属性:屏幕空间四边形顶点坐标 shaderBuilder.addAttribute("float", "a_splatIndex"); // 顶点属性:当前实例对应的高斯点索引 shaderBuilder.addVarying("vec4", "v_splatColor"); // Varying变量:从顶点着色器传递给片段着色器的颜色 shaderBuilder.addVarying("vec2", "v_vertPos"); // Varying变量:从顶点着色器传递给片段着色器的四边形局部坐标 shaderBuilder.addUniform( "highp usampler2D", "u_splatAttributeTexture", ShaderDestination.VERTEX, // Uniform变量:高斯点属性数据纹理,在顶点着色器中使用 ); shaderBuilder.addUniform( "float", "u_splatScale", ShaderDestination.VERTEX, // Uniform变量:高斯点云全局缩放因子,在顶点着色器中使用 ); // UniformMap 定义了如何获取 Uniform 变量的值 const uniformMap = renderResources.uniformMap; uniformMap.u_splatScale = function () { return primitive.splatScale; // 返回 Primitive 实例的 splatScale 属性值 }; uniformMap.u_splatAttributeTexture = function () { return primitive._gaussianSplatTexture; // 返回 Primitive 实例持有的高斯点数据纹理 };

这定义了GPU着色器的输入和输出接口

  • 属性(Attribute): a_screenQuadPosition 是屏幕空间四边形的四个顶点坐标 (-1到1),a_splatIndex 是当前绘制的实例化四边形对应的高斯点在总数据数组中的索引。这些数据是每个顶点/实例不同的。
  • Varying: v_splatColorv_vertPos 是从顶点着色器计算并插值后传递给片段着色器的数据。v_vertPos 包含了四边形顶点的局部坐标,用于片段着色器中计算高斯衰减。v_splatColor 传递了该高斯点的颜色信息。
  • Uniform: u_splatAttributeTexture 是存储所有高斯点属性的纹理,u_splatScale 是一个全局缩放因子。这些数据在一次绘制调用中对所有顶点/片段都是相同的。
  • uniformMap: 这是一个JavaScript对象,其属性名对应着色器中的 Uniform 变量名,属性值是返回该 Uniform 当前值的函数。Cesium渲染管线在绘制前会调用这些函数来设置 Uniform 的值。

几何体创建和顶点数组(第420-457行)

420:438:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.js
const geometry = new Geometry({ attributes: { screenQuadPosition: new GeometryAttribute({ componentDatatype: ComponentDatatype.FLOAT, componentsPerAttribute: 2, // 每个顶点2个分量(x,y) values: [-1, -1, 1, -1, 1, 1, -1, 1], // 屏幕空间四边形顶点坐标 name: "_SCREEN_QUAD_POS", variableName: "screenQuadPosition", // 对应着色器中的属性名 }), splatIndex: { ...idxAttr, variableName: "splatIndex" }, // 高斯点索引属性 }, primitiveType: PrimitiveType.TRIANGLE_STRIP, // 使用三角形带绘制 }); const vertexArray = VertexArray.fromGeometry({ context: frameState.context, // WebGL上下文 geometry: geometry, // 上面创建的几何体 attributeLocations: splatQuadAttrLocations, // 属性在Shader中的位置 (由 ShaderBuilder 配置) bufferUsage: BufferUsage.STATIC_DRAW, // 数据使用方式:静态绘制 interleave: false, // 不交错存储属性数据 });

这是Instance(实例化)渲染的精巧设计

  • 基础几何体: 创建一个非常简单的几何体,只包含一个覆盖屏幕空间的单位四边形(由4个顶点组成的 Triangle Strip)。
  • Instance: 每个高斯点被渲染为这个四边形的一个"实例"。这意味着GPU会为每个高斯点(由 a_splatIndex 指定)执行一次顶点着色器,但绘制的是同一个基础四边形。
  • Triangle Strip: 使用4个顶点以三角形带的形式定义四边形,这是最高效的绘制四边形的方式(只需要4个顶点而不是6个)。
  • screenQuadPosition: 提供了四边形顶点的屏幕空间坐标(范围 [-1,-1] 到 [1,1]),用于在顶点着色器中根据高斯点属性将其拉伸和定位到正确的大小和位置。
  • splatIndex: 这个属性通过实例化机制,为每个绘制的四边形实例提供其对应的高斯点在数据纹理中的索引。idxAttr.instanceDivisor = 1; 意味着每个实例(而不是每个顶点)都会推进一次 splatIndex
  • VertexArray: 封装了几何体数据和属性布局,供GPU绘制使用。

第六部分:着色器算法深度解析

现在我们来分析最核心的着色器算法,它定义了高斯点如何被渲染成屏幕上的2D椭圆。

顶点着色器 PrimitiveGaussianSplatVS.glsl

这个着色器的主要任务是计算每个高斯点在屏幕上的位置和大小,以及将其颜色等信息传递给片段着色器。

1:99:cesium/packages/engine/Source/Shaders/PrimitiveGaussianSplatVS.glsl
// ... calcCovVectors function ... // 协方差计算函数 void main() { // 1. 获取当前高斯点的索引 uint texIdx = uint(a_splatIndex); // 从顶点属性获取当前实例化四边形对应的高斯点索引 // 2. 计算在属性纹理中的坐标,读取位置和协方差数据 // 属性纹理的布局:每个高斯点的数据分布在多行(取决于打包方式),每行有多个像素。 // 这里假设位置和协方差数据打包在连续的两个 RGBA_UNSIGNED_INT 像素中。 // 计算位置数据所在的像素坐标 ivec2 posCoord = ivec2((texIdx & 0x3ffu) << 1, texIdx >> 10); // 使用位运算解码索引到纹理坐标 (x, y) // 从属性纹理采样获取位置数据 (以 RGBA_UNSIGNED_INT 格式存储的 x, y, z, w,w通常无用) vec4 splatPositionRaw = texelFetch(u_splatAttributeTexture, posCoord, 0); // 将存储在无符号整数中的浮点数解码出来 (floatBitsToUint 的逆操作) vec4 splatPosition = vec4( uintBitsToFloat(uvec4(splatPositionRaw)) ); // 3. 将高斯点位置从模型空间变换到视图空间和裁剪空间 vec4 splatViewPos = czm_modelView * vec4(splatPosition.xyz, 1.0); // 模型视图变换到视图空间 vec4 clipPosition = czm_projection * splatViewPos; // 投影变换到裁剪空间 (透视除法前) // 4. 基础视锥剔除 (基于高斯点中心位置) float clip = 1.2 * clipPosition.w; // 考虑一点裕量 if (clipPosition.z < -clip || clipPosition.x < -clip || clipPosition.x > clip || clipPosition.y < -clip || clipPosition.y > clip) { gl_Position = vec4(0.0, 0.0, 2.0, 1.0); // 将点放置在裁剪空间外丢弃 return; // 提前退出,不进行后续计算 } // 5. 计算协方差数据所在的纹理坐标,读取协方差数据 // 假设协方差数据紧随位置数据存储,所以 x 坐标加 1u ivec2 covCoord = ivec2(((texIdx & 0x3ffu) << 1) | 1u, texIdx >> 10); // 从属性纹理采样获取协方差和颜色数据 (以 RGBA_UNSIGNED_INT 格式存储) uvec4 covariance = uvec4(texelFetch(u_splatAttributeTexture, covCoord, 0)); // 6. 将高斯点中心位置作为基础的 gl_Position gl_Position = clipPosition; // 7. 从协方差数据中解码出原始的 3x3 协方差矩阵 Vrk // 假设协方差数据存储在 covariance.xyz 中,每个分量存储了两个 float16 打包的数据 vec2 u1 = unpackHalf2x16(covariance.x) ; // 解码 covariance.x (包含了 Vrk[0][0], Vrk[0][1]) vec2 u2 = unpackHalf2x16(covariance.y); // 解码 covariance.y (包含了 Vrk[0][2], Vrk[1][1]) vec2 u3 = unpackHalf2x16(covariance.z); // 解码 covariance.z (包含了 Vrk[1][2], Vrk[2][2]) // 重构 3x3 协方差矩阵 Vrk (对称矩阵) mat3 Vrk = mat3(u1.x, u1.y, u2.x, // Row 0 u1.y, u2.y, u3.x, // Row 1 u2.x, u3.x, u3.y); // Row 2 // 8. 应用全局缩放因子到协方差矩阵 (影响高斯椭球的大小) Vrk *= u_splatScale; // 9. 调用 calcCovVectors 函数计算投影到屏幕空间的 2x2 协方差矩阵对应的特征向量和特征值平方根 vec4 covVectors = calcCovVectors(splatViewPos.xyz, Vrk); // 10. 进一步剔除过小的点 if (dot(covVectors.xy, covVectors.xy) < 4.0 && dot(covVectors.zw, covVectors.zw) < 4.0) { gl_Position = discardVec; // 将点放置到丢弃位置 return; // 提前退出 } // 11. 根据屏幕空间四边形的角点 (`corner`) 和投影的协方差信息,计算最终顶点在屏幕上的位置 // `corner` 是 [-1,-1], [1,-1], [1,1], [-1,1] 中的一个点,由 gl_VertexID 决定 vec2 corner = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2) - 1.; // 计算屏幕空间偏移量: (corner.x * 第一特征轴方向 * 第一特征值平方根 + corner.y * 第二特征轴方向 * 第二特征值平方根) // `covVectors.xy` 存储了 sqrt(2*lambda1) * diagonalVector // `covVectors.zw` 存储了 sqrt(2*lambda2) * orthogonalVector // (corner.x * covVectors.xy + corner.y * covVectors.zw) 构成了在屏幕空间椭圆坐标系下的偏移 // 除以 czm_viewport.zw (视口宽度和高度),将屏幕像素单位转换为 NDC 单位 // 乘以 gl_Position.w 进行透视校正 gl_Position += vec4((corner.x * covVectors.xy + corner.y * covVectors.zw) / czm_viewport.zw * gl_Position.w, 0, 0); // 12. 钳制 gl_Position.z,防止出现深度问题 gl_Position.z = clamp(gl_Position.z, -abs(gl_Position.w), abs(gl_Position.w)); // 13. 将信息传递给片段着色器 v_vertPos = corner; // 将屏幕空间四边形局部坐标传递给片段着色器,用于高斯衰减计算 // 从 covariance.w 解码颜色和Alpha (RGBA 8位打包在一个 Uint32 中) v_splatColor = vec4( covariance.w & 0xffu, // R 分量 (低 8 位) (covariance.w >> 8) & 0xffu, // G 分量 (次 8 位) (covariance.w >> 16) & 0xffu, // B 分量 (第三 8 位) (covariance.w >> 24) & 0xffu // A 分量 (最高 8 位) ) / 255.0; // 将 0-255 的整数值转换为 0.0-1.0 的浮点值 // 14. 调试模式下降低Alpha值 #ifdef DEBUG_BOUNDING_VOLUMES v_splatColor.a *= 0.08; // 使边界框更透明,方便观察 #endif }

calcCovVectors 函数 (第1-46行) 详细解析

这个函数是高斯点云渲染中最关键的数学部分。它负责将3D空间中的高斯椭球投影到2D屏幕空间,并计算出表示投影后2D椭圆形状和方向的向量。

  1. 输入: viewPos (高斯点中心在视图空间的位置), Vrk (高斯点在模型空间的 3x3 协方差矩阵)。
  2. 计算雅可比矩阵 J (第2-11行): 这是从视图空间到屏幕空间的透视投影的局部导数矩阵。它描述了视图空间中微小的3D变化如何映射到屏幕空间的2D变化。focal 是相机的焦距,t.z 是高斯点在视图空间的深度。雅可比矩阵用于将视图空间的协方差矩阵投影到屏幕空间。
  3. 提取视图矩阵的纯旋转部分 Rs (第13-26行): czm_modelView 是模型视图矩阵,它包含旋转和平移。为了正确变换协方差矩阵,需要将其分解为旋转和缩放,并只使用旋转部分。这里通过计算矩阵列向量的长度来获取缩放,然后将列向量除以其长度得到纯旋转矩阵 Rs
  4. 视图空间协方差 Vrk_view (第29行): Vrk_view = Rs * Vrk * transpose(Rs)。将高斯点在模型空间的协方差矩阵 Vrk 变换到视图空间。这是协方差矩阵的标准变换公式。
  5. 屏幕空间协方差 cov (第31行): cov = transpose(J) * Vrk_view * J。将视图空间的协方差矩阵 Vrk_view 通过雅可比矩阵 J 投影到屏幕空间。结果 cov 是一个 2x2 的矩阵,表示投影后2D椭圆的协方差。
  6. 特征值分解 (第33-45行): 对 2x2 的屏幕空间协方差矩阵 cov 进行特征值分解。
    • 特征值 λ1 和 λ2 决定了投影后椭圆在主轴方向上的大小。
    • 特征向量决定了椭圆主轴的方向。
    • 代码中使用了快速计算 2x2 对称矩阵特征值的方法。
    • +0.3: 加一个小的常数是为了数值稳定性,防止协方差矩阵出现奇异性。
    • max(mid - radius, 0.1): 确保最小特征值不小于0.1,防止椭圆尺寸过小导致问题。
  7. 计算返回的向量 (第44-46行): 返回一个 vec4。
    • covVectors.xy 存储了 sqrt(2 * lambda1) 乘以与第一个特征值对应的归一化特征向量。
    • covVectors.zw 存储了 sqrt(2 * lambda2) 乘以与第二个特征值对应的归一化特征向量(第二个特征向量与第一个正交)。
    • min(..., 1024.0): 限制高斯点在屏幕上的最大尺寸,防止过大影响性能。
    • 这些向量将在 main 函数中用于将屏幕空间四边形的角点拉伸和旋转到正确的位置,形成覆盖投影椭圆的四边形。

片段着色器 PrimitiveGaussianSplatFS.glsl

这个着色器的主要任务是计算像素的颜色和透明度。

1:10:cesium/packages/engine/Source/Shaders/PrimitiveGaussianSplatFS.glsl
void main() { // 1. 计算当前片段在屏幕空间四边形局部坐标系中到中心的距离的平方 // v_vertPos 是从顶点着色器插值得到的屏幕空间四边形局部坐标 (-1到1) mediump float A = dot(v_vertPos, v_vertPos); // 2. 剔除超出高斯椭圆边界的片段 // 这里的"边界"是由 v_vertPos > 1.0 定义的单位圆,对应于高斯函数 exp(-distance^2 * scale) 中 distance^2 = 1/scale 以外的区域 // 实际上,由于高斯函数的特性,理论上是没有严格边界的,这里 A>1.0 是一种近似或优化 if(A > 1.0) { discard; // 丢弃当前片段,不对其进行着色 } // 3. 计算高斯衰减值 mediump float scale = 4.0; // 控制高斯函数衰减速度的参数 // 高斯函数 exp(-A * scale) 根据到中心的距离平方 A 计算衰减因子 (0到1之间) // 乘以 v_splatColor.a (从顶点着色器传递过来的高斯点原始Alpha值) 得到最终的片段透明度 mediump float B = exp(-A * scale) * (v_splatColor.a); // 4. 计算最终颜色并输出 (预乘Alpha格式) // v_splatColor.rgb 是从顶点着色器传递过来的高斯点原始颜色 // 将原始颜色乘以计算出的衰减因子/透明度 B // out_FragColor = vec4(R, G, B, Alpha) out_FragColor = vec4(v_splatColor.rgb * B, B); }

这是高斯点渲染的颜色计算算法

  • dot(v_vertPos, v_vertPos): 计算片段在单位四边形内的坐标到中心 (0,0) 的距离的平方。由于顶点着色器已经将四边形拉伸和旋转以覆盖投影的椭圆,这个距离平方 A 实际上是衡量片段在投影椭圆内部相对位置的一个度量,与高斯分布的指数项 distance^2 相关。
  • discard: 如果距离平方 A 大于 1.0,认为该片段在高斯椭圆的有效范围之外,直接丢弃,不进行着色和混合,这是一种性能优化。
  • exp(-A * scale): 这是高斯函数的核心部分,计算基于距离的衰减因子。scale 参数控制衰减的速度,值越大衰减越快,高斯点看起来越"尖锐"。
  • B = exp(-A * scale) * v_splatColor.a: 最终的片段透明度等于高斯衰减因子乘以高斯点原始的 Alpha 值。原始 Alpha 值通常来自输入数据,表示该点的初始不透明度。
  • out_FragColor = vec4(v_splatColor.rgb * B, B): 输出像素的最终颜色。这里使用了预乘Alpha格式:输出的 RGB 颜色分量是原始颜色乘以最终透明度 B 的结果。这种格式在进行后续的 Alpha 混合时非常方便和高效。最终的 Alpha 通道就是计算出的透明度 B

第七部分:深度排序和渲染循环

主更新循环 update (第481-569行)

481:569:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.js
GaussianSplatPrimitive.prototype.update = function (frameState) { const tileset = this._tileset; tileset.update(frameState); // 更新底层的 3D Tileset,处理瓦片的加载和卸载 // 检查是否有新的瓦片被选中(加载完成)或者有瓦片被卸载 if (tileset._selectedTiles.length > 0 && tileset._selectedTiles.length !== this._selectedTileLen) { // 如果瓦片集合发生变化,需要重新合并数据和生成纹理 this._numSplats = 0; this._positions = undefined; this._rotations = undefined; this._scales = undefined; this._colors = undefined; this._indexes = undefined; this._needsGaussianSplatTexture = true; // 标记需要重新生成纹理 this._gaussianSplatTexturePending = false; // 重置纹理生成状态 // 遍历当前选中的所有瓦片,合并其高斯点数据 tileset._selectedTiles.forEach((tile) => { const gsplatData = tile.content._gsplatData; // 获取瓦片中的高斯点数据 this.pushSplats({ // 调用 pushSplats 合并数据 positions: new Float32Array( ModelUtility.getAttributeBySemantic( gsplatData, VertexAttributeSemantic.POSITION, ).typedArray, ), scales: new Float32Array( ModelUtility.getAttributeBySemantic( gsplatData, VertexAttributeSemantic.SCALE, ).typedArray, ), rotations: new Float32Array( ModelUtility.getAttributeBySemantic( gsplatData, VertexAttributeSemantic.ROTATION, ).typedArray, ), colors: new Uint8Array( ModelUtility.getAttributeByName(gsplatData, "COLOR_0").typedArray, ), }); this._numSplats += gsplatData.attributes[0].count; // 累计总点数量 }); this._selectedTileLen = tileset._selectedTiles.length; // 更新已处理的瓦片数量 } // 如果没有高斯点,则直接返回,不做渲染 if (this._numSplats === 0) { return; } // 如果需要生成纹理且纹理生成未开始,则启动纹理生成过程 if (this._needsGaussianSplatTexture) { if (!this._gaussianSplatTexturePending) { GaussianSplatPrimitive.generateSplatTexture(this, frameState); } return; // 等待纹理生成完成 } // 检查对数深度缓冲状态是否改变 (可能影响渲染状态) if (this._useLogDepth !== frameState.useLogDepth) { this._useLogDepth = frameState.useLogDepth; // 如果对数深度状态改变,可能需要重建 DrawCommand 来更新 Shader Defines 等 // buildGSplatDrawCommand 内部会根据 _useLogDepth 设置 LOG_DEPTH_READ_ONLY 宏 } // 如果 DrawCommand 已经构建好,将其添加到当前帧的命令列表,等待渲染 if (this._drawCommand) { frameState.commandList.push(this._drawCommand); } // 计算用于排序的模型视图矩阵 (相机视图矩阵 * 根变换矩阵) Matrix4.multiply( frameState.camera.viewMatrix, // 相机视图矩阵 (将点从世界空间变换到视图空间) this._rootTransform, // 根变换矩阵 (将点从根瓦片本地空间变换到世界空间) scratchSplatMatrix, // 输出:模型视图矩阵 (将点从根瓦片本地空间变换到视图空间) ); // 调用 GaussianSplatSorter 进行深度排序 const promise = GaussianSplatSorter.radixSortIndexes({ primitive: { positions: new Float32Array(this._positions), // 所有点的位置 (在根瓦片本地坐标系) modelView: Float32Array.from(scratchSplatMatrix), // 模型视图矩阵 count: this._numSplats, // 点数量 }, sortType: "Index", // 排序类型:按索引排序 }); // 如果排序任务未开始或等待,则返回 if (promise === undefined) { return; } // 处理排序任务完成后的结果 promise .catch((err) => { throw err; // 排序出错,抛出错误 }) .then((sortedData) => { this._indexes = sortedData; // 更新为排序后的点索引数组 // 根据新的排序索引重建绘制命令,因为 VertexArray 中的索引需要更新 GaussianSplatPrimitive.buildGSplatDrawCommand(this, frameState); }); };

这个 update 方法是每一帧都会被调用的核心函数,它负责整个渲染流程的调度。

  1. 更新 Tileset (第482行): 首先调用底层 tileset.update 方法,这会根据相机位置和视锥体决定哪些瓦片需要加载或卸载。
  2. 处理瓦片变化 (第484-529行): 检测 _selectedTiles 数组长度是否变化。如果变化,说明有新的瓦片加载完成或旧瓦片被卸载。此时需要:
    • 清空之前合并的所有点数据 (_positions, _rotations, _scales, _colors, _indexes)。
    • 重置纹理生成状态 (_needsGaussianSplatTexture = true)。
    • 遍历当前所有选中的瓦片 (tileset._selectedTiles)。
    • 从每个瓦片中提取高斯点数据 (tile.content._gsplatData)。
    • 使用 pushSplats 方法将数据合并到 Primitive 实例的总数据数组中。
    • 累计总的点数量 _numSplats
    • 更新 _selectedTileLen 记录当前选中的瓦片数量。
  3. 检查点数量 (第531-533行): 如果合并后没有点 (_numSplats === 0),直接返回,本帧不渲染。
  4. 生成属性纹理 (第535-541行): 如果 _needsGaussianSplatTexture 为 true 且纹理生成未开始 (!_gaussianSplatTexturePending),调用 generateSplatTexture 方法启动纹理生成过程。纹理生成是异步的,如果已启动或正在等待,则返回,等待下一帧继续。
  5. 检查对数深度 (第542-544行): 检查 frameState 中的对数深度设置是否改变,如果改变可能需要更新绘制命令的Shader宏。
  6. 提交绘制命令 (第546-548行): 如果 _drawCommand 已经构建好(包含有效的 VertexArray 和 ShaderProgram),将其添加到 frameState.commandList 中。Cesium 的渲染循环稍后会执行命令列表中的所有绘制命令。
  7. 深度排序 (第550-569行):
    • 高斯点云的透明度混合依赖于从后往前的渲染顺序。因此,每一帧都需要根据相机位置对所有高斯点进行深度排序。
    • 首先计算用于排序的模型视图矩阵:frameState.camera.viewMatrix 将点从世界空间变换到视图空间,再乘以 this._rootTransform 将点从根瓦片本地空间变换到视图空间。排序通常在视图空间进行,因为深度就是视图空间的 Z 坐标。
    • 调用 GaussianSplatSorter.radixSortIndexes 方法启动排序任务。这是一个异步操作,通常在 Web Worker 中执行,以避免阻塞UI线程。排序算法使用基数排序(Radix Sort),它在某些情况下(如整数排序)比比较排序更快。排序的输入是所有点的位置和模型视图矩阵,输出是排序后的点索引数组 _indexes
    • 通过 Promise 处理排序结果:当排序完成后,获取排序后的索引数组 sortedData,更新 this._indexes
    • 重建绘制命令: 由于 VertexArray 中使用了 _indexes 数组来控制绘制顺序(实例化绘制时,a_splatIndex_indexes 数组中获取),当 _indexes 数组更新后,需要重新调用 buildGSplatDrawCommand 来构建一个新的 VertexArrayDrawCommand,包含最新的排序信息。

总结:完整的渲染流程

这个高斯点云渲染系统是一个高度优化的实时3D渲染引擎,整个流程如下:

1. 数据加载和预处理 (主要在 onTileLoadedupdate 的瓦片处理阶段)

  • 根据相机视锥和LOD策略,Cesium加载器会异步加载相关的3D Tiles瓦片。
  • 瓦片加载完成后 (onTileLoaded),对其内部的高斯点数据进行坐标系变换,将其统一到根瓦片的坐标系下。
  • update 循环中,检测到瓦片变化时,合并所有已加载瓦片的高斯点数据到 GaussianSplatPrimitive 实例的总数组中。
  • 调用 GaussianSplatTextureGenerator (通常在 Worker 中) 将合并后的高斯点属性数据打包成 GPU 纹理。

2. 实时更新和排序 (在每帧的 update 循环中)

  • 计算相机视图矩阵与点云根变换矩阵相乘得到模型视图矩阵。
  • 使用 GaussianSplatSorter (通常在 Worker 中) 根据高斯点在视图空间中的深度对其进行排序,生成一个排序后的索引数组。这是一个异步操作。

3. 构建和提交绘制命令 (在 update 和排序完成回调中)

  • 当高斯点数据合并完成且属性纹理生成后,或者排序完成后,调用 buildGSplatDrawCommand
  • 这个函数配置渲染状态(禁用深度测试/写入、启用Alpha混合等)。
  • 定义顶点属性和 Uniform。
  • 创建一个简单的屏幕空间四边形几何体和一个 VertexArray,其中一个属性 a_splatIndex 从排序后的索引数组 _indexes 中获取,并设置为实例化属性。
  • 构建一个 DrawCommand 对象,包含 VertexArray、ShaderProgram、UniformMap、RenderState等信息。
  • 将构建好的 DrawCommand 添加到当前帧的命令列表 frameState.commandList 中。

4. GPU绘制阶段 (由Cesium渲染管线执行命令列表)

  • Cesium渲染管线遍历命令列表,执行 GaussianSplatPrimitiveDrawCommand
  • 顶点着色器 (PrimitiveGaussianSplatVS.glsl):
    • 对每个实例化绘制的四边形顶点,根据 a_splatIndex 从属性纹理 u_splatAttributeTexture 中读取对应高斯点的属性(位置、协方差、颜色)。
    • 将高斯点从根瓦片本地空间通过模型视图投影矩阵变换到裁剪空间。
    • 调用 calcCovVectors 函数,利用视图空间位置和协方差矩阵,计算投影到屏幕空间的2D椭圆参数(特征值平方根乘以特征向量)。
    • 根据屏幕空间四边形顶点 (a_screenQuadPosition) 和投影椭圆参数 (covVectors),计算当前顶点在屏幕上的最终位置 gl_Position
    • 将颜色和屏幕空间局部坐标 (v_splatColor, v_vertPos) 传递给片段着色器。
    • 进行视锥剔除和尺寸剔除,丢弃不可见或过小的点对应的四边形。
  • 片段着色器 (PrimitiveGaussianSplatFS.glsl):
    • 对覆盖投影椭圆的屏幕空间四边形内的每个像素执行。
    • 根据插值得到的 v_vertPos 计算像素到屏幕空间椭圆中心的距离平方。
    • 使用高斯函数计算衰减因子。
    • 将衰减因子与高斯点原始 Alpha 相乘,得到像素的最终 Alpha 值 B
    • 计算像素的最终颜色:原始 RGB 颜色乘以 B(预乘Alpha)。
    • 输出最终的像素颜色和 Alpha (out_FragColor)。
  • Alpha混合: 由于渲染状态设置了预乘Alpha混合,GPU会根据片段着色器输出的 out_FragColor (预乘Alpha格式) 和帧缓冲中已有的颜色进行混合,实现透明效果。排序后的渲染顺序保证了从后往前的正确混合。

这个系统能够实时渲染数百万甚至数十亿个高斯点,提供照片级真实的3D场景重建效果。每个组件都经过精心优化,从数据结构、多线程处理到着色器算法,都体现了现代实时图形渲染的最佳实践。通过实例化渲染和属性纹理,它高效地将大量高斯点数据送入GPU管线,并通过深度排序和Alpha混合解决了透明度问题。

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:幽灵

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 幽灵AI 许可协议。转载请注明出处!