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