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