@d5techs/3dgs-lib 1.4.6 → 1.4.8

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
@@ -37,7 +37,7 @@ function isMobileDevice() {
37
37
  }
38
38
  function getRecommendedDPR() {
39
39
  const isMobile = isMobileDevice();
40
- const maxDpr = isMobile ? 1.5 : 3;
40
+ const maxDpr = isMobile ? 2 : 2;
41
41
  return Math.min(window.devicePixelRatio || 1, maxDpr);
42
42
  }
43
43
  function isWebGPUSupported() {
@@ -252,6 +252,13 @@ class Renderer {
252
252
  __publicField(this, "renderPassEncoder");
253
253
  // ResizeObserver 引用(用于清理)
254
254
  __publicField(this, "resizeObserver", null);
255
+ // 渲染缩放:0.5 = 半分辨率,1.0 = 正常,用于性能/质量权衡
256
+ __publicField(this, "_renderScale", 1);
257
+ // 自定义 DPR 覆盖:-1 表示使用自动推荐值
258
+ __publicField(this, "_customDPR", -1);
259
+ // 缓存最后一次的 CSS 尺寸,用于 renderScale 变更时重算
260
+ __publicField(this, "_lastCSSWidth", 0);
261
+ __publicField(this, "_lastCSSHeight", 0);
255
262
  // 背景颜色
256
263
  __publicField(this, "_clearColor", { r: 0.15, g: 0.15, b: 0.15, a: 1 });
257
264
  this.canvas = canvas;
@@ -362,6 +369,45 @@ class Renderer {
362
369
  });
363
370
  this._depthTextureView = this._depthTexture.createView();
364
371
  }
372
+ getEffectiveDPR() {
373
+ const baseDPR = this._customDPR > 0 ? this._customDPR : getRecommendedDPR();
374
+ return baseDPR * this._renderScale;
375
+ }
376
+ /**
377
+ * 设置渲染缩放比例,用于性能/质量权衡
378
+ * 0.5 = 半分辨率(性能提升约 4 倍),1.0 = 正常分辨率
379
+ * 内部等效于降低 DPR,不影响 CSS 布局尺寸
380
+ */
381
+ setRenderScale(scale) {
382
+ this._renderScale = Math.max(0.25, Math.min(2, scale));
383
+ if (this._lastCSSWidth > 0 && this._lastCSSHeight > 0) {
384
+ this.applySize(this._lastCSSWidth, this._lastCSSHeight);
385
+ }
386
+ }
387
+ getRenderScale() {
388
+ return this._renderScale;
389
+ }
390
+ /**
391
+ * 覆盖自动 DPR 推荐值
392
+ * 传入 -1 恢复自动模式
393
+ */
394
+ setDPR(dpr) {
395
+ this._customDPR = dpr;
396
+ if (this._lastCSSWidth > 0 && this._lastCSSHeight > 0) {
397
+ this.applySize(this._lastCSSWidth, this._lastCSSHeight);
398
+ }
399
+ }
400
+ getDPR() {
401
+ return this._customDPR > 0 ? this._customDPR : getRecommendedDPR();
402
+ }
403
+ applySize(cssWidth, cssHeight) {
404
+ this._lastCSSWidth = cssWidth;
405
+ this._lastCSSHeight = cssHeight;
406
+ const dpr = this.getEffectiveDPR();
407
+ this.canvas.width = Math.max(1, Math.floor(cssWidth * dpr));
408
+ this.canvas.height = Math.max(1, Math.floor(cssHeight * dpr));
409
+ this.createDepthTexture();
410
+ }
365
411
  /**
366
412
  * 设置 resize 监听
367
413
  */
