本文档详细解析了 cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.js
文件中实现的高斯点云渲染的完整流程,包括数据结构、瓦片处理、渲染管线配置以及顶点和片段着色器算法。
1:33:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.jsimport 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 ...
每个导入都有特定用途:
36:67:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.jsfunction 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 ...
这些数据结构是高斯点云渲染的核心:
58:62:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.jsthis._axisCorrectionMatrix = ModelUtility.getAxisCorrectionMatrix( Axis.Y, // 源坐标系的上方向 (通常是原始数据格式的上方向) Axis.X, // 目标坐标系的上方向 (Cesium使用X向上) new Matrix4(), );
这是关键的坐标系转换!不同的3D软件和数据格式使用不同的坐标系(例如,Y向上 vs Z向上,左手坐标系 vs 右手坐标系)。
ModelUtility.getAxisCorrectionMatrix
根据源和目标坐标系的上方向计算出转换矩阵。73:77:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.jsready: { get: function () { return this._ready; // 返回是否准备好渲染 }, }
这个属性是一个标志,指示 GaussianSplatPrimitive
是否已经完成了所有必要的数据加载和预处理(例如,瓦片加载、数据合并、纹理生成),可以开始进行渲染。渲染引擎在绘制前会检查此属性。
87:98:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.jsmodelMatrix: { 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渲染中最重要的变换矩阵:
return this.modelMatrix;
可能是一个循环引用,正确的应该是 return this._modelMatrix;
。Matrix4.clone
进行深拷贝是为了防止外部修改传入的矩阵对象影响内部状态。130:149:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.jsGaussianSplatPrimitive.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的分层渲染的核心逻辑:
onTileLoaded
是当一个瓦片加载完成后触发的回调函数。_rootTransform
)。transformTile
方法,将其内部的高斯点数据从瓦片本地坐标系变换到这个统一的根坐标系下,以确保所有瓦片的数据都在同一个空间中。192:227:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.jsGaussianSplatPrimitive.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 ... };
这是将瓦片数据变换到根坐标系的复杂坐标系变换链:
transform
): 瓦片在整个3D Tileset层级中的相对位置、旋转、缩放。_axisCorrectionMatrix
): 应用之前计算的坐标系修正矩阵,将数据从原始格式的坐标系转换到Cesium使用的坐标系。tile._content._worldTransform
): 瓦片内容可能还包含一个额外的世界变换矩阵,也需要应用。
通过连续矩阵乘法,得到一个将瓦片本地坐标系中的点变换到世界坐标系的完整 modelMatrix
。inverseRoot
): 虽然这里计算的是瓦片自身的逆矩阵,但其目的是将世界空间中的点变换回这个特定瓦片加载时所使用的坐标系(即根瓦片的坐标系,因为瓦片加载时 _rootTransform
被设置为第一个瓦片的 computedTransform
)。210:226:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.jsconst 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; }
这个循环对每个高斯点的位置进行两次矩阵变换:
worldPosition = modelMatrix * localPosition
。rootLocalPosition = inverseRoot * worldPosition
。这里 inverseRoot
是瓦片自身变换的逆矩阵,乘以世界位置相当于将点从世界空间拉回到该瓦片在加载时所处的空间(即根瓦片空间,因为后续所有瓦片都被拉到这里)。positions
数组,节省内存并避免创建新的数组。229:278:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.jsGaussianSplatPrimitive.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
持有的总数据数组中。280:326:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.jsGaussianSplatPrimitive.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; }); };
纹理生成是关键优化步骤:
GaussianSplatTextureGenerator
,它通常在 Web Worker 线程中运行,进行耗时的 CPU 计算,避免阻塞主线程。328:344:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.jsGaussianSplatPrimitive.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端的排序决定。blending = BlendingState.PRE_MULTIPLIED_ALPHA_BLEND
): 高斯点云的透明度渲染需要依赖Alpha混合。预乘Alpha混合是一种常用的透明度混合方式,可以正确处理颜色和透明度。Pass.GAUSSIAN_SPLATS
): 指定这些绘制命令属于高斯点云渲染通道,有助于渲染管线组织和状态管理。346:364:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.jsshaderBuilder.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
表示这个宏在顶点和片段着色器中都有效。366:387:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.jsshaderBuilder.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着色器的输入和输出接口:
a_screenQuadPosition
是屏幕空间四边形的四个顶点坐标 (-1到1),a_splatIndex
是当前绘制的实例化四边形对应的高斯点在总数据数组中的索引。这些数据是每个顶点/实例不同的。v_splatColor
和 v_vertPos
是从顶点着色器计算并插值后传递给片段着色器的数据。v_vertPos
包含了四边形顶点的局部坐标,用于片段着色器中计算高斯衰减。v_splatColor
传递了该高斯点的颜色信息。u_splatAttributeTexture
是存储所有高斯点属性的纹理,u_splatScale
是一个全局缩放因子。这些数据在一次绘制调用中对所有顶点/片段都是相同的。420:438:cesium/packages/engine/Source/Scene/GaussianSplatPrimitive.jsconst 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(实例化)渲染的精巧设计:
a_splatIndex
指定)执行一次顶点着色器,但绘制的是同一个基础四边形。screenQuadPosition
: 提供了四边形顶点的屏幕空间坐标(范围 [-1,-1] 到 [1,1]),用于在顶点着色器中根据高斯点属性将其拉伸和定位到正确的大小和位置。splatIndex
: 这个属性通过实例化机制,为每个绘制的四边形实例提供其对应的高斯点在数据纹理中的索引。idxAttr.instanceDivisor = 1;
意味着每个实例(而不是每个顶点)都会推进一次 splatIndex
。现在我们来分析最核心的着色器算法,它定义了高斯点如何被渲染成屏幕上的2D椭圆。
这个着色器的主要任务是计算每个高斯点在屏幕上的位置和大小,以及将其颜色等信息传递给片段着色器。
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椭圆形状和方向的向量。
viewPos
(高斯点中心在视图空间的位置), Vrk
(高斯点在模型空间的 3x3 协方差矩阵)。focal
是相机的焦距,t.z
是高斯点在视图空间的深度。雅可比矩阵用于将视图空间的协方差矩阵投影到屏幕空间。czm_modelView
是模型视图矩阵,它包含旋转和平移。为了正确变换协方差矩阵,需要将其分解为旋转和缩放,并只使用旋转部分。这里通过计算矩阵列向量的长度来获取缩放,然后将列向量除以其长度得到纯旋转矩阵 Rs
。Vrk_view = Rs * Vrk * transpose(Rs)
。将高斯点在模型空间的协方差矩阵 Vrk
变换到视图空间。这是协方差矩阵的标准变换公式。cov = transpose(J) * Vrk_view * J
。将视图空间的协方差矩阵 Vrk_view
通过雅可比矩阵 J
投影到屏幕空间。结果 cov
是一个 2x2 的矩阵,表示投影后2D椭圆的协方差。cov
进行特征值分解。
covVectors.xy
存储了 sqrt(2 * lambda1)
乘以与第一个特征值对应的归一化特征向量。covVectors.zw
存储了 sqrt(2 * lambda2)
乘以与第二个特征值对应的归一化特征向量(第二个特征向量与第一个正交)。min(..., 1024.0)
: 限制高斯点在屏幕上的最大尺寸,防止过大影响性能。main
函数中用于将屏幕空间四边形的角点拉伸和旋转到正确的位置,形成覆盖投影椭圆的四边形。这个着色器的主要任务是计算像素的颜色和透明度。
1:10:cesium/packages/engine/Source/Shaders/PrimitiveGaussianSplatFS.glslvoid 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.jsGaussianSplatPrimitive.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
方法是每一帧都会被调用的核心函数,它负责整个渲染流程的调度。
tileset.update
方法,这会根据相机位置和视锥体决定哪些瓦片需要加载或卸载。_selectedTiles
数组长度是否变化。如果变化,说明有新的瓦片加载完成或旧瓦片被卸载。此时需要:
_positions
, _rotations
, _scales
, _colors
, _indexes
)。_needsGaussianSplatTexture = true
)。tileset._selectedTiles
)。tile.content._gsplatData
)。pushSplats
方法将数据合并到 Primitive
实例的总数据数组中。_numSplats
。_selectedTileLen
记录当前选中的瓦片数量。_numSplats === 0
),直接返回,本帧不渲染。_needsGaussianSplatTexture
为 true 且纹理生成未开始 (!_gaussianSplatTexturePending
),调用 generateSplatTexture
方法启动纹理生成过程。纹理生成是异步的,如果已启动或正在等待,则返回,等待下一帧继续。frameState
中的对数深度设置是否改变,如果改变可能需要更新绘制命令的Shader宏。_drawCommand
已经构建好(包含有效的 VertexArray 和 ShaderProgram),将其添加到 frameState.commandList
中。Cesium 的渲染循环稍后会执行命令列表中的所有绘制命令。frameState.camera.viewMatrix
将点从世界空间变换到视图空间,再乘以 this._rootTransform
将点从根瓦片本地空间变换到视图空间。排序通常在视图空间进行,因为深度就是视图空间的 Z 坐标。GaussianSplatSorter.radixSortIndexes
方法启动排序任务。这是一个异步操作,通常在 Web Worker 中执行,以避免阻塞UI线程。排序算法使用基数排序(Radix Sort),它在某些情况下(如整数排序)比比较排序更快。排序的输入是所有点的位置和模型视图矩阵,输出是排序后的点索引数组 _indexes
。sortedData
,更新 this._indexes
。VertexArray
中使用了 _indexes
数组来控制绘制顺序(实例化绘制时,a_splatIndex
从 _indexes
数组中获取),当 _indexes
数组更新后,需要重新调用 buildGSplatDrawCommand
来构建一个新的 VertexArray
和 DrawCommand
,包含最新的排序信息。这个高斯点云渲染系统是一个高度优化的实时3D渲染引擎,整个流程如下:
onTileLoaded
和 update
的瓦片处理阶段)onTileLoaded
),对其内部的高斯点数据进行坐标系变换,将其统一到根瓦片的坐标系下。update
循环中,检测到瓦片变化时,合并所有已加载瓦片的高斯点数据到 GaussianSplatPrimitive
实例的总数组中。GaussianSplatTextureGenerator
(通常在 Worker 中) 将合并后的高斯点属性数据打包成 GPU 纹理。update
循环中)GaussianSplatSorter
(通常在 Worker 中) 根据高斯点在视图空间中的深度对其进行排序,生成一个排序后的索引数组。这是一个异步操作。update
和排序完成回调中)buildGSplatDrawCommand
。a_splatIndex
从排序后的索引数组 _indexes
中获取,并设置为实例化属性。DrawCommand
对象,包含 VertexArray、ShaderProgram、UniformMap、RenderState等信息。DrawCommand
添加到当前帧的命令列表 frameState.commandList
中。GaussianSplatPrimitive
的 DrawCommand
。PrimitiveGaussianSplatVS.glsl
):
a_splatIndex
从属性纹理 u_splatAttributeTexture
中读取对应高斯点的属性(位置、协方差、颜色)。calcCovVectors
函数,利用视图空间位置和协方差矩阵,计算投影到屏幕空间的2D椭圆参数(特征值平方根乘以特征向量)。a_screenQuadPosition
) 和投影椭圆参数 (covVectors
),计算当前顶点在屏幕上的最终位置 gl_Position
。v_splatColor
, v_vertPos
) 传递给片段着色器。PrimitiveGaussianSplatFS.glsl
):
v_vertPos
计算像素到屏幕空间椭圆中心的距离平方。B
。B
(预乘Alpha)。out_FragColor
)。out_FragColor
(预乘Alpha格式) 和帧缓冲中已有的颜色进行混合,实现透明效果。排序后的渲染顺序保证了从后往前的正确混合。这个系统能够实时渲染数百万甚至数十亿个高斯点,提供照片级真实的3D场景重建效果。每个组件都经过精心优化,从数据结构、多线程处理到着色器算法,都体现了现代实时图形渲染的最佳实践。通过实例化渲染和属性纹理,它高效地将大量高斯点数据送入GPU管线,并通过深度排序和Alpha混合解决了透明度问题。
本文作者:幽灵
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 幽灵AI 许可协议。转载请注明出处!