@d5techs/3dgs-lib 1.4.84 → 1.4.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/3dgs-lib.js CHANGED
@@ -36,8 +36,7 @@ function isMobileDevice() {
36
36
  return isMobileUA || isIPadAsMac || hasTouch && isSmallScreen;
37
37
  }
38
38
  function getRecommendedDPR() {
39
- const isMobile = isMobileDevice();
40
- const maxDpr = isMobile ? 2 : 2;
39
+ const maxDpr = isMobileDevice() ? 2 : 2;
41
40
  return Math.min(window.devicePixelRatio || 1, maxDpr);
42
41
  }
43
42
  function isWebGPUSupported() {
@@ -142,6 +141,140 @@ function transformBoundingBox(bbox, modelMatrix) {
142
141
  [maxX, maxY, maxZ]
143
142
  );
144
143
  }
144
+ class DebugOverlay {
145
+ constructor(maxEntries = 50) {
146
+ __publicField(this, "container");
147
+ __publicField(this, "logList");
148
+ __publicField(this, "maxEntries");
149
+ this.maxEntries = maxEntries;
150
+ this.container = document.createElement("div");
151
+ Object.assign(this.container.style, {
152
+ position: "fixed",
153
+ top: "0",
154
+ left: "0",
155
+ right: "0",
156
+ maxHeight: "40vh",
157
+ overflowY: "auto",
158
+ background: "rgba(0,0,0,0.85)",
159
+ color: "#0f0",
160
+ fontFamily: "monospace",
161
+ fontSize: "11px",
162
+ lineHeight: "1.4",
163
+ padding: "6px 8px",
164
+ zIndex: "99999",
165
+ pointerEvents: "auto",
166
+ whiteSpace: "pre-wrap",
167
+ wordBreak: "break-all"
168
+ });
169
+ const header = document.createElement("div");
170
+ Object.assign(header.style, {
171
+ display: "flex",
172
+ justifyContent: "space-between",
173
+ marginBottom: "4px",
174
+ borderBottom: "1px solid #333",
175
+ paddingBottom: "4px"
176
+ });
177
+ header.innerHTML = '<span style="color:#ff0;font-weight:bold">GPU Debug</span>';
178
+ const closeBtn = document.createElement("span");
179
+ closeBtn.textContent = "[X]";
180
+ closeBtn.style.color = "#f66";
181
+ closeBtn.style.cursor = "pointer";
182
+ closeBtn.onclick = () => this.hide();
183
+ header.appendChild(closeBtn);
184
+ this.logList = document.createElement("div");
185
+ this.container.appendChild(header);
186
+ this.container.appendChild(this.logList);
187
+ document.body.appendChild(this.container);
188
+ }
189
+ log(msg, level = "info") {
190
+ const colors = { info: "#0f0", warn: "#ff0", error: "#f44" };
191
+ const prefix = { info: "I", warn: "W", error: "E" };
192
+ const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
193
+ const entry = document.createElement("div");
194
+ entry.style.color = colors[level];
195
+ entry.textContent = `[${ts}][${prefix[level]}] ${msg}`;
196
+ this.logList.appendChild(entry);
197
+ while (this.logList.childElementCount > this.maxEntries) {
198
+ this.logList.removeChild(this.logList.firstChild);
199
+ }
200
+ this.container.scrollTop = this.container.scrollHeight;
201
+ }
202
+ info(msg) {
203
+ this.log(msg, "info");
204
+ }
205
+ warn(msg) {
206
+ this.log(msg, "warn");
207
+ }
208
+ error(msg) {
209
+ this.log(msg, "error");
210
+ }
211
+ hide() {
212
+ this.container.style.display = "none";
213
+ }
214
+ show() {
215
+ this.container.style.display = "block";
216
+ }
217
+ /**
218
+ * 绑定 WebGPU device 的全局错误捕获
219
+ */
220
+ attachDevice(device) {
221
+ device.onuncapturederror = (event) => {
222
+ this.error(`GPU Uncaptured: ${event.error.message}`);
223
+ };
224
+ this.info(
225
+ `Device attached. maxBuf=${(device.limits.maxBufferSize / 1048576).toFixed(0)}MB, maxStorage=${(device.limits.maxStorageBufferBindingSize / 1048576).toFixed(0)}MB`
226
+ );
227
+ }
228
+ /**
229
+ * 检查 shader 编译结果
230
+ */
231
+ async checkShader(module, label) {
232
+ try {
233
+ const info = await module.getCompilationInfo();
234
+ let hasError = false;
235
+ for (const msg of info.messages) {
236
+ const text = `Shader[${label}] ${msg.type}: L${msg.lineNum}:${msg.linePos} ${msg.message}`;
237
+ if (msg.type === "error") {
238
+ this.error(text);
239
+ hasError = true;
240
+ } else if (msg.type === "warning") {
241
+ this.warn(text);
242
+ }
243
+ }
244
+ if (!hasError) {
245
+ this.info(`Shader[${label}] compiled OK`);
246
+ }
247
+ return !hasError;
248
+ } catch (e) {
249
+ this.error(`Shader[${label}] getCompilationInfo failed: ${e}`);
250
+ return false;
251
+ }
252
+ }
253
+ /**
254
+ * 包裹一个异步操作,捕获 validation/oom 错误
255
+ */
256
+ async wrapGPU(device, label, fn) {
257
+ device.pushErrorScope("validation");
258
+ device.pushErrorScope("out-of-memory");
259
+ const result = fn();
260
+ const oomErr = await device.popErrorScope();
261
+ const valErr = await device.popErrorScope();
262
+ if (oomErr) this.error(`[${label}] OOM: ${oomErr.message}`);
263
+ if (valErr) this.error(`[${label}] Validation: ${valErr.message}`);
264
+ if (!oomErr && !valErr) this.info(`[${label}] OK`);
265
+ return result;
266
+ }
267
+ }
268
+ let _instance = null;
269
+ function getDebugOverlay() {
270
+ return _instance;
271
+ }
272
+ function createDebugOverlay() {
273
+ if (!_instance) {
274
+ _instance = new DebugOverlay();
275
+ }
276
+ return _instance;
277
+ }
145
278
  async function loadTextureFromURL(device, url) {
146
279
  try {
147
280
  const response = await fetch(url);
@@ -361,7 +494,16 @@ class Renderer {
361
494
  }
362
495
  });
363
496
  this._device.lost.then((info) => {
497
+ const overlay2 = getDebugOverlay();
498
+ overlay2 == null ? void 0 : overlay2.error(`GPU device lost: ${info.reason} - ${info.message}`);
364
499
  });
500
+ const overlay = getDebugOverlay();
501
+ if (overlay) {
502
+ overlay.attachDevice(this._device);
503
+ overlay.info(
504
+ `GPU: vendor=${this._gpuVendor || "unknown"}, arch=${this._gpuArchitecture || "unknown"}, apple=${this._isAppleGPU}, format=${navigator.gpu.getPreferredCanvasFormat()}`
505
+ );
506
+ }
365
507
  this._context = this.canvas.getContext("webgpu");