@@ -369,10 +415,7 @@ class Renderer {
369
415
  this.resizeObserver = new ResizeObserver((entries) => {
370
416
  for (const entry of entries) {
371
417
  const { width, height } = entry.contentRect;
372
- const dpr = getRecommendedDPR();
373
- this.canvas.width = Math.floor(width * dpr);
374
- this.canvas.height = Math.floor(height * dpr);
375
- this.createDepthTexture();
418
+ this.applySize(width, height);
376
419
  }
377
420
  });
378
421
  this.resizeObserver.observe(this.canvas);
@@ -4269,7 +4312,7 @@ function transformSHCoeffsYZSwap$1(sh, base, perChannel) {
4269
4312
  for (let ch = 0; ch < 3; ch++) {
4270
4313
  const a = sh[base + ch];
4271
4314
  const b = sh[base + 3 + ch];
4272
- sh[base + ch] = -b;
4315
+ sh[base + ch] = b;
4273
4316
  sh[base + 3 + ch] = -a;
4274
4317
  }
4275
4318
  }
@@ -4280,8 +4323,8 @@ function transformSHCoeffsYZSwap$1(sh, base, perChannel) {
4280
4323
  const g2 = sh[base + 15 + ch];
4281
4324
  const g3 = sh[base + 18 + ch];
4282
4325
  const g4 = sh[base + 21 + ch];
4283
- sh[base + 9 + ch] = -g3;
4284
- sh[base + 12 + ch] = g1;
4326
+ sh[base + 9 + ch] = g3;
4327
+ sh[base + 12 + ch] = -g1;
4285
4328
  sh[base + 15 + ch] = -0.5 * g2 - SQRT3_2 * g4;
4286
4329
  sh[base + 18 + ch] = -g0;
4287
4330
  sh[base + 21 + ch] = -SQRT3_2 * g2 + 0.5 * g4;
@@ -4299,9 +4342,9 @@ function transformSHCoeffsYZSwap$1(sh, base, perChannel) {
4299
4342
  const g4 = sh[base + 36 + ch];
4300
4343
  const g5 = sh[base + 39 + ch];
4301
4344
  const g6 = sh[base + 42 + ch];
4302
- sh[base + 24 + ch] = A * g3 - B * g5;
4303
- sh[base + 27 + ch] = g1;
4304
- sh[base + 30 + ch] = B * g3 + A * g5;
4345
+ sh[base + 24 + ch] = -A * g3 + B * g5;
4346
+ sh[base + 27 + ch] = -g1;
4347
+ sh[base + 30 + ch] = -B * g3 - A * g5;
4305
4348
  sh[base + 33 + ch] = A * g0 + B * g2;
4306
4349
  sh[base + 36 + ch] = -0.25 * g4 - P * g6;
4307
4350
  sh[base + 39 + ch] = -B * g0 + A * g2;
@@ -4539,13 +4582,13 @@ async function loadPLY(url, options = {}) {
4539
4582
  transformSHCoeffsYZSwap$1(shRestBuffer, shOffset, perChannel);
4540
4583
  }
4541
4584
  splats[i] = {
4542
- mean: swapYZ ? [x, z, y] : [x, y, z],
4585
+ mean: swapYZ ? [x, z, -y] : [x, y, z],
4543
4586
  scale: swapYZ ? [scale_0, scale_2, scale_1] : [scale_0, scale_1, scale_2],
4544
4587
  rotation: swapYZ ? [
4545
- -rot_0 * qnorm,
4588
+ rot_0 * qnorm,
4546
4589
  rot_1 * qnorm,
4547
4590
  rot_3 * qnorm,
4548
- rot_2 * qnorm
4591
+ -rot_2 * qnorm
4549
4592
  ] : [
4550
4593
  rot_0 * qnorm,
4551
4594
  rot_1 * qnorm,
@@ -4583,7 +4626,7 @@ function transformSHCoeffsYZSwap(sh, base, perChannel) {
4583
4626
  for (let ch = 0; ch < 3; ch++) {
4584
4627
  const a = sh[base + ch];
4585
4628
  const b = sh[base + 3 + ch];
4586
- sh[base + ch] = -b;
4629
+ sh[base + ch] = b;
4587
4630
  sh[base + 3 + ch] = -a;
4588
4631
  }
4589
4632
  }
@@ -4594,8 +4637,8 @@ function transformSHCoeffsYZSwap(sh, base, perChannel) {
4594
4637
  const g2 = sh[base + 15 + ch];
4595
4638
  const g3 = sh[base + 18 + ch];
4596
4639
  const g4 = sh[base + 21 + ch];
4597
- sh[base + 9 + ch] = -g3;
4598
- sh[base + 12 + ch] = g1;
4640
+ sh[base + 9 + ch] = g3;
4641
+ sh[base + 12 + ch] = -g1;
4599
4642
  sh[base + 15 + ch] = -0.5 * g2 - SQRT3_2 * g4;
4600
4643
  sh[base + 18 + ch] = -g0;
4601
4644
  sh[base + 21 + ch] = -SQRT3_2 * g2 + 0.5 * g4;
@@ -4613,9 +4656,9 @@ function transformSHCoeffsYZSwap(sh, base, perChannel) {
4613
4656
  const g4 = sh[base + 36 + ch];
4614
4657
  const g5 = sh[base + 39 + ch];
4615
4658
  const g6 = sh[base + 42 + ch];
4616
- sh[base + 24 + ch] = A * g3 - B * g5;
4617
- sh[base + 27 + ch] = g1;
4618
- sh[base + 30 + ch] = B * g3 + A * g5;
4659
+ sh[base + 24 + ch] = -A * g3 + B * g5;
4660
+ sh[base + 27 + ch] = -g1;
4661
+ sh[base + 30 + ch] = -B * g3 - A * g5;
4619
4662
  sh[base + 33 + ch] = A * g0 + B * g2;
4620
4663
  sh[base + 36 + ch] = -0.25 * g4 - P * g6;
4621
4664
  sh[base + 39 + ch] = -B * g0 + A * g2;
@@ -4910,7 +4953,7 @@ async function parsePLYBuffer(buffer, options = {}) {
4910
4953
  const pz = offsets.z >= 0 ? readProperty(dataView, base + offsets.z, types.z, littleEndian) : 0;
4911
4954
  positions[outputIdx * 3 + 0] = px;
4912
4955
  positions[outputIdx * 3 + 1] = swapYZ ? pz : py;
4913
- positions[outputIdx * 3 + 2] = swapYZ ? py : pz;
4956
+ positions[outputIdx * 3 + 2] = swapYZ ? -py : pz;
4914
4957
  const sx = offsets.scale_0 >= 0 ? Math.exp(readProperty(dataView, base + offsets.scale_0, types.scale_0, littleEndian)) : 1;
4915
4958
  const sy = offsets.scale_1 >= 0 ? Math.exp(readProperty(dataView, base + offsets.scale_1, types.scale_1, littleEndian)) : 1;
4916
4959
  const sz = offsets.scale_2 >= 0 ? Math.exp(readProperty(dataView, base + offsets.scale_2, types.scale_2, littleEndian)) : 1;
@@ -4924,10 +4967,10 @@ async function parsePLYBuffer(buffer, options = {}) {
4924
4967
  const qlen = Math.sqrt(rot_0 * rot_0 + rot_1 * rot_1 + rot_2 * rot_2 + rot_3 * rot_3);
4925
4968
  const qnorm = qlen > 0 ? 1 / qlen : 1;
4926
4969
  if (swapYZ) {
4927
- rotations[outputIdx * 4 + 0] = -rot_0 * qnorm;
4970
+ rotations[outputIdx * 4 + 0] = rot_0 * qnorm;
4928
4971
  rotations[outputIdx * 4 + 1] = rot_1 * qnorm;
4929
4972
  rotations[outputIdx * 4 + 2] = rot_3 * qnorm;
4930
- rotations[outputIdx * 4 + 3] = rot_2 * qnorm;
4973
+ rotations[outputIdx * 4 + 3] = -rot_2 * qnorm;
4931
4974
  } else {
4932
4975
  rotations[outputIdx * 4 + 0] = rot_0 * qnorm;
4933
4976
  rotations[outputIdx * 4 + 1] = rot_1 * qnorm;
@@ -5585,8 +5628,16 @@ async function decodeWebP(bytes) {
5585
5628
  });
5586
5629
  const w = bitmap.width;
5587
5630
  const h = bitmap.height;
5588
- const canvas = new OffscreenCanvas(w, h);
5589
- const ctx = canvas.getContext("2d");
5631
+ let ctx;
5632
+ if (typeof OffscreenCanvas !== "undefined") {
5633
+ const oc = new OffscreenCanvas(w, h);
5634
+ ctx = oc.getContext("2d");
5635
+ } else {
5636
+ const hc = document.createElement("canvas");
5637
+ hc.width = w;
5638
+ hc.height = h;
5639
+ ctx = hc.getContext("2d");
5640
+ }
5590
5641
  ctx.drawImage(bitmap, 0, 0);
5591
5642
  bitmap.close();
5592
5643
  return { width: w, height: h, data: ctx.getImageData(0, 0, w, h).data };
@@ -8426,6 +8477,65 @@ function compressSplatsToTextures(device, data) {
8426
8477
  { bytesPerRow: width * 4 },
8427
8478
  { width, height }
8428
8479
  );
8480
+ let shBasis0Texture = null;
8481
+ let shBasis1Texture = null;
8482
+ let shBasis2Texture = null;
8483
+ const hasSH = !!(data.shCoeffs && data.shCoeffs.length >= count * 45);
8484
+ if (hasSH) {
8485
+ const shCoeffs = data.shCoeffs;
8486
+ const sh0Data = new Float32Array(totalPixels * 4);
8487
+ const sh1Data = new Float32Array(totalPixels * 4);
8488
+ const sh2Data = new Float32Array(totalPixels * 4);
8489
+ for (let i = 0; i < count; i++) {
8490
+ const base = i * 45;
8491
+ const px = i * 4;
8492
+ sh0Data[px + 0] = shCoeffs[base + 0];
8493
+ sh0Data[px + 1] = shCoeffs[base + 1];
8494
+ sh0Data[px + 2] = shCoeffs[base + 2];
8495
+ sh1Data[px + 0] = shCoeffs[base + 3];
8496
+ sh1Data[px + 1] = shCoeffs[base + 4];
8497
+ sh1Data[px + 2] = shCoeffs[base + 5];
8498
+ sh2Data[px + 0] = shCoeffs[base + 6];
8499
+ sh2Data[px + 1] = shCoeffs[base + 7];
8500
+ sh2Data[px + 2] = shCoeffs[base + 8];
8501
+ }
8502
+ shBasis0Texture = device.createTexture({
8503
+ size: { width, height },
8504
+ format: "rgba32float",
8505
+ usage: textureUsage,
8506
+ label: "sh-basis0"
8507
+ });
8508
+ shBasis1Texture = device.createTexture({
8509
+ size: { width, height },
8510
+ format: "rgba32float",
8511
+ usage: textureUsage,
8512
+ label: "sh-basis1"
8513
+ });
8514
+ shBasis2Texture = device.createTexture({
8515
+ size: { width, height },
8516
+ format: "rgba32float",
8517
+ usage: textureUsage,
8518
+ label: "sh-basis2"
8519
+ });
8520
+ device.queue.writeTexture(
8521
+ { texture: shBasis0Texture },
8522
+ sh0Data,
8523
+ { bytesPerRow: width * 16 },
8524
+ { width, height }
8525
+ );
8526
+ device.queue.writeTexture(
8527
+ { texture: shBasis1Texture },
8528
+ sh1Data,
8529
+ { bytesPerRow: width * 16 },
8530
+ { width, height }
8531
+ );
8532
+ device.queue.writeTexture(
8533
+ { texture: shBasis2Texture },
8534
+ sh2Data,
8535
+ { bytesPerRow: width * 16 },
8536
+ { width, height }
8537
+ );
8538
+ }
8429
8539
  return {
8430
8540
  width,
8431
8541
  height,
@@ -8434,6 +8544,10 @@ function compressSplatsToTextures(device, data) {
8434
8544
  scaleRotTexture1,
8435
8545
  scaleRotTexture2,
8436
8546
  colorTexture,
8547
+ shBasis0Texture,
8548
+ shBasis1Texture,
8549
+ shBasis2Texture,
8550
+ hasSH,
8437
8551
  boundingBox
8438
8552
  };
8439
8553
  }
@@ -8442,9 +8556,12 @@ function destroyCompressedTextures(textures) {
8442
8556
  textures.scaleRotTexture1.destroy();
8443
8557
  textures.scaleRotTexture2.destroy();
8444
8558
  textures.colorTexture.destroy();
8559
+ if (textures.shBasis0Texture) textures.shBasis0Texture.destroy();
8560
+ if (textures.shBasis1Texture) textures.shBasis1Texture.destroy();
8561
+ if (textures.shBasis2Texture) textures.shBasis2Texture.destroy();
8445
8562
  }
8446
8563
  const DEFAULT_NUM_BUCKETS = 65536;
8447
- const IOS_NUM_BUCKETS = 4096;
8564
+ const IOS_NUM_BUCKETS = 16384;
8448
8565
  const WORKGROUP_SIZE = 256;
8449
8566
  function isIOSDevice() {
8450
8567
  if (typeof navigator === "undefined") return false;
@@ -8948,9 +9065,17 @@ class GSSplatSorterMobile {
8948
9065
  this.drawIndirectBuffer.destroy();
8949
9066
  }
8950
9067
  }
8951
- const shaderCodeMobileL0 = (
9068
+ const shaderCodeMobile = (
8952
9069
  /* wgsl */
8953
9070
  `
9071
+
9072
+ const SH_C1: f32 = 0.4886025119029199;
9073
+ const GAUSSIAN_K: f32 = 4.0;
9074
+ const EXP_NEG_K: f32 = 0.01831563888873418;
9075
+ const INV_ONE_MINUS_EXP_NEG_K: f32 = 1.01865736036377408;
9076
+ const ALPHA_CULL_THRESHOLD: f32 = 0.00392156863;
9077
+ const LOW_PASS_FILTER: f32 = 0.3;
9078
+
8954
9079
  struct Uniforms {
8955
9080
  view: mat4x4<f32>,
8956
9081
  proj: mat4x4<f32>,
@@ -8959,18 +9084,21 @@ struct Uniforms {
8959
9084
  _pad: f32,
8960
9085
  screenSize: vec2<f32>,
8961
9086
  _pad2: vec2<f32>,
8962
- textureSize: vec2<f32>, // 纹理尺寸 (用于坐标计算)
8963
- _pad3: vec2<f32>,
9087
+ textureSize: vec2<f32>,
9088
+ shEnabled: f32,
9089
+ _pad3: f32,
8964
9090
  }
8965
9091
 
8966
9092
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
8967
9093
  @group(0) @binding(1) var<storage, read> sortedIndices: array<u32>;
8968
9094
 
8969
- // 纹理绑定 - 4 张纹理(使用 RGBA32Float 保证精度)
8970
- @group(1) @binding(0) var positionTex: texture_2d<f32>; // RGBA32Float: xyz + unused
8971
- @group(1) @binding(1) var scaleRotTex1: texture_2d<f32>; // RGBA32Float: scale_xyz + rot_w
8972
- @group(1) @binding(2) var scaleRotTex2: texture_2d<f32>; // RGBA32Float: rot_xyz + unused
8973
- @group(1) @binding(3) var colorTex: texture_2d<f32>; // RGBA8Unorm: rgb + opacity
9095
+ @group(1) @binding(0) var positionTex: texture_2d<f32>;
9096
+ @group(1) @binding(1) var scaleRotTex1: texture_2d<f32>;
9097
+ @group(1) @binding(2) var scaleRotTex2: texture_2d<f32>;
9098
+ @group(1) @binding(3) var colorTex: texture_2d<f32>;
9099
+ @group(1) @binding(4) var shBasis0Tex: texture_2d<f32>;
9100
+ @group(1) @binding(5) var shBasis1Tex: texture_2d<f32>;
9101
+ @group(1) @binding(6) var shBasis2Tex: texture_2d<f32>;
8974
9102
 
8975
9103
  struct VertexOutput {
8976
9104
  @builtin(position) position: vec4<f32>,
@@ -8988,7 +9116,6 @@ const QUAD_POSITIONS = array<vec2<f32>, 4>(
8988
9116
 
8989
9117
  const ELLIPSE_SCALE: f32 = 3.0;
8990
9118
 
8991
- // 将索引转换为纹理坐标
8992
9119
  fn indexToTexCoord(index: u32) -> vec2<u32> {
8993
9120
  let texWidth = u32(uniforms.textureSize.x);
8994
9121
  let x = index % texWidth;
@@ -8996,7 +9123,6 @@ fn indexToTexCoord(index: u32) -> vec2<u32> {
8996
9123
  return vec2<u32>(x, y);
8997
9124
  }
8998
9125
 
8999
- // 四元数转旋转矩阵
9000
9126
  fn quatToMat3(q: vec4<f32>) -> mat3x3<f32> {
9001
9127
  let w = q[0]; let x = q[1]; let y = q[2]; let z = q[3];
9002
9128
  let x2 = x + x; let y2 = y + y; let z2 = z + z;
@@ -9010,15 +9136,12 @@ fn quatToMat3(q: vec4<f32>) -> mat3x3<f32> {
9010
9136
  );
9011
9137
  }
9012
9138
 
9013
- // 从模型矩阵提取统一缩放因子(取 X 轴向量长度)
9014
9139
  fn getModelScale(model: mat4x4<f32>) -> f32 {
9015
9140
  return length(model[0].xyz);
9016
9141
  }
9017
9142
 
9018
- // 计算 2D 协方差
9019
9143
  fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelView: mat4x4<f32>, proj: mat4x4<f32>, modelScale: f32) -> vec3<f32> {
9020
9144
  let R = quatToMat3(rotation);
9021
- // 应用模型缩放到 splat scale
9022
9145
  let scaledScale = scale * modelScale;
9023
9146
  let s2 = scaledScale * scaledScale;
9024
9147
  let M = mat3x3<f32>(R[0] * s2.x, R[1] * s2.y, R[2] * s2.z);
@@ -9030,8 +9153,6 @@ fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelVie
9030
9153
  let z = -viewPos.z;
9031
9154
  let z_clamped = max(z, 0.001);
9032
9155
  let z2 = z_clamped * z_clamped;
9033
- // 雅可比矩阵: 从相机坐标 (x_cam, y_cam, z_cam) 到 NDC 的偏导数
9034
- // x_ndc = fx * x_cam / (-z_cam), 所以 dx_ndc/dz_cam = fx * x_cam / z_cam^2 (正号!)
9035
9156
  let j1 = vec3<f32>(fx / z_clamped, 0.0, fx * viewPos.x / z2);
9036
9157
  let j2 = vec3<f32>(0.0, fy / z_clamped, fy * viewPos.y / z2);
9037
9158
  let Sj1 = SigmaView * j1;
@@ -9039,17 +9160,22 @@ fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelVie
9039
9160
  return vec3<f32>(dot(j1, Sj1), dot(j1, Sj2), dot(j2, Sj2));
9040
9161
  }
9041
9162
 
9042
- // 计算椭圆轴
9043
9163
  fn computeEllipseAxes(cov2D: vec3<f32>) -> mat2x2<f32> {
9044
- let a = cov2D.x; let b = cov2D.y; let c = cov2D.z;
9164
+ var cov = cov2D;
9165
+ cov.x += LOW_PASS_FILTER;
9166
+ cov.z += LOW_PASS_FILTER;
9167
+ let a = cov.x; let b = cov.y; let c = cov.z;
9045
9168
  let trace = a + c;
9046
9169
  let det = a * c - b * b;
9047
9170
  let disc = trace * trace - 4.0 * det;
9048
9171
  let sqrtDisc = sqrt(max(disc, 0.0));
9049
9172
  let lambda1 = max((trace + sqrtDisc) * 0.5, 0.0);
9050
9173
  let lambda2 = max((trace - sqrtDisc) * 0.5, 0.0);
9051
- let r1 = sqrt(lambda1);
9052
- let r2 = sqrt(lambda2);
9174
+ if (lambda2 <= 0.0) {
9175
+ return mat2x2<f32>(vec2<f32>(0.0), vec2<f32>(0.0));
9176
+ }
9177
+ let r1 = min(2.0 * sqrt(2.0 * lambda1), 1024.0);
9178
+ let r2 = min(2.0 * sqrt(2.0 * lambda2), 1024.0);
9053
9179
  var axis1: vec2<f32>; var axis2: vec2<f32>;
9054
9180
  if (abs(b) > 1e-6) {
9055
9181
  axis1 = normalize(vec2<f32>(b, lambda1 - a));
@@ -9065,59 +9191,88 @@ fn computeEllipseAxes(cov2D: vec3<f32>) -> mat2x2<f32> {
9065
9191
  fn vs_main(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> VertexOutput {
9066
9192
  var output: VertexOutput;
9067
9193
 
9068
- // 获取排序后的索引
9069
9194
  let splatIndex = sortedIndices[instanceIndex];
9070
9195
  let texCoord = indexToTexCoord(splatIndex);
9071
9196
 
9072
- // 从纹理采样位置数据(RGBA32Float,直接读取)
9073
9197
  let posSample = textureLoad(positionTex, texCoord, 0);
9074
9198
  let mean = posSample.xyz;
9075
9199
 
9076
- // 从纹理采样缩放和旋转(RGBA16Float,GPU 自动转换为 f32)
9077
9200
  let scaleRot1 = textureLoad(scaleRotTex1, texCoord, 0);
9078
9201
  let scaleRot2 = textureLoad(scaleRotTex2, texCoord, 0);
9079
-
9080
9202
  let scale = scaleRot1.xyz;
9081
9203
  let rotation = vec4<f32>(scaleRot1.w, scaleRot2.x, scaleRot2.y, scaleRot2.z);
9082
9204
 
9083
- // 从纹理采样颜色(RGBA8Unorm,GPU 自动归一化到 0-1)
9084
9205
  let colorSample = textureLoad(colorTex, texCoord, 0);
9085
- let color = colorSample.rgb;
9206
+ var color = colorSample.rgb;
9086
9207
  let opacity = colorSample.a;
9208
+
9209
+ if (opacity < ALPHA_CULL_THRESHOLD) {
9210
+ output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0);
9211
+ return output;
9212
+ }
9087
9213
 
9088
- // 计算顶点位置
9089
9214
  let quadPos = QUAD_POSITIONS[vertexIndex];
9090
9215
  output.localUV = quadPos;
9091
9216
 
9092
- // 计算 modelView 矩阵和模型缩放
9093
9217
  let modelView = uniforms.view * uniforms.model;
9094
9218
  let modelScale = getModelScale(uniforms.model);
9095
9219
 
9096
9220
  let cov2D = computeCov2D(mean, scale, rotation, modelView, uniforms.proj, modelScale);
9097
9221
  let axes = computeEllipseAxes(cov2D);
9098
- let screenOffset = axes[0] * quadPos.x * ELLIPSE_SCALE + axes[1] * quadPos.y * ELLIPSE_SCALE;
9222
+
9223
+ if (axes[0].x == 0.0 && axes[0].y == 0.0 && axes[1].x == 0.0 && axes[1].y == 0.0) {
9224
+ output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0);
9225
+ return output;
9226
+ }
9227
+
9228
+ let basisViewport = vec2<f32>(1.0 / uniforms.screenSize.x, 1.0 / uniforms.screenSize.y);
9229
+ let ndcOffset = (quadPos.x * axes[0] + quadPos.y * axes[1]) * basisViewport * 2.0;
9099
9230
 
9100
- // 应用 model 变换到 splat 位置
9101
9231
  let worldPos = uniforms.model * vec4<f32>(mean, 1.0);
9102
9232
  let viewPos = uniforms.view * worldPos;
9103
- var clipPos = uniforms.proj * viewPos;
9104
- clipPos.x = clipPos.x + screenOffset.x * clipPos.w;
9105
- clipPos.y = clipPos.y + screenOffset.y * clipPos.w;
9106
- output.position = clipPos;
9233
+ let clipPos = uniforms.proj * viewPos;
9234
+ let pW = 1.0 / (clipPos.w + 0.0000001);
9235
+ let ndcPos = clipPos * pW;
9236
+
9237
+ if (abs(ndcPos.x) > 1.3 || abs(ndcPos.y) > 1.3 || ndcPos.z < -0.2 || ndcPos.z > 1.0) {
9238
+ output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0);
9239
+ return output;
9240
+ }
9241
+
9242
+ output.position = vec4<f32>(ndcPos.xy + ndcOffset, ndcPos.z, 1.0);
9243
+
9244
+ // L1 SH evaluation
9245
+ if (uniforms.shEnabled > 0.5) {
9246
+ let sh_b0 = textureLoad(shBasis0Tex, texCoord, 0).xyz;
9247
+ let sh_b1 = textureLoad(shBasis1Tex, texCoord, 0).xyz;
9248
+ let sh_b2 = textureLoad(shBasis2Tex, texCoord, 0).xyz;
9249
+
9250
+ let viewDir = worldPos.xyz - uniforms.cameraPos;
9251
+ let shDir = normalize(vec3<f32>(
9252
+ dot(viewDir, uniforms.model[0].xyz),
9253
+ dot(viewDir, uniforms.model[1].xyz),
9254
+ dot(viewDir, uniforms.model[2].xyz)
9255
+ ));
9256
+
9257
+ color += (-SH_C1 * shDir.y) * sh_b0
9258
+ + ( SH_C1 * shDir.z) * sh_b1
9259
+ + (-SH_C1 * shDir.x) * sh_b2;
9260
+ }
9261
+
9107
9262
  output.color = color;
9108
9263
  output.opacity = opacity;
9109
-
9110
9264
  return output;
9111
9265
  }
9112
9266
 
9113
9267
  @fragment
9114
9268
  fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
9115
- let r = length(input.localUV);
9116
- if (r > 1.0) { discard; }
9117
- let gaussianWeight = exp(-r * r * 4.0);
9118
- let alpha = input.opacity * gaussianWeight;
9119
- if (alpha < 0.004) { discard; } // 丢弃几乎透明的像素
9120
- let color = clamp(input.color, vec3<f32>(0.0), vec3<f32>(1.0));
9269
+ if (input.opacity <= 0.0) { discard; }
9270
+ let A = dot(input.localUV, input.localUV);
9271
+ if (A > 1.0) { discard; }
9272
+ let weight = (exp(-GAUSSIAN_K * A) - EXP_NEG_K) * INV_ONE_MINUS_EXP_NEG_K;
9273
+ let alpha = input.opacity * weight;
9274
+ if (alpha < ALPHA_CULL_THRESHOLD) { discard; }
9275
+ let color = max(input.color, vec3<f32>(0.0));
9121
9276
  return vec4<f32>(color * alpha, alpha);
9122
9277
  }
9123
9278
  `
@@ -9147,6 +9302,10 @@ class GSSplatRendererMobile {
9147
9302
  // 帧计数(用于排序频率控制)
9148
9303
  __publicField(this, "frameCount", 0);
9149
9304
  __publicField(this, "sortEveryNFrames", 1);
9305
+ // 相机静止检测:跳过不必要的排序
9306
+ __publicField(this, "lastSortViewMatrix", new Float32Array(16));
9307
+ __publicField(this, "lastSortProjMatrix", new Float32Array(16));
9308
+ __publicField(this, "sortStateInitialized", false);
9150
9309
  // ============================================
9151
9310
  // 变换相关 (position, rotation, scale)
9152
9311
  // ============================================
@@ -9157,6 +9316,8 @@ class GSSplatRendererMobile {
9157
9316
  __publicField(this, "pivot", [0, 0, 0]);
9158
9317
  // 旋转/缩放中心点
9159
9318
  __publicField(this, "modelMatrix", new Float32Array(16));
9319
+ // 1x1 dummy SH 纹理(当无 SH 数据时使用)
9320
+ __publicField(this, "dummySHTexture", null);
9160
9321
  this.renderer = renderer;
9161
9322
  this.camera = camera;
9162
9323
  this.createPipeline();
@@ -9278,7 +9439,7 @@ class GSSplatRendererMobile {
9278
9439
  createPipeline() {
9279
9440
  const device = this.renderer.device;
9280
9441
  const shaderModule = device.createShaderModule({
9281
- code: shaderCodeMobileL0,
9442
+ code: shaderCodeMobile,
9282
9443
  label: "mobile-splat-shader"
9283
9444
  });
9284
9445
  this.uniformBindGroupLayout = device.createBindGroupLayout({
@@ -9295,32 +9456,27 @@ class GSSplatRendererMobile {
9295
9456
  }
9296
9457
  ]
9297
9458
  });
9459
+ const texEntry = (binding) => ({
9460
+ binding,
9461
+ visibility: GPUShaderStage.VERTEX,
9462
+ texture: { sampleType: "unfilterable-float" }
9463
+ });
9298
9464
  this.textureBindGroupLayout = device.createBindGroupLayout({
9299
9465
  entries: [
9300
- {
9301
- // positionTex (RGBA32Float)
9302
- binding: 0,
9303
- visibility: GPUShaderStage.VERTEX,
9304
- texture: { sampleType: "unfilterable-float" }
9305
- },
9306
- {
9307
- // scaleRotTex1 (RGBA32Float)
9308
- binding: 1,
9309
- visibility: GPUShaderStage.VERTEX,
9310
- texture: { sampleType: "unfilterable-float" }
9311
- },
9312
- {
9313
- // scaleRotTex2 (RGBA32Float)
9314
- binding: 2,
9315
- visibility: GPUShaderStage.VERTEX,
9316
- texture: { sampleType: "unfilterable-float" }
9317
- },
9318
- {
9319
- // colorTex (RGBA8Unorm)
9320
- binding: 3,
9321
- visibility: GPUShaderStage.VERTEX,
9322
- texture: { sampleType: "unfilterable-float" }
9323
- }
9466
+ texEntry(0),
9467
+ // positionTex
9468
+ texEntry(1),
9469
+ // scaleRotTex1
9470
+ texEntry(2),
9471
+ // scaleRotTex2
9472
+ texEntry(3),
9473
+ // colorTex
9474
+ texEntry(4),
9475
+ // shBasis0Tex
9476
+ texEntry(5),
9477
+ // shBasis1Tex
9478
+ texEntry(6)
9479
+ // shBasis2Tex
9324
9480
  ]
9325
9481
  });
9326
9482
  const pipelineLayout = device.createPipelineLayout({
@@ -9364,10 +9520,16 @@ class GSSplatRendererMobile {
9364
9520
  depthCompare: "always"
9365
9521
  }
9366
9522
  });
9523
+ this.dummySHTexture = device.createTexture({
9524
+ size: { width: 1, height: 1 },
9525
+ format: "rgba32float",
9526
+ usage: GPUTextureUsage.TEXTURE_BINDING,
9527
+ label: "dummy-sh"
9528
+ });
9367
9529
  }
9368
9530
  /**
9369
9531
  * 创建 uniform buffer
9370
- * 布局: view (64) + proj (64) + model (64) + cameraPos (12) + pad (4) + screenSize (8) + pad (8) + textureSize (8) + pad (8) = 240 bytes
9532
+ * 布局: view(64) + proj(64) + model(64) + cameraPos(12)+pad(4) + screenSize(8)+pad(8) + textureSize(8)+shEnabled(4)+pad(4) = 240 bytes
9371
9533
  */
9372
9534
  createUniformBuffer() {
9373
9535
  this.uniformBuffer = this.renderer.device.createBuffer({
@@ -9413,6 +9575,7 @@ class GSSplatRendererMobile {
9413
9575
  const memoryMB = (this.compressedTextures.width * this.compressedTextures.height * 52 / // 约 52 bytes per texel (16+16+16+4)
9414
9576
  (1024 * 1024)).toFixed(2);
9415
9577
  } catch (error) {
9578
+ console.error("[GSSplatRendererMobile] setCompactData failed:", error);
9416
9579
  this.splatCount = 0;
9417
9580
  this.compressedTextures = null;
9418
9581
  this.sorter = null;
@@ -9427,35 +9590,22 @@ class GSSplatRendererMobile {
9427
9590
  this.uniformBindGroup = device.createBindGroup({
9428
9591
  layout: this.uniformBindGroupLayout,
9429
9592
  entries: [
9430
- {
9431
- binding: 0,
9432
- resource: { buffer: this.uniformBuffer }
9433
- },
9434
- {
9435
- binding: 1,
9436
- resource: { buffer: this.sorter.getIndicesBuffer() }
9437
- }
9593
+ { binding: 0, resource: { buffer: this.uniformBuffer } },
9594
+ { binding: 1, resource: { buffer: this.sorter.getIndicesBuffer() } }
9438
9595
  ]
9439
9596
  });
9597
+ const dummyView = this.dummySHTexture.createView();
9598
+ const tex = this.compressedTextures;
9440
9599
  this.textureBindGroup = device.createBindGroup({
9441
9600
  layout: this.textureBindGroupLayout,
9442
9601
  entries: [
9443
- {
9444
- binding: 0,
9445
- resource: this.compressedTextures.positionTexture.createView()
9446
- },
9447
- {
9448
- binding: 1,
9449
- resource: this.compressedTextures.scaleRotTexture1.createView()
9450
- },
9451
- {
9452
- binding: 2,
9453
- resource: this.compressedTextures.scaleRotTexture2.createView()
9454
- },
9455
- {
9456
- binding: 3,
9457
- resource: this.compressedTextures.colorTexture.createView()
9458
- }
9602
+ { binding: 0, resource: tex.positionTexture.createView() },
9603
+ { binding: 1, resource: tex.scaleRotTexture1.createView() },
9604
+ { binding: 2, resource: tex.scaleRotTexture2.createView() },
9605
+ { binding: 3, resource: tex.colorTexture.createView() },
9606
+ { binding: 4, resource: tex.shBasis0Texture ? tex.shBasis0Texture.createView() : dummyView },
9607
+ { binding: 5, resource: tex.shBasis1Texture ? tex.shBasis1Texture.createView() : dummyView },
9608
+ { binding: 6, resource: tex.shBasis2Texture ? tex.shBasis2Texture.createView() : dummyView }
9459
9609
  ]
9460
9610
  });
9461
9611
  }
@@ -9510,10 +9660,11 @@ class GSSplatRendererMobile {
9510
9660
  208,
9511
9661
  new Float32Array([this.renderer.width, this.renderer.height, 0, 0])
9512
9662
  );
9663
+ const shEnabled = this.compressedTextures.hasSH ? 1 : 0;
9513
9664
  device.queue.writeBuffer(
9514
9665
  this.uniformBuffer,
9515
9666
  224,
9516
- new Float32Array([this.compressedTextures.width, this.compressedTextures.height, 0, 0])
9667
+ new Float32Array([this.compressedTextures.width, this.compressedTextures.height, shEnabled, 0])
9517
9668
  );
9518
9669
  this.sorter.setScreenSize(this.renderer.width, this.renderer.height);
9519
9670
  this.sorter.setCullingOptions({
@@ -9522,7 +9673,8 @@ class GSSplatRendererMobile {
9522
9673
  pixelThreshold: 1
9523
9674
  });
9524
9675
  const isFirstFrame = this.frameCount === 1;
9525
- const shouldSort = isFirstFrame || this.frameCount % this.sortEveryNFrames === 0;
9676
+ const cameraChanged = this.hasCameraChanged();
9677
+ const shouldSort = isFirstFrame || cameraChanged && this.frameCount % this.sortEveryNFrames === 0;
9526
9678
  if (shouldSort) {
9527
9679
  this.sorter.sort();
9528
9680
  }
@@ -9553,36 +9705,42 @@ class GSSplatRendererMobile {
9553
9705
  setSortFrequency(n) {
9554
9706
  this.sortEveryNFrames = Math.max(1, n);
9555
9707
  }
9708
+ hasCameraChanged() {
9709
+ const view = this.camera.viewMatrix;
9710
+ const proj = this.camera.projectionMatrix;
9711
+ if (!this.sortStateInitialized) {
9712
+ this.lastSortViewMatrix.set(view);
9713
+ this.lastSortProjMatrix.set(proj);
9714
+ this.sortStateInitialized = true;
9715
+ return true;
9716
+ }
9717
+ for (let i = 0; i < 16; i++) {
9718
+ if (Math.abs(view[i] - this.lastSortViewMatrix[i]) > 1e-6 || Math.abs(proj[i] - this.lastSortProjMatrix[i]) > 1e-6) {
9719
+ this.lastSortViewMatrix.set(view);
9720
+ this.lastSortProjMatrix.set(proj);
9721
+ return true;
9722
+ }
9723
+ }
9724
+ return false;
9725
+ }
9556
9726
  // ============================================
9557
9727
  // IGSSplatRenderer 接口实现 - SH 模式
9558
9728
  // ============================================
9559
- /**
9560
- * 设置 SH 模式(移动端仅支持 L0)
9561
- */
9562
- setSHMode(mode) {
9729
+ setSHMode(_mode) {
9563
9730
  }
9564
- /**
9565
- * 获取当前 SH 模式
9566
- */
9567
9731
  getSHMode() {
9568
- return SHMode.L0;
9732
+ var _a2;
9733
+ return ((_a2 = this.compressedTextures) == null ? void 0 : _a2.hasSH) ? SHMode.L1 : SHMode.L0;
9569
9734
  }
9570
- /**
9571
- * 是否支持指定的 SH 模式
9572
- */
9573
9735
  supportsSHMode(mode) {
9574
- return mode === SHMode.L0;
9736
+ return mode === SHMode.L0 || mode === SHMode.L1;
9575
9737
  }
9576
- /**
9577
- * 获取渲染器能力
9578
- */
9579
9738
  getCapabilities() {
9580
9739
  return {
9581
- maxSHMode: SHMode.L0,
9740
+ maxSHMode: SHMode.L1,
9582
9741
  supportsRawData: false,
9583
9742
  isMobileOptimized: true,
9584
9743
  maxSplatCount: 0
9585
- // 无限制(受 GPU 内存限制)
9586
9744
  };
9587
9745
  }
9588
9746
  /**
@@ -9611,6 +9769,10 @@ class GSSplatRendererMobile {
9611
9769
  */
9612
9770
  destroy() {
9613
9771
  this.destroyInternal();
9772
+ if (this.dummySHTexture) {
9773
+ this.dummySHTexture.destroy();
9774
+ this.dummySHTexture = null;
9775
+ }
9614
9776
  }
9615
9777
  }
9616
9778
  class SceneManager {
@@ -16346,6 +16508,7 @@ class App {
16346
16508
  */
16347
16509
  async addSplat(urlOrBuffer, onProgress, isLocalFile = false, coordinateSystem = "blender") {
16348
16510
  try {
16511
+ const isMobile = isMobileDevice();
16349
16512
  let buffer;
16350
16513
  if (typeof urlOrBuffer === "string") {
16351
16514
  buffer = await this.fetchWithProgress(urlOrBuffer, (downloadProgress) => {
@@ -16364,26 +16527,67 @@ class App {
16364
16527
  if (coordinateSystem === "blender") {
16365
16528
  for (const s of splats) {
16366
16529
  const [mx, my, mz] = s.mean;
16367
- s.mean = [mx, mz, my];
16530
+ s.mean = [mx, mz, -my];
16368
16531
  const [sx, sy, sz] = s.scale;
16369
16532
  s.scale = [sx, sz, sy];
16370
16533
  const [rw, rx, ry, rz] = s.rotation;
16371
- s.rotation = [-rw, rx, rz, ry];
16534
+ s.rotation = [rw, rx, rz, -ry];
16372
16535
  }
16373
16536
  }
16374
16537
  if (onProgress) onProgress(90, "parse");
16375
16538
  if (onProgress) onProgress(90, "upload");
16376
- const gsRenderer = new GSSplatRenderer(this.renderer, this.camera);
16377
- gsRenderer.setData(splats);
16539
+ let gsRenderer;
16540
+ if (isMobile) {
16541
+ const mobileRenderer = new GSSplatRendererMobile(this.renderer, this.camera);
16542
+ this.useMobileRenderer = true;
16543
+ const compactData = App.splatCpuToCompactData(splats);
16544
+ mobileRenderer.setCompactData(compactData);
16545
+ this.lastCompactData = compactData;
16546
+ gsRenderer = mobileRenderer;
16547
+ } else {
16548
+ const desktopRenderer = new GSSplatRenderer(this.renderer, this.camera);
16549
+ this.useMobileRenderer = false;
16550
+ desktopRenderer.setData(splats);
16551
+ gsRenderer = desktopRenderer;
16552
+ }
16378
16553
  this.sceneManager.setGSRenderer(gsRenderer);
16379
16554
  this.hotspotManager.setGSRenderer(gsRenderer);
16380
- this.useMobileRenderer = false;
16381
16555
  if (onProgress) onProgress(100, "upload");
16382
16556
  return splats.length;
16383
16557
  } catch (error) {
16384
16558
  throw error;
16385
16559
  }
16386
16560
  }
16561
+ /**
16562
+ * 将 SplatCPU[] 转换为 CompactSplatData(用于移动端渲染器)
16563
+ */
16564
+ static splatCpuToCompactData(splats) {
16565
+ const count = splats.length;
16566
+ const positions = new Float32Array(count * 3);
16567
+ const scales = new Float32Array(count * 3);
16568
+ const rotations = new Float32Array(count * 4);
16569
+ const colors = new Float32Array(count * 3);
16570
+ const opacities = new Float32Array(count);
16571
+ for (let i = 0; i < count; i++) {
16572
+ const s = splats[i];
16573
+ const i3 = i * 3, i4 = i * 4;
16574
+ positions[i3] = s.mean[0];
16575
+ positions[i3 + 1] = s.mean[1];
16576
+ positions[i3 + 2] = s.mean[2];
16577
+ scales[i3] = s.scale[0];
16578
+ scales[i3 + 1] = s.scale[1];
16579
+ scales[i3 + 2] = s.scale[2];
16580
+ rotations[i4] = s.rotation[0];
16581
+ rotations[i4 + 1] = s.rotation[1];
16582
+ rotations[i4 + 2] = s.rotation[2];
16583
+ rotations[i4 + 3] = s.rotation[3];
16584
+ colors[i3] = s.colorDC[0];
16585
+ colors[i3 + 1] = s.colorDC[1];
16586
+ colors[i3 + 2] = s.colorDC[2];
16587
+ opacities[i] = s.opacity;
16588
+ }
16589
+ return { count, positions, scales, rotations, colors, opacities };
16590
+ }
16387
16591
  /**
16388
16592
  * 加载 SOG 文件 (Spatially Ordered Gaussians)
16389
16593
  * @param coordinateSystem 源数据坐标系,默认 'blender'(Z-up → Y-up 自动转换)
@@ -16416,14 +16620,13 @@ class App {
16416
16620
  const i3 = i * 3, i4 = i * 4;
16417
16621
  const py = positions[i3 + 1], pz = positions[i3 + 2];
16418
16622
  positions[i3 + 1] = pz;
16419
- positions[i3 + 2] = py;
16623
+ positions[i3 + 2] = -py;
16420
16624
  const sy = scales[i3 + 1], sz = scales[i3 + 2];
16421
16625
  scales[i3 + 1] = sz;
16422
16626
  scales[i3 + 2] = sy;
16423
- const rw = rotations[i4], ry = rotations[i4 + 2], rz = rotations[i4 + 3];
16424
- rotations[i4] = -rw;
16627
+ const ry = rotations[i4 + 2], rz = rotations[i4 + 3];
16425
16628
  rotations[i4 + 2] = rz;
16426
- rotations[i4 + 3] = ry;
16629
+ rotations[i4 + 3] = -ry;
16427
16630
  if (shCoeffs) {
16428
16631
  transformSHCoeffsYZSwap(shCoeffs, i * 45, 15);
16429
16632
  }
@@ -16848,6 +17051,63 @@ class App {
16848
17051
  getSceneAidsRenderer() {
16849
17052
  return this.sceneAids;
16850
17053
  }
17054
+ // ============================================
17055
+ // 渲染性能控制
17056
+ // ============================================
17057
+ /**
17058
+ * 设置渲染缩放比例(影响内部分辨率)
17059
+ * 0.5 = 半分辨率(性能提升约 4 倍),1.0 = 正常
17060
+ * 适用于移动端提质或桌面端降负载
17061
+ */
17062
+ setRenderScale(scale) {
17063
+ this.renderer.setRenderScale(scale);
17064
+ }
17065
+ getRenderScale() {
17066
+ return this.renderer.getRenderScale();
17067
+ }
17068
+ /**
17069
+ * 覆盖自动 DPR,传 -1 恢复自动推荐
17070
+ */
17071
+ setDPR(dpr) {
17072
+ this.renderer.setDPR(dpr);
17073
+ }
17074
+ getDPR() {
17075
+ return this.renderer.getDPR();
17076
+ }
17077
+ /**
17078
+ * 设置桌面端亚像素剔除阈值(默认 1.0)
17079
+ * 值越大剔除越激进,近距离性能越好,但远处细节可能丢失
17080
+ */
17081
+ setPixelCullThreshold(threshold) {
17082
+ const gsRenderer = this.getGSRenderer();
17083
+ if (gsRenderer) {
17084
+ gsRenderer.setPixelCullThreshold(threshold);
17085
+ }
17086
+ }
17087
+ /**
17088
+ * 设置桌面端最大可见 splat 数(0 = 不限制)
17089
+ * 限制绘制数量是应对近距离卡顿最直接的手段
17090
+ */
17091
+ setMaxVisibleSplats(count) {
17092
+ const gsRenderer = this.getGSRenderer();
17093
+ if (gsRenderer) {
17094
+ gsRenderer.setMaxVisibleSplats(count);
17095
+ }
17096
+ }
17097
+ /**
17098
+ * 设置排序频率(1 = 每帧,2 = 每两帧,以此类推)
17099
+ * 降低排序频率可提升帧率,代价是移动时短暂排序瑕疵
17100
+ */
17101
+ setSortFrequency(frequency) {
17102
+ const gsRenderer = this.getGSRenderer();
17103
+ if (gsRenderer) {
17104
+ gsRenderer.setSortFrequency(frequency);
17105
+ }
17106
+ const mobileRenderer = this.getGSRendererMobile();
17107
+ if (mobileRenderer) {
17108
+ mobileRenderer.setSortFrequency(frequency);
17109
+ }
17110
+ }
16851
17111
  /**
16852
17112
  * 销毁应用及所有资源
16853
17113
  */