@d5techs/3dgs-lib 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/3dgs-lib.js CHANGED
@@ -1,6 +1,245 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ const DEFAULT_MATERIAL = {
5
+ baseColorFactor: [1, 1, 1, 1],
6
+ baseColorTexture: null,
7
+ metallicFactor: 0,
8
+ roughnessFactor: 0.5,
9
+ doubleSided: false
10
+ };
11
+ const DEFAULT_OBJ_MATERIAL = {
12
+ baseColorFactor: [1, 1, 1, 1],
13
+ baseColorTexture: null,
14
+ metallicFactor: 0,
15
+ roughnessFactor: 0.5,
16
+ doubleSided: true
17
+ };
18
+ var SHMode = /* @__PURE__ */ ((SHMode2) => {
19
+ SHMode2[SHMode2["L0"] = 0] = "L0";
20
+ SHMode2[SHMode2["L1"] = 1] = "L1";
21
+ SHMode2[SHMode2["L2"] = 2] = "L2";
22
+ SHMode2[SHMode2["L3"] = 3] = "L3";
23
+ return SHMode2;
24
+ })(SHMode || {});
25
+ function isMobileDevice() {
26
+ if (typeof navigator === "undefined" || typeof window === "undefined") {
27
+ return false;
28
+ }
29
+ const ua = navigator.userAgent || navigator.vendor || window.opera || "";
30
+ const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
31
+ ua.toLowerCase()
32
+ );
33
+ const hasTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
34
+ const isSmallScreen = window.innerWidth <= 768;
35
+ const isIPadAsMac = navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
36
+ return isMobileUA || isIPadAsMac || hasTouch && isSmallScreen;
37
+ }
38
+ function getRecommendedDPR() {
39
+ const isMobile = isMobileDevice();
40
+ const maxDpr = isMobile ? 1.5 : 3;
41
+ return Math.min(window.devicePixelRatio || 1, maxDpr);
42
+ }
43
+ function isWebGPUSupported() {
44
+ return typeof navigator !== "undefined" && "gpu" in navigator;
45
+ }
46
+ function computeBoundingBox$1(positions) {
47
+ if (positions.length < 3) {
48
+ return {
49
+ min: [0, 0, 0],
50
+ max: [0, 0, 0],
51
+ center: [0, 0, 0],
52
+ radius: 0
53
+ };
54
+ }
55
+ const min = [positions[0], positions[1], positions[2]];
56
+ const max = [positions[0], positions[1], positions[2]];
57
+ for (let i = 3; i < positions.length; i += 3) {
58
+ const x = positions[i];
59
+ const y = positions[i + 1];
60
+ const z = positions[i + 2];
61
+ min[0] = Math.min(min[0], x);
62
+ min[1] = Math.min(min[1], y);
63
+ min[2] = Math.min(min[2], z);
64
+ max[0] = Math.max(max[0], x);
65
+ max[1] = Math.max(max[1], y);
66
+ max[2] = Math.max(max[2], z);
67
+ }
68
+ const center = [
69
+ (min[0] + max[0]) / 2,
70
+ (min[1] + max[1]) / 2,
71
+ (min[2] + max[2]) / 2
72
+ ];
73
+ const dx = max[0] - min[0];
74
+ const dy = max[1] - min[1];
75
+ const dz = max[2] - min[2];
76
+ const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
77
+ return { min, max, center, radius };
78
+ }
79
+ function mergeBoundingBoxes(boxes) {
80
+ if (boxes.length === 0) return null;
81
+ let combinedMin = [...boxes[0].min];
82
+ let combinedMax = [...boxes[0].max];
83
+ for (let i = 1; i < boxes.length; i++) {
84
+ const box = boxes[i];
85
+ combinedMin[0] = Math.min(combinedMin[0], box.min[0]);
86
+ combinedMin[1] = Math.min(combinedMin[1], box.min[1]);
87
+ combinedMin[2] = Math.min(combinedMin[2], box.min[2]);
88
+ combinedMax[0] = Math.max(combinedMax[0], box.max[0]);
89
+ combinedMax[1] = Math.max(combinedMax[1], box.max[1]);
90
+ combinedMax[2] = Math.max(combinedMax[2], box.max[2]);
91
+ }
92
+ const center = [
93
+ (combinedMin[0] + combinedMax[0]) / 2,
94
+ (combinedMin[1] + combinedMax[1]) / 2,
95
+ (combinedMin[2] + combinedMax[2]) / 2
96
+ ];
97
+ const dx = combinedMax[0] - combinedMin[0];
98
+ const dy = combinedMax[1] - combinedMin[1];
99
+ const dz = combinedMax[2] - combinedMin[2];
100
+ const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
101
+ return { min: combinedMin, max: combinedMax, center, radius };
102
+ }
103
+ function createBoundingBoxFromMinMax(min, max) {
104
+ const center = [
105
+ (min[0] + max[0]) / 2,
106
+ (min[1] + max[1]) / 2,
107
+ (min[2] + max[2]) / 2
108
+ ];
109
+ const dx = max[0] - min[0];
110
+ const dy = max[1] - min[1];
111
+ const dz = max[2] - min[2];
112
+ const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
113
+ return { min, max, center, radius };
114
+ }
115
+ function transformBoundingBox(bbox, modelMatrix) {
116
+ const corners = [
117
+ [bbox.min[0], bbox.min[1], bbox.min[2]],
118
+ [bbox.max[0], bbox.min[1], bbox.min[2]],
119
+ [bbox.min[0], bbox.max[1], bbox.min[2]],
120
+ [bbox.max[0], bbox.max[1], bbox.min[2]],
121
+ [bbox.min[0], bbox.min[1], bbox.max[2]],
122
+ [bbox.max[0], bbox.min[1], bbox.max[2]],
123
+ [bbox.min[0], bbox.max[1], bbox.max[2]],
124
+ [bbox.max[0], bbox.max[1], bbox.max[2]]
125
+ ];
126
+ const m = modelMatrix;
127
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
128
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
129
+ for (const [x, y, z] of corners) {
130
+ const tx = m[0] * x + m[4] * y + m[8] * z + m[12];
131
+ const ty = m[1] * x + m[5] * y + m[9] * z + m[13];
132
+ const tz = m[2] * x + m[6] * y + m[10] * z + m[14];
133
+ minX = Math.min(minX, tx);
134
+ minY = Math.min(minY, ty);
135
+ minZ = Math.min(minZ, tz);
136
+ maxX = Math.max(maxX, tx);
137
+ maxY = Math.max(maxY, ty);
138
+ maxZ = Math.max(maxZ, tz);
139
+ }
140
+ return createBoundingBoxFromMinMax(
141
+ [minX, minY, minZ],
142
+ [maxX, maxY, maxZ]
143
+ );
144
+ }
145
+ async function loadTextureFromURL(device, url) {
146
+ try {
147
+ const response = await fetch(url);
148
+ if (!response.ok) {
149
+ return null;
150
+ }
151
+ const blob = await response.blob();
152
+ return loadTextureFromBlob(device, blob);
153
+ } catch (error) {
154
+ console.warn(`Failed to load texture from URL: ${url}`, error);
155
+ return null;
156
+ }
157
+ }
158
+ async function loadTextureFromBlob(device, blob) {
159
+ try {
160
+ const imageBitmap = await createImageBitmap(blob);
161
+ return createTextureFromImageBitmap(device, imageBitmap);
162
+ } catch (error) {
163
+ console.warn(`Failed to create texture from blob`, error);
164
+ return null;
165
+ }
166
+ }
167
+ async function loadTextureFromBuffer(device, buffer, mimeType = "image/png") {
168
+ try {
169
+ const uint8Array = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
170
+ const blob = new Blob([uint8Array], { type: mimeType });
171
+ return loadTextureFromBlob(device, blob);
172
+ } catch (error) {
173
+ console.warn(`Failed to create texture from buffer`, error);
174
+ return null;
175
+ }
176
+ }
177
+ function createTextureFromImageBitmap(device, imageBitmap) {
178
+ const texture = device.createTexture({
179
+ size: [imageBitmap.width, imageBitmap.height, 1],
180
+ format: "rgba8unorm",
181
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
182
+ });
183
+ device.queue.copyExternalImageToTexture(
184
+ { source: imageBitmap },
185
+ { texture },
186
+ [imageBitmap.width, imageBitmap.height]
187
+ );
188
+ return texture;
189
+ }
190
+ class TextureCache {
191
+ constructor(device) {
192
+ __publicField(this, "cache", /* @__PURE__ */ new Map());
193
+ __publicField(this, "device");
194
+ this.device = device;
195
+ }
196
+ /**
197
+ * 获取或加载纹理
198
+ */
199
+ async getOrLoad(url) {
200
+ if (this.cache.has(url)) {
201
+ return this.cache.get(url);
202
+ }
203
+ const texture = await loadTextureFromURL(this.device, url);
204
+ if (texture) {
205
+ this.cache.set(url, texture);
206
+ }
207
+ return texture;
208
+ }
209
+ /**
210
+ * 检查缓存中是否存在
211
+ */
212
+ has(url) {
213
+ return this.cache.has(url);
214
+ }
215
+ /**
216
+ * 从缓存获取
217
+ */
218
+ get(url) {
219
+ return this.cache.get(url);
220
+ }
221
+ /**
222
+ * 添加到缓存
223
+ */
224
+ set(url, texture) {
225
+ this.cache.set(url, texture);
226
+ }
227
+ /**
228
+ * 清空缓存
229
+ */
230
+ clear() {
231
+ this.cache.clear();
232
+ }
233
+ /**
234
+ * 销毁所有纹理并清空缓存
235
+ */
236
+ destroy() {
237
+ for (const texture of this.cache.values()) {
238
+ texture.destroy();
239
+ }
240
+ this.cache.clear();
241
+ }
242
+ }
4
243
  class Renderer {
5
244
  constructor(canvas) {
6
245
  __publicField(this, "canvas");
@@ -130,11 +369,7 @@ class Renderer {
130
369
  this.resizeObserver = new ResizeObserver((entries) => {
131
370
  for (const entry of entries) {
132
371
  const { width, height } = entry.contentRect;
133
- const isMobile = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
134
- navigator.userAgent.toLowerCase()
135
- );
136
- const maxDpr = isMobile ? 1.5 : 3;
137
- const dpr = Math.min(window.devicePixelRatio || 1, maxDpr);
372
+ const dpr = getRecommendedDPR();
138
373
  this.canvas.width = Math.floor(width * dpr);
139
374
  this.canvas.height = Math.floor(height * dpr);
140
375
  this.createDepthTexture();
@@ -203,8 +438,10 @@ class Camera {
203
438
  __publicField(this, "fov", Math.PI / 4);
204
439
  // 45度
205
440
  __publicField(this, "aspect", 1);
206
- __publicField(this, "near", 1e-3);
207
- __publicField(this, "far", 1e4);
441
+ __publicField(this, "near", 0.1);
442
+ // 增大近平面以提高深度精度 (参考实现使用 0.1)
443
+ __publicField(this, "far", 1e3);
444
+ // 减小远平面以提高深度精度
208
445
  // 矩阵
209
446
  __publicField(this, "viewMatrix", new Float32Array(16));
210
447
  __publicField(this, "projectionMatrix", new Float32Array(16));
@@ -2184,7 +2421,7 @@ class GLBLoader {
2184
2421
  this.device.queue.writeBuffer(indexBuffer, 0, indexData);
2185
2422
  }
2186
2423
  }
2187
- const boundingBox = this.computeBoundingBox(positions);
2424
+ const boundingBox = this.computeBoundingBoxFromPositions(positions);
2188
2425
  const material = await this.parseMaterial(gltf, primitive.material, binData);
2189
2426
  const mesh = new Mesh(vertexBuffer, vertexCount, indexBuffer, indexCount, boundingBox);
2190
2427
  mesh.hasUV = hasUV;
@@ -2290,38 +2527,8 @@ class GLBLoader {
2290
2527
  /**
2291
2528
  * 计算顶点数据的 bounding box
2292
2529
  */
2293
- computeBoundingBox(positions) {
2294
- if (positions.length < 3) {
2295
- return {
2296
- min: [0, 0, 0],
2297
- max: [0, 0, 0],
2298
- center: [0, 0, 0],
2299
- radius: 0
2300
- };
2301
- }
2302
- const min = [positions[0], positions[1], positions[2]];
2303
- const max = [positions[0], positions[1], positions[2]];
2304
- for (let i = 3; i < positions.length; i += 3) {
2305
- const x = positions[i];
2306
- const y = positions[i + 1];
2307
- const z = positions[i + 2];
2308
- min[0] = Math.min(min[0], x);
2309
- min[1] = Math.min(min[1], y);
2310
- min[2] = Math.min(min[2], z);
2311
- max[0] = Math.max(max[0], x);
2312
- max[1] = Math.max(max[1], y);
2313
- max[2] = Math.max(max[2], z);
2314
- }
2315
- const center = [
2316
- (min[0] + max[0]) / 2,
2317
- (min[1] + max[1]) / 2,
2318
- (min[2] + max[2]) / 2
2319
- ];
2320
- const dx = max[0] - min[0];
2321
- const dy = max[1] - min[1];
2322
- const dz = max[2] - min[2];
2323
- const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
2324
- return { min, max, center, radius };
2530
+ computeBoundingBoxFromPositions(positions) {
2531
+ return computeBoundingBox$1(positions);
2325
2532
  }
2326
2533
  /**
2327
2534
  * 获取访问器数据 - 修复字节对齐问题
@@ -3318,7 +3525,7 @@ class OBJLoader {
3318
3525
  });
3319
3526
  this.device.queue.writeBuffer(indexBuffer, 0, indexData);
3320
3527
  }
3321
- const boundingBox = this.computeBoundingBox(obj.positions);
3528
+ const boundingBox = this.computeBoundingBoxFromPositions(obj.positions);
3322
3529
  const mesh = new Mesh(vertexBuffer, vertexCount, indexBuffer, indexCount, boundingBox);
3323
3530
  mesh.hasUV = hasUVs;
3324
3531
  mesh.indexFormat = indexFormat;
@@ -3359,53 +3566,16 @@ class OBJLoader {
3359
3566
  * 计算顶点数据的 bounding box
3360
3567
  * Requirement 3.5: 计算并存储 bounding box 信息
3361
3568
  */
3362
- computeBoundingBox(positions) {
3363
- if (positions.length < 3) {
3364
- return {
3365
- min: [0, 0, 0],
3366
- max: [0, 0, 0],
3367
- center: [0, 0, 0],
3368
- radius: 0
3369
- };
3370
- }
3371
- const min = [positions[0], positions[1], positions[2]];
3372
- const max = [positions[0], positions[1], positions[2]];
3373
- for (let i = 3; i < positions.length; i += 3) {
3374
- const x = positions[i];
3375
- const y = positions[i + 1];
3376
- const z = positions[i + 2];
3377
- min[0] = Math.min(min[0], x);
3378
- min[1] = Math.min(min[1], y);
3379
- min[2] = Math.min(min[2], z);
3380
- max[0] = Math.max(max[0], x);
3381
- max[1] = Math.max(max[1], y);
3382
- max[2] = Math.max(max[2], z);
3383
- }
3384
- const center = [
3385
- (min[0] + max[0]) / 2,
3386
- (min[1] + max[1]) / 2,
3387
- (min[2] + max[2]) / 2
3388
- ];
3389
- const dx = max[0] - min[0];
3390
- const dy = max[1] - min[1];
3391
- const dz = max[2] - min[2];
3392
- const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
3393
- return { min, max, center, radius };
3569
+ computeBoundingBoxFromPositions(positions) {
3570
+ return computeBoundingBox$1(positions);
3394
3571
  }
3395
3572
  /**
3396
3573
  * 创建材质数据
3397
3574
  * Requirement 4.3: 将材质关联到网格
3398
3575
  */
3399
3576
  async createMaterial(materialName, materials, baseUrl) {
3400
- const defaultMaterial = {
3401
- baseColorFactor: [1, 1, 1, 1],
3402
- baseColorTexture: null,
3403
- metallicFactor: 0,
3404
- roughnessFactor: 0.5,
3405
- doubleSided: true
3406
- };
3407
3577
  if (!materialName || !materials.has(materialName)) {
3408
- return defaultMaterial;
3578
+ return { ...DEFAULT_OBJ_MATERIAL };
3409
3579
  }
3410
3580
  const parsedMaterial = materials.get(materialName);
3411
3581
  const material = {
@@ -4205,27 +4375,20 @@ function deserializeSplat(data) {
4205
4375
  }
4206
4376
  return splats;
4207
4377
  }
4208
- const DEFAULT_NUM_BUCKETS$1 = 65536;
4209
- const IOS_NUM_BUCKETS$1 = 4096;
4210
4378
  const WORKGROUP_SIZE$1 = 256;
4211
- function isIOSDevice$1() {
4212
- if (typeof navigator === "undefined") return false;
4213
- const ua = navigator.userAgent || "";
4214
- return /iphone|ipad|ipod/i.test(ua.toLowerCase()) || navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
4215
- }
4216
- function generateCullingShaderCode$1(numBuckets) {
4217
- const bucketBits = Math.log2(numBuckets);
4379
+ const RADIX_BITS = 8;
4380
+ const RADIX_SIZE = 256;
4381
+ const ELEMENTS_PER_THREAD = 4;
4382
+ const BLOCK_SIZE = WORKGROUP_SIZE$1 * ELEMENTS_PER_THREAD;
4383
+ function generateCullingShaderCode$1() {
4218
4384
  return (
4219
4385
  /* wgsl */
4220
4386
  `
4221
4387
  /**
4222
- * Pass 1: 剔除 + 深度计算 + 桶计数
4223
- * 桶数量: ${numBuckets}
4388
+ * Project & Cull Shader
4389
+ * 基于 rfs-gsplat-render 实现
4224
4390
  */
4225
4391
 
4226
- const NUM_BUCKETS: u32 = ${numBuckets}u;
4227
- const BUCKET_MAX: u32 = ${numBuckets - 1}u;
4228
-
4229
4392
  struct Splat {
4230
4393
  mean: vec3<f32>,
4231
4394
  _pad0: f32,
@@ -4254,29 +4417,22 @@ struct CullingParams {
4254
4417
  farPlane: f32,
4255
4418
  screenWidth: f32,
4256
4419
  screenHeight: f32,
4420
+ frustumDilation: f32,
4257
4421
  pixelThreshold: f32,
4258
- _pad0: f32,
4259
4422
  _pad1: f32,
4260
4423
  }
4261
4424
 
4262
- struct Counters {
4263
- visibleCount: atomic<u32>,
4264
- }
4265
-
4266
4425
  @group(0) @binding(0) var<storage, read> splats: array<Splat>;
4267
4426
  @group(0) @binding(1) var<uniform> camera: CameraUniforms;
4268
4427
  @group(0) @binding(2) var<uniform> params: CullingParams;
4269
- @group(0) @binding(3) var<storage, read_write> counters: Counters;
4428
+ @group(0) @binding(3) var<storage, read_write> depthKeys: array<u32>;
4270
4429
  @group(0) @binding(4) var<storage, read_write> visibleIndices: array<u32>;
4271
- @group(0) @binding(5) var<storage, read_write> depthKeys: array<u32>;
4272
- @group(0) @binding(6) var<storage, read_write> bucketCounts: array<atomic<u32>>;
4273
- @group(0) @binding(7) var<storage, read_write> drawIndirect: array<u32>;
4430
+ @group(0) @binding(5) var<storage, read_write> indirectBuffer: array<atomic<u32>, 4>;
4274
4431
 
4275
4432
  fn maxScale(scale: vec3<f32>) -> f32 {
4276
4433
  return max(max(scale.x, scale.y), scale.z);
4277
4434
  }
4278
4435
 
4279
- // 从模型矩阵提取最大缩放因子
4280
4436
  fn getModelMaxScale(model: mat4x4<f32>) -> f32 {
4281
4437
  let sx = length(model[0].xyz);
4282
4438
  let sy = length(model[1].xyz);
@@ -4284,422 +4440,504 @@ fn getModelMaxScale(model: mat4x4<f32>) -> f32 {
4284
4440
  return max(max(sx, sy), sz);
4285
4441
  }
4286
4442
 
4287
- @compute @workgroup_size(${WORKGROUP_SIZE$1})
4288
- fn cullAndCount(@builtin(global_invocation_id) gid: vec3<u32>) {
4289
- let i = gid.x;
4290
- if (i >= params.splatCount) {
4291
- return;
4292
- }
4443
+ // IEEE 754 位操作编码浮点数为可排序的 u32
4444
+ // 参考 rfs-gsplat-render encode_min_max_fp32 实现
4445
+ fn encodeDepthKey(val: f32) -> u32 {
4446
+ var bits = bitcast<u32>(val);
4447
+ bits ^= bitcast<u32>(bitcast<i32>(bits) >> 31) | 0x80000000u;
4448
+ return bits;
4449
+ }
4450
+
4451
+ // 视锥剔除检查
4452
+ // 基于 rfs-gsplat-render 的 is_in_frustum 实现
4453
+ fn isInFrustum(clipPos: vec4<f32>, frustumDilation: f32) -> bool {
4454
+ let clip = (1.0 + frustumDilation) * clipPos.w;
4293
4455
 
4294
- let splat = splats[i];
4295
- // 先应用模型矩阵变换到世界空间,再变换到视图空间
4296
- let worldPos = camera.model * vec4<f32>(splat.mean, 1.0);
4297
- let viewPos = camera.view * worldPos;
4298
- let z = -viewPos.z;
4456
+ if abs(clipPos.x) > clip { return false; }
4457
+ if abs(clipPos.y) > clip { return false; }
4299
4458
 
4300
- // 近平面剔除
4301
- if (z < params.nearPlane) {
4302
- return;
4459
+ let nearThreshold = (0.0 - frustumDilation) * clipPos.w;
4460
+ if clipPos.z < nearThreshold || clipPos.z > clipPos.w {
4461
+ return false;
4303
4462
  }
4304
4463
 
4305
- // 远平面剔除
4306
- if (z > params.farPlane) {
4307
- return;
4308
- }
4464
+ return true;
4465
+ }
4466
+
4467
+ @compute @workgroup_size(${WORKGROUP_SIZE$1})
4468
+ fn projectAndCull(@builtin(global_invocation_id) gid: vec3<u32>) {
4469
+ let i = gid.x;
4470
+ if i >= params.splatCount { return; }
4309
4471
 
4310
- // 视锥剔除
4311
- let fx = camera.proj[0][0];
4312
- let fy = camera.proj[1][1];
4313
- let x_ndc = viewPos.x * fx / z;
4314
- let y_ndc = viewPos.y * fy / z;
4315
- // 考虑模型缩放对 splat 半径的影响
4316
- let modelScale = getModelMaxScale(camera.model);
4317
- let worldRadius = maxScale(splat.scale) * modelScale * 3.0;
4318
- let r_ndc = worldRadius * max(fx, fy) / z;
4472
+ let splat = splats[i];
4319
4473
 
4320
- if (x_ndc < -1.0 - r_ndc || x_ndc > 1.0 + r_ndc) {
4321
- return;
4322
- }
4323
- if (y_ndc < -1.0 - r_ndc || y_ndc > 1.0 + r_ndc) {
4324
- return;
4325
- }
4474
+ // 透明度剔除
4475
+ if splat.opacity < 0.004 { return; }
4326
4476
 
4327
- // 屏幕尺寸剔除
4328
- let screenRadiusX = r_ndc * params.screenWidth * 0.5;
4329
- let screenRadiusY = r_ndc * params.screenHeight * 0.5;
4330
- let screenRadius = max(screenRadiusX, screenRadiusY);
4477
+ // 变换: Local -> World -> View -> Clip
4478
+ let worldPos = camera.model * vec4<f32>(splat.mean, 1.0);
4479
+ let viewPos = camera.view * worldPos;
4480
+ let clipPos = camera.proj * viewPos;
4331
4481
 
4332
- if (screenRadius < params.pixelThreshold) {
4333
- return;
4334
- }
4482
+ // 视锥剔除
4483
+ if !isInFrustum(clipPos, params.frustumDilation) { return; }
4335
4484
 
4336
- // 透明度剔除
4337
- if (splat.opacity < 0.004) {
4338
- return;
4339
- }
4485
+ // 深度编码 (viewPos.z 是负数)
4486
+ let depth = viewPos.z;
4487
+ let sortableDepth = encodeDepthKey(depth);
4340
4488
 
4341
- // 通过剔除,计算深度桶
4342
- let depthRange = params.farPlane - params.nearPlane;
4343
- let normalizedDepth = clamp((z - params.nearPlane) / depthRange, 0.0, 1.0);
4344
- // 反转:让远处的桶 ID 更小,这样 counting sort 后远处的在前面
4345
- let depthBucket = BUCKET_MAX - u32(normalizedDepth * f32(BUCKET_MAX));
4346
- // 复合 key: 深度桶 + 原始索引低位作为 tie-breaker
4347
- let depthKey = (depthBucket << ${32 - bucketBits}u) | (i & ${(1 << 32 - bucketBits) - 1}u);
4489
+ // 原子增加可见计数并获取索引
4490
+ // indirectBuffer[1] instance_count
4491
+ let visibleIdx = atomicAdd(&indirectBuffer[1], 1u);
4348
4492
 
4349
- // 分配可见索引位置
4350
- let visibleIdx = atomicAdd(&counters.visibleCount, 1u);
4493
+ // 写入可见点列表
4494
+ depthKeys[visibleIdx] = sortableDepth;
4351
4495
  visibleIndices[visibleIdx] = i;
4352
- depthKeys[visibleIdx] = depthKey;
4353
-
4354
- // 统计桶计数
4355
- atomicAdd(&bucketCounts[depthBucket], 1u);
4356
- }
4357
-
4358
- @compute @workgroup_size(1)
4359
- fn resetCounters() {
4360
- atomicStore(&counters.visibleCount, 0u);
4361
- }
4362
-
4363
- @compute @workgroup_size(${WORKGROUP_SIZE$1})
4364
- fn resetBucketCounts(@builtin(global_invocation_id) gid: vec3<u32>) {
4365
- let i = gid.x;
4366
- if (i < NUM_BUCKETS) {
4367
- atomicStore(&bucketCounts[i], 0u);
4368
- }
4369
4496
  }
4370
4497
 
4371
4498
  @compute @workgroup_size(1)
4372
- fn updateDrawIndirect() {
4373
- let count = atomicLoad(&counters.visibleCount);
4374
- drawIndirect[0] = 4u;
4375
- drawIndirect[1] = count;
4376
- drawIndirect[2] = 0u;
4377
- drawIndirect[3] = 0u;
4499
+ fn initIndirectBuffer() {
4500
+ // [vertex_count, instance_count, first_vertex, first_instance]
4501
+ atomicStore(&indirectBuffer[0], 4u);
4502
+ atomicStore(&indirectBuffer[1], 0u); // instance_count 由 cull shader 填充
4503
+ atomicStore(&indirectBuffer[2], 0u);
4504
+ atomicStore(&indirectBuffer[3], 0u);
4378
4505
  }
4379
4506
  `
4380
4507
  );
4381
4508
  }
4382
- function generatePrefixSumShaderCode$1(numBuckets) {
4509
+ function generateRadixSortShaderCode() {
4383
4510
  return (
4384
4511
  /* wgsl */
4385
4512
  `
4386
4513
  /**
4387
- * Pass 2: 串行前缀和
4388
- * ${numBuckets} 个桶
4514
+ * GPU Radix Sort - 3-Pass Architecture
4515
+ * 基于 rfs-gsplat-render 实现
4516
+ *
4517
+ * Pass 1: Upsweep - 构建局部直方图并累加到全局
4518
+ * Pass 2: Spine - 对分区和全局直方图进行前缀和
4519
+ * Pass 3: Downsweep - 使用计算的偏移量散射元素 (稳定排序)
4389
4520
  */
4390
4521
 
4391
- const NUM_BUCKETS: u32 = ${numBuckets}u;
4392
-
4393
- @group(0) @binding(0) var<storage, read_write> bucketCounts: array<u32>;
4394
- @group(0) @binding(1) var<storage, read_write> bucketOffsets: array<u32>;
4395
-
4396
- @compute @workgroup_size(1)
4397
- fn prefixSum() {
4398
- var sum = 0u;
4399
- for (var i = 0u; i < NUM_BUCKETS; i++) {
4400
- bucketOffsets[i] = sum;
4401
- sum += bucketCounts[i];
4522
+ const WG: u32 = ${WORKGROUP_SIZE$1}u;
4523
+ const RADIX_BITS: u32 = ${RADIX_BITS}u;
4524
+ const RADIX_SIZE: u32 = ${RADIX_SIZE}u;
4525
+ const RADIX_MASK: u32 = ${RADIX_SIZE - 1}u;
4526
+ const ELEMENTS_PER_THREAD: u32 = ${ELEMENTS_PER_THREAD}u;
4527
+ const BLOCK_SIZE: u32 = ${BLOCK_SIZE}u;
4528
+
4529
+ fn divCeil(a: u32, b: u32) -> u32 {
4530
+ return (a + b - 1u) / b;
4531
+ }
4532
+
4533
+ struct SortParams {
4534
+ maxElementCount: u32,
4535
+ bitShift: u32,
4536
+ passIndex: u32,
4537
+ _padding: u32,
4538
+ }
4539
+
4540
+ // ============================================================================
4541
+ // Pass 1: Upsweep - 计数局部直方图并累加到全局
4542
+ // ============================================================================
4543
+
4544
+ @group(0) @binding(0) var<uniform> upsweepParams: SortParams;
4545
+ @group(0) @binding(1) var<storage, read> indirectBufferUpsweep: array<u32>;
4546
+ @group(0) @binding(2) var<storage, read> keysIn: array<u32>;
4547
+ @group(0) @binding(3) var<storage, read_write> globalHistogram: array<atomic<u32>>;
4548
+ @group(0) @binding(4) var<storage, read_write> partitionHistogram: array<u32>;
4549
+
4550
+ var<workgroup> localHistogram: array<atomic<u32>, RADIX_SIZE>;
4551
+
4552
+ @compute @workgroup_size(256, 1, 1)
4553
+ fn upsweep(
4554
+ @builtin(local_invocation_id) localId: vec3<u32>,
4555
+ @builtin(workgroup_id) workgroupId: vec3<u32>,
4556
+ ) {
4557
+ // 从 indirectBuffer[1] 读取动态可见数量 (instance_count)
4558
+ let numKeys = indirectBufferUpsweep[1];
4559
+ let numPartitions = divCeil(numKeys, BLOCK_SIZE);
4560
+ let partitionId = workgroupId.x;
4561
+
4562
+ if partitionId >= numPartitions { return; }
4563
+
4564
+ let tid = localId.x;
4565
+ let partitionStart = partitionId * BLOCK_SIZE;
4566
+ let shift = upsweepParams.bitShift;
4567
+ let passIdx = upsweepParams.passIndex;
4568
+
4569
+ // 初始化局部直方图
4570
+ if tid < RADIX_SIZE {
4571
+ atomicStore(&localHistogram[tid], 0u);
4402
4572
  }
4403
- }
4404
- `
4405
- );
4406
- }
4407
- function generateScatterShaderCode$1(numBuckets) {
4408
- const bucketBits = Math.log2(numBuckets);
4409
- const depthShift = 32 - bucketBits;
4410
- return (
4411
- /* wgsl */
4412
- `
4413
- /**
4414
- * Pass 3: 散射到最终排序位置
4415
- * 桶数量: ${numBuckets}
4416
- */
4417
-
4418
- const NUM_BUCKETS: u32 = ${numBuckets}u;
4419
-
4420
- struct Counters {
4421
- visibleCount: u32,
4422
- }
4423
-
4424
- @group(0) @binding(0) var<storage, read> visibleIndices: array<u32>;
4425
- @group(0) @binding(1) var<storage, read> depthKeys: array<u32>;
4426
- @group(0) @binding(2) var<storage, read> bucketOffsets: array<u32>;
4427
- @group(0) @binding(3) var<storage, read_write> bucketPositions: array<atomic<u32>>;
4428
- @group(0) @binding(4) var<storage, read_write> sortedIndices: array<u32>;
4429
- @group(0) @binding(5) var<storage, read> counters: Counters;
4430
-
4431
- @compute @workgroup_size(${WORKGROUP_SIZE$1})
4432
- fn scatter(@builtin(global_invocation_id) gid: vec3<u32>) {
4433
- let i = gid.x;
4434
- if (i >= counters.visibleCount) {
4435
- return;
4573
+ workgroupBarrier();
4574
+
4575
+ // 构建局部直方图
4576
+ for (var j = 0u; j < ELEMENTS_PER_THREAD; j++) {
4577
+ let keyIdx = partitionStart + tid * ELEMENTS_PER_THREAD + j;
4578
+ if keyIdx < numKeys {
4579
+ let key = keysIn[keyIdx];
4580
+ let bin = (key >> shift) & RADIX_MASK;
4581
+ atomicAdd(&localHistogram[bin], 1u);
4582
+ }
4436
4583
  }
4437
4584
 
4438
- let depthKey = depthKeys[i];
4439
- let originalIndex = visibleIndices[i];
4585
+ workgroupBarrier();
4440
4586
 
4441
- // 从复合 key 提取深度桶
4442
- let depthBucket = depthKey >> ${depthShift}u;
4587
+ // 写入分区直方图并累加到全局直方图
4588
+ if tid < RADIX_SIZE {
4589
+ let count = atomicLoad(&localHistogram[tid]);
4590
+ partitionHistogram[RADIX_SIZE * partitionId + tid] = count;
4591
+ atomicAdd(&globalHistogram[RADIX_SIZE * passIdx + tid], count);
4592
+ }
4593
+ }
4594
+
4595
+ // ============================================================================
4596
+ // Pass 2: Spine - 对分区和全局直方图进行前缀和
4597
+ // ============================================================================
4598
+
4599
+ @group(0) @binding(0) var<storage, read> indirectBufferSpine: array<u32>;
4600
+ @group(0) @binding(1) var<storage, read_write> globalHistogramSpine: array<u32>;
4601
+ @group(0) @binding(2) var<storage, read_write> partitionHistogramSpine: array<u32>;
4602
+ @group(0) @binding(3) var<uniform> spineParams: SortParams;
4603
+
4604
+ // 双缓冲用于无数据竞争的 Hillis-Steele scan
4605
+ var<workgroup> scanA: array<u32, 256>;
4606
+ var<workgroup> scanB: array<u32, 256>;
4607
+ var<workgroup> reductionShared: u32;
4608
+
4609
+ @compute @workgroup_size(256, 1, 1)
4610
+ fn spine(
4611
+ @builtin(local_invocation_id) localId: vec3<u32>,
4612
+ @builtin(workgroup_id) workgroupId: vec3<u32>,
4613
+ ) {
4614
+ let numKeys = indirectBufferSpine[1];
4615
+ let numPartitions = divCeil(numKeys, BLOCK_SIZE);
4616
+ let bin = workgroupId.x;
4617
+ let tid = localId.x;
4443
4618
 
4444
- // 在桶内分配位置
4445
- let bucketOffset = bucketOffsets[depthBucket];
4446
- let posInBucket = atomicAdd(&bucketPositions[depthBucket], 1u);
4447
- let destIdx = bucketOffset + posInBucket;
4619
+ if bin >= RADIX_SIZE { return; }
4448
4620
 
4449
- // 写入最终排序位置
4450
- sortedIndices[destIdx] = originalIndex;
4451
- }
4452
-
4453
- @compute @workgroup_size(${WORKGROUP_SIZE$1})
4454
- fn resetBucketPositions(@builtin(global_invocation_id) gid: vec3<u32>) {
4455
- let i = gid.x;
4456
- if (i < NUM_BUCKETS) {
4457
- atomicStore(&bucketPositions[i], 0u);
4621
+ // 初始化共享 reduction
4622
+ if tid == 0u {
4623
+ reductionShared = 0u;
4624
+ }
4625
+ workgroupBarrier();
4626
+
4627
+ // 处理此 bin 的所有分区(分批处理)
4628
+ let MAX_BATCH_SIZE = 256u;
4629
+ for (var batchStart = 0u; batchStart < numPartitions; batchStart += MAX_BATCH_SIZE) {
4630
+ let partitionIdx = batchStart + tid;
4631
+ let batchSize = min(MAX_BATCH_SIZE, numPartitions - batchStart);
4632
+
4633
+ // 加载此批次的值
4634
+ if tid < batchSize && partitionIdx < numPartitions {
4635
+ scanA[tid] = partitionHistogramSpine[RADIX_SIZE * partitionIdx + bin];
4636
+ } else {
4637
+ scanA[tid] = 0u;
4638
+ }
4639
+ workgroupBarrier();
4640
+
4641
+ // Hillis-Steele inclusive prefix sum (双缓冲避免数据竞争)
4642
+ var useA = true;
4643
+ var offset = 1u;
4644
+ for (var d = 0u; d < 8u; d++) {
4645
+ if useA {
4646
+ if tid >= offset {
4647
+ scanB[tid] = scanA[tid] + scanA[tid - offset];
4648
+ } else {
4649
+ scanB[tid] = scanA[tid];
4650
+ }
4651
+ } else {
4652
+ if tid >= offset {
4653
+ scanA[tid] = scanB[tid] + scanB[tid - offset];
4654
+ } else {
4655
+ scanA[tid] = scanB[tid];
4656
+ }
4657
+ }
4658
+ workgroupBarrier();
4659
+ useA = !useA;
4660
+ offset <<= 1u;
4661
+ }
4662
+
4663
+ // 8 次迭代后结果在 scanA 中
4664
+
4665
+ // 写回为 exclusive prefix sum(加上 reduction)
4666
+ if tid < batchSize && partitionIdx < numPartitions {
4667
+ var exclusive = reductionShared;
4668
+ if tid > 0u {
4669
+ exclusive += scanA[tid - 1u];
4670
+ }
4671
+ partitionHistogramSpine[RADIX_SIZE * partitionIdx + bin] = exclusive;
4672
+ }
4673
+
4674
+ // 更新下一批的 reduction
4675
+ workgroupBarrier();
4676
+ if tid == 0u && batchSize > 0u {
4677
+ reductionShared += scanA[batchSize - 1u];
4678
+ }
4679
+ workgroupBarrier();
4680
+ }
4681
+
4682
+ // Bin 0 的工作组同时处理全局直方图前缀和
4683
+ if bin == 0u {
4684
+ let passIdx = spineParams.passIndex;
4685
+ scanA[tid] = globalHistogramSpine[RADIX_SIZE * passIdx + tid];
4686
+ workgroupBarrier();
4687
+
4688
+ // Hillis-Steele inclusive scan (双缓冲)
4689
+ var useA = true;
4690
+ var offset = 1u;
4691
+ for (var d = 0u; d < 8u; d++) {
4692
+ if useA {
4693
+ if tid >= offset {
4694
+ scanB[tid] = scanA[tid] + scanA[tid - offset];
4695
+ } else {
4696
+ scanB[tid] = scanA[tid];
4697
+ }
4698
+ } else {
4699
+ if tid >= offset {
4700
+ scanA[tid] = scanB[tid] + scanB[tid - offset];
4701
+ } else {
4702
+ scanA[tid] = scanB[tid];
4703
+ }
4704
+ }
4705
+ workgroupBarrier();
4706
+ useA = !useA;
4707
+ offset <<= 1u;
4708
+ }
4709
+
4710
+ // 转换为 exclusive (结果在 scanA 中)
4711
+ var exclusive = 0u;
4712
+ if tid > 0u {
4713
+ exclusive = scanA[tid - 1u];
4714
+ }
4715
+ globalHistogramSpine[RADIX_SIZE * passIdx + tid] = exclusive;
4716
+ }
4717
+ }
4718
+
4719
+ // ============================================================================
4720
+ // Pass 3: Downsweep - 使用偏移量散射元素 (稳定排序)
4721
+ // ============================================================================
4722
+
4723
+ @group(0) @binding(0) var<uniform> downsweepParams: SortParams;
4724
+ @group(0) @binding(1) var<storage, read> indirectBufferDownsweep: array<u32>;
4725
+ @group(0) @binding(2) var<storage, read> globalHistogramDownsweep: array<u32>;
4726
+ @group(0) @binding(3) var<storage, read> partitionHistogramDownsweep: array<u32>;
4727
+ @group(0) @binding(4) var<storage, read> downsweepKeysIn: array<u32>;
4728
+ @group(0) @binding(5) var<storage, read> downsweepValuesIn: array<u32>;
4729
+ @group(0) @binding(6) var<storage, read_write> downsweepKeysOut: array<u32>;
4730
+ @group(0) @binding(7) var<storage, read_write> downsweepValuesOut: array<u32>;
4731
+
4732
+ var<workgroup> localKeys: array<u32, BLOCK_SIZE>;
4733
+ var<workgroup> localValues: array<u32, BLOCK_SIZE>;
4734
+ var<workgroup> localBins: array<u32, BLOCK_SIZE>;
4735
+
4736
+ @compute @workgroup_size(256, 1, 1)
4737
+ fn downsweep(
4738
+ @builtin(local_invocation_id) localId: vec3<u32>,
4739
+ @builtin(workgroup_id) workgroupId: vec3<u32>,
4740
+ ) {
4741
+ let numKeys = indirectBufferDownsweep[1];
4742
+ let numPartitions = divCeil(numKeys, BLOCK_SIZE);
4743
+ let partitionId = workgroupId.x;
4744
+
4745
+ if partitionId >= numPartitions { return; }
4746
+
4747
+ let tid = localId.x;
4748
+ let partitionStart = partitionId * BLOCK_SIZE;
4749
+ let shift = downsweepParams.bitShift;
4750
+
4751
+ // 加载元素到共享内存
4752
+ for (var j = 0u; j < ELEMENTS_PER_THREAD; j++) {
4753
+ let keyIdx = partitionStart + tid * ELEMENTS_PER_THREAD + j;
4754
+ let localIdx = tid * ELEMENTS_PER_THREAD + j;
4755
+
4756
+ if keyIdx < numKeys {
4757
+ let key = downsweepKeysIn[keyIdx];
4758
+ localKeys[localIdx] = key;
4759
+ localValues[localIdx] = downsweepValuesIn[keyIdx];
4760
+ localBins[localIdx] = (key >> shift) & RADIX_MASK;
4761
+ } else {
4762
+ localBins[localIdx] = 0xFFFFFFFFu;
4763
+ }
4764
+ }
4765
+
4766
+ workgroupBarrier();
4767
+
4768
+ // 线程 0 执行顺序散射以保持稳定性
4769
+ // 这是 rfs-gsplat-render 的关键设计,确保稳定排序
4770
+ if tid == 0u {
4771
+ var binWritePos: array<u32, RADIX_SIZE>;
4772
+
4773
+ let passIdx = downsweepParams.passIndex;
4774
+
4775
+ // 从全局 + 分区偏移初始化写入位置
4776
+ for (var b = 0u; b < RADIX_SIZE; b++) {
4777
+ binWritePos[b] = globalHistogramDownsweep[RADIX_SIZE * passIdx + b] +
4778
+ partitionHistogramDownsweep[RADIX_SIZE * partitionId + b];
4779
+ }
4780
+
4781
+ // 按输入顺序顺序写入 (稳定)
4782
+ let partitionEnd = min(partitionStart + BLOCK_SIZE, numKeys);
4783
+ for (var k = 0u; k < BLOCK_SIZE; k++) {
4784
+ let keyIdx = partitionStart + k;
4785
+ if keyIdx < partitionEnd {
4786
+ let b = localBins[k];
4787
+ if b != 0xFFFFFFFFu {
4788
+ let writePos = binWritePos[b];
4789
+ if writePos < numKeys {
4790
+ downsweepKeysOut[writePos] = localKeys[k];
4791
+ downsweepValuesOut[writePos] = localValues[k];
4792
+ binWritePos[b]++;
4793
+ }
4794
+ }
4795
+ }
4796
+ }
4458
4797
  }
4459
4798
  }
4460
4799
  `
4461
4800
  );
4462
4801
  }
4463
4802
  class GSSplatSorter {
4464
- constructor(device, splatCount, splatBuffer, cameraBuffer, options = {}) {
4803
+ constructor(device, splatCount, splatBuffer, cameraBuffer, _options = {}) {
4465
4804
  __publicField(this, "device");
4466
4805
  __publicField(this, "splatCount");
4467
- // ============================================
4468
- // Buffers
4469
- // ============================================
4806
+ // Culling Buffers
4470
4807
  __publicField(this, "cullingParamsBuffer");
4471
- __publicField(this, "countersBuffer");
4472
- __publicField(this, "visibleIndicesBuffer");
4473
4808
  __publicField(this, "depthKeysBuffer");
4474
- __publicField(this, "bucketCountsBuffer");
4475
- __publicField(this, "bucketOffsetsBuffer");
4476
- __publicField(this, "bucketPositionsBuffer");
4809
+ __publicField(this, "visibleIndicesBuffer");
4810
+ __publicField(this, "indirectBuffer");
4811
+ // Radix Sort Buffers
4812
+ __publicField(this, "globalHistogramBuffer");
4813
+ __publicField(this, "partitionHistogramBuffer");
4814
+ __publicField(this, "keysTempBuffer");
4815
+ __publicField(this, "valuesTempBuffer");
4816
+ // 每个 pass 独立的参数 buffer (避免竞争)
4817
+ __publicField(this, "sortParamsBuffers", []);
4818
+ // Sorted output
4477
4819
  __publicField(this, "sortedIndicesBuffer");
4478
- __publicField(this, "drawIndirectBuffer");
4479
- // ============================================
4480
- // Pipelines
4481
- // ============================================
4482
- __publicField(this, "resetCountersPipeline");
4483
- __publicField(this, "resetBucketCountsPipeline");
4484
- __publicField(this, "cullAndCountPipeline");
4485
- __publicField(this, "updateDrawIndirectPipeline");
4486
- __publicField(this, "prefixSumPipeline");
4487
- __publicField(this, "resetBucketPositionsPipeline");
4488
- __publicField(this, "scatterPipeline");
4489
- // ============================================
4490
- // Bind Groups
4491
- // ============================================
4820
+ // Culling Pipelines
4821
+ __publicField(this, "initIndirectPipeline");
4822
+ __publicField(this, "projectCullPipeline");
4492
4823
  __publicField(this, "cullingBindGroupLayout");
4493
4824
  __publicField(this, "cullingBindGroup");
4494
- __publicField(this, "prefixSumBindGroupLayout");
4495
- __publicField(this, "prefixSumBindGroup");
4496
- __publicField(this, "scatterBindGroupLayout");
4497
- __publicField(this, "scatterBindGroup");
4498
- // 工作组大小和桶数量
4499
- __publicField(this, "WORKGROUP_SIZE", WORKGROUP_SIZE$1);
4500
- __publicField(this, "numBuckets");
4501
- // 当前屏幕信息
4825
+ // Radix Sort Pipelines
4826
+ __publicField(this, "upsweepPipeline");
4827
+ __publicField(this, "spinePipeline");
4828
+ __publicField(this, "downsweepPipeline");
4829
+ __publicField(this, "upsweepBindGroupLayout");
4830
+ __publicField(this, "spineBindGroupLayout");
4831
+ __publicField(this, "downsweepBindGroupLayout");
4832
+ // Bind groups for each pass (4 passes)
4833
+ __publicField(this, "upsweepBindGroups", []);
4834
+ __publicField(this, "spineBindGroups", []);
4835
+ __publicField(this, "downsweepBindGroups", []);
4836
+ __publicField(this, "numPartitions");
4837
+ // 屏幕信息和剔除选项
4502
4838
  __publicField(this, "screenWidth", 1920);
4503
4839
  __publicField(this, "screenHeight", 1080);
4504
- // 剔除选项
4505
4840
  __publicField(this, "cullingOptions", {
4506
4841
  nearPlane: 0.1,
4507
4842
  farPlane: 1e3,
4508
- pixelThreshold: 1
4843
+ pixelThreshold: 0,
4844
+ frustumDilation: 0.2
4509
4845
  });
4510
4846
  this.device = device;
4511
4847
  this.splatCount = splatCount;
4512
- const isIOS = isIOSDevice$1();
4513
- this.numBuckets = options.numBuckets ?? (isIOS ? IOS_NUM_BUCKETS$1 : DEFAULT_NUM_BUCKETS$1);
4514
- const cullingCode = generateCullingShaderCode$1(this.numBuckets);
4515
- const prefixSumCode = generatePrefixSumShaderCode$1(this.numBuckets);
4516
- const scatterCode = generateScatterShaderCode$1(this.numBuckets);
4848
+ this.numPartitions = Math.ceil(splatCount / BLOCK_SIZE);
4517
4849
  const cullingModule = device.createShaderModule({
4518
- code: cullingCode,
4850
+ code: generateCullingShaderCode$1(),
4519
4851
  label: "culling-shader"
4520
4852
  });
4521
- const prefixSumModule = device.createShaderModule({
4522
- code: prefixSumCode,
4523
- label: "prefix-sum-shader"
4524
- });
4525
- const scatterModule = device.createShaderModule({
4526
- code: scatterCode,
4527
- label: "scatter-shader"
4528
- });
4529
- cullingModule.getCompilationInfo().then((info) => {
4530
- if (info.messages.length > 0) ;
4531
- });
4532
- prefixSumModule.getCompilationInfo().then((info) => {
4533
- if (info.messages.length > 0) ;
4534
- });
4535
- scatterModule.getCompilationInfo().then((info) => {
4536
- if (info.messages.length > 0) ;
4853
+ const radixSortModule = device.createShaderModule({
4854
+ code: generateRadixSortShaderCode(),
4855
+ label: "radix-sort-shader"
4537
4856
  });
4538
4857
  this.cullingParamsBuffer = device.createBuffer({
4539
4858
  size: 32,
4540
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
4859
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
4860
+ label: "culling-params"
4541
4861
  });
4542
- this.countersBuffer = device.createBuffer({
4543
- size: 16,
4544
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
4862
+ this.depthKeysBuffer = device.createBuffer({
4863
+ size: splatCount * 4,
4864
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
4865
+ label: "depth-keys"
4545
4866
  });
4546
4867
  this.visibleIndicesBuffer = device.createBuffer({
4547
4868
  size: splatCount * 4,
4548
- usage: GPUBufferUsage.STORAGE
4869
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
4870
+ label: "visible-indices"
4549
4871
  });
4550
- this.depthKeysBuffer = device.createBuffer({
4551
- size: splatCount * 4,
4552
- usage: GPUBufferUsage.STORAGE
4872
+ this.indirectBuffer = device.createBuffer({
4873
+ size: 16,
4874
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST,
4875
+ label: "indirect-buffer"
4553
4876
  });
4554
- this.bucketCountsBuffer = device.createBuffer({
4555
- size: this.numBuckets * 4,
4556
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
4877
+ this.globalHistogramBuffer = device.createBuffer({
4878
+ size: RADIX_SIZE * 4 * 4,
4879
+ // 4 passes * 256 bins * 4 bytes
4880
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
4881
+ label: "global-histogram"
4557
4882
  });
4558
- this.bucketOffsetsBuffer = device.createBuffer({
4559
- size: this.numBuckets * 4,
4560
- usage: GPUBufferUsage.STORAGE
4883
+ this.partitionHistogramBuffer = device.createBuffer({
4884
+ size: this.numPartitions * RADIX_SIZE * 4,
4885
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
4886
+ label: "partition-histogram"
4561
4887
  });
4562
- this.bucketPositionsBuffer = device.createBuffer({
4563
- size: this.numBuckets * 4,
4564
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
4888
+ this.keysTempBuffer = device.createBuffer({
4889
+ size: splatCount * 4,
4890
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
4891
+ label: "keys-temp"
4565
4892
  });
4566
- this.sortedIndicesBuffer = device.createBuffer({
4893
+ this.valuesTempBuffer = device.createBuffer({
4567
4894
  size: splatCount * 4,
4568
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
4895
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
4896
+ label: "values-temp"
4569
4897
  });
4570
- this.drawIndirectBuffer = device.createBuffer({
4571
- size: 16,
4572
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST
4898
+ for (let i = 0; i < 4; i++) {
4899
+ const paramsBuffer = device.createBuffer({
4900
+ size: 16,
4901
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
4902
+ label: `sort-params-${i}`
4903
+ });
4904
+ this.sortParamsBuffers.push(paramsBuffer);
4905
+ const sortParams = new ArrayBuffer(16);
4906
+ const sortView = new DataView(sortParams);
4907
+ sortView.setUint32(0, splatCount, true);
4908
+ sortView.setUint32(4, i * RADIX_BITS, true);
4909
+ sortView.setUint32(8, i, true);
4910
+ sortView.setUint32(12, 0, true);
4911
+ device.queue.writeBuffer(paramsBuffer, 0, sortParams);
4912
+ }
4913
+ this.sortedIndicesBuffer = device.createBuffer({
4914
+ size: splatCount * 4,
4915
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
4916
+ label: "sorted-indices"
4573
4917
  });
4574
4918
  this.cullingBindGroupLayout = device.createBindGroupLayout({
4919
+ label: "culling-bind-group-layout",
4575
4920
  entries: [
4576
- {
4577
- binding: 0,
4578
- visibility: GPUShaderStage.COMPUTE,
4579
- buffer: { type: "read-only-storage" }
4580
- },
4581
- {
4582
- binding: 1,
4583
- visibility: GPUShaderStage.COMPUTE,
4584
- buffer: { type: "uniform" }
4585
- },
4586
- {
4587
- binding: 2,
4588
- visibility: GPUShaderStage.COMPUTE,
4589
- buffer: { type: "uniform" }
4590
- },
4591
- {
4592
- binding: 3,
4593
- visibility: GPUShaderStage.COMPUTE,
4594
- buffer: { type: "storage" }
4595
- },
4596
- {
4597
- binding: 4,
4598
- visibility: GPUShaderStage.COMPUTE,
4599
- buffer: { type: "storage" }
4600
- },
4601
- {
4602
- binding: 5,
4603
- visibility: GPUShaderStage.COMPUTE,
4604
- buffer: { type: "storage" }
4605
- },
4606
- {
4607
- binding: 6,
4608
- visibility: GPUShaderStage.COMPUTE,
4609
- buffer: { type: "storage" }
4610
- },
4611
- {
4612
- binding: 7,
4613
- visibility: GPUShaderStage.COMPUTE,
4614
- buffer: { type: "storage" }
4615
- }
4921
+ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
4922
+ { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
4923
+ { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
4924
+ { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
4925
+ { binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
4926
+ { binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }
4616
4927
  ]
4617
4928
  });
4618
4929
  const cullingPipelineLayout = device.createPipelineLayout({
4619
4930
  bindGroupLayouts: [this.cullingBindGroupLayout]
4620
4931
  });
4621
- this.resetCountersPipeline = device.createComputePipeline({
4622
- layout: cullingPipelineLayout,
4623
- compute: { module: cullingModule, entryPoint: "resetCounters" }
4624
- });
4625
- this.resetBucketCountsPipeline = device.createComputePipeline({
4626
- layout: cullingPipelineLayout,
4627
- compute: { module: cullingModule, entryPoint: "resetBucketCounts" }
4628
- });
4629
- this.cullAndCountPipeline = device.createComputePipeline({
4932
+ this.initIndirectPipeline = device.createComputePipeline({
4630
4933
  layout: cullingPipelineLayout,
4631
- compute: { module: cullingModule, entryPoint: "cullAndCount" }
4934
+ compute: { module: cullingModule, entryPoint: "initIndirectBuffer" },
4935
+ label: "init-indirect-pipeline"
4632
4936
  });
4633
- this.updateDrawIndirectPipeline = device.createComputePipeline({
4937
+ this.projectCullPipeline = device.createComputePipeline({
4634
4938
  layout: cullingPipelineLayout,
4635
- compute: { module: cullingModule, entryPoint: "updateDrawIndirect" }
4636
- });
4637
- this.prefixSumBindGroupLayout = device.createBindGroupLayout({
4638
- entries: [
4639
- {
4640
- binding: 0,
4641
- visibility: GPUShaderStage.COMPUTE,
4642
- buffer: { type: "storage" }
4643
- },
4644
- {
4645
- binding: 1,
4646
- visibility: GPUShaderStage.COMPUTE,
4647
- buffer: { type: "storage" }
4648
- }
4649
- ]
4650
- });
4651
- const prefixSumPipelineLayout = device.createPipelineLayout({
4652
- bindGroupLayouts: [this.prefixSumBindGroupLayout]
4653
- });
4654
- this.prefixSumPipeline = device.createComputePipeline({
4655
- layout: prefixSumPipelineLayout,
4656
- compute: { module: prefixSumModule, entryPoint: "prefixSum" }
4657
- });
4658
- this.scatterBindGroupLayout = device.createBindGroupLayout({
4659
- entries: [
4660
- {
4661
- binding: 0,
4662
- visibility: GPUShaderStage.COMPUTE,
4663
- buffer: { type: "read-only-storage" }
4664
- },
4665
- {
4666
- binding: 1,
4667
- visibility: GPUShaderStage.COMPUTE,
4668
- buffer: { type: "read-only-storage" }
4669
- },
4670
- {
4671
- binding: 2,
4672
- visibility: GPUShaderStage.COMPUTE,
4673
- buffer: { type: "read-only-storage" }
4674
- },
4675
- {
4676
- binding: 3,
4677
- visibility: GPUShaderStage.COMPUTE,
4678
- buffer: { type: "storage" }
4679
- },
4680
- {
4681
- binding: 4,
4682
- visibility: GPUShaderStage.COMPUTE,
4683
- buffer: { type: "storage" }
4684
- },
4685
- {
4686
- binding: 5,
4687
- visibility: GPUShaderStage.COMPUTE,
4688
- buffer: { type: "read-only-storage" }
4689
- }
4690
- // countersBuffer
4691
- ]
4692
- });
4693
- const scatterPipelineLayout = device.createPipelineLayout({
4694
- bindGroupLayouts: [this.scatterBindGroupLayout]
4695
- });
4696
- this.resetBucketPositionsPipeline = device.createComputePipeline({
4697
- layout: scatterPipelineLayout,
4698
- compute: { module: scatterModule, entryPoint: "resetBucketPositions" }
4699
- });
4700
- this.scatterPipeline = device.createComputePipeline({
4701
- layout: scatterPipelineLayout,
4702
- compute: { module: scatterModule, entryPoint: "scatter" }
4939
+ compute: { module: cullingModule, entryPoint: "projectAndCull" },
4940
+ label: "project-cull-pipeline"
4703
4941
  });
4704
4942
  this.cullingBindGroup = device.createBindGroup({
4705
4943
  layout: this.cullingBindGroupLayout,
@@ -4707,127 +4945,196 @@ class GSSplatSorter {
4707
4945
  { binding: 0, resource: { buffer: splatBuffer } },
4708
4946
  { binding: 1, resource: { buffer: cameraBuffer } },
4709
4947
  { binding: 2, resource: { buffer: this.cullingParamsBuffer } },
4710
- { binding: 3, resource: { buffer: this.countersBuffer } },
4948
+ { binding: 3, resource: { buffer: this.depthKeysBuffer } },
4711
4949
  { binding: 4, resource: { buffer: this.visibleIndicesBuffer } },
4712
- { binding: 5, resource: { buffer: this.depthKeysBuffer } },
4713
- { binding: 6, resource: { buffer: this.bucketCountsBuffer } },
4714
- { binding: 7, resource: { buffer: this.drawIndirectBuffer } }
4950
+ { binding: 5, resource: { buffer: this.indirectBuffer } }
4951
+ ],
4952
+ label: "culling-bind-group"
4953
+ });
4954
+ this.upsweepBindGroupLayout = device.createBindGroupLayout({
4955
+ label: "upsweep-layout",
4956
+ entries: [
4957
+ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
4958
+ { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
4959
+ { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
4960
+ { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
4961
+ { binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }
4715
4962
  ]
4716
4963
  });
4717
- this.prefixSumBindGroup = device.createBindGroup({
4718
- layout: this.prefixSumBindGroupLayout,
4964
+ this.spineBindGroupLayout = device.createBindGroupLayout({
4965
+ label: "spine-layout",
4719
4966
  entries: [
4720
- { binding: 0, resource: { buffer: this.bucketCountsBuffer } },
4721
- { binding: 1, resource: { buffer: this.bucketOffsetsBuffer } }
4967
+ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
4968
+ { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
4969
+ { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
4970
+ { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } }
4722
4971
  ]
4723
4972
  });
4724
- this.scatterBindGroup = device.createBindGroup({
4725
- layout: this.scatterBindGroupLayout,
4973
+ this.downsweepBindGroupLayout = device.createBindGroupLayout({
4974
+ label: "downsweep-layout",
4726
4975
  entries: [
4727
- { binding: 0, resource: { buffer: this.visibleIndicesBuffer } },
4728
- { binding: 1, resource: { buffer: this.depthKeysBuffer } },
4729
- { binding: 2, resource: { buffer: this.bucketOffsetsBuffer } },
4730
- { binding: 3, resource: { buffer: this.bucketPositionsBuffer } },
4731
- { binding: 4, resource: { buffer: this.sortedIndicesBuffer } },
4732
- { binding: 5, resource: { buffer: this.countersBuffer } }
4733
- // 使用 GPU countersBuffer
4976
+ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
4977
+ { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
4978
+ { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
4979
+ { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
4980
+ { binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
4981
+ { binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
4982
+ { binding: 6, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
4983
+ { binding: 7, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }
4734
4984
  ]
4735
4985
  });
4986
+ this.upsweepPipeline = device.createComputePipeline({
4987
+ layout: device.createPipelineLayout({ bindGroupLayouts: [this.upsweepBindGroupLayout] }),
4988
+ compute: { module: radixSortModule, entryPoint: "upsweep" },
4989
+ label: "upsweep-pipeline"
4990
+ });
4991
+ this.spinePipeline = device.createComputePipeline({
4992
+ layout: device.createPipelineLayout({ bindGroupLayouts: [this.spineBindGroupLayout] }),
4993
+ compute: { module: radixSortModule, entryPoint: "spine" },
4994
+ label: "spine-pipeline"
4995
+ });
4996
+ this.downsweepPipeline = device.createComputePipeline({
4997
+ layout: device.createPipelineLayout({ bindGroupLayouts: [this.downsweepBindGroupLayout] }),
4998
+ compute: { module: radixSortModule, entryPoint: "downsweep" },
4999
+ label: "downsweep-pipeline"
5000
+ });
5001
+ this.createRadixSortBindGroups();
4736
5002
  }
4737
5003
  /**
4738
- * 设置屏幕尺寸
4739
- */
4740
- setScreenSize(width, height) {
4741
- this.screenWidth = width;
4742
- this.screenHeight = height;
4743
- }
4744
- /**
4745
- * 设置剔除参数
4746
- */
4747
- setCullingOptions(options) {
4748
- this.cullingOptions = { ...this.cullingOptions, ...options };
5004
+ * 创建 Radix Sort 的 bind groups
5005
+ * 4 个 pass,使用 ping-pong buffers
5006
+ *
5007
+ * Ping-pong 模式:
5008
+ * - Pass 0: depthKeys/visibleIndices -> keysTempBuffer/valuesTempBuffer
5009
+ * - Pass 1: keysTempBuffer/valuesTempBuffer -> depthKeys/visibleIndices
5010
+ * - Pass 2: depthKeys/visibleIndices -> keysTempBuffer/valuesTempBuffer
5011
+ * - Pass 3: keysTempBuffer/valuesTempBuffer -> (depthKeys)/sortedIndicesBuffer
5012
+ */
5013
+ createRadixSortBindGroups() {
5014
+ for (let passIdx = 0; passIdx < 4; passIdx++) {
5015
+ const isEvenPass = passIdx % 2 === 0;
5016
+ const keysIn = isEvenPass ? this.depthKeysBuffer : this.keysTempBuffer;
5017
+ const valuesIn = isEvenPass ? this.visibleIndicesBuffer : this.valuesTempBuffer;
5018
+ let keysOut;
5019
+ let valuesOut;
5020
+ if (isEvenPass) {
5021
+ keysOut = this.keysTempBuffer;
5022
+ valuesOut = this.valuesTempBuffer;
5023
+ } else {
5024
+ keysOut = this.depthKeysBuffer;
5025
+ valuesOut = passIdx === 3 ? this.sortedIndicesBuffer : this.visibleIndicesBuffer;
5026
+ }
5027
+ this.upsweepBindGroups[passIdx] = this.device.createBindGroup({
5028
+ layout: this.upsweepBindGroupLayout,
5029
+ entries: [
5030
+ { binding: 0, resource: { buffer: this.sortParamsBuffers[passIdx] } },
5031
+ { binding: 1, resource: { buffer: this.indirectBuffer } },
5032
+ { binding: 2, resource: { buffer: keysIn } },
5033
+ { binding: 3, resource: { buffer: this.globalHistogramBuffer } },
5034
+ { binding: 4, resource: { buffer: this.partitionHistogramBuffer } }
5035
+ ],
5036
+ label: `upsweep-bind-group-${passIdx}`
5037
+ });
5038
+ this.spineBindGroups[passIdx] = this.device.createBindGroup({
5039
+ layout: this.spineBindGroupLayout,
5040
+ entries: [
5041
+ { binding: 0, resource: { buffer: this.indirectBuffer } },
5042
+ { binding: 1, resource: { buffer: this.globalHistogramBuffer } },
5043
+ { binding: 2, resource: { buffer: this.partitionHistogramBuffer } },
5044
+ { binding: 3, resource: { buffer: this.sortParamsBuffers[passIdx] } }
5045
+ ],
5046
+ label: `spine-bind-group-${passIdx}`
5047
+ });
5048
+ this.downsweepBindGroups[passIdx] = this.device.createBindGroup({
5049
+ layout: this.downsweepBindGroupLayout,
5050
+ entries: [
5051
+ { binding: 0, resource: { buffer: this.sortParamsBuffers[passIdx] } },
5052
+ { binding: 1, resource: { buffer: this.indirectBuffer } },
5053
+ { binding: 2, resource: { buffer: this.globalHistogramBuffer } },
5054
+ { binding: 3, resource: { buffer: this.partitionHistogramBuffer } },
5055
+ { binding: 4, resource: { buffer: keysIn } },
5056
+ { binding: 5, resource: { buffer: valuesIn } },
5057
+ { binding: 6, resource: { buffer: keysOut } },
5058
+ { binding: 7, resource: { buffer: valuesOut } }
5059
+ ],
5060
+ label: `downsweep-bind-group-${passIdx}`
5061
+ });
5062
+ }
5063
+ }
5064
+ /**
5065
+ * 设置屏幕尺寸
5066
+ */
5067
+ setScreenSize(width, height) {
5068
+ this.screenWidth = width;
5069
+ this.screenHeight = height;
5070
+ }
5071
+ /**
5072
+ * 设置剔除参数
5073
+ */
5074
+ setCullingOptions(options) {
5075
+ this.cullingOptions = { ...this.cullingOptions, ...options };
4749
5076
  }
4750
5077
  /**
4751
5078
  * 执行剔除和排序
4752
5079
  * 每帧调用
4753
- *
4754
- * 优化:合并所有 compute pass 到单次 GPU 提交
4755
- * WebGPU 保证同一 command buffer 中的命令按顺序执行
4756
5080
  */
4757
5081
  sort() {
4758
- try {
4759
- const cullingParamsData = new ArrayBuffer(32);
4760
- const cullingParamsView = new DataView(cullingParamsData);
4761
- cullingParamsView.setUint32(0, this.splatCount, true);
4762
- cullingParamsView.setFloat32(4, this.cullingOptions.nearPlane, true);
4763
- cullingParamsView.setFloat32(8, this.cullingOptions.farPlane, true);
4764
- cullingParamsView.setFloat32(12, this.screenWidth, true);
4765
- cullingParamsView.setFloat32(16, this.screenHeight, true);
4766
- cullingParamsView.setFloat32(20, this.cullingOptions.pixelThreshold, true);
4767
- cullingParamsView.setFloat32(24, 0, true);
4768
- cullingParamsView.setFloat32(28, 0, true);
4769
- this.device.queue.writeBuffer(
4770
- this.cullingParamsBuffer,
4771
- 0,
4772
- cullingParamsData
4773
- );
4774
- const cullWorkgroupCount = Math.ceil(this.splatCount / this.WORKGROUP_SIZE);
4775
- const bucketResetWorkgroups = Math.ceil(
4776
- this.numBuckets / this.WORKGROUP_SIZE
4777
- );
4778
- const encoder = this.device.createCommandEncoder();
4779
- {
4780
- const pass = encoder.beginComputePass();
4781
- pass.setPipeline(this.resetCountersPipeline);
4782
- pass.setBindGroup(0, this.cullingBindGroup);
4783
- pass.dispatchWorkgroups(1);
4784
- pass.end();
4785
- }
4786
- {
4787
- const pass = encoder.beginComputePass();
4788
- pass.setPipeline(this.resetBucketCountsPipeline);
4789
- pass.setBindGroup(0, this.cullingBindGroup);
4790
- pass.dispatchWorkgroups(bucketResetWorkgroups);
4791
- pass.end();
4792
- }
4793
- {
4794
- const pass = encoder.beginComputePass();
4795
- pass.setPipeline(this.cullAndCountPipeline);
4796
- pass.setBindGroup(0, this.cullingBindGroup);
4797
- pass.dispatchWorkgroups(cullWorkgroupCount);
4798
- pass.end();
4799
- }
4800
- {
4801
- const pass = encoder.beginComputePass();
4802
- pass.setPipeline(this.updateDrawIndirectPipeline);
4803
- pass.setBindGroup(0, this.cullingBindGroup);
4804
- pass.dispatchWorkgroups(1);
4805
- pass.end();
4806
- }
5082
+ const cullingParamsData = new ArrayBuffer(32);
5083
+ const view = new DataView(cullingParamsData);
5084
+ view.setUint32(0, this.splatCount, true);
5085
+ view.setFloat32(4, this.cullingOptions.nearPlane, true);
5086
+ view.setFloat32(8, this.cullingOptions.farPlane, true);
5087
+ view.setFloat32(12, this.screenWidth, true);
5088
+ view.setFloat32(16, this.screenHeight, true);
5089
+ view.setFloat32(20, this.cullingOptions.frustumDilation ?? 0.2, true);
5090
+ view.setFloat32(24, this.cullingOptions.pixelThreshold, true);
5091
+ view.setFloat32(28, 0, true);
5092
+ this.device.queue.writeBuffer(this.cullingParamsBuffer, 0, cullingParamsData);
5093
+ const encoder = this.device.createCommandEncoder({ label: "splat-sort-encoder" });
5094
+ encoder.clearBuffer(this.depthKeysBuffer);
5095
+ encoder.clearBuffer(this.visibleIndicesBuffer);
5096
+ encoder.clearBuffer(this.keysTempBuffer);
5097
+ encoder.clearBuffer(this.valuesTempBuffer);
5098
+ encoder.clearBuffer(this.globalHistogramBuffer);
5099
+ encoder.clearBuffer(this.partitionHistogramBuffer);
5100
+ {
5101
+ const pass = encoder.beginComputePass({ label: "init-indirect" });
5102
+ pass.setPipeline(this.initIndirectPipeline);
5103
+ pass.setBindGroup(0, this.cullingBindGroup);
5104
+ pass.dispatchWorkgroups(1);
5105
+ pass.end();
5106
+ }
5107
+ {
5108
+ const pass = encoder.beginComputePass({ label: "project-cull" });
5109
+ pass.setPipeline(this.projectCullPipeline);
5110
+ pass.setBindGroup(0, this.cullingBindGroup);
5111
+ pass.dispatchWorkgroups(Math.ceil(this.splatCount / WORKGROUP_SIZE$1));
5112
+ pass.end();
5113
+ }
5114
+ for (let passIdx = 0; passIdx < 4; passIdx++) {
4807
5115
  {
4808
- const pass = encoder.beginComputePass();
4809
- pass.setPipeline(this.prefixSumPipeline);
4810
- pass.setBindGroup(0, this.prefixSumBindGroup);
4811
- pass.dispatchWorkgroups(1);
5116
+ const pass = encoder.beginComputePass({ label: `upsweep-p${passIdx}` });
5117
+ pass.setPipeline(this.upsweepPipeline);
5118
+ pass.setBindGroup(0, this.upsweepBindGroups[passIdx]);
5119
+ pass.dispatchWorkgroups(this.numPartitions);
4812
5120
  pass.end();
4813
5121
  }
4814
5122
  {
4815
- const pass = encoder.beginComputePass();
4816
- pass.setPipeline(this.resetBucketPositionsPipeline);
4817
- pass.setBindGroup(0, this.scatterBindGroup);
4818
- pass.dispatchWorkgroups(bucketResetWorkgroups);
5123
+ const pass = encoder.beginComputePass({ label: `spine-p${passIdx}` });
5124
+ pass.setPipeline(this.spinePipeline);
5125
+ pass.setBindGroup(0, this.spineBindGroups[passIdx]);
5126
+ pass.dispatchWorkgroups(RADIX_SIZE);
4819
5127
  pass.end();
4820
5128
  }
4821
5129
  {
4822
- const pass = encoder.beginComputePass();
4823
- pass.setPipeline(this.scatterPipeline);
4824
- pass.setBindGroup(0, this.scatterBindGroup);
4825
- pass.dispatchWorkgroups(cullWorkgroupCount);
5130
+ const pass = encoder.beginComputePass({ label: `downsweep-p${passIdx}` });
5131
+ pass.setPipeline(this.downsweepPipeline);
5132
+ pass.setBindGroup(0, this.downsweepBindGroups[passIdx]);
5133
+ pass.dispatchWorkgroups(this.numPartitions);
4826
5134
  pass.end();
4827
5135
  }
4828
- this.device.queue.submit([encoder.finish()]);
4829
- } catch (error) {
4830
5136
  }
5137
+ this.device.queue.submit([encoder.finish()]);
4831
5138
  }
4832
5139
  /**
4833
5140
  * 获取排序后的索引 buffer(用于渲染)
@@ -4839,7 +5146,7 @@ class GSSplatSorter {
4839
5146
  * 获取 DrawIndirect buffer
4840
5147
  */
4841
5148
  getDrawIndirectBuffer() {
4842
- return this.drawIndirectBuffer;
5149
+ return this.indirectBuffer;
4843
5150
  }
4844
5151
  /**
4845
5152
  * 获取 splat 总数量
@@ -4852,58 +5159,37 @@ class GSSplatSorter {
4852
5159
  */
4853
5160
  destroy() {
4854
5161
  this.cullingParamsBuffer.destroy();
4855
- this.countersBuffer.destroy();
4856
- this.visibleIndicesBuffer.destroy();
4857
5162
  this.depthKeysBuffer.destroy();
4858
- this.bucketCountsBuffer.destroy();
4859
- this.bucketOffsetsBuffer.destroy();
4860
- this.bucketPositionsBuffer.destroy();
5163
+ this.visibleIndicesBuffer.destroy();
5164
+ this.indirectBuffer.destroy();
5165
+ this.globalHistogramBuffer.destroy();
5166
+ this.partitionHistogramBuffer.destroy();
5167
+ this.keysTempBuffer.destroy();
5168
+ this.valuesTempBuffer.destroy();
5169
+ for (const buffer of this.sortParamsBuffers) {
5170
+ buffer.destroy();
5171
+ }
4861
5172
  this.sortedIndicesBuffer.destroy();
4862
- this.drawIndirectBuffer.destroy();
4863
5173
  }
4864
5174
  }
4865
- var SHMode$1 = /* @__PURE__ */ ((SHMode2) => {
4866
- SHMode2[SHMode2["L0"] = 0] = "L0";
4867
- SHMode2[SHMode2["L1"] = 1] = "L1";
4868
- SHMode2[SHMode2["L2"] = 2] = "L2";
4869
- SHMode2[SHMode2["L3"] = 3] = "L3";
4870
- return SHMode2;
4871
- })(SHMode$1 || {});
4872
- function isMobileDevice$1() {
4873
- if (typeof navigator === "undefined") return false;
4874
- const ua = navigator.userAgent || navigator.vendor || window.opera || "";
4875
- const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
4876
- ua.toLowerCase()
4877
- );
4878
- const hasTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
4879
- const isSmallScreen = window.innerWidth <= 768;
4880
- const isIPadAsMac = navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
4881
- const result = isMobileUA || isIPadAsMac || hasTouch && isSmallScreen;
4882
- return result;
4883
- }
4884
- var PerformanceTier = /* @__PURE__ */ ((PerformanceTier2) => {
4885
- PerformanceTier2["HIGH"] = "high";
4886
- PerformanceTier2["MEDIUM"] = "medium";
4887
- PerformanceTier2["LOW"] = "low";
4888
- return PerformanceTier2;
4889
- })(PerformanceTier || {});
4890
- function detectPerformanceTier(device) {
4891
- const isMobile = isMobileDevice$1();
4892
- device.limits.maxBufferSize;
4893
- const maxStorageBufferBindingSize = device.limits.maxStorageBufferBindingSize;
4894
- if (isMobile) {
4895
- return "low";
4896
- }
4897
- if (maxStorageBufferBindingSize >= 1024 * 1024 * 1024) {
4898
- return "high";
4899
- } else if (maxStorageBufferBindingSize >= 256 * 1024 * 1024) {
4900
- return "medium";
4901
- }
4902
- return "low";
4903
- }
4904
- const shaderCodeL0 = (
5175
+ const gsOptimizedShader = (
4905
5176
  /* wgsl */
4906
5177
  `
5178
+ /**
5179
+ * 优化的 3D Gaussian Splatting Shader
5180
+ * 参考 rfs-gsplat-render 实现,修复颜色和抗锯齿问题
5181
+ */
5182
+
5183
+ const SQRT_8: f32 = 2.82842712475;
5184
+ const SH_C0: f32 = 0.28209479177387814;
5185
+ const SH_C1: f32 = 0.4886025119029199;
5186
+ // Normalized Gaussian 常量 (匹配 SuperSplat)
5187
+ const EXP_NEG4: f32 = 0.01831563888873418;
5188
+ const INV_ONE_MINUS_EXP_NEG4: f32 = 1.01865736036377408;
5189
+ // 低通滤波器 (正则化协方差矩阵)
5190
+ const LOW_PASS_FILTER: f32 = 0.3;
5191
+ const ALPHA_CULL_THRESHOLD: f32 = 0.00392156863;
5192
+
4907
5193
  struct Uniforms {
4908
5194
  view: mat4x4<f32>,
4909
5195
  proj: mat4x4<f32>,
@@ -4915,17 +5201,15 @@ struct Uniforms {
4915
5201
  }
4916
5202
 
4917
5203
  struct Splat {
4918
- mean: vec3<f32>,
4919
- _pad0: f32,
4920
- scale: vec3<f32>,
4921
- _pad1: f32,
5204
+ mean: vec3<f32>, _pad0: f32,
5205
+ scale: vec3<f32>, _pad1: f32,
4922
5206
  rotation: vec4<f32>,
4923
- colorDC: vec3<f32>,
4924
- opacity: f32,
4925
- sh1: array<f32, 9>,
4926
- sh2: array<f32, 15>,
4927
- sh3: array<f32, 21>,
4928
- _pad2: array<f32, 3>,
5207
+ colorDC: vec3<f32>,
5208
+ opacity: f32,
5209
+ sh1: array<f32, 9>,
5210
+ sh2: array<f32, 15>,
5211
+ sh3: array<f32, 21>,
5212
+ _pad2: array<f32, 3>,
4929
5213
  }
4930
5214
 
4931
5215
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
@@ -4934,100 +5218,137 @@ struct Splat {
4934
5218
 
4935
5219
  struct VertexOutput {
4936
5220
  @builtin(position) position: vec4<f32>,
4937
- @location(0) localUV: vec2<f32>,
5221
+ @location(0) fragPos: vec2<f32>,
4938
5222
  @location(1) color: vec3<f32>,
4939
5223
  @location(2) opacity: f32,
4940
5224
  }
4941
5225
 
4942
5226
  const QUAD_POSITIONS = array<vec2<f32>, 4>(
4943
- vec2<f32>(-1.0, -1.0),
4944
- vec2<f32>( 1.0, -1.0),
4945
- vec2<f32>(-1.0, 1.0),
4946
- vec2<f32>( 1.0, 1.0),
5227
+ vec2<f32>(-1.0, -1.0), vec2<f32>(-1.0, 1.0),
5228
+ vec2<f32>(1.0, -1.0), vec2<f32>(1.0, 1.0),
4947
5229
  );
4948
5230
 
4949
- // 椭圆缩放因子: 控制高斯 splat 的渲染范围
4950
- // 3.0 = 覆盖 99.7%,更大的值会让 splat 更大更柔和
4951
- const ELLIPSE_SCALE: f32 = 3.0;
4952
- // 高斯衰减系数: 在 r=1 ( ) 处衰减到 exp(-4.5) ≈ 0.011
4953
- const GAUSSIAN_DECAY: f32 = 4.5;
4954
- // SH L0 常数: sqrt(1/(4*pi)) - 用于 DC 颜色计算
4955
- const SH_C0: f32 = 0.28209479177387814;
5231
+ // ClipCorner 优化 (精确匹配 PlayCanvas/SuperSplat)
5232
+ // 从 PlayCanvas: clip = min(1.0, sqrt(-log(1.0 / (255.0 * alpha))) / 2.0)
5233
+ // 这根据透明度缩小 quad,排除 alpha < 1/255 的 Gaussian 区域
5234
+ fn computeClipFactor(alpha: f32) -> f32 {
5235
+ // 保护非常小的 alpha
5236
+ // alpha <= 1/255 时,splat 不可见
5237
+ if alpha <= ALPHA_CULL_THRESHOLD { return 0.0; }
5238
+ // PlayCanvas 公式: clip = min(1.0, sqrt(-log(1.0 / (255.0 * alpha))) / 2.0)
5239
+ // 简化: -log(1/(255*a)) = log(255*a)
5240
+ return min(1.0, sqrt(log(255.0 * alpha)) / 2.0);
5241
+ }
4956
5242
 
5243
+ // 四元数转旋转矩阵 (PLY 格式: w, x, y, z)
4957
5244
  fn quatToMat3(q: vec4<f32>) -> mat3x3<f32> {
4958
- let w = q[0]; let x = q[1]; let y = q[2]; let z = q[3];
4959
- let x2 = x + x; let y2 = y + y; let z2 = z + z;
4960
- let xx = x * x2; let xy = x * y2; let xz = x * z2;
4961
- let yy = y * y2; let yz = y * z2; let zz = z * z2;
4962
- let wx = w * x2; let wy = w * y2; let wz = w * z2;
5245
+ let r = q.x; let x = q.y; let y = q.z; let z = q.w;
4963
5246
  return mat3x3<f32>(
4964
- vec3<f32>(1.0 - (yy + zz), xy + wz, xz - wy),
4965
- vec3<f32>(xy - wz, 1.0 - (xx + zz), yz + wx),
4966
- vec3<f32>(xz + wy, yz - wx, 1.0 - (xx + yy))
5247
+ vec3<f32>(1.0 - 2.0 * (y * y + z * z), 2.0 * (x * y + r * z), 2.0 * (x * z - r * y)),
5248
+ vec3<f32>(2.0 * (x * y - r * z), 1.0 - 2.0 * (x * x + z * z), 2.0 * (y * z + r * x)),
5249
+ vec3<f32>(2.0 * (x * z + r * y), 2.0 * (y * z - r * x), 1.0 - 2.0 * (x * x + y * y))
4967
5250
  );
4968
5251
  }
4969
5252
 
4970
- // 从模型矩阵提取三轴缩放因子
4971
- fn getModelScale3(model: mat4x4<f32>) -> vec3<f32> {
4972
- return vec3<f32>(
4973
- length(model[0].xyz),
4974
- length(model[1].xyz),
4975
- length(model[2].xyz)
4976
- );
5253
+ fn computeCovariance3D(scale: vec3<f32>, rotation: vec4<f32>) -> mat3x3<f32> {
5254
+ let R = quatToMat3(rotation);
5255
+ let S = mat3x3<f32>(vec3<f32>(scale.x, 0.0, 0.0), vec3<f32>(0.0, scale.y, 0.0), vec3<f32>(0.0, 0.0, scale.z));
5256
+ let M = R * S;
5257
+ return M * transpose(M);
4977
5258
  }
4978
5259
 
4979
- fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelView: mat4x4<f32>, proj: mat4x4<f32>, modelScale: vec3<f32>) -> vec3<f32> {
4980
- let R = quatToMat3(rotation);
4981
- // 应用模型缩放到 splat scale (支持非均匀缩放)
4982
- let scaledScale = scale * modelScale;
4983
- let s2 = scaledScale * scaledScale;
4984
- // 构建协方差矩阵 Sigma = R * diag(s²) * R^T
4985
- let M = mat3x3<f32>(R[0] * s2.x, R[1] * s2.y, R[2] * s2.z);
4986
- let Sigma = M * transpose(R);
4987
-
4988
- let viewPos = (modelView * vec4<f32>(mean, 1.0)).xyz;
4989
- let viewRot = mat3x3<f32>(modelView[0].xyz, modelView[1].xyz, modelView[2].xyz);
4990
- let SigmaView = viewRot * Sigma * transpose(viewRot);
5260
+ // 协方差投影 (匹配参考实现)
5261
+ // 注意: viewCenter 是 vec4,直接使用 .xyz (不除以 w)
5262
+ fn projectCovariance(cov3d: mat3x3<f32>, viewCenter: vec4<f32>, focal: vec2<f32>, modelViewMat: mat4x4<f32>) -> vec3<f32> {
5263
+ let v = viewCenter.xyz; // 直接使用,不除以 w
5264
+ let s = 1.0 / (v.z * v.z);
4991
5265
 
4992
- let fx = proj[0][0]; let fy = proj[1][1];
4993
- let z = -viewPos.z;
4994
- let z_clamped = max(z, 0.001);
4995
- let z2 = z_clamped * z_clamped;
5266
+ // Jacobian 矩阵
5267
+ let J = mat3x3<f32>(
5268
+ vec3<f32>(focal.x / v.z, 0.0, 0.0),
5269
+ vec3<f32>(0.0, focal.y / v.z, 0.0),
5270
+ vec3<f32>(-(focal.x * v.x) * s, -(focal.y * v.y) * s, 0.0)
5271
+ );
4996
5272
 
4997
- // 雅可比矩阵: 从相机坐标到 NDC 的偏导数
4998
- // 对于 WebGPU (相机看向 -Z): x_ndc = fx * x_cam / (-z_cam)
4999
- let j1 = vec3<f32>(fx / z_clamped, 0.0, fx * viewPos.x / z2);
5000
- let j2 = vec3<f32>(0.0, fy / z_clamped, fy * viewPos.y / z2);
5273
+ // model-view 矩阵提取 3x3 旋转部分 (匹配参考实现)
5274
+ let W = mat3x3<f32>(
5275
+ vec3<f32>(modelViewMat[0][0], modelViewMat[0][1], modelViewMat[0][2]),
5276
+ vec3<f32>(modelViewMat[1][0], modelViewMat[1][1], modelViewMat[1][2]),
5277
+ vec3<f32>(modelViewMat[2][0], modelViewMat[2][1], modelViewMat[2][2])
5278
+ );
5001
5279
 
5002
- // 投影协方差: J * Sigma * J^T (只需要 2x2 上三角)
5003
- let Sj1 = SigmaView * j1;
5004
- let Sj2 = SigmaView * j2;
5005
- return vec3<f32>(dot(j1, Sj1), dot(j1, Sj2), dot(j2, Sj2));
5280
+ let T = J * W;
5281
+ let cov2d = T * cov3d * transpose(T);
5282
+ return vec3<f32>(cov2d[0][0], cov2d[0][1], cov2d[1][1]);
5006
5283
  }
5007
5284
 
5008
- fn computeEllipseAxes(cov2D: vec3<f32>) -> mat2x2<f32> {
5009
- let a = cov2D.x; let b = cov2D.y; let c = cov2D.z;
5010
- let trace = a + c;
5011
- let det = a * c - b * b;
5012
- let disc = trace * trace - 4.0 * det;
5013
- let sqrtDisc = sqrt(max(disc, 0.0));
5014
- let lambda1 = max((trace + sqrtDisc) * 0.5, 0.0);
5015
- let lambda2 = max((trace - sqrtDisc) * 0.5, 0.0);
5016
- let r1 = sqrt(lambda1);
5017
- let r2 = sqrt(lambda2);
5285
+ struct ExtentResult {
5286
+ basis: vec4<f32>,
5287
+ adjustedOpacity: f32,
5288
+ }
5289
+
5290
+ // 计算 2D 投影范围
5291
+ // 精确匹配 PlayCanvas/SuperSplat 实现
5292
+ // 注意: MipSplatting 抗锯齿默认禁用,因为大多数模型不是用 MipSplatting 训练的
5293
+ // 如果模型是用 MipSplatting 训练的,可以启用 GSPLAT_AA 模式
5294
+ fn computeExtentBasisAA(cov2dIn: vec3<f32>, opacity: f32, viewportSize: vec2<f32>) -> ExtentResult {
5295
+ var result: ExtentResult;
5296
+ var cov2d = cov2dIn;
5297
+ var alpha = opacity;
5018
5298
 
5019
- var axis1: vec2<f32>; var axis2: vec2<f32>;
5020
- // 改进的数值稳定性: 检查特征向量是否可靠
5021
- let eigenvecLen = abs(b) + abs(lambda1 - a);
5022
- if (eigenvecLen > 1e-6) {
5023
- axis1 = normalize(vec2<f32>(b, lambda1 - a));
5024
- axis2 = vec2<f32>(-axis1.y, axis1.x);
5025
- } else {
5026
- // 退化情况: 使用标准轴
5027
- if (a >= c) { axis1 = vec2<f32>(1.0, 0.0); axis2 = vec2<f32>(0.0, 1.0); }
5028
- else { axis1 = vec2<f32>(0.0, 1.0); axis2 = vec2<f32>(1.0, 0.0); }
5299
+ // 添加低通滤波 (正则化) - 匹配 PlayCanvas: +0.3
5300
+ // 这避免了非常小的特征值导致的数值问题
5301
+ cov2d.x += LOW_PASS_FILTER;
5302
+ cov2d.z += LOW_PASS_FILTER;
5303
+
5304
+ // 特征值分解 (使用 PlayCanvas 公式)
5305
+ let a = cov2d.x; // diagonal1
5306
+ let d = cov2d.z; // diagonal2
5307
+ let b = cov2d.y; // offDiagonal
5308
+
5309
+ let mid = 0.5 * (a + d);
5310
+ let radius = length(vec2<f32>((a - d) * 0.5, b));
5311
+
5312
+ let lambda1 = mid + radius;
5313
+ let lambda2 = max(mid - radius, 0.1); // PlayCanvas 使用 0.1 最小值
5314
+
5315
+ // 检查特征值是否有效
5316
+ if lambda2 <= 0.0 {
5317
+ result.basis = vec4<f32>(0.0);
5318
+ result.adjustedOpacity = 0.0;
5319
+ return result;
5029
5320
  }
5030
- return mat2x2<f32>(axis1 * r1, axis2 * r2);
5321
+
5322
+ // 使用基于视口的最大限制 (匹配 PlayCanvas)
5323
+ let vmin = min(1024.0, min(viewportSize.x, viewportSize.y));
5324
+
5325
+ // 计算轴长度: l = 2.0 * min(sqrt(2.0 * lambda), vmin)
5326
+ // 这等价于 std_dev * sqrt(lambda),因为 std_dev = sqrt(8) ≈ 2.83
5327
+ let l1 = 2.0 * min(sqrt(2.0 * lambda1), vmin);
5328
+ let l2 = 2.0 * min(sqrt(2.0 * lambda2), vmin);
5329
+
5330
+ // 关键: 剔除小于 2 像素的 Gaussian (匹配 PlayCanvas)
5331
+ // 这消除了导致"雾化"伪影的亚像素 splat
5332
+ if l1 < 2.0 && l2 < 2.0 {
5333
+ result.basis = vec4<f32>(0.0);
5334
+ result.adjustedOpacity = 0.0;
5335
+ return result;
5336
+ }
5337
+
5338
+ // 从 offDiagonal 和特征值差计算特征向量
5339
+ // diagonalVector = normalize(vec2(offDiagonal, lambda1 - diagonal1))
5340
+ let diagVec = normalize(vec2<f32>(b, lambda1 - a));
5341
+ let eigenvector1 = diagVec;
5342
+ let eigenvector2 = vec2<f32>(diagVec.y, -diagVec.x);
5343
+
5344
+ // 计算基向量 (不应用额外的 splat_scale,因为我们使用默认值 1.0)
5345
+ result.basis = vec4<f32>(eigenvector1 * l1, eigenvector2 * l2);
5346
+ result.adjustedOpacity = alpha;
5347
+ return result;
5348
+ }
5349
+
5350
+ fn getModelScale3(model: mat4x4<f32>) -> vec3<f32> {
5351
+ return vec3<f32>(length(model[0].xyz), length(model[1].xyz), length(model[2].xyz));
5031
5352
  }
5032
5353
 
5033
5354
  @vertex
@@ -5036,861 +5357,238 @@ fn vs_main(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) ins
5036
5357
  let splatIndex = sortedIndices[instanceIndex];
5037
5358
  let splat = splats[splatIndex];
5038
5359
  let quadPos = QUAD_POSITIONS[vertexIndex];
5039
- output.localUV = quadPos;
5040
5360
 
5041
- // 计算 modelView 矩阵和模型缩放
5042
- let modelView = uniforms.view * uniforms.model;
5043
- let modelScale = getModelScale3(uniforms.model);
5361
+ // 透明度剔除
5362
+ if splat.opacity < ALPHA_CULL_THRESHOLD { output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output; }
5044
5363
 
5045
- let cov2D = computeCov2D(splat.mean, splat.scale, splat.rotation, modelView, uniforms.proj, modelScale);
5046
- let axes = computeEllipseAxes(cov2D);
5047
- let screenOffset = axes[0] * quadPos.x * ELLIPSE_SCALE + axes[1] * quadPos.y * ELLIPSE_SCALE;
5364
+ // 四元数有效性检查
5365
+ let quatNormSqr = dot(splat.rotation, splat.rotation);
5366
+ if quatNormSqr < 1e-6 { output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output; }
5048
5367
 
5049
- // 应用 model 变换到 splat 位置
5368
+ // 变换到视图空间 (匹配参考实现: Local -> World -> View -> Clip)
5050
5369
  let worldPos = uniforms.model * vec4<f32>(splat.mean, 1.0);
5051
- let viewPos = uniforms.view * worldPos;
5052
- var clipPos = uniforms.proj * viewPos;
5053
- clipPos.x = clipPos.x + screenOffset.x * clipPos.w;
5054
- clipPos.y = clipPos.y + screenOffset.y * clipPos.w;
5055
- output.position = clipPos;
5056
- // DC 颜色已经在 PLYLoader 中预计算: colorDC = 0.5 + SH_C0 * f_dc
5057
- output.color = splat.colorDC;
5058
- output.opacity = splat.opacity;
5370
+ let viewPos = uniforms.view * worldPos; // vec4, 保持 w 分量
5371
+ let clipPos = uniforms.proj * viewPos;
5059
5372
 
5060
- return output;
5061
- }
5062
-
5063
- @fragment
5064
- fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
5065
- let r = length(input.localUV);
5066
- if (r > 1.0) { discard; }
5067
- let gaussianWeight = exp(-r * r * GAUSSIAN_DECAY);
5068
- let alpha = input.opacity * gaussianWeight;
5069
- if (alpha < 0.004) { discard; }
5070
- let color = clamp(input.color, vec3<f32>(0.0), vec3<f32>(1.0));
5071
- return vec4<f32>(color * alpha, alpha);
5072
- }
5073
- `
5074
- );
5075
- const shaderCodeL1 = (
5076
- /* wgsl */
5077
- `
5078
- struct Uniforms {
5079
- view: mat4x4<f32>,
5080
- proj: mat4x4<f32>,
5081
- model: mat4x4<f32>,
5082
- cameraPos: vec3<f32>,
5083
- _pad: f32,
5084
- screenSize: vec2<f32>,
5085
- _pad2: vec2<f32>,
5086
- }
5087
-
5088
- struct Splat {
5089
- mean: vec3<f32>,
5090
- _pad0: f32,
5091
- scale: vec3<f32>,
5092
- _pad1: f32,
5093
- rotation: vec4<f32>,
5094
- colorDC: vec3<f32>,
5095
- opacity: f32,
5096
- sh1: array<f32, 9>,
5097
- sh2: array<f32, 15>,
5098
- sh3: array<f32, 21>,
5099
- _pad2: array<f32, 3>,
5100
- }
5101
-
5102
- @group(0) @binding(0) var<uniform> uniforms: Uniforms;
5103
- @group(0) @binding(1) var<storage, read> splats: array<Splat>;
5104
- @group(0) @binding(2) var<storage, read> sortedIndices: array<u32>;
5105
-
5106
- struct VertexOutput {
5107
- @builtin(position) position: vec4<f32>,
5108
- @location(0) localUV: vec2<f32>,
5109
- @location(1) color: vec3<f32>,
5110
- @location(2) opacity: f32,
5111
- }
5112
-
5113
- const QUAD_POSITIONS = array<vec2<f32>, 4>(
5114
- vec2<f32>(-1.0, -1.0),
5115
- vec2<f32>( 1.0, -1.0),
5116
- vec2<f32>(-1.0, 1.0),
5117
- vec2<f32>( 1.0, 1.0),
5118
- );
5119
-
5120
- const ELLIPSE_SCALE: f32 = 3.0;
5121
- const GAUSSIAN_DECAY: f32 = 4.5;
5122
-
5123
- // SH 常数
5124
- const SH_C0: f32 = 0.28209479177387814; // sqrt(1/(4*pi)) - DC 颜色
5125
- const SH_C1: f32 = 0.4886025119029199; // sqrt(3/(4*pi)) - L1
5126
-
5127
- // 计算 L1 球谐函数贡献
5128
- // 数据布局 (按基函数分组): [basis0_R, basis0_G, basis0_B, basis1_R, basis1_G, basis1_B, ...]
5129
- // 即 sh1[0..2] = 第一个基函数的 RGB, sh1[3..5] = 第二个基函数的 RGB, sh1[6..8] = 第三个基函数的 RGB
5130
- // 原始 3DGS 公式: result = SH_C1 * (-sh[0] * y + sh[1] * z - sh[2] * x)
5131
- // 其中 sh[0], sh[1], sh[2] 是 vec3 (RGB)
5132
- fn evalSH1(dir: vec3<f32>, sh1: array<f32, 9>) -> vec3<f32> {
5133
- let x = dir.x;
5134
- let y = dir.y;
5135
- let z = dir.z;
5136
-
5137
- // sh[0] = vec3(sh1[0], sh1[1], sh1[2]) - 第一个基函数 (Y_1^{-1})
5138
- // sh[1] = vec3(sh1[3], sh1[4], sh1[5]) - 第二个基函数 (Y_1^0)
5139
- // sh[2] = vec3(sh1[6], sh1[7], sh1[8]) - 第三个基函数 (Y_1^1)
5140
- let sh0 = vec3<f32>(sh1[0], sh1[1], sh1[2]);
5141
- let sh1_1 = vec3<f32>(sh1[3], sh1[4], sh1[5]);
5142
- let sh2 = vec3<f32>(sh1[6], sh1[7], sh1[8]);
5143
-
5144
- return SH_C1 * (-sh0 * y + sh1_1 * z - sh2 * x);
5145
- }
5146
-
5147
- fn quatToMat3(q: vec4<f32>) -> mat3x3<f32> {
5148
- let w = q[0]; let x = q[1]; let y = q[2]; let z = q[3];
5149
- let x2 = x + x; let y2 = y + y; let z2 = z + z;
5150
- let xx = x * x2; let xy = x * y2; let xz = x * z2;
5151
- let yy = y * y2; let yz = y * z2; let zz = z * z2;
5152
- let wx = w * x2; let wy = w * y2; let wz = w * z2;
5153
- return mat3x3<f32>(
5154
- vec3<f32>(1.0 - (yy + zz), xy + wz, xz - wy),
5155
- vec3<f32>(xy - wz, 1.0 - (xx + zz), yz + wx),
5156
- vec3<f32>(xz + wy, yz - wx, 1.0 - (xx + yy))
5157
- );
5158
- }
5159
-
5160
- fn getModelScale3(model: mat4x4<f32>) -> vec3<f32> {
5161
- return vec3<f32>(
5162
- length(model[0].xyz),
5163
- length(model[1].xyz),
5164
- length(model[2].xyz)
5165
- );
5166
- }
5167
-
5168
- fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelView: mat4x4<f32>, proj: mat4x4<f32>, modelScale: vec3<f32>) -> vec3<f32> {
5169
- let R = quatToMat3(rotation);
5170
- let scaledScale = scale * modelScale;
5171
- let s2 = scaledScale * scaledScale;
5172
- let M = mat3x3<f32>(R[0] * s2.x, R[1] * s2.y, R[2] * s2.z);
5173
- let Sigma = M * transpose(R);
5174
-
5175
- let viewPos = (modelView * vec4<f32>(mean, 1.0)).xyz;
5176
- let viewRot = mat3x3<f32>(modelView[0].xyz, modelView[1].xyz, modelView[2].xyz);
5177
- let SigmaView = viewRot * Sigma * transpose(viewRot);
5178
-
5179
- let fx = proj[0][0]; let fy = proj[1][1];
5180
- let z = -viewPos.z;
5181
- let z_clamped = max(z, 0.001);
5182
- let z2 = z_clamped * z_clamped;
5183
-
5184
- let j1 = vec3<f32>(fx / z_clamped, 0.0, fx * viewPos.x / z2);
5185
- let j2 = vec3<f32>(0.0, fy / z_clamped, fy * viewPos.y / z2);
5186
- let Sj1 = SigmaView * j1;
5187
- let Sj2 = SigmaView * j2;
5188
- return vec3<f32>(dot(j1, Sj1), dot(j1, Sj2), dot(j2, Sj2));
5189
- }
5190
-
5191
- fn computeEllipseAxes(cov2D: vec3<f32>) -> mat2x2<f32> {
5192
- let a = cov2D.x; let b = cov2D.y; let c = cov2D.z;
5193
- let trace = a + c;
5194
- let det = a * c - b * b;
5195
- let disc = trace * trace - 4.0 * det;
5196
- let sqrtDisc = sqrt(max(disc, 0.0));
5197
- let lambda1 = max((trace + sqrtDisc) * 0.5, 0.0);
5198
- let lambda2 = max((trace - sqrtDisc) * 0.5, 0.0);
5199
- let r1 = sqrt(lambda1);
5200
- let r2 = sqrt(lambda2);
5201
-
5202
- var axis1: vec2<f32>; var axis2: vec2<f32>;
5203
- let eigenvecLen = abs(b) + abs(lambda1 - a);
5204
- if (eigenvecLen > 1e-6) {
5205
- axis1 = normalize(vec2<f32>(b, lambda1 - a));
5206
- axis2 = vec2<f32>(-axis1.y, axis1.x);
5207
- } else {
5208
- if (a >= c) { axis1 = vec2<f32>(1.0, 0.0); axis2 = vec2<f32>(0.0, 1.0); }
5209
- else { axis1 = vec2<f32>(0.0, 1.0); axis2 = vec2<f32>(1.0, 0.0); }
5210
- }
5211
- return mat2x2<f32>(axis1 * r1, axis2 * r2);
5212
- }
5213
-
5214
- @vertex
5215
- fn vs_main(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> VertexOutput {
5216
- var output: VertexOutput;
5217
- let splatIndex = sortedIndices[instanceIndex];
5218
- let splat = splats[splatIndex];
5219
- let quadPos = QUAD_POSITIONS[vertexIndex];
5220
- output.localUV = quadPos;
5221
-
5222
- let modelView = uniforms.view * uniforms.model;
5223
- let modelScale = getModelScale3(uniforms.model);
5224
-
5225
- let cov2D = computeCov2D(splat.mean, splat.scale, splat.rotation, modelView, uniforms.proj, modelScale);
5226
- let axes = computeEllipseAxes(cov2D);
5227
- let screenOffset = axes[0] * quadPos.x * ELLIPSE_SCALE + axes[1] * quadPos.y * ELLIPSE_SCALE;
5228
-
5229
- let worldPos = uniforms.model * vec4<f32>(splat.mean, 1.0);
5230
- let viewPos = uniforms.view * worldPos;
5231
- var clipPos = uniforms.proj * viewPos;
5232
- clipPos.x = clipPos.x + screenOffset.x * clipPos.w;
5233
- clipPos.y = clipPos.y + screenOffset.y * clipPos.w;
5234
- output.position = clipPos;
5235
-
5236
- // SH 计算 - 使用局部坐标系中的方向
5237
- // 将相机位置转换到局部坐标系
5238
- // cam_local = A^{-1} * (cam_world - t) = (A^T / s^2) * (cam_world - t)
5239
- // 其中 A 是模型矩阵的 3x3 部分,s^2 是缩放的平方
5240
- let A = mat3x3<f32>(
5241
- uniforms.model[0].xyz,
5242
- uniforms.model[1].xyz,
5243
- uniforms.model[2].xyz
5244
- );
5245
- let modelTranslation = uniforms.model[3].xyz;
5246
- let s2 = max(1e-12, (dot(A[0], A[0]) + dot(A[1], A[1]) + dot(A[2], A[2])) / 3.0);
5247
- let camLocal = (transpose(A) * (uniforms.cameraPos - modelTranslation)) / s2;
5248
- // 方向:从相机指向 splat(与 visionary 一致)
5249
- let dirLocal = normalize(splat.mean - camLocal);
5250
-
5251
- let shColor = evalSH1(dirLocal, splat.sh1);
5252
- // DC 颜色已经在 PLYLoader 中预计算,这里只加 SH 贡献
5253
- // 使用 max 确保颜色不为负(与 visionary 一致)
5254
- output.color = max(vec3<f32>(0.0), splat.colorDC + shColor);
5255
- output.opacity = splat.opacity;
5256
-
5257
- return output;
5258
- }
5259
-
5260
- @fragment
5261
- fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
5262
- let r = length(input.localUV);
5263
- if (r > 1.0) { discard; }
5264
- let gaussianWeight = exp(-r * r * GAUSSIAN_DECAY);
5265
- let alpha = input.opacity * gaussianWeight;
5266
- if (alpha < 0.004) { discard; }
5267
- let color = clamp(input.color, vec3<f32>(0.0), vec3<f32>(1.0));
5268
- return vec4<f32>(color * alpha, alpha);
5269
- }
5270
- `
5271
- );
5272
- const shaderCodeL2 = (
5273
- /* wgsl */
5274
- `
5275
- struct Uniforms {
5276
- view: mat4x4<f32>,
5277
- proj: mat4x4<f32>,
5278
- model: mat4x4<f32>,
5279
- cameraPos: vec3<f32>,
5280
- _pad: f32,
5281
- screenSize: vec2<f32>,
5282
- _pad2: vec2<f32>,
5283
- }
5284
-
5285
- struct Splat {
5286
- mean: vec3<f32>,
5287
- _pad0: f32,
5288
- scale: vec3<f32>,
5289
- _pad1: f32,
5290
- rotation: vec4<f32>,
5291
- colorDC: vec3<f32>,
5292
- opacity: f32,
5293
- sh1: array<f32, 9>,
5294
- sh2: array<f32, 15>,
5295
- sh3: array<f32, 21>,
5296
- _pad2: array<f32, 3>,
5297
- }
5298
-
5299
- @group(0) @binding(0) var<uniform> uniforms: Uniforms;
5300
- @group(0) @binding(1) var<storage, read> splats: array<Splat>;
5301
- @group(0) @binding(2) var<storage, read> sortedIndices: array<u32>;
5302
-
5303
- struct VertexOutput {
5304
- @builtin(position) position: vec4<f32>,
5305
- @location(0) localUV: vec2<f32>,
5306
- @location(1) color: vec3<f32>,
5307
- @location(2) opacity: f32,
5308
- }
5309
-
5310
- const QUAD_POSITIONS = array<vec2<f32>, 4>(
5311
- vec2<f32>(-1.0, -1.0),
5312
- vec2<f32>( 1.0, -1.0),
5313
- vec2<f32>(-1.0, 1.0),
5314
- vec2<f32>( 1.0, 1.0),
5315
- );
5316
-
5317
- const ELLIPSE_SCALE: f32 = 3.0;
5318
- const GAUSSIAN_DECAY: f32 = 4.5;
5319
-
5320
- // SH 常数
5321
- const SH_C0: f32 = 0.28209479177387814; // sqrt(1/(4*pi)) - DC 颜色
5322
- const SH_C1: f32 = 0.4886025119029199; // sqrt(3/(4*pi)) - L1
5323
- const SH_C2_0: f32 = 1.0925484305920792;
5324
- const SH_C2_1: f32 = -1.0925484305920792;
5325
- const SH_C2_2: f32 = 0.31539156525252005;
5326
- const SH_C2_3: f32 = -1.0925484305920792;
5327
- const SH_C2_4: f32 = 0.5462742152960396;
5328
-
5329
- // 数据布局 (按基函数分组): [basis0_RGB, basis1_RGB, basis2_RGB]
5330
- fn evalSH1(dir: vec3<f32>, sh1: array<f32, 9>) -> vec3<f32> {
5331
- let x = dir.x;
5332
- let y = dir.y;
5333
- let z = dir.z;
5334
- let sh0 = vec3<f32>(sh1[0], sh1[1], sh1[2]);
5335
- let sh1_1 = vec3<f32>(sh1[3], sh1[4], sh1[5]);
5336
- let sh2 = vec3<f32>(sh1[6], sh1[7], sh1[8]);
5337
- return SH_C1 * (-sh0 * y + sh1_1 * z - sh2 * x);
5338
- }
5339
-
5340
- // 数据布局 (按基函数分组): [basis0_RGB, basis1_RGB, ..., basis4_RGB]
5341
- fn evalSH2(dir: vec3<f32>, sh2: array<f32, 15>) -> vec3<f32> {
5342
- let x = dir.x; let y = dir.y; let z = dir.z;
5343
- let xx = x * x; let yy = y * y; let zz = z * z;
5344
- let xy = x * y; let yz = y * z; let xz = x * z;
5345
-
5346
- // L2 基函数
5347
- let b0 = SH_C2_0 * xy;
5348
- let b1 = SH_C2_1 * yz;
5349
- let b2 = SH_C2_2 * (2.0 * zz - xx - yy);
5350
- let b3 = SH_C2_3 * xz;
5351
- let b4 = SH_C2_4 * (xx - yy);
5352
-
5353
- // 数据按基函数分组: sh2[0..2]=basis0_RGB, sh2[3..5]=basis1_RGB, ...
5354
- let c0 = vec3<f32>(sh2[0], sh2[1], sh2[2]);
5355
- let c1 = vec3<f32>(sh2[3], sh2[4], sh2[5]);
5356
- let c2 = vec3<f32>(sh2[6], sh2[7], sh2[8]);
5357
- let c3 = vec3<f32>(sh2[9], sh2[10], sh2[11]);
5358
- let c4 = vec3<f32>(sh2[12], sh2[13], sh2[14]);
5359
-
5360
- return c0 * b0 + c1 * b1 + c2 * b2 + c3 * b3 + c4 * b4;
5361
- }
5362
-
5363
- fn quatToMat3(q: vec4<f32>) -> mat3x3<f32> {
5364
- let w = q[0]; let x = q[1]; let y = q[2]; let z = q[3];
5365
- let x2 = x + x; let y2 = y + y; let z2 = z + z;
5366
- let xx = x * x2; let xy = x * y2; let xz = x * z2;
5367
- let yy = y * y2; let yz = y * z2; let zz = z * z2;
5368
- let wx = w * x2; let wy = w * y2; let wz = w * z2;
5369
- return mat3x3<f32>(
5370
- vec3<f32>(1.0 - (yy + zz), xy + wz, xz - wy),
5371
- vec3<f32>(xy - wz, 1.0 - (xx + zz), yz + wx),
5372
- vec3<f32>(xz + wy, yz - wx, 1.0 - (xx + yy))
5373
- );
5374
- }
5375
-
5376
- fn getModelScale3(model: mat4x4<f32>) -> vec3<f32> {
5377
- return vec3<f32>(
5378
- length(model[0].xyz),
5379
- length(model[1].xyz),
5380
- length(model[2].xyz)
5381
- );
5382
- }
5383
-
5384
- fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelView: mat4x4<f32>, proj: mat4x4<f32>, modelScale: vec3<f32>) -> vec3<f32> {
5385
- let R = quatToMat3(rotation);
5386
- let scaledScale = scale * modelScale;
5387
- let s2 = scaledScale * scaledScale;
5388
- let M = mat3x3<f32>(R[0] * s2.x, R[1] * s2.y, R[2] * s2.z);
5389
- let Sigma = M * transpose(R);
5390
-
5391
- let viewPos = (modelView * vec4<f32>(mean, 1.0)).xyz;
5392
- let viewRot = mat3x3<f32>(modelView[0].xyz, modelView[1].xyz, modelView[2].xyz);
5393
- let SigmaView = viewRot * Sigma * transpose(viewRot);
5394
-
5395
- let fx = proj[0][0]; let fy = proj[1][1];
5396
- let z = -viewPos.z;
5397
- let z_clamped = max(z, 0.001);
5398
- let z2 = z_clamped * z_clamped;
5373
+ // 近平面剔除 (viewPos.z 是负数,相机看向 -Z)
5374
+ if viewPos.z >= 0.0 { output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output; }
5399
5375
 
5400
- let j1 = vec3<f32>(fx / z_clamped, 0.0, fx * viewPos.x / z2);
5401
- let j2 = vec3<f32>(0.0, fy / z_clamped, fy * viewPos.y / z2);
5402
- let Sj1 = SigmaView * j1;
5403
- let Sj2 = SigmaView * j2;
5404
- return vec3<f32>(dot(j1, Sj1), dot(j1, Sj2), dot(j2, Sj2));
5405
- }
5406
-
5407
- fn computeEllipseAxes(cov2D: vec3<f32>) -> mat2x2<f32> {
5408
- let a = cov2D.x; let b = cov2D.y; let c = cov2D.z;
5409
- let trace = a + c;
5410
- let det = a * c - b * b;
5411
- let disc = trace * trace - 4.0 * det;
5412
- let sqrtDisc = sqrt(max(disc, 0.0));
5413
- let lambda1 = max((trace + sqrtDisc) * 0.5, 0.0);
5414
- let lambda2 = max((trace - sqrtDisc) * 0.5, 0.0);
5415
- let r1 = sqrt(lambda1);
5416
- let r2 = sqrt(lambda2);
5376
+ // NDC 计算
5377
+ let pW = 1.0 / (clipPos.w + 0.0000001);
5378
+ let ndcPos = clipPos * pW;
5417
5379
 
5418
- var axis1: vec2<f32>; var axis2: vec2<f32>;
5419
- let eigenvecLen = abs(b) + abs(lambda1 - a);
5420
- if (eigenvecLen > 1e-6) {
5421
- axis1 = normalize(vec2<f32>(b, lambda1 - a));
5422
- axis2 = vec2<f32>(-axis1.y, axis1.x);
5423
- } else {
5424
- if (a >= c) { axis1 = vec2<f32>(1.0, 0.0); axis2 = vec2<f32>(0.0, 1.0); }
5425
- else { axis1 = vec2<f32>(0.0, 1.0); axis2 = vec2<f32>(1.0, 0.0); }
5380
+ // 视锥剔除 (放宽边界以避免 pop-in)
5381
+ let clipBound = 1.3;
5382
+ if abs(ndcPos.x) > clipBound || abs(ndcPos.y) > clipBound || ndcPos.z < -0.2 || ndcPos.z > 1.0 {
5383
+ output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output;
5426
5384
  }
5427
- return mat2x2<f32>(axis1 * r1, axis2 * r2);
5428
- }
5429
-
5430
- @vertex
5431
- fn vs_main(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> VertexOutput {
5432
- var output: VertexOutput;
5433
- let splatIndex = sortedIndices[instanceIndex];
5434
- let splat = splats[splatIndex];
5435
- let quadPos = QUAD_POSITIONS[vertexIndex];
5436
- output.localUV = quadPos;
5437
-
5438
- let modelView = uniforms.view * uniforms.model;
5439
- let modelScale = getModelScale3(uniforms.model);
5440
-
5441
- let cov2D = computeCov2D(splat.mean, splat.scale, splat.rotation, modelView, uniforms.proj, modelScale);
5442
- let axes = computeEllipseAxes(cov2D);
5443
- let screenOffset = axes[0] * quadPos.x * ELLIPSE_SCALE + axes[1] * quadPos.y * ELLIPSE_SCALE;
5444
-
5445
- let worldPos = uniforms.model * vec4<f32>(splat.mean, 1.0);
5446
- let viewPos = uniforms.view * worldPos;
5447
- var clipPos = uniforms.proj * viewPos;
5448
- clipPos.x = clipPos.x + screenOffset.x * clipPos.w;
5449
- clipPos.y = clipPos.y + screenOffset.y * clipPos.w;
5450
- output.position = clipPos;
5451
-
5452
- // SH 计算 - 使用局部坐标系中的方向 (与 visionary 一致)
5453
- let A = mat3x3<f32>(
5454
- uniforms.model[0].xyz,
5455
- uniforms.model[1].xyz,
5456
- uniforms.model[2].xyz
5457
- );
5458
- let modelTranslation = uniforms.model[3].xyz;
5459
- let s2 = max(1e-12, (dot(A[0], A[0]) + dot(A[1], A[1]) + dot(A[2], A[2])) / 3.0);
5460
- let camLocal = (transpose(A) * (uniforms.cameraPos - modelTranslation)) / s2;
5461
- let dirLocal = normalize(splat.mean - camLocal);
5462
-
5463
- let shColor1 = evalSH1(dirLocal, splat.sh1);
5464
- let shColor2 = evalSH2(dirLocal, splat.sh2);
5465
- output.color = max(vec3<f32>(0.0), splat.colorDC + shColor1 + shColor2);
5466
- output.opacity = splat.opacity;
5467
-
5468
- return output;
5469
- }
5470
-
5471
- @fragment
5472
- fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
5473
- let r = length(input.localUV);
5474
- if (r > 1.0) { discard; }
5475
- let gaussianWeight = exp(-r * r * GAUSSIAN_DECAY);
5476
- let alpha = input.opacity * gaussianWeight;
5477
- if (alpha < 0.004) { discard; }
5478
- let color = clamp(input.color, vec3<f32>(0.0), vec3<f32>(1.0));
5479
- return vec4<f32>(color * alpha, alpha);
5480
- }
5481
- `
5482
- );
5483
- const shaderCodeL3 = (
5484
- /* wgsl */
5485
- `
5486
- struct Uniforms {
5487
- view: mat4x4<f32>,
5488
- proj: mat4x4<f32>,
5489
- model: mat4x4<f32>,
5490
- cameraPos: vec3<f32>,
5491
- _pad: f32,
5492
- screenSize: vec2<f32>,
5493
- _pad2: vec2<f32>,
5494
- }
5495
-
5496
- struct Splat {
5497
- mean: vec3<f32>,
5498
- _pad0: f32,
5499
- scale: vec3<f32>,
5500
- _pad1: f32,
5501
- rotation: vec4<f32>,
5502
- colorDC: vec3<f32>,
5503
- opacity: f32,
5504
- sh1: array<f32, 9>,
5505
- sh2: array<f32, 15>,
5506
- sh3: array<f32, 21>,
5507
- _pad2: array<f32, 3>,
5508
- }
5509
-
5510
- @group(0) @binding(0) var<uniform> uniforms: Uniforms;
5511
- @group(0) @binding(1) var<storage, read> splats: array<Splat>;
5512
- @group(0) @binding(2) var<storage, read> sortedIndices: array<u32>;
5513
-
5514
- struct VertexOutput {
5515
- @builtin(position) position: vec4<f32>,
5516
- @location(0) localUV: vec2<f32>,
5517
- @location(1) color: vec3<f32>,
5518
- @location(2) opacity: f32,
5519
- }
5520
-
5521
- const QUAD_POSITIONS = array<vec2<f32>, 4>(
5522
- vec2<f32>(-1.0, -1.0),
5523
- vec2<f32>( 1.0, -1.0),
5524
- vec2<f32>(-1.0, 1.0),
5525
- vec2<f32>( 1.0, 1.0),
5526
- );
5527
-
5528
- const ELLIPSE_SCALE: f32 = 3.0;
5529
- const GAUSSIAN_DECAY: f32 = 4.5;
5530
-
5531
- // SH 常数
5532
- const SH_C0: f32 = 0.28209479177387814; // sqrt(1/(4*pi)) - DC 颜色
5533
- const SH_C1: f32 = 0.4886025119029199; // sqrt(3/(4*pi)) - L1
5534
- const SH_C2_0: f32 = 1.0925484305920792;
5535
- const SH_C2_1: f32 = -1.0925484305920792;
5536
- const SH_C2_2: f32 = 0.31539156525252005;
5537
- const SH_C2_3: f32 = -1.0925484305920792;
5538
- const SH_C2_4: f32 = 0.5462742152960396;
5539
- const SH_C3_0: f32 = -0.5900435899266435;
5540
- const SH_C3_1: f32 = 2.890611442640554;
5541
- const SH_C3_2: f32 = -0.4570457994644658;
5542
- const SH_C3_3: f32 = 0.3731763325901154;
5543
- const SH_C3_4: f32 = -0.4570457994644658;
5544
- const SH_C3_5: f32 = 1.445305721320277;
5545
- const SH_C3_6: f32 = -0.5900435899266435;
5546
-
5547
- // 数据布局 (按基函数分组): [basis0_RGB, basis1_RGB, basis2_RGB]
5548
- fn evalSH1(dir: vec3<f32>, sh1: array<f32, 9>) -> vec3<f32> {
5549
- let x = dir.x;
5550
- let y = dir.y;
5551
- let z = dir.z;
5552
- let sh0 = vec3<f32>(sh1[0], sh1[1], sh1[2]);
5553
- let sh1_1 = vec3<f32>(sh1[3], sh1[4], sh1[5]);
5554
- let sh2 = vec3<f32>(sh1[6], sh1[7], sh1[8]);
5555
- return SH_C1 * (-sh0 * y + sh1_1 * z - sh2 * x);
5556
- }
5557
-
5558
- // 数据布局 (按基函数分组): [basis0_RGB, ..., basis4_RGB]
5559
- fn evalSH2(dir: vec3<f32>, sh2: array<f32, 15>) -> vec3<f32> {
5560
- let x = dir.x; let y = dir.y; let z = dir.z;
5561
- let xx = x * x; let yy = y * y; let zz = z * z;
5562
- let xy = x * y; let yz = y * z; let xz = x * z;
5563
-
5564
- let b0 = SH_C2_0 * xy;
5565
- let b1 = SH_C2_1 * yz;
5566
- let b2 = SH_C2_2 * (2.0 * zz - xx - yy);
5567
- let b3 = SH_C2_3 * xz;
5568
- let b4 = SH_C2_4 * (xx - yy);
5569
-
5570
- let c0 = vec3<f32>(sh2[0], sh2[1], sh2[2]);
5571
- let c1 = vec3<f32>(sh2[3], sh2[4], sh2[5]);
5572
- let c2 = vec3<f32>(sh2[6], sh2[7], sh2[8]);
5573
- let c3 = vec3<f32>(sh2[9], sh2[10], sh2[11]);
5574
- let c4 = vec3<f32>(sh2[12], sh2[13], sh2[14]);
5575
5385
 
5576
- return c0 * b0 + c1 * b1 + c2 * b2 + c3 * b3 + c4 * b4;
5577
- }
5578
-
5579
- // 数据布局 (按基函数分组): [basis0_RGB, ..., basis6_RGB]
5580
- fn evalSH3(dir: vec3<f32>, sh3: array<f32, 21>) -> vec3<f32> {
5581
- let x = dir.x; let y = dir.y; let z = dir.z;
5582
- let xx = x * x; let yy = y * y; let zz = z * z;
5583
- let xy = x * y; let yz = y * z; let xz = x * z;
5584
-
5585
- let b0 = SH_C3_0 * y * (3.0 * xx - yy);
5586
- let b1 = SH_C3_1 * xy * z;
5587
- let b2 = SH_C3_2 * y * (4.0 * zz - xx - yy);
5588
- let b3 = SH_C3_3 * z * (2.0 * zz - 3.0 * xx - 3.0 * yy);
5589
- let b4 = SH_C3_4 * x * (4.0 * zz - xx - yy);
5590
- let b5 = SH_C3_5 * z * (xx - yy);
5591
- let b6 = SH_C3_6 * x * (xx - 3.0 * yy);
5592
-
5593
- let c0 = vec3<f32>(sh3[0], sh3[1], sh3[2]);
5594
- let c1 = vec3<f32>(sh3[3], sh3[4], sh3[5]);
5595
- let c2 = vec3<f32>(sh3[6], sh3[7], sh3[8]);
5596
- let c3 = vec3<f32>(sh3[9], sh3[10], sh3[11]);
5597
- let c4 = vec3<f32>(sh3[12], sh3[13], sh3[14]);
5598
- let c5 = vec3<f32>(sh3[15], sh3[16], sh3[17]);
5599
- let c6 = vec3<f32>(sh3[18], sh3[19], sh3[20]);
5386
+ // 计算 3D 协方差 (使用原始 scale,模型缩放通过 model-view 矩阵处理)
5387
+ // 关键: 不要在这里应用模型缩放,协方差投影会通过 model-view 矩阵正确处理
5388
+ let cov3d = computeCovariance3D(splat.scale, splat.rotation);
5600
5389
 
5601
- return c0 * b0 + c1 * b1 + c2 * b2 + c3 * b3 + c4 * b4 + c5 * b5 + c6 * b6;
5602
- }
5603
-
5604
- fn quatToMat3(q: vec4<f32>) -> mat3x3<f32> {
5605
- let w = q[0]; let x = q[1]; let y = q[2]; let z = q[3];
5606
- let x2 = x + x; let y2 = y + y; let z2 = z + z;
5607
- let xx = x * x2; let xy = x * y2; let xz = x * z2;
5608
- let yy = y * y2; let yz = y * z2; let zz = z * z2;
5609
- let wx = w * x2; let wy = w * y2; let wz = w * z2;
5610
- return mat3x3<f32>(
5611
- vec3<f32>(1.0 - (yy + zz), xy + wz, xz - wy),
5612
- vec3<f32>(xy - wz, 1.0 - (xx + zz), yz + wx),
5613
- vec3<f32>(xz + wy, yz - wx, 1.0 - (xx + yy))
5614
- );
5615
- }
5616
-
5617
- fn getModelScale3(model: mat4x4<f32>) -> vec3<f32> {
5618
- return vec3<f32>(
5619
- length(model[0].xyz),
5620
- length(model[1].xyz),
5621
- length(model[2].xyz)
5390
+ // 计算焦距 (匹配参考实现: abs(proj[0][0]) * 0.5 * width)
5391
+ let focal = vec2<f32>(
5392
+ abs(uniforms.proj[0][0]) * 0.5 * uniforms.screenSize.x,
5393
+ abs(uniforms.proj[1][1]) * 0.5 * uniforms.screenSize.y
5622
5394
  );
5623
- }
5624
-
5625
- fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelView: mat4x4<f32>, proj: mat4x4<f32>, modelScale: vec3<f32>) -> vec3<f32> {
5626
- let R = quatToMat3(rotation);
5627
- let scaledScale = scale * modelScale;
5628
- let s2 = scaledScale * scaledScale;
5629
- let M = mat3x3<f32>(R[0] * s2.x, R[1] * s2.y, R[2] * s2.z);
5630
- let Sigma = M * transpose(R);
5631
-
5632
- let viewPos = (modelView * vec4<f32>(mean, 1.0)).xyz;
5633
- let viewRot = mat3x3<f32>(modelView[0].xyz, modelView[1].xyz, modelView[2].xyz);
5634
- let SigmaView = viewRot * Sigma * transpose(viewRot);
5635
-
5636
- let fx = proj[0][0]; let fy = proj[1][1];
5637
- let z = -viewPos.z;
5638
- let z_clamped = max(z, 0.001);
5639
- let z2 = z_clamped * z_clamped;
5640
5395
 
5641
- let j1 = vec3<f32>(fx / z_clamped, 0.0, fx * viewPos.x / z2);
5642
- let j2 = vec3<f32>(0.0, fy / z_clamped, fy * viewPos.y / z2);
5643
- let Sj1 = SigmaView * j1;
5644
- let Sj2 = SigmaView * j2;
5645
- return vec3<f32>(dot(j1, Sj1), dot(j1, Sj2), dot(j2, Sj2));
5646
- }
5647
-
5648
- fn computeEllipseAxes(cov2D: vec3<f32>) -> mat2x2<f32> {
5649
- let a = cov2D.x; let b = cov2D.y; let c = cov2D.z;
5650
- let trace = a + c;
5651
- let det = a * c - b * b;
5652
- let disc = trace * trace - 4.0 * det;
5653
- let sqrtDisc = sqrt(max(disc, 0.0));
5654
- let lambda1 = max((trace + sqrtDisc) * 0.5, 0.0);
5655
- let lambda2 = max((trace - sqrtDisc) * 0.5, 0.0);
5656
- let r1 = sqrt(lambda1);
5657
- let r2 = sqrt(lambda2);
5396
+ // 计算 model-view 矩阵 (匹配参考实现)
5397
+ let modelViewMat = uniforms.view * uniforms.model;
5658
5398
 
5659
- var axis1: vec2<f32>; var axis2: vec2<f32>;
5660
- let eigenvecLen = abs(b) + abs(lambda1 - a);
5661
- if (eigenvecLen > 1e-6) {
5662
- axis1 = normalize(vec2<f32>(b, lambda1 - a));
5663
- axis2 = vec2<f32>(-axis1.y, axis1.x);
5664
- } else {
5665
- if (a >= c) { axis1 = vec2<f32>(1.0, 0.0); axis2 = vec2<f32>(0.0, 1.0); }
5666
- else { axis1 = vec2<f32>(0.0, 1.0); axis2 = vec2<f32>(1.0, 0.0); }
5667
- }
5668
- return mat2x2<f32>(axis1 * r1, axis2 * r2);
5669
- }
5670
-
5671
- @vertex
5672
- fn vs_main(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> VertexOutput {
5673
- var output: VertexOutput;
5674
- let splatIndex = sortedIndices[instanceIndex];
5675
- let splat = splats[splatIndex];
5676
- let quadPos = QUAD_POSITIONS[vertexIndex];
5677
- output.localUV = quadPos;
5399
+ // 投影协方差到 2D (传入 viewPos 作为 vec4,不除以 w)
5400
+ let cov2d = projectCovariance(cov3d, viewPos, focal, modelViewMat);
5678
5401
 
5679
- // 计算 modelView 矩阵和模型缩放
5680
- let modelView = uniforms.view * uniforms.model;
5681
- let modelScale = getModelScale3(uniforms.model);
5682
-
5683
- let cov2D = computeCov2D(splat.mean, splat.scale, splat.rotation, modelView, uniforms.proj, modelScale);
5684
- let axes = computeEllipseAxes(cov2D);
5685
- let screenOffset = axes[0] * quadPos.x * ELLIPSE_SCALE + axes[1] * quadPos.y * ELLIPSE_SCALE;
5402
+ // 计算范围基向量 (带抗锯齿)
5403
+ let extentResult = computeExtentBasisAA(cov2d, splat.opacity, uniforms.screenSize);
5404
+ let basis = extentResult.basis;
5405
+ let adjustedOpacity = extentResult.adjustedOpacity;
5686
5406
 
5687
- // 应用 model 变换到 splat 位置
5688
- let worldPos = uniforms.model * vec4<f32>(splat.mean, 1.0);
5689
- let viewPos = uniforms.view * worldPos;
5690
- var clipPos = uniforms.proj * viewPos;
5691
- clipPos.x = clipPos.x + screenOffset.x * clipPos.w;
5692
- clipPos.y = clipPos.y + screenOffset.y * clipPos.w;
5693
- output.position = clipPos;
5407
+ if basis.x == 0.0 && basis.y == 0.0 && basis.z == 0.0 && basis.w == 0.0 {
5408
+ output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output;
5409
+ }
5694
5410
 
5695
- // SH 计算 - 使用局部坐标系中的方向 ( visionary 一致)
5696
- let A = mat3x3<f32>(
5697
- uniforms.model[0].xyz,
5698
- uniforms.model[1].xyz,
5699
- uniforms.model[2].xyz
5700
- );
5701
- let modelTranslation = uniforms.model[3].xyz;
5702
- let s2 = max(1e-12, (dot(A[0], A[0]) + dot(A[1], A[1]) + dot(A[2], A[2])) / 3.0);
5703
- let camLocal = (transpose(A) * (uniforms.cameraPos - modelTranslation)) / s2;
5704
- let dirLocal = normalize(splat.mean - camLocal);
5411
+ // 视锥边缘剔除 (匹配 PlayCanvas)
5412
+ let maxExtentPixels = max(length(basis.xy), length(basis.zw));
5413
+ let pixelToClip = vec2<f32>(clipPos.w, clipPos.w) / uniforms.screenSize;
5414
+ let splatExtentClip = vec2<f32>(maxExtentPixels, maxExtentPixels) * pixelToClip;
5415
+ if any((abs(clipPos.xy) - splatExtentClip) > vec2<f32>(clipPos.w, clipPos.w)) {
5416
+ output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output;
5417
+ }
5418
+
5419
+ // ClipCorner 优化 (匹配 PlayCanvas/SuperSplat)
5420
+ // 根据透明度缩小 quad,排除 alpha < 1/255 的区域
5421
+ let clipFactor = computeClipFactor(adjustedOpacity);
5422
+ if clipFactor <= 0.0 { output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output; }
5705
5423
 
5706
- let shColor1 = evalSH1(dirLocal, splat.sh1);
5707
- let shColor2 = evalSH2(dirLocal, splat.sh2);
5708
- let shColor3 = evalSH3(dirLocal, splat.sh3);
5709
- output.color = max(vec3<f32>(0.0), splat.colorDC + shColor1 + shColor2 + shColor3);
5710
- output.opacity = splat.opacity;
5424
+ // 计算最终顶点位置
5425
+ // basis_viewport: 从像素转换到 NDC 空间
5426
+ let basisViewport = vec2<f32>(1.0 / uniforms.screenSize.x, 1.0 / uniforms.screenSize.y);
5711
5427
 
5428
+ // 用 clipFactor 缩放基向量 (缩小 quad)
5429
+ let basisVector1 = basis.xy * clipFactor;
5430
+ let basisVector2 = basis.zw * clipFactor;
5431
+
5432
+ // 计算 NDC 偏移
5433
+ // 注意: quadPos 在 [-1, 1] 范围内,clipFactor 只影响 quad 大小 (basis_vector)
5434
+ let ndcOffset = (quadPos.x * basisVector1 + quadPos.y * basisVector2) * basisViewport * 2.0;
5435
+ output.position = vec4<f32>(ndcPos.xy + ndcOffset, ndcPos.z, 1.0);
5436
+
5437
+ // UV 输出 - 用 clipFactor 缩放以获得正确的 Gaussian 权重
5438
+ output.fragPos = quadPos * clipFactor;
5439
+
5440
+ // 颜色已在 CPU 端预处理为 (dc * SH_C0 + 0.5),直接使用
5441
+ // 这是 3DGS 的标准颜色格式,在 sRGB 空间中
5442
+ output.color = splat.colorDC;
5443
+ output.opacity = adjustedOpacity;
5712
5444
  return output;
5713
5445
  }
5714
5446
 
5715
5447
  @fragment
5716
5448
  fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
5717
- let r = length(input.localUV);
5718
- if (r > 1.0) { discard; }
5719
- let gaussianWeight = exp(-r * r * GAUSSIAN_DECAY);
5720
- let alpha = input.opacity * gaussianWeight;
5721
- if (alpha < 0.004) { discard; }
5722
- let color = clamp(input.color, vec3<f32>(0.0), vec3<f32>(1.0));
5723
- return vec4<f32>(color * alpha, alpha);
5449
+ if input.opacity <= 0.0 { discard; }
5450
+
5451
+ // A = 到中心的平方距离,在 UV 空间中
5452
+ // 由于 clipCorner 优化,fragPos [-clip, clip] 范围内
5453
+ let A = dot(input.fragPos, input.fragPos);
5454
+
5455
+ // 丢弃单位圆外的片段
5456
+ if A > 1.0 { discard; }
5457
+
5458
+ // Normalized Gaussian 衰减 (精确匹配 SuperSplat normExp)
5459
+ // 关键修复: 在 A=1 (边界) 时返回精确的 0.0,消除边缘雾化
5460
+ // 在 A=0 (中心): weight = 1.0
5461
+ // 在 A=1 (边界): weight = 精确的 0.0 (而不是标准 exp(-4) ≈ 0.018)
5462
+ let weight = (exp(-4.0 * A) - EXP_NEG4) * INV_ONE_MINUS_EXP_NEG4;
5463
+
5464
+ // 组合 splat 透明度
5465
+ let opacity = weight * input.opacity;
5466
+
5467
+ // Alpha 阈值丢弃 (匹配 SuperSplat: if (alpha < 1.0 / 255.0) discard)
5468
+ if opacity < ALPHA_CULL_THRESHOLD { discard; }
5469
+
5470
+ // 颜色 clamp 到有效范围 (防止负值)
5471
+ let color = max(input.color, vec3<f32>(0.0));
5472
+
5473
+ // 预乘 alpha 输出 (匹配 blend mode: src=ONE, dst=ONE_MINUS_SRC_ALPHA)
5474
+ // 这是 3DGS 渲染的标准混合模式
5475
+ return vec4<f32>(color * opacity, opacity);
5724
5476
  }
5725
5477
  `
5726
5478
  );
5727
- var SHMode = /* @__PURE__ */ ((SHMode2) => {
5728
- SHMode2[SHMode2["L0"] = 0] = "L0";
5729
- SHMode2[SHMode2["L1"] = 1] = "L1";
5730
- SHMode2[SHMode2["L2"] = 2] = "L2";
5731
- SHMode2[SHMode2["L3"] = 3] = "L3";
5732
- return SHMode2;
5733
- })(SHMode || {});
5734
5479
  const SPLAT_FLOAT_COUNT = 64;
5735
- const SPLAT_COMPACT_FLOAT_COUNT = 16;
5736
- const PERFORMANCE_CONFIGS = {
5737
- [
5738
- "high"
5739
- /* HIGH */
5740
- ]: {
5741
- maxVisibleSplats: Infinity,
5742
- enableSorting: true,
5743
- sortEveryNFrames: 1,
5744
- useCompactFormat: false,
5745
- pixelCullThreshold: 1,
5746
- defaultSHMode: 1
5747
- /* L1 */
5748
- },
5749
- [
5750
- "medium"
5751
- /* MEDIUM */
5752
- ]: {
5753
- maxVisibleSplats: Infinity,
5754
- enableSorting: true,
5755
- sortEveryNFrames: 1,
5756
- useCompactFormat: false,
5757
- pixelCullThreshold: 1,
5758
- defaultSHMode: 1
5759
- /* L1 */
5760
- },
5761
- [
5762
- "low"
5763
- /* LOW */
5764
- ]: {
5765
- maxVisibleSplats: Infinity,
5766
- enableSorting: true,
5767
- sortEveryNFrames: 1,
5768
- useCompactFormat: false,
5769
- pixelCullThreshold: 1,
5770
- defaultSHMode: 0
5771
- /* L0 */
5772
- }
5773
- };
5774
5480
  class GSSplatRenderer {
5775
5481
  constructor(renderer, camera) {
5776
5482
  __publicField(this, "renderer");
5777
5483
  __publicField(this, "camera");
5778
- __publicField(this, "pipelineL0");
5779
- __publicField(this, "pipelineL1");
5780
- __publicField(this, "pipelineL2");
5781
- __publicField(this, "pipelineL3");
5782
- __publicField(this, "pipelineL0Compact");
5783
- // 移动端紧凑格式管线
5484
+ __publicField(this, "pipeline");
5784
5485
  __publicField(this, "bindGroupLayout");
5785
- __publicField(this, "bindGroupLayoutCompact");
5786
- // 紧凑格式的 layout
5787
5486
  __publicField(this, "uniformBuffer");
5788
5487
  __publicField(this, "splatBuffer", null);
5789
5488
  __publicField(this, "splatCount", 0);
5790
5489
  __publicField(this, "bindGroup", null);
5791
- // 深度排序器(含剔除功能)- 使用 V2 分桶稳定排序
5792
5490
  __publicField(this, "sorter", null);
5793
- // 是否启用 DrawIndirect (剔除优化)
5794
- // 注意:在某些移动设备上可能有问题,可以禁用作为备用
5795
- __publicField(this, "useDrawIndirect", true);
5796
- // 是否为移动设备(用于调试)
5797
- __publicField(this, "isMobile", false);
5798
- // 像素剔除阈值 (小于此像素的 splat 会被剔除)
5799
- __publicField(this, "pixelCullThreshold", 1);
5800
- // SH 模式:L0/L1/L2/L3
5801
- __publicField(this, "shMode", 1);
5802
- // 点云的 bounding box(在 setData 时计算)
5491
+ __publicField(this, "shMode", SHMode.L0);
5803
5492
  __publicField(this, "boundingBox", null);
5804
- // ============================================
5805
- // 变换相关 (position, rotation, scale)
5806
- // ============================================
5493
+ // Transform
5807
5494
  __publicField(this, "position", [0, 0, 0]);
5808
5495
  __publicField(this, "rotation", [0, 0, 0]);
5809
- // Euler angles (radians)
5810
5496
  __publicField(this, "scale", [1, 1, 1]);
5811
5497
  __publicField(this, "pivot", [0, 0, 0]);
5812
- // 旋转/缩放中心点
5813
5498
  __publicField(this, "modelMatrix", new Float32Array(16));
5814
- // 4x4 model matrix
5815
- // ============================================
5816
- // 移动端优化相关
5817
- // ============================================
5818
- __publicField(this, "performanceTier");
5819
- __publicField(this, "optimizationConfig");
5820
- __publicField(this, "frameCount", 0);
5821
- __publicField(this, "useCompactFormat", false);
5499
+ // 剔除选项
5500
+ __publicField(this, "pixelCullThreshold", 1);
5822
5501
  this.renderer = renderer;
5823
5502
  this.camera = camera;
5824
- this.isMobile = isMobileDevice$1();
5825
- this.performanceTier = detectPerformanceTier(renderer.device);
5826
- this.optimizationConfig = { ...PERFORMANCE_CONFIGS[this.performanceTier] };
5827
- this.pixelCullThreshold = this.optimizationConfig.pixelCullThreshold;
5828
- this.shMode = this.optimizationConfig.defaultSHMode;
5829
- this.useCompactFormat = this.optimizationConfig.useCompactFormat;
5830
- this.createPipelines();
5503
+ this.createPipeline();
5831
5504
  this.createUniformBuffer();
5832
5505
  this.updateModelMatrix();
5833
5506
  }
5834
- // ============================================
5835
- // Transform 方法
5836
- // ============================================
5837
- /**
5838
- * 设置位置
5839
- */
5507
+ createPipeline() {
5508
+ const device = this.renderer.device;
5509
+ const shaderModule = device.createShaderModule({
5510
+ code: gsOptimizedShader
5511
+ });
5512
+ this.bindGroupLayout = device.createBindGroupLayout({
5513
+ entries: [
5514
+ { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
5515
+ { binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
5516
+ { binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }
5517
+ ]
5518
+ });
5519
+ const pipelineLayout = device.createPipelineLayout({
5520
+ bindGroupLayouts: [this.bindGroupLayout]
5521
+ });
5522
+ this.pipeline = device.createRenderPipeline({
5523
+ layout: pipelineLayout,
5524
+ vertex: {
5525
+ module: shaderModule,
5526
+ entryPoint: "vs_main",
5527
+ buffers: []
5528
+ },
5529
+ fragment: {
5530
+ module: shaderModule,
5531
+ entryPoint: "fs_main",
5532
+ targets: [{
5533
+ format: this.renderer.format,
5534
+ blend: {
5535
+ color: {
5536
+ srcFactor: "one",
5537
+ dstFactor: "one-minus-src-alpha",
5538
+ operation: "add"
5539
+ },
5540
+ alpha: {
5541
+ srcFactor: "one",
5542
+ dstFactor: "one-minus-src-alpha",
5543
+ operation: "add"
5544
+ }
5545
+ }
5546
+ }]
5547
+ },
5548
+ primitive: {
5549
+ topology: "triangle-strip"
5550
+ },
5551
+ depthStencil: {
5552
+ format: this.renderer.depthFormat,
5553
+ depthWriteEnabled: false,
5554
+ depthCompare: "always"
5555
+ }
5556
+ });
5557
+ }
5558
+ createUniformBuffer() {
5559
+ this.uniformBuffer = this.renderer.device.createBuffer({
5560
+ size: 224,
5561
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
5562
+ });
5563
+ }
5840
5564
  setPosition(x, y, z) {
5841
5565
  this.position = [x, y, z];
5842
5566
  this.updateModelMatrix();
5843
5567
  }
5844
- /**
5845
- * 获取位置
5846
- */
5847
5568
  getPosition() {
5848
5569
  return [...this.position];
5849
5570
  }
5850
- /**
5851
- * 设置旋转 (欧拉角, 弧度)
5852
- */
5853
5571
  setRotation(x, y, z) {
5854
5572
  this.rotation = [x, y, z];
5855
5573
  this.updateModelMatrix();
5856
5574
  }
5857
- /**
5858
- * 获取旋转
5859
- */
5860
5575
  getRotation() {
5861
5576
  return [...this.rotation];
5862
5577
  }
5863
- /**
5864
- * 设置缩放
5865
- */
5866
5578
  setScale(x, y, z) {
5867
5579
  this.scale = [x, y, z];
5868
5580
  this.updateModelMatrix();
5869
5581
  }
5870
- /**
5871
- * 获取缩放
5872
- */
5873
5582
  getScale() {
5874
5583
  return [...this.scale];
5875
5584
  }
5876
- /**
5877
- * 设置旋转/缩放中心点 (pivot)
5878
- */
5879
5585
  setPivot(x, y, z) {
5880
5586
  this.pivot = [x, y, z];
5881
5587
  this.updateModelMatrix();
5882
5588
  }
5883
- /**
5884
- * 获取旋转/缩放中心点 (pivot)
5885
- */
5886
5589
  getPivot() {
5887
5590
  return [...this.pivot];
5888
5591
  }
5889
- /**
5890
- * 更新模型矩阵
5891
- * 变换顺序: T * Tp * R * S * Tp^-1
5892
- * 即: 先移到原点,缩放,旋转,再移回pivot,最后应用用户平移
5893
- */
5894
5592
  updateModelMatrix() {
5895
5593
  const [tx, ty, tz] = this.position;
5896
5594
  const [rx, ry, rz] = this.rotation;
@@ -5934,148 +5632,18 @@ class GSSplatRenderer {
5934
5632
  this.modelMatrix[14] = finalTz;
5935
5633
  this.modelMatrix[15] = 1;
5936
5634
  }
5937
- /**
5938
- * 获取当前模型矩阵
5939
- */
5940
5635
  getModelMatrix() {
5941
5636
  return this.modelMatrix;
5942
5637
  }
5943
- /**
5944
- * 获取当前性能等级
5945
- */
5946
- getPerformanceTier() {
5947
- return this.performanceTier;
5948
- }
5949
- /**
5950
- * 手动设置优化配置
5951
- */
5952
- setOptimizationConfig(config) {
5953
- this.optimizationConfig = { ...this.optimizationConfig, ...config };
5954
- this.pixelCullThreshold = this.optimizationConfig.pixelCullThreshold;
5955
- if (config.defaultSHMode !== void 0) {
5956
- this.shMode = config.defaultSHMode;
5957
- }
5958
- }
5959
- /**
5960
- * 获取当前优化配置
5961
- */
5962
- getOptimizationConfig() {
5963
- return { ...this.optimizationConfig };
5964
- }
5965
- /**
5966
- * 设置 SH 模式
5967
- * @param mode L0/L1/L2/L3
5968
- */
5969
5638
  setSHMode(mode) {
5970
5639
  this.shMode = mode;
5971
5640
  }
5972
- /**
5973
- * 获取当前 SH 模式
5974
- */
5975
5641
  getSHMode() {
5976
5642
  return this.shMode;
5977
5643
  }
5978
- /**
5979
- * 设置是否启用 DrawIndirect (剔除优化)
5980
- * 启用后会在 GPU 上进行可见性剔除,仅绘制可见 splat
5981
- */
5982
- setUseDrawIndirect(enabled) {
5983
- this.useDrawIndirect = enabled;
5984
- }
5985
- /**
5986
- * 设置像素剔除阈值
5987
- * 屏幕上小于此像素数的 splat 会被剔除
5988
- * @param threshold 像素阈值,默认 1.0
5989
- */
5990
5644
  setPixelCullThreshold(threshold) {
5991
5645
  this.pixelCullThreshold = threshold;
5992
5646
  }
5993
- /**
5994
- * 创建渲染管线 (L0/L1/L2/L3 四个版本)
5995
- */
5996
- createPipelines() {
5997
- const device = this.renderer.device;
5998
- const shaderModuleL0 = device.createShaderModule({ code: shaderCodeL0 });
5999
- const shaderModuleL1 = device.createShaderModule({ code: shaderCodeL1 });
6000
- const shaderModuleL2 = device.createShaderModule({ code: shaderCodeL2 });
6001
- const shaderModuleL3 = device.createShaderModule({ code: shaderCodeL3 });
6002
- this.bindGroupLayout = device.createBindGroupLayout({
6003
- entries: [
6004
- {
6005
- // uniform buffer (view + proj matrices)
6006
- binding: 0,
6007
- visibility: GPUShaderStage.VERTEX,
6008
- buffer: { type: "uniform" }
6009
- },
6010
- {
6011
- // storage buffer (splats array)
6012
- binding: 1,
6013
- visibility: GPUShaderStage.VERTEX,
6014
- buffer: { type: "read-only-storage" }
6015
- },
6016
- {
6017
- // storage buffer (sorted indices)
6018
- binding: 2,
6019
- visibility: GPUShaderStage.VERTEX,
6020
- buffer: { type: "read-only-storage" }
6021
- }
6022
- ]
6023
- });
6024
- const pipelineLayout = device.createPipelineLayout({
6025
- bindGroupLayouts: [this.bindGroupLayout]
6026
- });
6027
- const basePipelineDesc = {
6028
- layout: pipelineLayout,
6029
- primitive: {
6030
- topology: "triangle-strip"
6031
- },
6032
- depthStencil: {
6033
- format: this.renderer.depthFormat,
6034
- depthWriteEnabled: false,
6035
- depthCompare: "always"
6036
- }
6037
- };
6038
- const blendState = {
6039
- color: {
6040
- srcFactor: "one",
6041
- dstFactor: "one-minus-src-alpha",
6042
- operation: "add"
6043
- },
6044
- alpha: {
6045
- srcFactor: "one",
6046
- dstFactor: "one-minus-src-alpha",
6047
- operation: "add"
6048
- }
6049
- };
6050
- const createPipeline = (module) => device.createRenderPipeline({
6051
- ...basePipelineDesc,
6052
- vertex: { module, entryPoint: "vs_main", buffers: [] },
6053
- fragment: {
6054
- module,
6055
- entryPoint: "fs_main",
6056
- targets: [{ format: this.renderer.format, blend: blendState }]
6057
- }
6058
- });
6059
- this.pipelineL0 = createPipeline(shaderModuleL0);
6060
- this.pipelineL1 = createPipeline(shaderModuleL1);
6061
- this.pipelineL2 = createPipeline(shaderModuleL2);
6062
- this.pipelineL3 = createPipeline(shaderModuleL3);
6063
- }
6064
- /**
6065
- * 创建 uniform buffer
6066
- * 布局: view (64 bytes) + proj (64 bytes) + model (64 bytes) + cameraPos (12 bytes) + padding (4 bytes) + screenSize (8 bytes) + padding (8 bytes) = 224 bytes
6067
- */
6068
- createUniformBuffer() {
6069
- this.uniformBuffer = this.renderer.device.createBuffer({
6070
- size: 224,
6071
- // view (64) + proj (64) + model (64) + cameraPos (12) + padding (4) + screenSize (8) + padding (8)
6072
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
6073
- });
6074
- }
6075
- /**
6076
- * 设置 splat 数据
6077
- * @param splats CPU 端的 splat 数组
6078
- */
6079
5647
  setData(splats) {
6080
5648
  const device = this.renderer.device;
6081
5649
  if (this.splatBuffer) {
@@ -6085,19 +5653,7 @@ class GSSplatRenderer {
6085
5653
  this.sorter.destroy();
6086
5654
  this.sorter = null;
6087
5655
  }
6088
- splats.length;
6089
- const maxSplats = this.optimizationConfig.maxVisibleSplats;
6090
- if (splats.length > maxSplats && maxSplats !== Infinity) {
6091
- const step = splats.length / maxSplats;
6092
- const sampledSplats = [];
6093
- for (let i = 0; i < maxSplats; i++) {
6094
- const idx = Math.floor(i * step);
6095
- sampledSplats.push(splats[idx]);
6096
- }
6097
- splats = sampledSplats;
6098
- }
6099
5656
  this.splatCount = splats.length;
6100
- this.frameCount = 0;
6101
5657
  if (this.splatCount === 0) {
6102
5658
  this.splatBuffer = null;
6103
5659
  this.bindGroup = null;
@@ -6105,12 +5661,10 @@ class GSSplatRenderer {
6105
5661
  return;
6106
5662
  }
6107
5663
  this.boundingBox = this.computeBoundingBox(splats);
6108
- const useCompact = this.useCompactFormat;
6109
- const floatCount = useCompact ? SPLAT_COMPACT_FLOAT_COUNT : SPLAT_FLOAT_COUNT;
6110
- const data = new Float32Array(this.splatCount * floatCount);
5664
+ const data = new Float32Array(this.splatCount * SPLAT_FLOAT_COUNT);
6111
5665
  for (let i = 0; i < this.splatCount; i++) {
6112
5666
  const splat = splats[i];
6113
- const offset = i * floatCount;
5667
+ const offset = i * SPLAT_FLOAT_COUNT;
6114
5668
  data[offset + 0] = splat.mean[0];
6115
5669
  data[offset + 1] = splat.mean[1];
6116
5670
  data[offset + 2] = splat.mean[2];
@@ -6127,27 +5681,70 @@ class GSSplatRenderer {
6127
5681
  data[offset + 13] = splat.colorDC[1];
6128
5682
  data[offset + 14] = splat.colorDC[2];
6129
5683
  data[offset + 15] = splat.opacity;
6130
- if (!useCompact) {
6131
- const shRest = splat.shRest;
6132
- for (let j = 0; j < 9; j++) {
6133
- data[offset + 16 + j] = shRest ? shRest[j] : 0;
6134
- }
6135
- for (let j = 0; j < 15; j++) {
6136
- data[offset + 25 + j] = shRest ? shRest[9 + j] : 0;
6137
- }
6138
- for (let j = 0; j < 21; j++) {
6139
- data[offset + 40 + j] = shRest ? shRest[24 + j] : 0;
6140
- }
6141
- data[offset + 61] = 0;
6142
- data[offset + 62] = 0;
6143
- data[offset + 63] = 0;
5684
+ const shRest = splat.shRest;
5685
+ for (let j = 0; j < 9; j++) {
5686
+ data[offset + 16 + j] = shRest ? shRest[j] : 0;
5687
+ }
5688
+ for (let j = 0; j < 15; j++) {
5689
+ data[offset + 25 + j] = shRest ? shRest[9 + j] : 0;
6144
5690
  }
5691
+ for (let j = 0; j < 21; j++) {
5692
+ data[offset + 40 + j] = shRest ? shRest[24 + j] : 0;
5693
+ }
5694
+ data[offset + 61] = 0;
5695
+ data[offset + 62] = 0;
5696
+ data[offset + 63] = 0;
6145
5697
  }
6146
5698
  this.splatBuffer = device.createBuffer({
6147
5699
  size: data.byteLength,
6148
5700
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
6149
5701
  });
6150
- device.queue.writeBuffer(this.splatBuffer, 0, data);
5702
+ device.queue.writeBuffer(this.splatBuffer, 0, data);
5703
+ this.sorter = new GSSplatSorter(
5704
+ device,
5705
+ this.splatCount,
5706
+ this.splatBuffer,
5707
+ this.uniformBuffer
5708
+ );
5709
+ this.sorter.setScreenSize(this.renderer.width, this.renderer.height);
5710
+ this.sorter.setCullingOptions({
5711
+ nearPlane: this.camera.near,
5712
+ farPlane: this.camera.far,
5713
+ pixelThreshold: this.pixelCullThreshold
5714
+ });
5715
+ this.bindGroup = device.createBindGroup({
5716
+ layout: this.bindGroupLayout,
5717
+ entries: [
5718
+ { binding: 0, resource: { buffer: this.uniformBuffer } },
5719
+ { binding: 1, resource: { buffer: this.splatBuffer } },
5720
+ { binding: 2, resource: { buffer: this.sorter.getIndicesBuffer() } }
5721
+ ]
5722
+ });
5723
+ }
5724
+ setCompactData(compactData) {
5725
+ const device = this.renderer.device;
5726
+ if (this.splatBuffer) {
5727
+ this.splatBuffer.destroy();
5728
+ }
5729
+ if (this.sorter) {
5730
+ this.sorter.destroy();
5731
+ this.sorter = null;
5732
+ }
5733
+ this.splatCount = compactData.count;
5734
+ if (this.splatCount === 0) {
5735
+ this.splatBuffer = null;
5736
+ this.bindGroup = null;
5737
+ this.boundingBox = null;
5738
+ return;
5739
+ }
5740
+ this.boundingBox = this.computeBoundingBoxFromCompact(compactData);
5741
+ const includeSH = compactData.shCoeffs !== void 0;
5742
+ const gpuData = compactDataToGPUBuffer(compactData, includeSH);
5743
+ this.splatBuffer = device.createBuffer({
5744
+ size: gpuData.byteLength,
5745
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
5746
+ });
5747
+ device.queue.writeBuffer(this.splatBuffer, 0, gpuData.buffer);
6151
5748
  this.sorter = new GSSplatSorter(
6152
5749
  device,
6153
5750
  this.splatCount,
@@ -6163,130 +5760,16 @@ class GSSplatRenderer {
6163
5760
  this.bindGroup = device.createBindGroup({
6164
5761
  layout: this.bindGroupLayout,
6165
5762
  entries: [
6166
- {
6167
- binding: 0,
6168
- resource: { buffer: this.uniformBuffer }
6169
- },
6170
- {
6171
- binding: 1,
6172
- resource: { buffer: this.splatBuffer }
6173
- },
6174
- {
6175
- binding: 2,
6176
- resource: { buffer: this.sorter.getIndicesBuffer() }
6177
- }
5763
+ { binding: 0, resource: { buffer: this.uniformBuffer } },
5764
+ { binding: 1, resource: { buffer: this.splatBuffer } },
5765
+ { binding: 2, resource: { buffer: this.sorter.getIndicesBuffer() } }
6178
5766
  ]
6179
5767
  });
6180
- (data.byteLength / (1024 * 1024)).toFixed(2);
6181
- }
6182
- /**
6183
- * 设置紧凑格式的 splat 数据(移动端优化)
6184
- * 直接接受 CompactSplatData,避免创建中间对象
6185
- * @param compactData 紧凑格式的 splat 数据
6186
- */
6187
- setCompactData(compactData) {
6188
- try {
6189
- const device = this.renderer.device;
6190
- if (this.splatBuffer) {
6191
- this.splatBuffer.destroy();
6192
- }
6193
- if (this.sorter) {
6194
- this.sorter.destroy();
6195
- this.sorter = null;
6196
- }
6197
- this.splatCount = compactData.count;
6198
- this.frameCount = 0;
6199
- if (this.splatCount === 0) {
6200
- this.splatBuffer = null;
6201
- this.bindGroup = null;
6202
- this.boundingBox = null;
6203
- return;
6204
- }
6205
- this.boundingBox = this.computeBoundingBoxFromCompact(compactData);
6206
- const includeSH = compactData.shCoeffs !== void 0;
6207
- const gpuData = compactDataToGPUBuffer(compactData, includeSH);
6208
- this.splatBuffer = device.createBuffer({
6209
- size: gpuData.byteLength,
6210
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
6211
- });
6212
- device.queue.writeBuffer(this.splatBuffer, 0, gpuData.buffer);
6213
- this.sorter = new GSSplatSorter(
6214
- device,
6215
- this.splatCount,
6216
- this.splatBuffer,
6217
- this.uniformBuffer
6218
- );
6219
- this.sorter.setScreenSize(this.renderer.width, this.renderer.height);
6220
- this.sorter.setCullingOptions({
6221
- nearPlane: this.camera.near,
6222
- farPlane: this.camera.far,
6223
- pixelThreshold: this.pixelCullThreshold
6224
- });
6225
- this.bindGroup = device.createBindGroup({
6226
- layout: this.bindGroupLayout,
6227
- entries: [
6228
- { binding: 0, resource: { buffer: this.uniformBuffer } },
6229
- { binding: 1, resource: { buffer: this.splatBuffer } },
6230
- { binding: 2, resource: { buffer: this.sorter.getIndicesBuffer() } }
6231
- ]
6232
- });
6233
- const memoryMB = (gpuData.byteLength / (1024 * 1024)).toFixed(2);
6234
- } catch (error) {
6235
- this.splatCount = 0;
6236
- this.splatBuffer = null;
6237
- this.bindGroup = null;
6238
- this.sorter = null;
6239
- }
6240
- }
6241
- /**
6242
- * 从紧凑数据计算 bounding box
6243
- */
6244
- computeBoundingBoxFromCompact(data) {
6245
- if (data.count === 0) {
6246
- return { min: [0, 0, 0], max: [0, 0, 0], center: [0, 0, 0], radius: 0 };
6247
- }
6248
- const positions = data.positions;
6249
- const min = [
6250
- positions[0],
6251
- positions[1],
6252
- positions[2]
6253
- ];
6254
- const max = [
6255
- positions[0],
6256
- positions[1],
6257
- positions[2]
6258
- ];
6259
- for (let i = 1; i < data.count; i++) {
6260
- const x = positions[i * 3 + 0];
6261
- const y = positions[i * 3 + 1];
6262
- const z = positions[i * 3 + 2];
6263
- min[0] = Math.min(min[0], x);
6264
- min[1] = Math.min(min[1], y);
6265
- min[2] = Math.min(min[2], z);
6266
- max[0] = Math.max(max[0], x);
6267
- max[1] = Math.max(max[1], y);
6268
- max[2] = Math.max(max[2], z);
6269
- }
6270
- const center = [
6271
- (min[0] + max[0]) / 2,
6272
- (min[1] + max[1]) / 2,
6273
- (min[2] + max[2]) / 2
6274
- ];
6275
- const dx = max[0] - min[0];
6276
- const dy = max[1] - min[1];
6277
- const dz = max[2] - min[2];
6278
- const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
6279
- return { min, max, center, radius };
6280
5768
  }
6281
- /**
6282
- * 渲染 splats
6283
- * @param pass 渲染通道编码器
6284
- */
6285
5769
  render(pass) {
6286
5770
  if (this.splatCount === 0 || !this.bindGroup || !this.sorter) {
6287
5771
  return;
6288
5772
  }
6289
- this.frameCount++;
6290
5773
  this.renderer.device.queue.writeBuffer(
6291
5774
  this.uniformBuffer,
6292
5775
  0,
@@ -6318,63 +5801,23 @@ class GSSplatRenderer {
6318
5801
  farPlane: this.camera.far,
6319
5802
  pixelThreshold: this.pixelCullThreshold
6320
5803
  });
6321
- const isFirstFrame = this.frameCount === 1;
6322
- const shouldSort = this.optimizationConfig.enableSorting && (isFirstFrame || this.frameCount % this.optimizationConfig.sortEveryNFrames === 0);
6323
- if (shouldSort) {
6324
- this.sorter.sort();
6325
- }
6326
- const pipelines = [
6327
- this.pipelineL0,
6328
- this.pipelineL1,
6329
- this.pipelineL2,
6330
- this.pipelineL3
6331
- ];
6332
- const pipeline = pipelines[this.shMode];
6333
- pass.setPipeline(pipeline);
5804
+ this.sorter.sort();
5805
+ pass.setPipeline(this.pipeline);
6334
5806
  pass.setBindGroup(0, this.bindGroup);
6335
- if (this.useDrawIndirect) {
6336
- pass.drawIndirect(this.sorter.getDrawIndirectBuffer(), 0);
6337
- } else {
6338
- pass.draw(4, this.splatCount);
6339
- }
5807
+ pass.drawIndirect(this.sorter.getDrawIndirectBuffer(), 0);
6340
5808
  }
6341
- /**
6342
- * 获取 splat 数量
6343
- */
6344
5809
  getSplatCount() {
6345
5810
  return this.splatCount;
6346
5811
  }
6347
- /**
6348
- * 获取点云的 bounding box
6349
- * @returns BoundingBox 或 null(如果没有点云数据)
6350
- */
6351
5812
  getBoundingBox() {
6352
5813
  return this.boundingBox;
6353
5814
  }
6354
- /**
6355
- * 计算点云的 bounding box
6356
- * @param splats splat 数组
6357
- * @returns BoundingBox
6358
- */
6359
5815
  computeBoundingBox(splats) {
6360
5816
  if (splats.length === 0) {
6361
- return {
6362
- min: [0, 0, 0],
6363
- max: [0, 0, 0],
6364
- center: [0, 0, 0],
6365
- radius: 0
6366
- };
5817
+ return { min: [0, 0, 0], max: [0, 0, 0], center: [0, 0, 0], radius: 0 };
6367
5818
  }
6368
- const min = [
6369
- splats[0].mean[0],
6370
- splats[0].mean[1],
6371
- splats[0].mean[2]
6372
- ];
6373
- const max = [
6374
- splats[0].mean[0],
6375
- splats[0].mean[1],
6376
- splats[0].mean[2]
6377
- ];
5819
+ const min = [splats[0].mean[0], splats[0].mean[1], splats[0].mean[2]];
5820
+ const max = [splats[0].mean[0], splats[0].mean[1], splats[0].mean[2]];
6378
5821
  for (let i = 1; i < splats.length; i++) {
6379
5822
  const [x, y, z] = splats[i].mean;
6380
5823
  min[0] = Math.min(min[0], x);
@@ -6395,30 +5838,46 @@ class GSSplatRenderer {
6395
5838
  const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
6396
5839
  return { min, max, center, radius };
6397
5840
  }
6398
- // ============================================
6399
- // IGSSplatRenderer 接口实现
6400
- // ============================================
6401
- /**
6402
- * 是否支持指定的 SH 模式
6403
- */
5841
+ computeBoundingBoxFromCompact(data) {
5842
+ if (data.count === 0) {
5843
+ return { min: [0, 0, 0], max: [0, 0, 0], center: [0, 0, 0], radius: 0 };
5844
+ }
5845
+ const positions = data.positions;
5846
+ const min = [positions[0], positions[1], positions[2]];
5847
+ const max = [positions[0], positions[1], positions[2]];
5848
+ for (let i = 1; i < data.count; i++) {
5849
+ const x = positions[i * 3 + 0];
5850
+ const y = positions[i * 3 + 1];
5851
+ const z = positions[i * 3 + 2];
5852
+ min[0] = Math.min(min[0], x);
5853
+ min[1] = Math.min(min[1], y);
5854
+ min[2] = Math.min(min[2], z);
5855
+ max[0] = Math.max(max[0], x);
5856
+ max[1] = Math.max(max[1], y);
5857
+ max[2] = Math.max(max[2], z);
5858
+ }
5859
+ const center = [
5860
+ (min[0] + max[0]) / 2,
5861
+ (min[1] + max[1]) / 2,
5862
+ (min[2] + max[2]) / 2
5863
+ ];
5864
+ const dx = max[0] - min[0];
5865
+ const dy = max[1] - min[1];
5866
+ const dz = max[2] - min[2];
5867
+ const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
5868
+ return { min, max, center, radius };
5869
+ }
6404
5870
  supportsSHMode(mode) {
6405
- return mode >= SHMode$1.L0 && mode <= SHMode$1.L3;
5871
+ return mode >= SHMode.L0 && mode <= SHMode.L3;
6406
5872
  }
6407
- /**
6408
- * 获取渲染器能力
6409
- */
6410
5873
  getCapabilities() {
6411
5874
  return {
6412
- maxSHMode: SHMode$1.L3,
5875
+ maxSHMode: SHMode.L3,
6413
5876
  supportsRawData: true,
6414
5877
  isMobileOptimized: false,
6415
5878
  maxSplatCount: 0
6416
- // 无限制(受 GPU 内存限制)
6417
5879
  };
6418
5880
  }
6419
- /**
6420
- * 销毁资源
6421
- */
6422
5881
  destroy() {
6423
5882
  if (this.splatBuffer) {
6424
5883
  this.splatBuffer.destroy();
@@ -7679,20 +7138,20 @@ class GSSplatRendererMobile {
7679
7138
  * 获取当前 SH 模式
7680
7139
  */
7681
7140
  getSHMode() {
7682
- return SHMode$1.L0;
7141
+ return SHMode.L0;
7683
7142
  }
7684
7143
  /**
7685
7144
  * 是否支持指定的 SH 模式
7686
7145
  */
7687
7146
  supportsSHMode(mode) {
7688
- return mode === SHMode$1.L0;
7147
+ return mode === SHMode.L0;
7689
7148
  }
7690
7149
  /**
7691
7150
  * 获取渲染器能力
7692
7151
  */
7693
7152
  getCapabilities() {
7694
7153
  return {
7695
- maxSHMode: SHMode$1.L0,
7154
+ maxSHMode: SHMode.L0,
7696
7155
  supportsRawData: false,
7697
7156
  isMobileOptimized: true,
7698
7157
  maxSplatCount: 0
@@ -7988,11 +7447,189 @@ class SceneManager {
7988
7447
  setMeshRangeColor(startIndex, count, r, g, b, a = 1) {
7989
7448
  return this.meshRenderer.setMeshRangeColor(startIndex, count, r, g, b, a);
7990
7449
  }
7991
- /**
7992
- * 销毁场景管理器
7993
- */
7994
- destroy() {
7995
- this.clearSplats();
7450
+ /**
7451
+ * 销毁场景管理器
7452
+ */
7453
+ destroy() {
7454
+ this.clearSplats();
7455
+ }
7456
+ }
7457
+ class SplatTransformProxy {
7458
+ constructor(renderer, center) {
7459
+ __publicField(this, "position");
7460
+ __publicField(this, "rotation");
7461
+ __publicField(this, "scale");
7462
+ __publicField(this, "renderer");
7463
+ __publicField(this, "center");
7464
+ this.renderer = renderer;
7465
+ this.center = [...center];
7466
+ renderer.setPivot(center[0], center[1], center[2]);
7467
+ const pos = renderer.getPosition();
7468
+ const rot = renderer.getRotation();
7469
+ const scl = renderer.getScale();
7470
+ this.position = [
7471
+ pos[0] + center[0],
7472
+ pos[1] + center[1],
7473
+ pos[2] + center[2]
7474
+ ];
7475
+ this.rotation = [...rot];
7476
+ this.scale = [...scl];
7477
+ }
7478
+ setPosition(x, y, z) {
7479
+ this.position = [x, y, z];
7480
+ this.renderer.setPosition(
7481
+ x - this.center[0],
7482
+ y - this.center[1],
7483
+ z - this.center[2]
7484
+ );
7485
+ }
7486
+ setRotation(x, y, z) {
7487
+ this.rotation = [x, y, z];
7488
+ this.renderer.setRotation(x, y, z);
7489
+ }
7490
+ setScale(x, y, z) {
7491
+ this.scale = [x, y, z];
7492
+ this.renderer.setScale(x, y, z);
7493
+ }
7494
+ }
7495
+ class MeshGroupProxy {
7496
+ constructor(meshes) {
7497
+ __publicField(this, "position");
7498
+ __publicField(this, "rotation");
7499
+ __publicField(this, "scale");
7500
+ __publicField(this, "meshes");
7501
+ this.meshes = meshes;
7502
+ if (meshes.length > 0) {
7503
+ const firstMesh = meshes[0];
7504
+ this.position = [
7505
+ firstMesh.position[0],
7506
+ firstMesh.position[1],
7507
+ firstMesh.position[2]
7508
+ ];
7509
+ this.rotation = [
7510
+ firstMesh.rotation[0],
7511
+ firstMesh.rotation[1],
7512
+ firstMesh.rotation[2]
7513
+ ];
7514
+ this.scale = [
7515
+ firstMesh.scale[0],
7516
+ firstMesh.scale[1],
7517
+ firstMesh.scale[2]
7518
+ ];
7519
+ } else {
7520
+ this.position = [0, 0, 0];
7521
+ this.rotation = [0, 0, 0];
7522
+ this.scale = [1, 1, 1];
7523
+ }
7524
+ }
7525
+ setPosition(x, y, z) {
7526
+ this.position = [x, y, z];
7527
+ for (const mesh of this.meshes) {
7528
+ mesh.setPosition(x, y, z);
7529
+ }
7530
+ }
7531
+ setRotation(x, y, z) {
7532
+ this.rotation = [x, y, z];
7533
+ for (const mesh of this.meshes) {
7534
+ mesh.setRotation(x, y, z);
7535
+ }
7536
+ }
7537
+ setScale(x, y, z) {
7538
+ this.scale = [x, y, z];
7539
+ for (const mesh of this.meshes) {
7540
+ mesh.setScale(x, y, z);
7541
+ }
7542
+ }
7543
+ /**
7544
+ * 获取组合包围盒
7545
+ */
7546
+ getBoundingBox() {
7547
+ if (this.meshes.length === 0) return null;
7548
+ let combinedMin = null;
7549
+ let combinedMax = null;
7550
+ for (const mesh of this.meshes) {
7551
+ const bbox = mesh.getWorldBoundingBox();
7552
+ if (!bbox) continue;
7553
+ if (combinedMin === null || combinedMax === null) {
7554
+ combinedMin = [...bbox.min];
7555
+ combinedMax = [...bbox.max];
7556
+ } else {
7557
+ combinedMin[0] = Math.min(combinedMin[0], bbox.min[0]);
7558
+ combinedMin[1] = Math.min(combinedMin[1], bbox.min[1]);
7559
+ combinedMin[2] = Math.min(combinedMin[2], bbox.min[2]);
7560
+ combinedMax[0] = Math.max(combinedMax[0], bbox.max[0]);
7561
+ combinedMax[1] = Math.max(combinedMax[1], bbox.max[1]);
7562
+ combinedMax[2] = Math.max(combinedMax[2], bbox.max[2]);
7563
+ }
7564
+ }
7565
+ if (combinedMin === null || combinedMax === null) return null;
7566
+ return { min: combinedMin, max: combinedMax };
7567
+ }
7568
+ }
7569
+ class SplatBoundingBoxProvider {
7570
+ constructor(renderer) {
7571
+ __publicField(this, "renderer");
7572
+ this.renderer = renderer;
7573
+ }
7574
+ getBoundingBox() {
7575
+ const bbox = this.renderer.getBoundingBox();
7576
+ if (!bbox) return null;
7577
+ const position = this.renderer.getPosition();
7578
+ const rotation = this.renderer.getRotation();
7579
+ const scale = this.renderer.getScale();
7580
+ const pivot = this.renderer.getPivot();
7581
+ const corners = [
7582
+ [bbox.min[0], bbox.min[1], bbox.min[2]],
7583
+ [bbox.max[0], bbox.min[1], bbox.min[2]],
7584
+ [bbox.min[0], bbox.max[1], bbox.min[2]],
7585
+ [bbox.max[0], bbox.max[1], bbox.min[2]],
7586
+ [bbox.min[0], bbox.min[1], bbox.max[2]],
7587
+ [bbox.max[0], bbox.min[1], bbox.max[2]],
7588
+ [bbox.min[0], bbox.max[1], bbox.max[2]],
7589
+ [bbox.max[0], bbox.max[1], bbox.max[2]]
7590
+ ];
7591
+ const [sx, sy, sz] = scale;
7592
+ const [rx, ry, rz] = rotation;
7593
+ const [tx, ty, tz] = position;
7594
+ const [px, py, pz] = pivot;
7595
+ const cx = Math.cos(rx), sx1 = Math.sin(rx);
7596
+ const cy = Math.cos(ry), sy1 = Math.sin(ry);
7597
+ const cz = Math.cos(rz), sz1 = Math.sin(rz);
7598
+ const r00 = cy * cz;
7599
+ const r01 = sx1 * sy1 * cz - cx * sz1;
7600
+ const r02 = cx * sy1 * cz + sx1 * sz1;
7601
+ const r10 = cy * sz1;
7602
+ const r11 = sx1 * sy1 * sz1 + cx * cz;
7603
+ const r12 = cx * sy1 * sz1 - sx1 * cz;
7604
+ const r20 = -sy1;
7605
+ const r21 = sx1 * cy;
7606
+ const r22 = cx * cy;
7607
+ const rs00 = r00 * sx, rs01 = r01 * sy, rs02 = r02 * sz;
7608
+ const rs10 = r10 * sx, rs11 = r11 * sy, rs12 = r12 * sz;
7609
+ const rs20 = r20 * sx, rs21 = r21 * sy, rs22 = r22 * sz;
7610
+ const dpx = px - (rs00 * px + rs01 * py + rs02 * pz);
7611
+ const dpy = py - (rs10 * px + rs11 * py + rs12 * pz);
7612
+ const dpz = pz - (rs20 * px + rs21 * py + rs22 * pz);
7613
+ const finalTx = tx + dpx;
7614
+ const finalTy = ty + dpy;
7615
+ const finalTz = tz + dpz;
7616
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
7617
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
7618
+ for (const [x, y, z] of corners) {
7619
+ const wx = rs00 * x + rs01 * y + rs02 * z + finalTx;
7620
+ const wy = rs10 * x + rs11 * y + rs12 * z + finalTy;
7621
+ const wz = rs20 * x + rs21 * y + rs22 * z + finalTz;
7622
+ minX = Math.min(minX, wx);
7623
+ minY = Math.min(minY, wy);
7624
+ minZ = Math.min(minZ, wz);
7625
+ maxX = Math.max(maxX, wx);
7626
+ maxY = Math.max(maxY, wy);
7627
+ maxZ = Math.max(maxZ, wz);
7628
+ }
7629
+ return {
7630
+ min: [minX, minY, minZ],
7631
+ max: [maxX, maxY, maxZ]
7632
+ };
7996
7633
  }
7997
7634
  }
7998
7635
  class Vec3 {
@@ -11177,181 +10814,6 @@ class TransformGizmoV2 {
11177
10814
  this.bindGroupLayout = null;
11178
10815
  }
11179
10816
  }
11180
- class SplatTransformProxy {
11181
- constructor(renderer, center) {
11182
- __publicField(this, "position");
11183
- __publicField(this, "rotation");
11184
- __publicField(this, "scale");
11185
- __publicField(this, "renderer");
11186
- __publicField(this, "center");
11187
- this.renderer = renderer;
11188
- this.center = [...center];
11189
- renderer.setPivot(center[0], center[1], center[2]);
11190
- const pos = renderer.getPosition();
11191
- const rot = renderer.getRotation();
11192
- const scl = renderer.getScale();
11193
- this.position = [
11194
- pos[0] + center[0],
11195
- pos[1] + center[1],
11196
- pos[2] + center[2]
11197
- ];
11198
- this.rotation = [...rot];
11199
- this.scale = [...scl];
11200
- }
11201
- setPosition(x, y, z) {
11202
- this.position = [x, y, z];
11203
- this.renderer.setPosition(
11204
- x - this.center[0],
11205
- y - this.center[1],
11206
- z - this.center[2]
11207
- );
11208
- }
11209
- setRotation(x, y, z) {
11210
- this.rotation = [x, y, z];
11211
- this.renderer.setRotation(x, y, z);
11212
- }
11213
- setScale(x, y, z) {
11214
- this.scale = [x, y, z];
11215
- this.renderer.setScale(x, y, z);
11216
- }
11217
- }
11218
- class MeshGroupProxy {
11219
- constructor(meshes) {
11220
- __publicField(this, "position");
11221
- __publicField(this, "rotation");
11222
- __publicField(this, "scale");
11223
- __publicField(this, "meshes");
11224
- this.meshes = meshes;
11225
- if (meshes.length > 0) {
11226
- const firstMesh = meshes[0];
11227
- this.position = [
11228
- firstMesh.position[0],
11229
- firstMesh.position[1],
11230
- firstMesh.position[2]
11231
- ];
11232
- this.rotation = [
11233
- firstMesh.rotation[0],
11234
- firstMesh.rotation[1],
11235
- firstMesh.rotation[2]
11236
- ];
11237
- this.scale = [
11238
- firstMesh.scale[0],
11239
- firstMesh.scale[1],
11240
- firstMesh.scale[2]
11241
- ];
11242
- } else {
11243
- this.position = [0, 0, 0];
11244
- this.rotation = [0, 0, 0];
11245
- this.scale = [1, 1, 1];
11246
- }
11247
- }
11248
- setPosition(x, y, z) {
11249
- this.position = [x, y, z];
11250
- for (const mesh of this.meshes) {
11251
- mesh.setPosition(x, y, z);
11252
- }
11253
- }
11254
- setRotation(x, y, z) {
11255
- this.rotation = [x, y, z];
11256
- for (const mesh of this.meshes) {
11257
- mesh.setRotation(x, y, z);
11258
- }
11259
- }
11260
- setScale(x, y, z) {
11261
- this.scale = [x, y, z];
11262
- for (const mesh of this.meshes) {
11263
- mesh.setScale(x, y, z);
11264
- }
11265
- }
11266
- getBoundingBox() {
11267
- if (this.meshes.length === 0) return null;
11268
- let combinedMin = null;
11269
- let combinedMax = null;
11270
- for (const mesh of this.meshes) {
11271
- const bbox = mesh.getWorldBoundingBox();
11272
- if (!bbox) continue;
11273
- if (combinedMin === null || combinedMax === null) {
11274
- combinedMin = [...bbox.min];
11275
- combinedMax = [...bbox.max];
11276
- } else {
11277
- combinedMin[0] = Math.min(combinedMin[0], bbox.min[0]);
11278
- combinedMin[1] = Math.min(combinedMin[1], bbox.min[1]);
11279
- combinedMin[2] = Math.min(combinedMin[2], bbox.min[2]);
11280
- combinedMax[0] = Math.max(combinedMax[0], bbox.max[0]);
11281
- combinedMax[1] = Math.max(combinedMax[1], bbox.max[1]);
11282
- combinedMax[2] = Math.max(combinedMax[2], bbox.max[2]);
11283
- }
11284
- }
11285
- if (combinedMin === null || combinedMax === null) return null;
11286
- return { min: combinedMin, max: combinedMax };
11287
- }
11288
- }
11289
- class SplatBoundingBoxProvider {
11290
- constructor(renderer) {
11291
- __publicField(this, "renderer");
11292
- this.renderer = renderer;
11293
- }
11294
- getBoundingBox() {
11295
- const bbox = this.renderer.getBoundingBox();
11296
- if (!bbox) return null;
11297
- const position = this.renderer.getPosition();
11298
- const rotation = this.renderer.getRotation();
11299
- const scale = this.renderer.getScale();
11300
- const pivot = this.renderer.getPivot();
11301
- const corners = [
11302
- [bbox.min[0], bbox.min[1], bbox.min[2]],
11303
- [bbox.max[0], bbox.min[1], bbox.min[2]],
11304
- [bbox.min[0], bbox.max[1], bbox.min[2]],
11305
- [bbox.max[0], bbox.max[1], bbox.min[2]],
11306
- [bbox.min[0], bbox.min[1], bbox.max[2]],
11307
- [bbox.max[0], bbox.min[1], bbox.max[2]],
11308
- [bbox.min[0], bbox.max[1], bbox.max[2]],
11309
- [bbox.max[0], bbox.max[1], bbox.max[2]]
11310
- ];
11311
- const [sx, sy, sz] = scale;
11312
- const [rx, ry, rz] = rotation;
11313
- const [tx, ty, tz] = position;
11314
- const [px, py, pz] = pivot;
11315
- const cx = Math.cos(rx), sx1 = Math.sin(rx);
11316
- const cy = Math.cos(ry), sy1 = Math.sin(ry);
11317
- const cz = Math.cos(rz), sz1 = Math.sin(rz);
11318
- const r00 = cy * cz;
11319
- const r01 = sx1 * sy1 * cz - cx * sz1;
11320
- const r02 = cx * sy1 * cz + sx1 * sz1;
11321
- const r10 = cy * sz1;
11322
- const r11 = sx1 * sy1 * sz1 + cx * cz;
11323
- const r12 = cx * sy1 * sz1 - sx1 * cz;
11324
- const r20 = -sy1;
11325
- const r21 = sx1 * cy;
11326
- const r22 = cx * cy;
11327
- const rs00 = r00 * sx, rs01 = r01 * sy, rs02 = r02 * sz;
11328
- const rs10 = r10 * sx, rs11 = r11 * sy, rs12 = r12 * sz;
11329
- const rs20 = r20 * sx, rs21 = r21 * sy, rs22 = r22 * sz;
11330
- const dpx = px - (rs00 * px + rs01 * py + rs02 * pz);
11331
- const dpy = py - (rs10 * px + rs11 * py + rs12 * pz);
11332
- const dpz = pz - (rs20 * px + rs21 * py + rs22 * pz);
11333
- const finalTx = tx + dpx;
11334
- const finalTy = ty + dpy;
11335
- const finalTz = tz + dpz;
11336
- let minX = Infinity, minY = Infinity, minZ = Infinity;
11337
- let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
11338
- for (const [x, y, z] of corners) {
11339
- const wx = rs00 * x + rs01 * y + rs02 * z + finalTx;
11340
- const wy = rs10 * x + rs11 * y + rs12 * z + finalTy;
11341
- const wz = rs20 * x + rs21 * y + rs22 * z + finalTz;
11342
- minX = Math.min(minX, wx);
11343
- minY = Math.min(minY, wy);
11344
- minZ = Math.min(minZ, wz);
11345
- maxX = Math.max(maxX, wx);
11346
- maxY = Math.max(maxY, wy);
11347
- maxZ = Math.max(maxZ, wz);
11348
- }
11349
- return {
11350
- min: [minX, minY, minZ],
11351
- max: [maxX, maxY, maxZ]
11352
- };
11353
- }
11354
- }
11355
10817
  class GizmoManager {
11356
10818
  constructor(renderer, camera, canvas, controls) {
11357
10819
  __publicField(this, "renderer");
@@ -11511,15 +10973,6 @@ class GizmoManager {
11511
10973
  this.boundingBoxRenderer.destroy();
11512
10974
  }
11513
10975
  }
11514
- function isMobileDevice() {
11515
- if (typeof navigator === "undefined") return false;
11516
- const ua = navigator.userAgent || navigator.vendor || window.opera || "";
11517
- const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(ua.toLowerCase());
11518
- const hasTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
11519
- const isSmallScreen = window.innerWidth <= 768;
11520
- const isIPadAsMac = navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
11521
- return isMobileUA || isIPadAsMac || hasTouch && isSmallScreen;
11522
- }
11523
10976
  class App {
11524
10977
  constructor(canvas) {
11525
10978
  __publicField(this, "canvas");
@@ -11637,7 +11090,6 @@ class App {
11637
11090
  } else {
11638
11091
  gsRenderer = new GSSplatRenderer(this.renderer, this.camera);
11639
11092
  this.useMobileRenderer = false;
11640
- const tier = gsRenderer.getPerformanceTier();
11641
11093
  const compactData = await this.parsePLYBuffer(buffer, {
11642
11094
  maxSplats: Infinity,
11643
11095
  loadSH: true,
@@ -11974,6 +11426,8 @@ export {
11974
11426
  App,
11975
11427
  BoundingBoxRenderer,
11976
11428
  Camera,
11429
+ DEFAULT_MATERIAL,
11430
+ DEFAULT_OBJ_MATERIAL,
11977
11431
  GLBLoader,
11978
11432
  SHMode as GSSHMode,
11979
11433
  GSSplatRenderer,
@@ -11992,21 +11446,33 @@ export {
11992
11446
  OBJLoader,
11993
11447
  OBJParser,
11994
11448
  OrbitControls,
11995
- PerformanceTier,
11996
11449
  Renderer,
11450
+ SHMode,
11997
11451
  SceneManager,
11998
11452
  SplatBoundingBoxProvider,
11999
11453
  SplatTransformProxy,
11454
+ TextureCache,
12000
11455
  TransformGizmoV2,
12001
11456
  ViewportGizmo,
12002
11457
  calculateTextureDimensions,
12003
11458
  compactDataToGPUBuffer,
12004
11459
  compressSplatsToTextures,
11460
+ computeBoundingBox$1 as computeBoundingBox,
11461
+ createBoundingBoxFromMinMax,
11462
+ createTextureFromImageBitmap,
12005
11463
  deserializeSplat,
12006
11464
  destroyCompressedTextures,
11465
+ getRecommendedDPR,
11466
+ isMobileDevice,
11467
+ isWebGPUSupported,
12007
11468
  loadPLY,
12008
11469
  loadPLYMobile,
12009
11470
  loadSplat,
12010
- parsePLYBuffer
11471
+ loadTextureFromBlob,
11472
+ loadTextureFromBuffer,
11473
+ loadTextureFromURL,
11474
+ mergeBoundingBoxes,
11475
+ parsePLYBuffer,
11476
+ transformBoundingBox
12011
11477
  };
12012
11478
  //# sourceMappingURL=3dgs-lib.js.map