366
508
  if (!this._context) {
367
509
  throw new Error("无法获取 WebGPU 上下文");
@@ -685,6 +827,9 @@ const _OrbitControls = class _OrbitControls {
685
827
  this.setupEventListeners();
686
828
  this.applySpherical();
687
829
  }
830
+ get isInteracting() {
831
+ return this.isDragging;
832
+ }
688
833
  setupEventListeners() {
689
834
  this.canvas.addEventListener("mousedown", this.boundOnMouseDown);
690
835
  this.canvas.addEventListener("mousemove", this.boundOnMouseMove);
@@ -5229,9 +5374,10 @@ function compactDataToGPUBuffer(data, includeFullSH = false) {
5229
5374
  }
5230
5375
  return buffer;
5231
5376
  } else {
5232
- const buffer = new Float32Array(count * 64);
5377
+ const COMPACT_FLOATS = 16;
5378
+ const buffer = new Float32Array(count * COMPACT_FLOATS);
5233
5379
  for (let i = 0; i < count; i++) {
5234
- const offset = i * 64;
5380
+ const offset = i * COMPACT_FLOATS;
5235
5381
  buffer[offset + 0] = data.positions[i * 3 + 0];
5236
5382
  buffer[offset + 1] = data.positions[i * 3 + 1];
5237
5383
  buffer[offset + 2] = data.positions[i * 3 + 2];
@@ -5252,9 +5398,56 @@ function compactDataToGPUBuffer(data, includeFullSH = false) {
5252
5398
  return buffer;
5253
5399
  }
5254
5400
  }
5401
+ const _f16Scratch = new Float32Array(1);
5402
+ const _f16ScratchU32 = new Uint32Array(_f16Scratch.buffer);
5403
+ function f32ToF16Bits(val) {
5404
+ _f16Scratch[0] = val;
5405
+ const bits2 = _f16ScratchU32[0];
5406
+ const sign = bits2 >>> 31 & 1;
5407
+ let exp = bits2 >>> 23 & 255;
5408
+ let frac = bits2 & 8388607;
5409
+ if (exp === 255) {
5410
+ return sign << 15 | 31744 | (frac ? 512 : 0);
5411
+ }
5412
+ exp = exp - 127 + 15;
5413
+ if (exp >= 31) {
5414
+ return sign << 15 | 31744;
5415
+ }
5416
+ if (exp <= 0) {
5417
+ if (exp < -10) return sign << 15;
5418
+ frac = (frac | 8388608) >> 1 - exp;
5419
+ return sign << 15 | frac >> 13;
5420
+ }
5421
+ return sign << 15 | exp << 10 | frac >> 13;
5422
+ }
5423
+ function compactDataToGPUBufferHalf(data) {
5424
+ const count = data.count;
5425
+ const U32_PER_SPLAT = 8;
5426
+ const buffer = new Uint32Array(count * U32_PER_SPLAT);
5427
+ const pos = data.positions;
5428
+ const scl = data.scales;
5429
+ const rot = data.rotations;
5430
+ const col = data.colors;
5431
+ const opa = data.opacities;
5432
+ for (let i = 0; i < count; i++) {
5433
+ const o = i * U32_PER_SPLAT;
5434
+ const i3 = i * 3;
5435
+ const i4 = i * 4;
5436
+ buffer[o] = f32ToF16Bits(pos[i3 + 1]) << 16 | f32ToF16Bits(pos[i3]);
5437
+ buffer[o + 1] = f32ToF16Bits(pos[i3 + 2]);
5438
+ buffer[o + 2] = f32ToF16Bits(scl[i3 + 1]) << 16 | f32ToF16Bits(scl[i3]);
5439
+ buffer[o + 3] = f32ToF16Bits(opa[i]) << 16 | f32ToF16Bits(scl[i3 + 2]);
5440
+ buffer[o + 4] = f32ToF16Bits(rot[i4 + 1]) << 16 | f32ToF16Bits(rot[i4]);
5441
+ buffer[o + 5] = f32ToF16Bits(rot[i4 + 3]) << 16 | f32ToF16Bits(rot[i4 + 2]);
5442
+ buffer[o + 6] = f32ToF16Bits(col[i3 + 1]) << 16 | f32ToF16Bits(col[i3]);
5443
+ buffer[o + 7] = f32ToF16Bits(col[i3 + 2]);
5444
+ }
5445
+ return buffer;
5446
+ }
5255
5447
  const PLYLoaderMobile = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
5256
5448
  __proto__: null,
5257
5449
  compactDataToGPUBuffer,
5450
+ compactDataToGPUBufferHalf,
5258
5451
  loadPLYMobile,
5259
5452
  parsePLYBuffer,
5260
5453
  transformSHCoeffsYZSwap
@@ -7314,16 +7507,38 @@ const RADIX_BITS = 8;
7314
7507
  const RADIX_SIZE = 256;
7315
7508
  const ELEMENTS_PER_THREAD = 4;
7316
7509
  const BLOCK_SIZE = WORKGROUP_SIZE$1 * ELEMENTS_PER_THREAD;
7317
- function generateCullingShaderCode$1() {
7318
- return (
7319
- /* wgsl */
7320
- `
7321
- /**
7322
- * Project & Cull Shader
7323
- * 基于 rfs-gsplat-render 实现
7324
- */
7325
-
7326
- struct Splat {
7510
+ function generateCullingShaderCode$1(compact = false, half = false, sortBits = 32) {
7511
+ let splatBinding;
7512
+ let splatAccessCode;
7513
+ if (half) {
7514
+ splatBinding = `@group(0) @binding(0) var<storage, read> splatDataU32: array<u32>;`;
7515
+ splatAccessCode = `
7516
+ const HALF_U32S: u32 = 8u;
7517
+ fn getSplatMean(idx: u32) -> vec3<f32> {
7518
+ let b = idx * HALF_U32S;
7519
+ let xy = unpack2x16float(splatDataU32[b]);
7520
+ let zp = unpack2x16float(splatDataU32[b + 1u]);
7521
+ return vec3<f32>(xy.x, xy.y, zp.x);
7522
+ }
7523
+ fn getSplatScale(idx: u32) -> vec3<f32> {
7524
+ let b = idx * HALF_U32S;
7525
+ let xy = unpack2x16float(splatDataU32[b + 2u]);
7526
+ return vec3<f32>(xy.x, xy.y, unpack2x16float(splatDataU32[b + 3u]).x);
7527
+ }
7528
+ fn getSplatOpacity(idx: u32) -> f32 {
7529
+ let b = idx * HALF_U32S;
7530
+ return unpack2x16float(splatDataU32[b + 3u]).y;
7531
+ }`;
7532
+ } else {
7533
+ const splatStruct = compact ? `struct Splat {
7534
+ mean: vec3<f32>,
7535
+ _pad0: f32,
7536
+ scale: vec3<f32>,
7537
+ _pad1: f32,
7538
+ rotation: vec4<f32>,
7539
+ colorDC: vec3<f32>,
7540
+ opacity: f32,
7541
+ }` : `struct Splat {
7327
7542
  mean: vec3<f32>,
7328
7543
  _pad0: f32,
7329
7544
  scale: vec3<f32>,
@@ -7335,7 +7550,24 @@ struct Splat {
7335
7550
  sh2: array<f32, 15>,
7336
7551
  sh3: array<f32, 21>,
7337
7552
  _pad2: array<f32, 3>,
7338
- }
7553
+ }`;
7554
+ splatBinding = `${splatStruct}
7555
+ @group(0) @binding(0) var<storage, read> splats: array<Splat>;`;
7556
+ splatAccessCode = `
7557
+ fn getSplatMean(idx: u32) -> vec3<f32> { return splats[idx].mean; }
7558
+ fn getSplatScale(idx: u32) -> vec3<f32> { return splats[idx].scale; }
7559
+ fn getSplatOpacity(idx: u32) -> f32 { return splats[idx].opacity; }`;
7560
+ }
7561
+ return (
7562
+ /* wgsl */
7563
+ `
7564
+ /**
7565
+ * Project & Cull Shader
7566
+ * 基于 rfs-gsplat-render 实现
7567
+ */
7568
+
7569
+ ${splatBinding}
7570
+ ${splatAccessCode}
7339
7571
 
7340
7572
  struct CameraUniforms {
7341
7573
  view: mat4x4<f32>,
@@ -7355,10 +7587,9 @@ struct CullingParams {
7355
7587
  pixelThreshold: f32,
7356
7588
  maxVisibleCount: u32,
7357
7589
  depthRangeLimit: f32,
7358
- _pad_cull: f32,
7590
+ lodSkipRate: f32,
7359
7591
  }
7360
7592
 
7361
- @group(0) @binding(0) var<storage, read> splats: array<Splat>;
7362
7593
  @group(0) @binding(1) var<uniform> camera: CameraUniforms;
7363
7594
  @group(0) @binding(2) var<uniform> params: CullingParams;
7364
7595
  @group(0) @binding(3) var<storage, read_write> depthKeys: array<u32>;
@@ -7376,16 +7607,12 @@ fn getModelMaxScale(model: mat4x4<f32>) -> f32 {
7376
7607
  return max(max(sx, sy), sz);
7377
7608
  }
7378
7609
 
7379
- // IEEE 754 位操作编码浮点数为可排序的 u32
7380
- // 参考 rfs-gsplat-render 的 encode_min_max_fp32 实现
7381
7610
  fn encodeDepthKey(val: f32) -> u32 {
7382
7611
  var bits = bitcast<u32>(val);
7383
7612
  bits ^= bitcast<u32>(bitcast<i32>(bits) >> 31) | 0x80000000u;
7384
- return bits;
7613
+ return bits >> ${sortBits === 16 ? "16u" : "0u"};
7385
7614
  }
7386
7615
 
7387
- // 视锥剔除检查
7388
- // 基于 rfs-gsplat-render 的 is_in_frustum 实现
7389
7616
  fn isInFrustum(clipPos: vec4<f32>, frustumDilation: f32) -> bool {
7390
7617
  let clip = (1.0 + frustumDilation) * clipPos.w;
7391
7618
 
@@ -7404,43 +7631,47 @@ fn isInFrustum(clipPos: vec4<f32>, frustumDilation: f32) -> bool {
7404
7631
  fn projectAndCull(@builtin(global_invocation_id) gid: vec3<u32>) {
7405
7632
  let i = gid.x;
7406
7633
  if i >= params.splatCount { return; }
7634
+
7635
+ // LOD 抽稀放最前面:跳过的 splat 不做任何数据加载和矩阵运算
7636
+ // 哈希仅依赖 splat index → 被跳过的集合永远不变 → 零闪烁
7637
+ if params.lodSkipRate > 0.0 {
7638
+ let hash = ((i * 2654435761u) >> 16u) & 0xFFFFu;
7639
+ if f32(hash) < params.lodSkipRate * 65535.0 {
7640
+ return;
7641
+ }
7642
+ }
7407
7643
 
7408
- let splat = splats[i];
7644
+ let splatMean = getSplatMean(i);
7645
+ let splatScale = getSplatScale(i);
7646
+ let splatOpacity = getSplatOpacity(i);
7409
7647
 
7410
- // 透明度剔除
7411
- if splat.opacity < 0.004 { return; }
7648
+ if splatOpacity < 0.004 { return; }
7412
7649
 
7413
- // 变换: Local -> World -> View -> Clip
7414
- let worldPos = camera.model * vec4<f32>(splat.mean, 1.0);
7650
+ let worldPos = camera.model * vec4<f32>(splatMean, 1.0);
7415
7651
  let viewPos = camera.view * worldPos;
7416
7652
  let clipPos = camera.proj * viewPos;
7417
7653
 
7418
- // 视锥剔除
7419
7654
  if !isInFrustum(clipPos, params.frustumDilation) { return; }
7420
7655
 
7421
- // 亚像素剔除:Gaussian 可见范围小于阈值的 splat 跳过渲染和排序
7422
- // scale σ(标准差),Gaussian 可见范围约 3σ(覆盖 99.7%)
7423
- if params.pixelThreshold > 0.0 {
7424
- let splatSigma = maxScale(splat.scale) * getModelMaxScale(camera.model);
7425
- let focalY = abs(camera.proj[1][1]) * params.screenHeight * 0.5;
7426
- let projectedExtent = splatSigma * 3.0 * focalY / max(abs(viewPos.z), 0.001);
7427
- if projectedExtent < params.pixelThreshold { return; }
7428
- }
7656
+ let splatSigma = maxScale(splatScale) * getModelMaxScale(camera.model);
7657
+ let focalY = abs(camera.proj[1][1]) * params.screenHeight * 0.5;
7658
+ let projectedExtent = splatSigma * 3.0 * focalY / max(abs(viewPos.z), 0.001);
7659
+
7660
+ // 剔除投影尺寸过大的 splat(远离模型时去除周围遮挡物)
7661
+ if projectedExtent > params.screenHeight * 0.5 { return; }
7662
+
7663
+ // 剔除投影尺寸过小的 splat
7664
+ if params.pixelThreshold > 0.0 && projectedExtent < params.pixelThreshold { return; }
7429
7665
 
7430
- // 深度范围限制:只渲染距相机一定深度范围内的 splat
7431
7666
  if params.depthRangeLimit > 0.0 {
7432
7667
  if abs(viewPos.z) > params.depthRangeLimit { return; }
7433
7668
  }
7434
7669
 
7435
- // 深度编码 (viewPos.z 是负数)
7436
7670
  let depth = viewPos.z;
7437
7671
  let sortableDepth = encodeDepthKey(depth);
7438
7672
 
7439
- // 原子增加可见计数并获取索引
7440
- // indirectBuffer[1] 是 instance_count
7441
7673
  let visibleIdx = atomicAdd(&indirectBuffer[1], 1u);
7442
7674
 
7443
- // 写入可见点列表
7444
7675
  depthKeys[visibleIdx] = sortableDepth;
7445
7676
  visibleIndices[visibleIdx] = i;
7446
7677
  }
@@ -7755,7 +7986,7 @@ fn downsweep(
7755
7986
  );
7756
7987
  }
7757
7988
  class GSSplatSorter {
7758
- constructor(device, splatCount, splatBuffer, cameraBuffer, _options = {}) {
7989
+ constructor(device, splatCount, splatBuffer, cameraBuffer, _options = {}, compact = false, half = false, sortBits = 32) {
7759
7990
  __publicField(this, "device");
7760
7991
  __publicField(this, "splatCount");
7761
7992
  // Culling Buffers
@@ -7785,11 +8016,12 @@ class GSSplatSorter {
7785
8016
  __publicField(this, "upsweepBindGroupLayout");
7786
8017
  __publicField(this, "spineBindGroupLayout");
7787
8018
  __publicField(this, "downsweepBindGroupLayout");
7788
- // Bind groups for each pass (4 passes)
8019
+ // Bind groups for each pass
7789
8020
  __publicField(this, "upsweepBindGroups", []);
7790
8021
  __publicField(this, "spineBindGroups", []);
7791
8022
  __publicField(this, "downsweepBindGroups", []);
7792
8023
  __publicField(this, "numPartitions");
8024
+ __publicField(this, "numSortPasses");
7793
8025
  // 屏幕信息和剔除选项
7794
8026
  __publicField(this, "screenWidth", 1920);
7795
8027
  __publicField(this, "screenHeight", 1080);
@@ -7802,14 +8034,19 @@ class GSSplatSorter {
7802
8034
  this.device = device;
7803
8035
  this.splatCount = splatCount;
7804
8036
  this.numPartitions = Math.ceil(splatCount / BLOCK_SIZE);
8037
+ this.numSortPasses = sortBits / RADIX_BITS;
8038
+ const dbg = getDebugOverlay();
8039
+ dbg == null ? void 0 : dbg.info(`Sorter init: ${splatCount} splats, compact=${compact}, half=${half}, sortBits=${sortBits}`);
7805
8040
  const cullingModule = device.createShaderModule({
7806
- code: generateCullingShaderCode$1(),
8041
+ code: generateCullingShaderCode$1(compact, half, sortBits),
7807
8042
  label: "culling-shader"
7808
8043
  });
8044
+ dbg == null ? void 0 : dbg.checkShader(cullingModule, "culling");
7809
8045
  const radixSortModule = device.createShaderModule({
7810
8046
  code: generateRadixSortShaderCode(),
7811
8047
  label: "radix-sort-shader"
7812
8048
  });
8049
+ dbg == null ? void 0 : dbg.checkShader(radixSortModule, "radix-sort");
7813
8050
  this.cullingParamsBuffer = device.createBuffer({
7814
8051
  size: 48,
7815
8052
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
@@ -7827,12 +8064,11 @@ class GSSplatSorter {
7827
8064
  });
7828
8065
  this.indirectBuffer = device.createBuffer({
7829
8066
  size: 16,
7830
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST,
8067
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
7831
8068
  label: "indirect-buffer"
7832
8069
  });
7833
8070
  this.globalHistogramBuffer = device.createBuffer({
7834
- size: RADIX_SIZE * 4 * 4,
7835
- // 4 passes * 256 bins * 4 bytes
8071
+ size: RADIX_SIZE * this.numSortPasses * 4,
7836
8072
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
7837
8073
  label: "global-histogram"
7838
8074
  });
@@ -7851,7 +8087,7 @@ class GSSplatSorter {
7851
8087
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
7852
8088
  label: "values-temp"
7853
8089
  });
7854
- for (let i = 0; i < 4; i++) {
8090
+ for (let i = 0; i < this.numSortPasses; i++) {
7855
8091
  const paramsBuffer = device.createBuffer({
7856
8092
  size: 16,
7857
8093
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
@@ -7963,16 +8199,12 @@ class GSSplatSorter {
7963
8199
  }
7964
8200
  /**
7965
8201
  * 创建 Radix Sort 的 bind groups
7966
- * 4 个 pass,使用 ping-pong buffers
7967
- *
7968
- * Ping-pong 模式:
7969
- * - Pass 0: depthKeys/visibleIndices -> keysTempBuffer/valuesTempBuffer
7970
- * - Pass 1: keysTempBuffer/valuesTempBuffer -> depthKeys/visibleIndices
7971
- * - Pass 2: depthKeys/visibleIndices -> keysTempBuffer/valuesTempBuffer
7972
- * - Pass 3: keysTempBuffer/valuesTempBuffer -> (depthKeys)/sortedIndicesBuffer
8202
+ * numSortPasses 个 pass,使用 ping-pong buffers
8203
+ * 最后一个 pass 的 values 输出到 sortedIndicesBuffer
7973
8204
  */
7974
8205
  createRadixSortBindGroups() {
7975
- for (let passIdx = 0; passIdx < 4; passIdx++) {
8206
+ const lastPassIdx = this.numSortPasses - 1;
8207
+ for (let passIdx = 0; passIdx < this.numSortPasses; passIdx++) {
7976
8208
  const isEvenPass = passIdx % 2 === 0;
7977
8209
  const keysIn = isEvenPass ? this.depthKeysBuffer : this.keysTempBuffer;
7978
8210
  const valuesIn = isEvenPass ? this.visibleIndicesBuffer : this.valuesTempBuffer;
@@ -7980,10 +8212,10 @@ class GSSplatSorter {
7980
8212
  let valuesOut;
7981
8213
  if (isEvenPass) {
7982
8214
  keysOut = this.keysTempBuffer;
7983
- valuesOut = this.valuesTempBuffer;
8215
+ valuesOut = passIdx === lastPassIdx ? this.sortedIndicesBuffer : this.valuesTempBuffer;
7984
8216
  } else {
7985
8217
  keysOut = this.depthKeysBuffer;
7986
- valuesOut = passIdx === 3 ? this.sortedIndicesBuffer : this.visibleIndicesBuffer;
8218
+ valuesOut = passIdx === lastPassIdx ? this.sortedIndicesBuffer : this.visibleIndicesBuffer;
7987
8219
  }
7988
8220
  this.upsweepBindGroups[passIdx] = this.device.createBindGroup({
7989
8221
  layout: this.upsweepBindGroupLayout,
@@ -8051,16 +8283,15 @@ class GSSplatSorter {
8051
8283
  view.setFloat32(24, this.cullingOptions.pixelThreshold, true);
8052
8284
  view.setUint32(28, this.cullingOptions.maxVisibleCount ?? 0, true);
8053
8285
  view.setFloat32(32, this.cullingOptions.depthRangeLimit ?? 0, true);
8286
+ view.setFloat32(36, this.cullingOptions.lodSkipRate ?? 0, true);
8054
8287
  this.device.queue.writeBuffer(this.cullingParamsBuffer, 0, cullingParamsData);
8288
+ this.device.queue.writeBuffer(
8289
+ this.indirectBuffer,
8290
+ 0,
8291
+ new Uint32Array([4, 0, 0, 0])
8292
+ );
8055
8293
  const encoder = this.device.createCommandEncoder({ label: "splat-sort-encoder" });
8056
8294
  encoder.clearBuffer(this.globalHistogramBuffer);
8057
- {
8058
- const pass = encoder.beginComputePass({ label: "init-indirect" });
8059
- pass.setPipeline(this.initIndirectPipeline);
8060
- pass.setBindGroup(0, this.cullingBindGroup);
8061
- pass.dispatchWorkgroups(1);
8062
- pass.end();
8063
- }
8064
8295
  {
8065
8296
  const pass = encoder.beginComputePass({ label: "project-cull" });
8066
8297
  pass.setPipeline(this.projectCullPipeline);
@@ -8068,7 +8299,7 @@ class GSSplatSorter {
8068
8299
  pass.dispatchWorkgroups(Math.ceil(this.splatCount / WORKGROUP_SIZE$1));
8069
8300
  pass.end();
8070
8301
  }
8071
- for (let passIdx = 0; passIdx < 4; passIdx++) {
8302
+ for (let passIdx = 0; passIdx < this.numSortPasses; passIdx++) {
8072
8303
  {
8073
8304
  const pass = encoder.beginComputePass({ label: `upsweep-p${passIdx}` });
8074
8305
  pass.setPipeline(this.upsweepPipeline);
@@ -8112,6 +8343,24 @@ class GSSplatSorter {
8112
8343
  getDrawIndirectBuffer() {
8113
8344
  return this.indirectBuffer;
8114
8345
  }
8346
+ /**
8347
+ * 异步读回 drawIndirect buffer 内容(调试用)
8348
+ * 返回 [vertexCount, instanceCount, firstVertex, firstInstance]
8349
+ */
8350
+ async readbackIndirect() {
8351
+ const staging = this.device.createBuffer({
8352
+ size: 16,
8353
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
8354
+ });
8355
+ const encoder = this.device.createCommandEncoder();
8356
+ encoder.copyBufferToBuffer(this.indirectBuffer, 0, staging, 0, 16);
8357
+ this.device.queue.submit([encoder.finish()]);
8358
+ await staging.mapAsync(GPUMapMode.READ);
8359
+ const result = new Uint32Array(staging.getMappedRange().slice(0));
8360
+ staging.unmap();
8361
+ staging.destroy();
8362
+ return result;
8363
+ }
8115
8364
  /**
8116
8365
  * 获取 splat 总数量
8117
8366
  */
@@ -8359,10 +8608,6 @@ fn computeExtentBasisAA(cov2dIn: vec3<f32>, opacity: f32, viewportSize: vec2<f32
8359
8608
  // 使用基于视口的最大限制 (匹配 PlayCanvas)
8360
8609
  let vmin = min(1024.0, min(viewportSize.x, viewportSize.y));
8361
8610
 
8362
- // 计算轴长度: l = 2 * sqrt(2 * lambda) ≈ 2.83 * sqrt(lambda)
8363
- // 这与 GAUSSIAN_K=4 配套使用:
8364
- // 在 UV=1 边界,对应 2*sqrt(2) 个标准差的位置
8365
- // exp(-4 * 1) = exp(-4) ≈ 0.018,Normalized 后精确为 0
8366
8611
  let l1 = min(2.0 * sqrt(2.0 * lambda1), vmin);
8367
8612
  let l2 = min(2.0 * sqrt(2.0 * lambda2), vmin);
8368
8613
 
@@ -8379,7 +8624,6 @@ fn computeExtentBasisAA(cov2dIn: vec3<f32>, opacity: f32, viewportSize: vec2<f32
8379
8624
  let eigenvector1 = diagVec;
8380
8625
  let eigenvector2 = vec2<f32>(diagVec.y, -diagVec.x);
8381
8626
 
8382
- // 计算基向量 (不应用额外的 splat_scale,因为我们使用默认值 1.0)
8383
8627
  result.basis = vec4<f32>(eigenvector1 * l1, eigenvector2 * l2);
8384
8628
  result.adjustedOpacity = alpha;
8385
8629
  return result;
@@ -8773,7 +9017,79 @@ fn fs_depth_normal(input: VertexOutput) -> FragOutput {
8773
9017
  }
8774
9018
  `
8775
9019
  );
9020
+ const SPLAT_BYTE_SIZE = 256;
8776
9021
  const SPLAT_FLOAT_COUNT = 64;
9022
+ const COMPACT_SPLAT_BYTE_SIZE = 64;
9023
+ const COMPACT_SPLAT_FLOAT_COUNT = 16;
9024
+ const HALF_SPLAT_BYTE_SIZE = 32;
9025
+ const HALF_SPLAT_U32_COUNT = 8;
9026
+ function transformShaderForCompact(code) {
9027
+ code = code.replace(
9028
+ /struct Splat \{[\s\S]*?\n\}/,
9029
+ `struct Splat {
9030
+ mean: vec3<f32>, _pad0: f32,
9031
+ scale: vec3<f32>, _pad1: f32,
9032
+ rotation: vec4<f32>,
9033
+ colorDC: vec3<f32>,
9034
+ opacity: f32,
9035
+ }`
9036
+ );
9037
+ code = code.replace(
9038
+ /fn evalSH\(splat: Splat, dir: vec3<f32>\) -> vec3<f32> \{[\s\S]*?\n\}/,
9039
+ `fn evalSH(splat: Splat, dir: vec3<f32>) -> vec3<f32> {
9040
+ return vec3<f32>(0.0);
9041
+ }`
9042
+ );
9043
+ return code;
9044
+ }
9045
+ function transformShaderForHalf(code) {
9046
+ code = code.replace(
9047
+ /struct Splat \{[\s\S]*?\n\}/,
9048
+ `struct Splat {
9049
+ mean: vec3<f32>, _pad0: f32,
9050
+ scale: vec3<f32>, _pad1: f32,
9051
+ rotation: vec4<f32>,
9052
+ colorDC: vec3<f32>,
9053
+ opacity: f32,
9054
+ }`
9055
+ );
9056
+ code = code.replace(
9057
+ /@group\(0\)\s*@binding\((\d+)\)\s*var<storage,\s*read>\s*splats\s*:\s*array<Splat>/,
9058
+ `@group(0) @binding($1) var<storage, read> splatDataU32: array<u32>`
9059
+ );
9060
+ const unpackFunctions = `
9061
+ const HALF_U32S: u32 = 8u;
9062
+ fn unpackSplatFromHalf(idx: u32) -> Splat {
9063
+ let b = idx * HALF_U32S;
9064
+ let mean_xy = unpack2x16float(splatDataU32[b]);
9065
+ let mean_zp = unpack2x16float(splatDataU32[b + 1u]);
9066
+ let sc_xy = unpack2x16float(splatDataU32[b + 2u]);
9067
+ let sc_z_op = unpack2x16float(splatDataU32[b + 3u]);
9068
+ let r_xy = unpack2x16float(splatDataU32[b + 4u]);
9069
+ let r_zw = unpack2x16float(splatDataU32[b + 5u]);
9070
+ let c_rg = unpack2x16float(splatDataU32[b + 6u]);
9071
+ let c_bp = unpack2x16float(splatDataU32[b + 7u]);
9072
+ var s: Splat;
9073
+ s.mean = vec3<f32>(mean_xy.x, mean_xy.y, mean_zp.x);
9074
+ s._pad0 = 0.0;
9075
+ s.scale = vec3<f32>(sc_xy.x, sc_xy.y, sc_z_op.x);
9076
+ s._pad1 = 0.0;
9077
+ s.rotation = vec4<f32>(r_xy.x, r_xy.y, r_zw.x, r_zw.y);
9078
+ s.colorDC = vec3<f32>(c_rg.x, c_rg.y, c_bp.x);
9079
+ s.opacity = sc_z_op.y;
9080
+ return s;
9081
+ }
9082
+ `;
9083
+ code = code.replace(/(@vertex|@compute)/, unpackFunctions + "$1");
9084
+ code = code.replace(/splats\[([^\]]+)\]/g, "unpackSplatFromHalf($1)");
9085
+ code = code.replace(
9086
+ /fn evalSH\(splat: Splat, dir: vec3<f32>\) -> vec3<f32> \{[\s\S]*?\n\}/,
9087
+ `fn evalSH(splat: Splat, dir: vec3<f32>) -> vec3<f32> {
9088
+ return vec3<f32>(0.0);
9089
+ }`
9090
+ );
9091
+ return code;
9092
+ }
8777
9093
  const _GSSplatRenderer = class _GSSplatRenderer {
8778
9094
  constructor(renderer, camera) {
8779
9095
  __publicField(this, "renderer");
@@ -8787,6 +9103,12 @@ const _GSSplatRenderer = class _GSSplatRenderer {
8787
9103
  __publicField(this, "sorter", null);
8788
9104
  __publicField(this, "shMode", SHMode.L3);
8789
9105
  __publicField(this, "boundingBox", null);
9106
+ /** 紧凑布局模式:64B/splat,无 SH,适合移动端 */
9107
+ __publicField(this, "compactLayout", false);
9108
+ /** 半精度模式:32B/splat,所有数据用 f16 打包 */
9109
+ __publicField(this, "halfPrecision", false);
9110
+ /** 排序位宽:16-bit 排序减少一半 radix sort pass */
9111
+ __publicField(this, "sortBits", 32);
8790
9112
  // 预分配 uniform 上传缓冲区,避免每帧 GC(56 floats = 224 bytes)
8791
9113
  __publicField(this, "uniformData", new Float32Array(56));
8792
9114
  __publicField(this, "cpuPositions", null);
@@ -8800,6 +9122,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
8800
9122
  __publicField(this, "pixelCullThreshold", 1);
8801
9123
  __publicField(this, "maxVisibleSplats", 0);
8802
9124
  __publicField(this, "depthRangeLimit", 0);
9125
+ __publicField(this, "lodSkipRate", 0);
8803
9126
  // 排序优化:相机变化检测 + 频率控制
8804
9127
  __publicField(this, "lastSortViewMatrix", new Float32Array(16));
8805
9128
  __publicField(this, "lastSortProjMatrix", new Float32Array(16));
@@ -8812,6 +9135,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
8812
9135
  __publicField(this, "sortStateInitialized", false);
8813
9136
  __publicField(this, "sortFrequency", 1);
8814
9137
  __publicField(this, "frameCounter", 0);
9138
+ __publicField(this, "debugFrameLogged", false);
8815
9139
  // 编辑器状态
8816
9140
  __publicField(this, "editorStateBuffer", null);
8817
9141
  __publicField(this, "editorPipeline", null);
@@ -8836,15 +9160,56 @@ const _GSSplatRenderer = class _GSSplatRenderer {
8836
9160
  this.createUniformBuffer();
8837
9161
  this.updateModelMatrix();
8838
9162
  }
9163
+ /**
9164
+ * 启用/禁用紧凑布局模式(64B/splat,无 SH)。
9165
+ * 必须在 setCompactData / setData 之前调用。
9166
+ */
9167
+ setCompactLayout(compact) {
9168
+ if (this.compactLayout === compact) return;
9169
+ this.compactLayout = compact;
9170
+ if (compact) {
9171
+ this.shMode = SHMode.L0;
9172
+ }
9173
+ this.createPipeline();
9174
+ }
9175
+ /**
9176
+ * 设置排序位宽。16-bit 模式排序 pass 从 4 降到 2,排序速度翻倍。
9177
+ * 必须在 setData / setCompactData 之前调用。
9178
+ */
9179
+ setSortBits(bits2) {
9180
+ this.sortBits = bits2;
9181
+ }
9182
+ /**
9183
+ * 启用/禁用半精度模式(32B/splat,所有数据 f16 打包)。
9184
+ * 自动启用 compactLayout。必须在 setCompactData 之前调用。
9185
+ */
9186
+ setHalfPrecision(half) {
9187
+ if (this.halfPrecision === half) return;
9188
+ this.halfPrecision = half;
9189
+ if (half) {
9190
+ this.compactLayout = true;
9191
+ this.shMode = SHMode.L0;
9192
+ }
9193
+ this.createPipeline();
9194
+ }
8839
9195
  createPipeline() {
8840
9196
  const device = this.renderer.device;
8841
- const shaderCode = gsOptimizedShader.replace(
9197
+ const dbg = getDebugOverlay();
9198
+ let shaderCode = gsOptimizedShader.replace(
8842
9199
  "const SH_LEVEL: u32 = 3u; // @SH_LEVEL_INJECT@",
8843
9200
  `const SH_LEVEL: u32 = ${this.shMode}u;`
8844
9201
  );
9202
+ if (this.halfPrecision) {
9203
+ shaderCode = transformShaderForHalf(shaderCode);
9204
+ } else if (this.compactLayout) {
9205
+ shaderCode = transformShaderForCompact(shaderCode);
9206
+ }
9207
+ dbg == null ? void 0 : dbg.info(`createPipeline: SH=${this.shMode}, compact=${this.compactLayout}, half=${this.halfPrecision}`);
8845
9208
  const shaderModule = device.createShaderModule({
8846
- code: shaderCode
9209
+ code: shaderCode,
9210
+ label: "gs-main-shader"
8847
9211
  });
9212
+ dbg == null ? void 0 : dbg.checkShader(shaderModule, "gs-main");
8848
9213
  this.bindGroupLayout = device.createBindGroupLayout({
8849
9214
  entries: [
8850
9215
  {
@@ -8921,8 +9286,12 @@ const _GSSplatRenderer = class _GSSplatRenderer {
8921
9286
  // A采用"zero add one-minus-src-alpha"的混合模式计算Transmittance
8922
9287
  createDepthNormalPipeline() {
8923
9288
  const device = this.renderer.device;
9289
+ let dnShader = gsDepthNormalShader;
9290
+ if (this.compactLayout) {
9291
+ dnShader = transformShaderForCompact(dnShader);
9292
+ }
8924
9293
  const shaderModule = device.createShaderModule({
8925
- code: gsDepthNormalShader
9294
+ code: dnShader
8926
9295
  });
8927
9296
  const pipelineLayout = device.createPipelineLayout({
8928
9297
  bindGroupLayouts: [this.bindGroupLayout]
@@ -9101,6 +9470,17 @@ const _GSSplatRenderer = class _GSSplatRenderer {
9101
9470
  getDepthRangeLimit() {
9102
9471
  return this.depthRangeLimit;
9103
9472
  }
9473
+ /**
9474
+ * 设置 LOD 抽稀率(0~1)
9475
+ * 远处 splat 按此比例随机跳过(确定性哈希,无闪烁)
9476
+ * 0 = 不抽稀
9477
+ */
9478
+ setLodSkipRate(rate) {
9479
+ this.lodSkipRate = Math.max(0, Math.min(1, rate));
9480
+ }
9481
+ getLodSkipRate() {
9482
+ return this.lodSkipRate;
9483
+ }
9104
9484
  /**
9105
9485
  * 设置排序频率
9106
9486
  * 1 = 每帧排序(默认),2 = 每 2 帧排序一次,以此类推
@@ -9143,6 +9523,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
9143
9523
  }
9144
9524
  setData(splats) {
9145
9525
  const device = this.renderer.device;
9526
+ const dbg = getDebugOverlay();
9146
9527
  if (this.splatBuffer) {
9147
9528
  this.splatBuffer.destroy();
9148
9529
  }
@@ -9157,12 +9538,24 @@ const _GSSplatRenderer = class _GSSplatRenderer {
9157
9538
  this.boundingBox = null;
9158
9539
  return;
9159
9540
  }
9541
+ const floatsPerSplat = this.compactLayout ? COMPACT_SPLAT_FLOAT_COUNT : SPLAT_FLOAT_COUNT;
9542
+ const bytesPerSplat = floatsPerSplat * 4;
9543
+ const maxStorageSize = device.limits.maxStorageBufferBindingSize;
9544
+ const maxSplatsForGPU = Math.floor(maxStorageSize / bytesPerSplat);
9545
+ if (this.splatCount > maxSplatsForGPU) {
9546
+ dbg == null ? void 0 : dbg.warn(
9547
+ `setData: truncating ${this.splatCount} -> ${maxSplatsForGPU} splats (maxStorageBufferBindingSize=${(maxStorageSize / 1048576).toFixed(0)}MB)`
9548
+ );
9549
+ this.splatCount = maxSplatsForGPU;
9550
+ splats = splats.slice(0, this.splatCount);
9551
+ }
9552
+ dbg == null ? void 0 : dbg.info(`setData: ${this.splatCount} splats, compact=${this.compactLayout}`);
9160
9553
  this.boundingBox = this.computeBoundingBox(splats);
9161
9554
  const positions = new Float32Array(this.splatCount * 3);
9162
- const data = new Float32Array(this.splatCount * SPLAT_FLOAT_COUNT);
9555
+ const data = new Float32Array(this.splatCount * floatsPerSplat);
9163
9556
  for (let i = 0; i < this.splatCount; i++) {
9164
9557
  const splat = splats[i];
9165
- const offset = i * SPLAT_FLOAT_COUNT;
9558
+ const offset = i * floatsPerSplat;
9166
9559
  positions[i * 3 + 0] = splat.mean[0];
9167
9560
  positions[i * 3 + 1] = splat.mean[1];
9168
9561
  positions[i * 3 + 2] = splat.mean[2];
@@ -9182,31 +9575,39 @@ const _GSSplatRenderer = class _GSSplatRenderer {
9182
9575
  data[offset + 13] = splat.colorDC[1];
9183
9576
  data[offset + 14] = splat.colorDC[2];
9184
9577
  data[offset + 15] = splat.opacity;
9185
- const shRest = splat.shRest;
9186
- for (let j = 0; j < 9; j++) {
9187
- data[offset + 16 + j] = shRest ? shRest[j] : 0;
9188
- }
9189
- for (let j = 0; j < 15; j++) {
9190
- data[offset + 25 + j] = shRest ? shRest[9 + j] : 0;
9191
- }
9192
- for (let j = 0; j < 21; j++) {
9193
- data[offset + 40 + j] = shRest ? shRest[24 + j] : 0;
9578
+ if (!this.compactLayout) {
9579
+ const shRest = splat.shRest;
9580
+ for (let j = 0; j < 9; j++) {
9581
+ data[offset + 16 + j] = shRest ? shRest[j] : 0;
9582
+ }
9583
+ for (let j = 0; j < 15; j++) {
9584
+ data[offset + 25 + j] = shRest ? shRest[9 + j] : 0;
9585
+ }
9586
+ for (let j = 0; j < 21; j++) {
9587
+ data[offset + 40 + j] = shRest ? shRest[24 + j] : 0;
9588
+ }
9589
+ data[offset + 61] = 0;
9590
+ data[offset + 62] = 0;
9591
+ data[offset + 63] = 0;
9194
9592
  }
9195
- data[offset + 61] = 0;
9196
- data[offset + 62] = 0;
9197
- data[offset + 63] = 0;
9198
9593
  }
9594
+ dbg == null ? void 0 : dbg.info(`splatBuffer: ${(data.byteLength / 1048576).toFixed(1)}MB (${floatsPerSplat} floats/splat)`);
9199
9595
  this.splatBuffer = device.createBuffer({
9200
9596
  size: data.byteLength,
9201
9597
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
9202
9598
  });
9203
9599
  device.queue.writeBuffer(this.splatBuffer, 0, data);
9204
9600
  this.cpuPositions = positions;
9601
+ dbg == null ? void 0 : dbg.info(`Creating sorter: compact=${this.compactLayout}, sortBits=${this.sortBits}`);
9205
9602
  this.sorter = new GSSplatSorter(
9206
9603
  device,
9207
9604
  this.splatCount,
9208
9605
  this.splatBuffer,
9209
- this.uniformBuffer
9606
+ this.uniformBuffer,
9607
+ {},
9608
+ this.compactLayout,
9609
+ false,
9610
+ this.sortBits
9210
9611
  );
9211
9612
  this.sorter.setScreenSize(this.renderer.width, this.renderer.height);
9212
9613
  this.sorter.setCullingOptions({
@@ -9222,10 +9623,12 @@ const _GSSplatRenderer = class _GSSplatRenderer {
9222
9623
  { binding: 2, resource: { buffer: this.sorter.getIndicesBuffer() } }
9223
9624
  ]
9224
9625
  });
9626
+ dbg == null ? void 0 : dbg.info(`setData complete, bindGroup created`);
9225
9627
  if (this.editorEnabled) this.rebuildEditorBindGroup();
9226
9628
  }
9227
9629
  setCompactData(compactData) {
9228
9630
  const device = this.renderer.device;
9631
+ const dbg = getDebugOverlay();
9229
9632
  if (this.splatBuffer) {
9230
9633
  this.splatBuffer.destroy();
9231
9634
  }
@@ -9240,20 +9643,98 @@ const _GSSplatRenderer = class _GSSplatRenderer {
9240
9643
  this.boundingBox = null;
9241
9644
  return;
9242
9645
  }
9646
+ const bytesPerSplat = this.halfPrecision ? HALF_SPLAT_BYTE_SIZE : this.compactLayout ? COMPACT_SPLAT_BYTE_SIZE : SPLAT_BYTE_SIZE;
9647
+ const maxStorageSize = device.limits.maxStorageBufferBindingSize;
9648
+ const maxSplatsForGPU = Math.floor(maxStorageSize / bytesPerSplat);
9649
+ dbg == null ? void 0 : dbg.info(
9650
+ `GPU limits: maxStorageBinding=${(maxStorageSize / 1048576).toFixed(0)}MB, maxBuf=${(device.limits.maxBufferSize / 1048576).toFixed(0)}MB, maxSplats=${maxSplatsForGPU} (at ${bytesPerSplat}B/splat), half=${this.halfPrecision}`
9651
+ );
9652
+ if (this.splatCount > maxSplatsForGPU) {
9653
+ dbg == null ? void 0 : dbg.warn(
9654
+ `Splat count ${this.splatCount} exceeds GPU maxStorageBufferBindingSize (${(maxStorageSize / 1048576).toFixed(0)}MB = ${maxSplatsForGPU} splats). Truncating to ${maxSplatsForGPU}.`
9655
+ );
9656
+ this.splatCount = maxSplatsForGPU;
9657
+ }
9243
9658
  this.boundingBox = this.computeBoundingBoxFromCompact(compactData);
9244
- this.cpuPositions = new Float32Array(compactData.positions);
9245
- const includeSH = compactData.shCoeffs !== void 0;
9246
- const gpuData = compactDataToGPUBuffer(compactData, includeSH);
9659
+ let trimmedData;
9660
+ if (this.halfPrecision && this.lodSkipRate > 0) {
9661
+ const skipRate = this.lodSkipRate;
9662
+ const threshold = skipRate * 65535;
9663
+ const kept = [];
9664
+ for (let i = 0; i < this.splatCount; i++) {
9665
+ const hash = (i * 2654435761 | 0) >>> 16 & 65535;
9666
+ if (hash >= threshold) kept.push(i);
9667
+ }
9668
+ const n = kept.length;
9669
+ const pos = new Float32Array(n * 3);
9670
+ const scl = new Float32Array(n * 3);
9671
+ const rot = new Float32Array(n * 4);
9672
+ const col = new Float32Array(n * 3);
9673
+ const opa = new Float32Array(n);
9674
+ for (let k = 0; k < n; k++) {
9675
+ const i = kept[k];
9676
+ pos[k * 3] = compactData.positions[i * 3];
9677
+ pos[k * 3 + 1] = compactData.positions[i * 3 + 1];
9678
+ pos[k * 3 + 2] = compactData.positions[i * 3 + 2];
9679
+ scl[k * 3] = compactData.scales[i * 3];
9680
+ scl[k * 3 + 1] = compactData.scales[i * 3 + 1];
9681
+ scl[k * 3 + 2] = compactData.scales[i * 3 + 2];
9682
+ rot[k * 4] = compactData.rotations[i * 4];
9683
+ rot[k * 4 + 1] = compactData.rotations[i * 4 + 1];
9684
+ rot[k * 4 + 2] = compactData.rotations[i * 4 + 2];
9685
+ rot[k * 4 + 3] = compactData.rotations[i * 4 + 3];
9686
+ col[k * 3] = compactData.colors[i * 3];
9687
+ col[k * 3 + 1] = compactData.colors[i * 3 + 1];
9688
+ col[k * 3 + 2] = compactData.colors[i * 3 + 2];
9689
+ opa[k] = compactData.opacities[i];
9690
+ }
9691
+ trimmedData = { count: n, positions: pos, scales: scl, rotations: rot, colors: col, opacities: opa };
9692
+ this.splatCount = n;
9693
+ dbg == null ? void 0 : dbg.info(`CPU LOD filter: ${compactData.count} → ${n} splats (skip ${(skipRate * 100).toFixed(0)}%)`);
9694
+ } else {
9695
+ const includeSH = !this.compactLayout && compactData.shCoeffs !== void 0;
9696
+ trimmedData = this.splatCount < compactData.count ? {
9697
+ count: this.splatCount,
9698
+ positions: compactData.positions.slice(0, this.splatCount * 3),
9699
+ scales: compactData.scales.slice(0, this.splatCount * 3),
9700
+ rotations: compactData.rotations.slice(0, this.splatCount * 4),
9701
+ colors: compactData.colors.slice(0, this.splatCount * 3),
9702
+ opacities: compactData.opacities.slice(0, this.splatCount),
9703
+ shCoeffs: includeSH && compactData.shCoeffs ? compactData.shCoeffs.slice(0, this.splatCount * compactData.shCoeffs.length / compactData.count) : void 0
9704
+ } : compactData;
9705
+ }
9706
+ dbg == null ? void 0 : dbg.info(`setCompactData: ${this.splatCount} splats, compact=${this.compactLayout}, half=${this.halfPrecision}`);
9707
+ this.cpuPositions = new Float32Array(trimmedData.positions);
9708
+ let gpuBuf;
9709
+ if (this.halfPrecision) {
9710
+ const halfData = compactDataToGPUBufferHalf(trimmedData);
9711
+ gpuBuf = halfData.buffer;
9712
+ dbg == null ? void 0 : dbg.info(
9713
+ `gpuData(half): ${(halfData.byteLength / 1048576).toFixed(1)}MB, u32s=${halfData.length}, u32/splat=${HALF_SPLAT_U32_COUNT}`
9714
+ );
9715
+ } else {
9716
+ const includeSH = !this.compactLayout && compactData.shCoeffs !== void 0;
9717
+ const f32Data = compactDataToGPUBuffer(trimmedData, includeSH);
9718
+ gpuBuf = f32Data.buffer;
9719
+ dbg == null ? void 0 : dbg.info(
9720
+ `gpuData: ${(f32Data.byteLength / 1048576).toFixed(1)}MB, includeSH=${includeSH}, floats=${f32Data.length}, floats/splat=${f32Data.length / this.splatCount}`
9721
+ );
9722
+ }
9247
9723
  this.splatBuffer = device.createBuffer({
9248
- size: gpuData.byteLength,
9724
+ size: gpuBuf.byteLength,
9249
9725
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
9250
9726
  });
9251
- device.queue.writeBuffer(this.splatBuffer, 0, gpuData.buffer);
9727
+ device.queue.writeBuffer(this.splatBuffer, 0, gpuBuf);
9728
+ dbg == null ? void 0 : dbg.info(`Creating sorter: compact=${this.compactLayout}, half=${this.halfPrecision}, sortBits=${this.sortBits}`);
9252
9729
  this.sorter = new GSSplatSorter(
9253
9730
  device,
9254
9731
  this.splatCount,
9255
9732
  this.splatBuffer,
9256
- this.uniformBuffer
9733
+ this.uniformBuffer,
9734
+ {},
9735
+ this.compactLayout,
9736
+ this.halfPrecision,
9737
+ this.sortBits
9257
9738
  );
9258
9739
  this.sorter.setScreenSize(this.renderer.width, this.renderer.height);
9259
9740
  this.sorter.setCullingOptions({
@@ -9269,6 +9750,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
9269
9750
  { binding: 2, resource: { buffer: this.sorter.getIndicesBuffer() } }
9270
9751
  ]
9271
9752
  });
9753
+ dbg == null ? void 0 : dbg.info(`setCompactData complete, bindGroup created`);
9272
9754
  if (this.editorEnabled) this.rebuildEditorBindGroup();
9273
9755
  this.sortStateInitialized = false;
9274
9756
  }
@@ -9292,7 +9774,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
9292
9774
  this.renderer.device.queue.writeBuffer(this.uniformBuffer, 0, ud);
9293
9775
  const changed = this.needsSort();
9294
9776
  this.frameCounter++;
9295
- const shouldSort = changed || this.sortFrequency > 1 && this.frameCounter % this.sortFrequency === 0;
9777
+ const shouldSort = !this.sortStateInitialized || changed;
9296
9778
  if (shouldSort) {
9297
9779
  this.sorter.setScreenSize(this.renderer.width, this.renderer.height);
9298
9780
  this.sorter.setCullingOptions({
@@ -9300,9 +9782,24 @@ const _GSSplatRenderer = class _GSSplatRenderer {
9300
9782
  farPlane: this.camera.far,
9301
9783
  pixelThreshold: this.pixelCullThreshold,
9302
9784
  maxVisibleCount: this.maxVisibleSplats,
9303
- depthRangeLimit: this.depthRangeLimit
9785
+ depthRangeLimit: this.depthRangeLimit,
9786
+ lodSkipRate: this.lodSkipRate
9304
9787
  });
9305
- this.sorter.sort();
9788
+ if (!this.debugFrameLogged) {
9789
+ const dbg = getDebugOverlay();
9790
+ const dev = this.renderer.device;
9791
+ dev.pushErrorScope("validation");
9792
+ dev.pushErrorScope("out-of-memory");
9793
+ this.sorter.sort();
9794
+ dev.popErrorScope().then((err2) => {
9795
+ if (err2) dbg == null ? void 0 : dbg.error(`Sort OOM: ${err2.message}`);
9796
+ });
9797
+ dev.popErrorScope().then((err2) => {
9798
+ if (err2) dbg == null ? void 0 : dbg.error(`Sort Validation: ${err2.message}`);
9799
+ });
9800
+ } else {
9801
+ this.sorter.sort();
9802
+ }
9306
9803
  }
9307
9804
  if (this.editorEnabled && this.editorPipeline && this.editorBindGroup) {
9308
9805
  pass.setPipeline(this.editorPipeline);
@@ -9312,6 +9809,28 @@ const _GSSplatRenderer = class _GSSplatRenderer {
9312
9809
  pass.setBindGroup(0, this.bindGroup);
9313
9810
  }
9314
9811
  pass.drawIndirect(this.sorter.getDrawIndirectBuffer(), 0);
9812
+ if (!this.debugFrameLogged) {
9813
+ this.debugFrameLogged = true;
9814
+ const dbg = getDebugOverlay();
9815
+ dbg == null ? void 0 : dbg.info(
9816
+ `First render: splats=${this.splatCount}, canvas=${this.renderer.width}x${this.renderer.height}, editor=${this.editorEnabled}, depthWrite=${this.depthWriteEnabled}`
9817
+ );
9818
+ if (dbg && this.sorter) {
9819
+ const pos2 = this.camera.position;
9820
+ dbg.info(`Camera pos: [${pos2[0].toFixed(2)}, ${pos2[1].toFixed(2)}, ${pos2[2].toFixed(2)}]`);
9821
+ dbg.info(
9822
+ `Culling cfg: pixelThresh=${this.pixelCullThreshold.toFixed(2)}, maxVisible=${this.maxVisibleSplats}, depthRange=${this.depthRangeLimit.toFixed(2)}, near=${this.camera.near}, far=${this.camera.far}`
9823
+ );
9824
+ this.sorter.readbackIndirect().then((data) => {
9825
+ dbg.info(
9826
+ `DrawIndirect: vtx=${data[0]}, inst=${data[1]}, firstVtx=${data[2]}, firstInst=${data[3]}`
9827
+ );
9828
+ if (data[0] === 0 && data[1] === 0) {
9829
+ dbg.error("ALL splats culled! visible=0");
9830
+ }
9831
+ });
9832
+ }
9833
+ }
9315
9834
  }
9316
9835
  getSplatCount() {
9317
9836
  return this.splatCount;
@@ -9629,10 +10148,13 @@ const _GSSplatRenderer = class _GSSplatRenderer {
9629
10148
  }
9630
10149
  createEditorPipeline() {
9631
10150
  const device = this.renderer.device;
9632
- const editorShaderCode = this.buildEditorShader().replace(
10151
+ let editorShaderCode = this.buildEditorShader().replace(
9633
10152
  "const SH_LEVEL: u32 = 3u; // @SH_LEVEL_INJECT@",
9634
10153
  `const SH_LEVEL: u32 = ${this.shMode}u;`
9635
10154
  );
10155
+ if (this.compactLayout) {
10156
+ editorShaderCode = transformShaderForCompact(editorShaderCode);
10157
+ }
9636
10158
  const shaderModule = device.createShaderModule({ code: editorShaderCode });
9637
10159
  this.editorBindGroupLayout = device.createBindGroupLayout({
9638
10160
  entries: [
@@ -18725,6 +19247,10 @@ class App {
18725
19247
  __publicField(this, "animationId", 0);
18726
19248
  // 是否使用移动端渲染器
18727
19249
  __publicField(this, "useMobileRenderer", false);
19250
+ // 缓存移动端检测结果,避免每帧调用
19251
+ __publicField(this, "isMobile", false);
19252
+ // 移动端可见 splat 硬上限(保证稳定帧率)
19253
+ __publicField(this, "mobileMaxVisibleCap", 0);
18728
19254
  // 最近加载的 CompactSplatData(用于编辑器导出)
18729
19255
  __publicField(this, "lastCompactData", null);
18730
19256
  // 自适应性能控制
@@ -18734,6 +19260,17 @@ class App {
18734
19260
  });
18735
19261
  __publicField(this, "lastAppliedRenderScale", 1);
18736
19262
  __publicField(this, "baseRenderScale", 1);
19263
+ // 动态分辨率:移动时降分辨率,静止后恢复
19264
+ __publicField(this, "dynamicResolutionEnabled", false);
19265
+ __publicField(this, "dynResLastViewMatrix", new Float32Array(16));
19266
+ __publicField(this, "dynResStillFrames", 0);
19267
+ __publicField(this, "dynResCurrentScale", 1);
19268
+ __publicField(this, "DYNRES_MOVE_SCALE", 0.6);
19269
+ // 移动时 DPR*0.6(2.0*0.6=1.2)
19270
+ __publicField(this, "DYNRES_STILL_SCALE", 1);
19271
+ // 静止时 DPR*1.0
19272
+ __publicField(this, "DYNRES_STILL_THRESHOLD", 2);
19273
+ // 静止 2 帧即恢复
18737
19274
  // 绑定的事件处理函数
18738
19275
  __publicField(this, "boundOnResize");
18739
19276
  /** 额外渲染回调(在 gizmo 之前、场景辅助之后执行) */
@@ -18745,6 +19282,7 @@ class App {
18745
19282
  * 初始化应用
18746
19283
  */
18747
19284
  async init() {
19285
+ var _a2;
18748
19286
  this.renderer = new Renderer(this.canvas);
18749
19287
  await this.renderer.init();
18750
19288
  this.camera = new Camera();
@@ -18773,6 +19311,14 @@ class App {
18773
19311
  if (this.renderer.isAppleGPU) {
18774
19312
  this.applyAppleGPUDefaults();
18775
19313
  }
19314
+ this.isMobile = isMobileDevice();
19315
+ if (this.isMobile) {
19316
+ createDebugOverlay();
19317
+ (_a2 = getDebugOverlay()) == null ? void 0 : _a2.info(
19318
+ `Mobile detected. DPR=${window.devicePixelRatio}, canvas=${this.canvas.width}x${this.canvas.height}`
19319
+ );
19320
+ this.applyMobileDefaults();
19321
+ }
18776
19322
  }
18777
19323
  /**
18778
19324
  * Apple GPU (M1/M2/M3 等) 自动优化配置
@@ -18787,16 +19333,32 @@ class App {
18787
19333
  nearDepthRangeRatio: 2
18788
19334
  };
18789
19335
  console.log(
18790
- `[3DGS] Apple GPU detected (${this.renderer.gpuVendor}/${this.renderer.gpuArchitecture}), applying TBDR optimizations: SH→L1, depthWrite off, aggressive culling`
19336
+ `[3DGS] Apple GPU detected (${this.renderer.gpuVendor}/${this.renderer.gpuArchitecture}), applying TBDR optimizations: depthWrite off, aggressive culling`
18791
19337
  );
18792
19338
  }
18793
19339
  /**
18794
- * 创建桌面端 GSSplatRenderer 并自动应用平台优化
19340
+ * 移动端自动性能优化
19341
+ * 核心策略:降 DPR + 降分辨率 + 激进剔除 + 降排序频率
19342
+ */
19343
+ applyMobileDefaults() {
19344
+ this.dynamicResolutionEnabled = true;
19345
+ console.log(`[3DGS] Mobile: f16, sortBits=16, pixelThreshold=2.5, DPR=1.5, dynamicRes=ON`);
19346
+ }
19347
+ /**
19348
+ * 创建 GSSplatRenderer 并自动应用平台优化
19349
+ * @param forMobile 移动端模式:半精度(32B/splat)+ SH L0,内存降为 1/8
18795
19350
  */
18796
- createDesktopGSRenderer() {
19351
+ createGSRendererUnified(forMobile = false) {
18797
19352
  const renderer = new GSSplatRenderer(this.renderer, this.camera);
18798
- if (this.renderer.isAppleGPU) {
19353
+ if (forMobile) {
19354
+ renderer.setHalfPrecision(true);
19355
+ renderer.setSortBits(16);
19356
+ renderer.setPixelCullThreshold(1.5);
19357
+ renderer.setSortFrequency(2);
19358
+ } else if (this.renderer.isAppleGPU) {
18799
19359
  renderer.setSHMode(SHMode.L1);
19360
+ }
19361
+ if (this.renderer.isAppleGPU) {
18800
19362
  renderer.setDepthWriteEnabled(false);
18801
19363
  }
18802
19364
  return renderer;
@@ -18866,40 +19428,26 @@ class App {
18866
19428
  onProgress(50 + parseProgress, "parse");
18867
19429
  }
18868
19430
  };
18869
- let gsRenderer;
18870
19431
  if (isMobile) {
18871
- gsRenderer = new GSSplatRendererMobile(this.renderer, this.camera);
18872
- this.useMobileRenderer = true;
18873
- const compactData = await this.parsePLYBuffer(buffer, {
18874
- maxSplats: Infinity,
18875
- loadSH: false,
18876
- onProgress: parseProgressCallback,
18877
- coordinateSystem
18878
- });
18879
- if (onProgress) onProgress(90, "upload");
18880
- gsRenderer.setCompactData(compactData);
18881
- if (onProgress) onProgress(100, "upload");
18882
- this.lastCompactData = compactData;
18883
- this.sceneManager.setGSRenderer(gsRenderer);
18884
- this.hotspotManager.setGSRenderer(gsRenderer);
18885
- return compactData.count;
18886
- } else {
18887
- gsRenderer = this.createDesktopGSRenderer();
18888
- this.useMobileRenderer = false;
18889
- const compactData = await this.parsePLYBuffer(buffer, {
18890
- maxSplats: Infinity,
18891
- loadSH: true,
18892
- onProgress: parseProgressCallback,
18893
- coordinateSystem
18894
- });
18895
- if (onProgress) onProgress(90, "upload");
18896
- gsRenderer.setCompactData(compactData);
18897
- if (onProgress) onProgress(100, "upload");
18898
- this.lastCompactData = compactData;
18899
- this.sceneManager.setGSRenderer(gsRenderer);
18900
- this.hotspotManager.setGSRenderer(gsRenderer);
18901
- return compactData.count;
19432
+ console.log(
19433
+ "[3DGS] Mobile device detected, using unified desktop renderer with SH L0"
19434
+ );
18902
19435
  }
19436
+ const gsRenderer = this.createGSRendererUnified(isMobile);
19437
+ this.useMobileRenderer = false;
19438
+ const compactData = await this.parsePLYBuffer(buffer, {
19439
+ maxSplats: Infinity,
19440
+ loadSH: !isMobile,
19441
+ onProgress: parseProgressCallback,
19442
+ coordinateSystem
19443
+ });
19444
+ if (onProgress) onProgress(90, "upload");
19445
+ gsRenderer.setCompactData(compactData);
19446
+ if (onProgress) onProgress(100, "upload");
19447
+ this.lastCompactData = compactData;
19448
+ this.sceneManager.setGSRenderer(gsRenderer);
19449
+ this.hotspotManager.setGSRenderer(gsRenderer);
19450
+ return compactData.count;
18903
19451
  } catch (error) {
18904
19452
  throw error;
18905
19453
  }
@@ -18941,23 +19489,9 @@ class App {
18941
19489
  }
18942
19490
  if (onProgress) onProgress(90, "parse");
18943
19491
  if (onProgress) onProgress(90, "upload");
18944
- let gsRenderer;
18945
- if (isMobile) {
18946
- const mobileRenderer = new GSSplatRendererMobile(
18947
- this.renderer,
18948
- this.camera
18949
- );
18950
- this.useMobileRenderer = true;
18951
- const compactData = App.splatCpuToCompactData(splats);
18952
- mobileRenderer.setCompactData(compactData);
18953
- this.lastCompactData = compactData;
18954
- gsRenderer = mobileRenderer;
18955
- } else {
18956
- const desktopRenderer = this.createDesktopGSRenderer();
18957
- this.useMobileRenderer = false;
18958
- desktopRenderer.setData(splats);
18959
- gsRenderer = desktopRenderer;
18960
- }
19492
+ const gsRenderer = this.createGSRendererUnified(isMobile);
19493
+ this.useMobileRenderer = false;
19494
+ gsRenderer.setData(splats);
18961
19495
  this.sceneManager.setGSRenderer(gsRenderer);
18962
19496
  this.hotspotManager.setGSRenderer(gsRenderer);
18963
19497
  if (onProgress) onProgress(100, "upload");
@@ -19044,14 +19578,8 @@ class App {
19044
19578
  }
19045
19579
  }
19046
19580
  if (onProgress) onProgress(90, "upload");
19047
- let gsRenderer;
19048
- if (isMobile) {
19049
- gsRenderer = new GSSplatRendererMobile(this.renderer, this.camera);
19050
- this.useMobileRenderer = true;
19051
- } else {
19052
- gsRenderer = this.createDesktopGSRenderer();
19053
- this.useMobileRenderer = false;
19054
- }
19581
+ const gsRenderer = this.createGSRendererUnified(isMobile);
19582
+ this.useMobileRenderer = false;
19055
19583
  gsRenderer.setCompactData(compactData);
19056
19584
  this.lastCompactData = compactData;
19057
19585
  this.sceneManager.setGSRenderer(gsRenderer);
@@ -19102,6 +19630,21 @@ class App {
19102
19630
  this.render();
19103
19631
  this.animationId = requestAnimationFrame(this.animate.bind(this));
19104
19632
  }
19633
+ updateDynamicResolution() {
19634
+ if (!this.dynamicResolutionEnabled) return;
19635
+ const interacting = this.controls.isInteracting;
19636
+ if (interacting) {
19637
+ if (this.dynResCurrentScale !== this.DYNRES_MOVE_SCALE) {
19638
+ this.dynResCurrentScale = this.DYNRES_MOVE_SCALE;
19639
+ this.renderer.setRenderScale(this.DYNRES_MOVE_SCALE);
19640
+ }
19641
+ } else {
19642
+ if (this.dynResCurrentScale !== this.DYNRES_STILL_SCALE) {
19643
+ this.dynResCurrentScale = this.DYNRES_STILL_SCALE;
19644
+ this.renderer.setRenderScale(this.DYNRES_STILL_SCALE);
19645
+ }
19646
+ }
19647
+ }
19105
19648
  updateAdaptivePerformance() {
19106
19649
  if (!this.adaptivePerformanceEnabled) return;
19107
19650
  const gsRenderer = this.getGSRenderer();
@@ -19145,7 +19688,11 @@ class App {
19145
19688
  gsRenderer.setMaxVisibleSplats(0);
19146
19689
  } else {
19147
19690
  const ratio = cfg.nearVisibleRatio + (1 - cfg.nearVisibleRatio) * t;
19148
- gsRenderer.setMaxVisibleSplats(Math.round(splatCount * ratio));
19691
+ let maxVisible = Math.round(splatCount * ratio);
19692
+ if (this.mobileMaxVisibleCap > 0) {
19693
+ maxVisible = Math.min(maxVisible, this.mobileMaxVisibleCap);
19694
+ }
19695
+ gsRenderer.setMaxVisibleSplats(maxVisible);
19149
19696
  }
19150
19697
  if (cfg.enableDepthRangeLimit) {
19151
19698
  if (t >= 0.99) {
@@ -19155,16 +19702,13 @@ class App {
19155
19702
  gsRenderer.setDepthRangeLimit(depthRange);
19156
19703
  }
19157
19704
  }
19158
- if (t < 0.3) {
19159
- gsRenderer.setSortFrequency(2);
19160
- } else {
19161
- gsRenderer.setSortFrequency(1);
19162
- }
19705
+ gsRenderer.setSortFrequency(1);
19163
19706
  }
19164
19707
  render() {
19165
19708
  var _a2, _b2;
19166
19709
  this.camera.setAspect(this.renderer.getAspectRatio());
19167
19710
  this.controls.update();
19711
+ this.updateDynamicResolution();
19168
19712
  this.updateAdaptivePerformance();
19169
19713
  this.hotspotManager.updateBillboards();
19170
19714
  if ((_a2 = this.skyboxRenderer) == null ? void 0 : _a2.isActive) {
@@ -19740,10 +20284,6 @@ class App {
19740
20284
  if (gsRenderer) {
19741
20285
  gsRenderer.setSortFrequency(frequency);
19742
20286
  }
19743
- const mobileRenderer = this.getGSRendererMobile();
19744
- if (mobileRenderer) {
19745
- mobileRenderer.setSortFrequency(frequency);
19746
- }
19747
20287
  }
19748
20288
  /**
19749
20289
  * 是否检测到 Apple GPU(M1/M2/M3 等)