@d5techs/3dgs-lib 1.4.14 → 1.4.16

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
@@ -263,6 +263,10 @@ class Renderer {
263
263
  __publicField(this, "_lastCSSHeight", 0);
264
264
  // 背景颜色
265
265
  __publicField(this, "_clearColor", { r: 0.15, g: 0.15, b: 0.15, a: 1 });
266
+ // GPU 平台信息
267
+ __publicField(this, "_isAppleGPU", false);
268
+ __publicField(this, "_gpuVendor", "");
269
+ __publicField(this, "_gpuArchitecture", "");
266
270
  this.canvas = canvas;
267
271
  }
268
272
  /**
@@ -306,6 +310,15 @@ class Renderer {
306
310
  get depthFormat() {
307
311
  return "depth24plus";
308
312
  }
313
+ get isAppleGPU() {
314
+ return this._isAppleGPU;
315
+ }
316
+ get gpuVendor() {
317
+ return this._gpuVendor;
318
+ }
319
+ get gpuArchitecture() {
320
+ return this._gpuArchitecture;
321
+ }
309
322
  /**
310
323
  * 获取渲染宽度(像素)
311
324
  */
@@ -331,6 +344,17 @@ class Renderer {
331
344
  if (!adapter) {
332
345
  throw new Error("无法获取 GPU 适配器");
333
346
  }
347
+ try {
348
+ const adapterAny = adapter;
349
+ const adapterInfo = adapterAny.requestAdapterInfo ? await adapterAny.requestAdapterInfo() : adapterAny.info;
350
+ if (adapterInfo) {
351
+ this._gpuVendor = (adapterInfo.vendor ?? "").toLowerCase();
352
+ this._gpuArchitecture = (adapterInfo.architecture ?? "").toLowerCase();
353
+ }
354
+ this._isAppleGPU = this._gpuVendor.includes("apple") || typeof navigator !== "undefined" && /mac/i.test(navigator.platform ?? "");
355
+ } catch {
356
+ this._isAppleGPU = typeof navigator !== "undefined" && /mac/i.test(navigator.platform ?? "");
357
+ }
334
358
  const adapterLimits = adapter.limits;
335
359
  this._device = await adapter.requestDevice({
336
360
  requiredLimits: {
@@ -537,7 +561,7 @@ class Camera {
537
561
  this.viewMatrix[15] = 1;
538
562
  }
539
563
  /**
540
- * 计算投影矩阵 (透视投影)
564
+ * 计算投影矩阵 (透视投影, OpenGL [-1,1] 深度)
541
565
  */
542
566
  updateProjectionMatrix() {
543
567
  const f = 1 / Math.tan(this.fov / 2);
@@ -586,7 +610,7 @@ class Camera {
586
610
  }
587
611
  }
588
612
  }
589
- class OrbitControls {
613
+ const _OrbitControls = class _OrbitControls {
590
614
  constructor(camera, canvas) {
591
615
  __publicField(this, "camera");
592
616
  __publicField(this, "canvas");
@@ -620,6 +644,9 @@ class OrbitControls {
620
644
  __publicField(this, "velocityPanX", 0);
621
645
  __publicField(this, "velocityPanY", 0);
622
646
  __publicField(this, "velocityPanZ", 0);
647
+ // 键盘移动
648
+ __publicField(this, "moveSpeed", 0.015);
649
+ __publicField(this, "pressedKeys", /* @__PURE__ */ new Set());
623
650
  // 触摸手势状态
624
651
  __publicField(this, "touchMode", "none");
625
652
  __publicField(this, "lastTouchDistance", 0);
@@ -631,6 +658,9 @@ class OrbitControls {
631
658
  __publicField(this, "boundOnMouseMove");
632
659
  __publicField(this, "boundOnMouseUp");
633
660
  __publicField(this, "boundOnWheel");
661
+ __publicField(this, "boundOnDblClick");
662
+ __publicField(this, "boundOnKeyDown");
663
+ __publicField(this, "boundOnKeyUp");
634
664
  __publicField(this, "boundOnTouchStart");
635
665
  __publicField(this, "boundOnTouchMove");
636
666
  __publicField(this, "boundOnTouchEnd");
@@ -641,6 +671,9 @@ class OrbitControls {
641
671
  this.boundOnMouseMove = this.onMouseMove.bind(this);
642
672
  this.boundOnMouseUp = this.onMouseUp.bind(this);
643
673
  this.boundOnWheel = this.onWheel.bind(this);
674
+ this.boundOnDblClick = this.onDblClick.bind(this);
675
+ this.boundOnKeyDown = this.onKeyDown.bind(this);
676
+ this.boundOnKeyUp = this.onKeyUp.bind(this);
644
677
  this.boundOnTouchStart = this.onTouchStart.bind(this);
645
678
  this.boundOnTouchMove = this.onTouchMove.bind(this);
646
679
  this.boundOnTouchEnd = this.onTouchEnd.bind(this);
@@ -664,6 +697,9 @@ class OrbitControls {
664
697
  });
665
698
  this.canvas.addEventListener("touchend", this.boundOnTouchEnd);
666
699
  this.canvas.addEventListener("contextmenu", this.boundOnContextMenu);
700
+ this.canvas.addEventListener("dblclick", this.boundOnDblClick);
701
+ window.addEventListener("keydown", this.boundOnKeyDown);
702
+ window.addEventListener("keyup", this.boundOnKeyUp);
667
703
  }
668
704
  removeEventListeners() {
669
705
  this.canvas.removeEventListener("mousedown", this.boundOnMouseDown);
@@ -675,6 +711,9 @@ class OrbitControls {
675
711
  this.canvas.removeEventListener("touchmove", this.boundOnTouchMove);
676
712
  this.canvas.removeEventListener("touchend", this.boundOnTouchEnd);
677
713
  this.canvas.removeEventListener("contextmenu", this.boundOnContextMenu);
714
+ this.canvas.removeEventListener("dblclick", this.boundOnDblClick);
715
+ window.removeEventListener("keydown", this.boundOnKeyDown);
716
+ window.removeEventListener("keyup", this.boundOnKeyUp);
678
717
  }
679
718
  destroy() {
680
719
  this.removeEventListeners();
@@ -753,6 +792,100 @@ class OrbitControls {
753
792
  this.applySpherical();
754
793
  }
755
794
  }
795
+ onKeyDown(e) {
796
+ var _a2;
797
+ if (!this.enabled) return;
798
+ const tag = (_a2 = e.target) == null ? void 0 : _a2.tagName;
799
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
800
+ const key = e.key.toLowerCase();
801
+ if (_OrbitControls.MOVE_KEYS.has(key)) {
802
+ this.pressedKeys.add(key);
803
+ e.preventDefault();
804
+ }
805
+ }
806
+ onKeyUp(e) {
807
+ this.pressedKeys.delete(e.key.toLowerCase());
808
+ }
809
+ applyKeyboardMovement() {
810
+ if (this.pressedKeys.size === 0) return;
811
+ const m = this.camera.viewMatrix;
812
+ const right = [m[0], m[4], m[8]];
813
+ const forward = [-m[2], -m[6], -m[10]];
814
+ const speed = this.moveSpeed * this.distance;
815
+ let dx = 0, dy = 0, dz = 0;
816
+ if (this.pressedKeys.has("w") || this.pressedKeys.has("arrowup")) {
817
+ dx += forward[0] * speed;
818
+ dy += forward[1] * speed;
819
+ dz += forward[2] * speed;
820
+ }
821
+ if (this.pressedKeys.has("s") || this.pressedKeys.has("arrowdown")) {
822
+ dx -= forward[0] * speed;
823
+ dy -= forward[1] * speed;
824
+ dz -= forward[2] * speed;
825
+ }
826
+ if (this.pressedKeys.has("a") || this.pressedKeys.has("arrowleft")) {
827
+ dx -= right[0] * speed;
828
+ dy -= right[1] * speed;
829
+ dz -= right[2] * speed;
830
+ }
831
+ if (this.pressedKeys.has("d") || this.pressedKeys.has("arrowright")) {
832
+ dx += right[0] * speed;
833
+ dy += right[1] * speed;
834
+ dz += right[2] * speed;
835
+ }
836
+ this.camera.target[0] += dx;
837
+ this.camera.target[1] += dy;
838
+ this.camera.target[2] += dz;
839
+ }
840
+ onDblClick(e) {
841
+ if (!this.enabled) return;
842
+ const rect = this.canvas.getBoundingClientRect();
843
+ const ndcX = (e.clientX - rect.left) / rect.width * 2 - 1;
844
+ const ndcY = -((e.clientY - rect.top) / rect.height * 2 - 1);
845
+ const m = this.camera.viewMatrix;
846
+ const right = [m[0], m[4], m[8]];
847
+ const up = [m[1], m[5], m[9]];
848
+ const forward = [-m[2], -m[6], -m[10]];
849
+ const halfFovTan = Math.tan(this.camera.fov / 2);
850
+ const aspect = this.camera.aspect;
851
+ const dirX = forward[0] + right[0] * ndcX * halfFovTan * aspect + up[0] * ndcY * halfFovTan;
852
+ const dirY = forward[1] + right[1] * ndcX * halfFovTan * aspect + up[1] * ndcY * halfFovTan;
853
+ const dirZ = forward[2] + right[2] * ndcX * halfFovTan * aspect + up[2] * ndcY * halfFovTan;
854
+ const len = Math.sqrt(dirX * dirX + dirY * dirY + dirZ * dirZ);
855
+ const newTarget = [
856
+ this.camera.position[0] + dirX / len * this.distance,
857
+ this.camera.position[1] + dirY / len * this.distance,
858
+ this.camera.position[2] + dirZ / len * this.distance
859
+ ];
860
+ this.animateToTarget(newTarget);
861
+ }
862
+ animateToTarget(target) {
863
+ this.clearVelocity();
864
+ const startTarget = [
865
+ this.camera.target[0],
866
+ this.camera.target[1],
867
+ this.camera.target[2]
868
+ ];
869
+ const startTheta = this.theta;
870
+ const startPhi = this.phi;
871
+ const duration = 300;
872
+ const startTime = performance.now();
873
+ const animate = (currentTime) => {
874
+ const elapsed = currentTime - startTime;
875
+ const progress = Math.min(elapsed / duration, 1);
876
+ const eased = 1 - Math.pow(1 - progress, 3);
877
+ this.camera.target[0] = startTarget[0] + (target[0] - startTarget[0]) * eased;
878
+ this.camera.target[1] = startTarget[1] + (target[1] - startTarget[1]) * eased;
879
+ this.camera.target[2] = startTarget[2] + (target[2] - startTarget[2]) * eased;
880
+ this.theta = startTheta;
881
+ this.phi = startPhi;
882
+ this.applySpherical();
883
+ if (progress < 1) {
884
+ requestAnimationFrame(animate);
885
+ }
886
+ };
887
+ requestAnimationFrame(animate);
888
+ }
756
889
  onTouchStart(e) {
757
890
  e.preventDefault();
758
891
  if (!this.enabled) return;
@@ -863,6 +996,7 @@ class OrbitControls {
863
996
  * 在渲染循环中调用此方法以获得平滑惯性效果
864
997
  */
865
998
  update() {
999
+ this.applyKeyboardMovement();
866
1000
  if (this.enableDamping) {
867
1001
  const EPS = 1e-6;
868
1002
  const f = this.dampingFactor;
@@ -1002,7 +1136,19 @@ class OrbitControls {
1002
1136
  this.velocityPanY = 0;
1003
1137
  this.velocityPanZ = 0;
1004
1138
  }
1005
- }
1139
+ };
1140
+ /** 移动键 */
1141
+ __publicField(_OrbitControls, "MOVE_KEYS", /* @__PURE__ */ new Set([
1142
+ "w",
1143
+ "a",
1144
+ "s",
1145
+ "d",
1146
+ "arrowup",
1147
+ "arrowdown",
1148
+ "arrowleft",
1149
+ "arrowright"
1150
+ ]));
1151
+ let OrbitControls = _OrbitControls;
1006
1152
  class ViewportGizmo {
1007
1153
  constructor(_renderer, camera, canvas) {
1008
1154
  __publicField(this, "camera");
@@ -5549,6 +5695,8 @@ struct CullingParams {
5549
5695
  frustumDilation: f32,
5550
5696
  pixelThreshold: f32,
5551
5697
  maxVisibleCount: u32,
5698
+ depthRangeLimit: f32,
5699
+ _pad_cull: f32,
5552
5700
  }
5553
5701
 
5554
5702
  @group(0) @binding(0) var<storage, read> splats: array<Splat>;
@@ -5620,6 +5768,11 @@ fn projectAndCull(@builtin(global_invocation_id) gid: vec3<u32>) {
5620
5768
  if projectedExtent < params.pixelThreshold { return; }
5621
5769
  }
5622
5770
 
5771
+ // 深度范围限制:只渲染距相机一定深度范围内的 splat
5772
+ if params.depthRangeLimit > 0.0 {
5773
+ if abs(viewPos.z) > params.depthRangeLimit { return; }
5774
+ }
5775
+
5623
5776
  // 深度编码 (viewPos.z 是负数)
5624
5777
  let depth = viewPos.z;
5625
5778
  let sortableDepth = encodeDepthKey(depth);
@@ -5642,8 +5795,7 @@ fn initIndirectBuffer() {
5642
5795
  atomicStore(&indirectBuffer[3], 0u);
5643
5796
  }
5644
5797
 
5645
- // 排序后截断:只保留最近的 maxVisibleCount 个 splat
5646
- // 因为 radix sort 是按深度从近到远排序,截断尾部等于丢弃被遮挡的远处 splat
5798
+ // 排序后截断:限制最大绘制数量
5647
5799
  @compute @workgroup_size(1)
5648
5800
  fn clampDrawCount() {
5649
5801
  let maxCount = params.maxVisibleCount;
@@ -5883,8 +6035,8 @@ fn spine(
5883
6035
 
5884
6036
  var<workgroup> localKeys: array<u32, BLOCK_SIZE>;
5885
6037
  var<workgroup> localValues: array<u32, BLOCK_SIZE>;
5886
- var<workgroup> localBins: array<u32, BLOCK_SIZE>;
5887
6038
  var<workgroup> binBasePos: array<u32, RADIX_SIZE>;
6039
+ var<workgroup> binCounter: array<u32, RADIX_SIZE>;
5888
6040
 
5889
6041
  @compute @workgroup_size(256, 1, 1)
5890
6042
  fn downsweep(
@@ -5903,50 +6055,40 @@ fn downsweep(
5903
6055
  let partitionEnd = min(partitionStart + BLOCK_SIZE, numKeys);
5904
6056
  let elemsInPartition = partitionEnd - partitionStart;
5905
6057
 
5906
- // Phase 1: 所有线程并行加载元素到共享内存
6058
+ // Phase 1: 所有 256 个线程并行加载元素到共享内存(合并全局内存访问)
5907
6059
  for (var j = 0u; j < ELEMENTS_PER_THREAD; j++) {
5908
6060
  let keyIdx = partitionStart + tid * ELEMENTS_PER_THREAD + j;
5909
6061
  let localIdx = tid * ELEMENTS_PER_THREAD + j;
5910
6062
 
5911
6063
  if keyIdx < numKeys {
5912
- let key = downsweepKeysIn[keyIdx];
5913
- localKeys[localIdx] = key;
6064
+ localKeys[localIdx] = downsweepKeysIn[keyIdx];
5914
6065
  localValues[localIdx] = downsweepValuesIn[keyIdx];
5915
- localBins[localIdx] = (key >> shift) & RADIX_MASK;
5916
- } else {
5917
- localBins[localIdx] = 0xFFFFFFFFu;
5918
6066
  }
5919
6067
  }
5920
6068
 
5921
- // Phase 2: 初始化 bin 基础写入位置(利用 256 线程并行)
6069
+ // Phase 2: 初始化 bin 基础写入位置 + 计数器归零
5922
6070
  if tid < RADIX_SIZE {
5923
6071
  let passIdx = downsweepParams.passIndex;
5924
6072
  binBasePos[tid] = globalHistogramDownsweep[RADIX_SIZE * passIdx + tid] +
5925
6073
  partitionHistogramDownsweep[RADIX_SIZE * partitionId + tid];
6074
+ binCounter[tid] = 0u;
5926
6075
  }
5927
6076
 
5928
6077
  workgroupBarrier();
5929
6078
 
5930
- // Phase 3: 并行计算排名并散射
5931
- // 稳定性保证:rank = 当前元素之前具有相同 bin 的元素数量
5932
- // 每个线程处理自己的 4 个元素,通过扫描 localBins 确定排名
5933
- for (var j = 0u; j < ELEMENTS_PER_THREAD; j++) {
5934
- let localIdx = tid * ELEMENTS_PER_THREAD + j;
5935
- if localIdx >= elemsInPartition { break; }
5936
-
5937
- let b = localBins[localIdx];
5938
- if b == 0xFFFFFFFFu { continue; }
5939
-
5940
- // 计算 rank:扫描本分区中在当前元素之前、且属于同一 bin 的元素数
5941
- var rank = 0u;
5942
- for (var p = 0u; p < localIdx; p++) {
5943
- if localBins[p] == b { rank++; }
5944
- }
5945
-
5946
- let writePos = binBasePos[b] + rank;
5947
- if writePos < numKeys {
5948
- downsweepKeysOut[writePos] = localKeys[localIdx];
5949
- downsweepValuesOut[writePos] = localValues[localIdx];
6079
+ // Phase 3: 单线程顺序计数散射 — O(n) 稳定
6080
+ // 按元素顺序遍历,用 counter 数组跟踪每个 bin 的写入偏移
6081
+ // 稳定性由遍历顺序保证,无需任何比较或原子操作
6082
+ if tid == 0u {
6083
+ for (var j = 0u; j < elemsInPartition; j++) {
6084
+ let key = localKeys[j];
6085
+ let b = (key >> shift) & RADIX_MASK;
6086
+ let writePos = binBasePos[b] + binCounter[b];
6087
+ binCounter[b] = binCounter[b] + 1u;
6088
+ if writePos < numKeys {
6089
+ downsweepKeysOut[writePos] = key;
6090
+ downsweepValuesOut[writePos] = localValues[j];
6091
+ }
5950
6092
  }
5951
6093
  }
5952
6094
  }
@@ -6010,7 +6152,7 @@ class GSSplatSorter {
6010
6152
  label: "radix-sort-shader"
6011
6153
  });
6012
6154
  this.cullingParamsBuffer = device.createBuffer({
6013
- size: 32,
6155
+ size: 48,
6014
6156
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
6015
6157
  label: "culling-params"
6016
6158
  });
@@ -6239,7 +6381,7 @@ class GSSplatSorter {
6239
6381
  * 每帧调用
6240
6382
  */
6241
6383
  sort() {
6242
- const cullingParamsData = new ArrayBuffer(32);
6384
+ const cullingParamsData = new ArrayBuffer(48);
6243
6385
  const view = new DataView(cullingParamsData);
6244
6386
  view.setUint32(0, this.splatCount, true);
6245
6387
  view.setFloat32(4, this.cullingOptions.nearPlane, true);
@@ -6249,6 +6391,7 @@ class GSSplatSorter {
6249
6391
  view.setFloat32(20, this.cullingOptions.frustumDilation ?? 0.2, true);
6250
6392
  view.setFloat32(24, this.cullingOptions.pixelThreshold, true);
6251
6393
  view.setUint32(28, this.cullingOptions.maxVisibleCount ?? 0, true);
6394
+ view.setFloat32(32, this.cullingOptions.depthRangeLimit ?? 0, true);
6252
6395
  this.device.queue.writeBuffer(this.cullingParamsBuffer, 0, cullingParamsData);
6253
6396
  const encoder = this.device.createCommandEncoder({ label: "splat-sort-encoder" });
6254
6397
  encoder.clearBuffer(this.globalHistogramBuffer);
@@ -6379,7 +6522,8 @@ struct Uniforms {
6379
6522
  cameraPos: vec3<f32>,
6380
6523
  _pad: f32,
6381
6524
  screenSize: vec2<f32>,
6382
- _pad2: vec2<f32>,
6525
+ shMode: u32,
6526
+ _pad2: f32,
6383
6527
  }
6384
6528
 
6385
6529
  struct Splat {
@@ -6394,10 +6538,13 @@ struct Splat {
6394
6538
  _pad2: array<f32, 3>,
6395
6539
  }
6396
6540
 
6397
- // 完整 L3 球谐函数求值 (匹配原始 3DGS Python 实现)
6398
- // SH 系数以 interleaved 格式存储: [R0,G0,B0, R1,G1,B1, ...]
6399
- // dir: 从相机指向 splat 的归一化方向向量(模型空间)
6541
+ // SH_LEVEL createPipeline 时注入为编译期常量,GPU 编译器会折叠死代码
6542
+ // L3 模式下生成的 GPU 指令与无分支版本完全一致
6543
+ const SH_LEVEL: u32 = 3u; // @SH_LEVEL_INJECT@
6544
+
6400
6545
  fn evalSH(splat: Splat, dir: vec3<f32>) -> vec3<f32> {
6546
+ if SH_LEVEL == 0u { return vec3<f32>(0.0); }
6547
+
6401
6548
  let x = dir.x;
6402
6549
  let y = dir.y;
6403
6550
  let z = dir.z;
@@ -6409,6 +6556,8 @@ fn evalSH(splat: Splat, dir: vec3<f32>) -> vec3<f32> {
6409
6556
  result += ( SH_C1 * z) * vec3<f32>(splat.sh1[3], splat.sh1[4], splat.sh1[5]);
6410
6557
  result += (-SH_C1 * x) * vec3<f32>(splat.sh1[6], splat.sh1[7], splat.sh1[8]);
6411
6558
 
6559
+ if SH_LEVEL == 1u { return result; }
6560
+
6412
6561
  // L2: 5 个基函数
6413
6562
  let xx = x * x; let yy = y * y; let zz = z * z;
6414
6563
  let xy = x * y; let yz = y * z; let xz = x * z;
@@ -6419,6 +6568,8 @@ fn evalSH(splat: Splat, dir: vec3<f32>) -> vec3<f32> {
6419
6568
  result += (SH_C2_3 * xz) * vec3<f32>(splat.sh2[9], splat.sh2[10], splat.sh2[11]);
6420
6569
  result += (SH_C2_4 * (xx - yy)) * vec3<f32>(splat.sh2[12], splat.sh2[13], splat.sh2[14]);
6421
6570
 
6571
+ if SH_LEVEL == 2u { return result; }
6572
+
6422
6573
  // L3: 7 个基函数
6423
6574
  result += (SH_C3_0 * y * (3.0 * xx - yy)) * vec3<f32>(splat.sh3[0], splat.sh3[1], splat.sh3[2]);
6424
6575
  result += (SH_C3_1 * xy * z) * vec3<f32>(splat.sh3[3], splat.sh3[4], splat.sh3[5]);
@@ -6728,7 +6879,8 @@ struct Uniforms {
6728
6879
  cameraPos: vec3<f32>,
6729
6880
  _pad: f32,
6730
6881
  screenSize: vec2<f32>,
6731
- _pad2: vec2<f32>,
6882
+ shMode: u32,
6883
+ _pad2: f32,
6732
6884
  }
6733
6885
 
6734
6886
  struct Splat {
@@ -6976,6 +7128,8 @@ const _GSSplatRenderer = class _GSSplatRenderer {
6976
7128
  __publicField(this, "sorter", null);
6977
7129
  __publicField(this, "shMode", SHMode.L3);
6978
7130
  __publicField(this, "boundingBox", null);
7131
+ // 预分配 uniform 上传缓冲区,避免每帧 GC(56 floats = 224 bytes)
7132
+ __publicField(this, "uniformData", new Float32Array(56));
6979
7133
  __publicField(this, "cpuPositions", null);
6980
7134
  // Transform
6981
7135
  __publicField(this, "position", [0, 0, 0]);
@@ -6986,6 +7140,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
6986
7140
  // 剔除选项
6987
7141
  __publicField(this, "pixelCullThreshold", 1);
6988
7142
  __publicField(this, "maxVisibleSplats", 0);
7143
+ __publicField(this, "depthRangeLimit", 0);
6989
7144
  // 排序优化:相机变化检测 + 频率控制
6990
7145
  __publicField(this, "lastSortViewMatrix", new Float32Array(16));
6991
7146
  __publicField(this, "lastSortProjMatrix", new Float32Array(16));
@@ -6994,6 +7149,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
6994
7149
  __publicField(this, "lastSortHeight", 0);
6995
7150
  __publicField(this, "lastSortPixelThreshold", -1);
6996
7151
  __publicField(this, "lastSortMaxVisible", -1);
7152
+ __publicField(this, "lastSortDepthRange", -1);
6997
7153
  __publicField(this, "sortStateInitialized", false);
6998
7154
  __publicField(this, "sortFrequency", 1);
6999
7155
  __publicField(this, "frameCounter", 0);
@@ -7003,6 +7159,8 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7003
7159
  __publicField(this, "editorBindGroupLayout", null);
7004
7160
  __publicField(this, "editorBindGroup", null);
7005
7161
  __publicField(this, "editorEnabled", false);
7162
+ // Apple GPU 优化:禁用深度写入让 TBDR HSR 生效
7163
+ __publicField(this, "depthWriteEnabled", true);
7006
7164
  // 深度法线Pass依赖资源
7007
7165
  __publicField(this, "depthNormalPipeline", null);
7008
7166
  __publicField(this, "depthRT", null);
@@ -7021,8 +7179,12 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7021
7179
  }
7022
7180
  createPipeline() {
7023
7181
  const device = this.renderer.device;
7182
+ const shaderCode = gsOptimizedShader.replace(
7183
+ "const SH_LEVEL: u32 = 3u; // @SH_LEVEL_INJECT@",
7184
+ `const SH_LEVEL: u32 = ${this.shMode}u;`
7185
+ );
7024
7186
  const shaderModule = device.createShaderModule({
7025
- code: gsOptimizedShader
7187
+ code: shaderCode
7026
7188
  });
7027
7189
  this.bindGroupLayout = device.createBindGroupLayout({
7028
7190
  entries: [
@@ -7079,11 +7241,16 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7079
7241
  },
7080
7242
  depthStencil: {
7081
7243
  format: this.renderer.depthFormat,
7082
- depthWriteEnabled: true,
7244
+ depthWriteEnabled: this.depthWriteEnabled,
7083
7245
  depthCompare: "always"
7084
7246
  }
7085
7247
  });
7086
7248
  }
7249
+ setDepthWriteEnabled(enabled) {
7250
+ if (this.depthWriteEnabled === enabled) return;
7251
+ this.depthWriteEnabled = enabled;
7252
+ this.createPipeline();
7253
+ }
7087
7254
  createUniformBuffer() {
7088
7255
  this.uniformBuffer = this.renderer.device.createBuffer({
7089
7256
  size: 224,
@@ -7095,7 +7262,9 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7095
7262
  // A采用"zero add one-minus-src-alpha"的混合模式计算Transmittance
7096
7263
  createDepthNormalPipeline() {
7097
7264
  const device = this.renderer.device;
7098
- const shaderModule = device.createShaderModule({ code: gsDepthNormalShader });
7265
+ const shaderModule = device.createShaderModule({
7266
+ code: gsDepthNormalShader
7267
+ });
7099
7268
  const pipelineLayout = device.createPipelineLayout({
7100
7269
  bindGroupLayouts: [this.bindGroupLayout]
7101
7270
  });
@@ -7113,15 +7282,31 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7113
7282
  {
7114
7283
  format: "rgba16float",
7115
7284
  blend: {
7116
- color: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
7117
- alpha: { srcFactor: "zero", dstFactor: "one-minus-src-alpha", operation: "add" }
7285
+ color: {
7286
+ srcFactor: "one",
7287
+ dstFactor: "one-minus-src-alpha",
7288
+ operation: "add"
7289
+ },
7290
+ alpha: {
7291
+ srcFactor: "zero",
7292
+ dstFactor: "one-minus-src-alpha",
7293
+ operation: "add"
7294
+ }
7118
7295
  }
7119
7296
  },
7120
7297
  {
7121
7298
  format: "rgba16float",
7122
7299
  blend: {
7123
- color: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
7124
- alpha: { srcFactor: "zero", dstFactor: "one-minus-src-alpha", operation: "add" }
7300
+ color: {
7301
+ srcFactor: "one",
7302
+ dstFactor: "one-minus-src-alpha",
7303
+ operation: "add"
7304
+ },
7305
+ alpha: {
7306
+ srcFactor: "zero",
7307
+ dstFactor: "one-minus-src-alpha",
7308
+ operation: "add"
7309
+ }
7125
7310
  }
7126
7311
  }
7127
7312
  ]
@@ -7224,7 +7409,9 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7224
7409
  return this.modelMatrix;
7225
7410
  }
7226
7411
  setSHMode(mode) {
7412
+ if (this.shMode === mode) return;
7227
7413
  this.shMode = mode;
7414
+ this.createPipeline();
7228
7415
  }
7229
7416
  getSHMode() {
7230
7417
  return this.shMode;
@@ -7243,6 +7430,18 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7243
7430
  getMaxVisibleSplats() {
7244
7431
  return this.maxVisibleSplats;
7245
7432
  }
7433
+ /**
7434
+ * 设置深度范围限制(view-space 距离)
7435
+ * 只渲染距相机此距离范围内的 splat,超出部分被剔除
7436
+ * 近似 transmittance cutoff:近距离时大量 splat 堆叠在后方但被前方遮挡
7437
+ * 0 = 不限制
7438
+ */
7439
+ setDepthRangeLimit(limit) {
7440
+ this.depthRangeLimit = limit;
7441
+ }
7442
+ getDepthRangeLimit() {
7443
+ return this.depthRangeLimit;
7444
+ }
7246
7445
  /**
7247
7446
  * 设置排序频率
7248
7447
  * 1 = 每帧排序(默认),2 = 每 2 帧排序一次,以此类推
@@ -7260,7 +7459,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7260
7459
  const model = this.modelMatrix;
7261
7460
  const w = this.renderer.width;
7262
7461
  const h = this.renderer.height;
7263
- if (!this.sortStateInitialized || w !== this.lastSortWidth || h !== this.lastSortHeight || this.pixelCullThreshold !== this.lastSortPixelThreshold || this.maxVisibleSplats !== this.lastSortMaxVisible) {
7462
+ if (!this.sortStateInitialized || w !== this.lastSortWidth || h !== this.lastSortHeight || this.pixelCullThreshold !== this.lastSortPixelThreshold || this.maxVisibleSplats !== this.lastSortMaxVisible || this.depthRangeLimit !== this.lastSortDepthRange) {
7264
7463
  this.saveSortState(view, proj, model, w, h);
7265
7464
  return true;
7266
7465
  }
@@ -7280,6 +7479,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7280
7479
  this.lastSortHeight = h;
7281
7480
  this.lastSortPixelThreshold = this.pixelCullThreshold;
7282
7481
  this.lastSortMaxVisible = this.maxVisibleSplats;
7482
+ this.lastSortDepthRange = this.depthRangeLimit;
7283
7483
  this.sortStateInitialized = true;
7284
7484
  }
7285
7485
  setData(splats) {
@@ -7411,36 +7611,26 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7411
7611
  ]
7412
7612
  });
7413
7613
  if (this.editorEnabled) this.rebuildEditorBindGroup();
7614
+ this.sortStateInitialized = false;
7414
7615
  }
7415
7616
  render(pass) {
7416
7617
  if (this.splatCount === 0 || !this.bindGroup || !this.sorter) {
7417
7618
  return;
7418
7619
  }
7419
- this.renderer.device.queue.writeBuffer(
7420
- this.uniformBuffer,
7421
- 0,
7422
- new Float32Array(this.camera.viewMatrix)
7423
- );
7424
- this.renderer.device.queue.writeBuffer(
7425
- this.uniformBuffer,
7426
- 64,
7427
- new Float32Array(this.camera.projectionMatrix)
7428
- );
7429
- this.renderer.device.queue.writeBuffer(
7430
- this.uniformBuffer,
7431
- 128,
7432
- new Float32Array(this.modelMatrix)
7433
- );
7434
- this.renderer.device.queue.writeBuffer(
7435
- this.uniformBuffer,
7436
- 192,
7437
- new Float32Array(this.camera.position)
7438
- );
7439
- this.renderer.device.queue.writeBuffer(
7440
- this.uniformBuffer,
7441
- 208,
7442
- new Float32Array([this.renderer.width, this.renderer.height, 0, 0])
7443
- );
7620
+ const ud = this.uniformData;
7621
+ ud.set(this.camera.viewMatrix, 0);
7622
+ ud.set(this.camera.projectionMatrix, 16);
7623
+ ud.set(this.modelMatrix, 32);
7624
+ const pos = this.camera.position;
7625
+ ud[48] = pos[0];
7626
+ ud[49] = pos[1];
7627
+ ud[50] = pos[2];
7628
+ ud[51] = 0;
7629
+ ud[52] = this.renderer.width;
7630
+ ud[53] = this.renderer.height;
7631
+ ud[54] = 0;
7632
+ ud[55] = 0;
7633
+ this.renderer.device.queue.writeBuffer(this.uniformBuffer, 0, ud);
7444
7634
  const changed = this.needsSort();
7445
7635
  this.frameCounter++;
7446
7636
  const shouldSort = changed || this.sortFrequency > 1 && this.frameCounter % this.sortFrequency === 0;
@@ -7450,7 +7640,8 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7450
7640
  nearPlane: this.camera.near,
7451
7641
  farPlane: this.camera.far,
7452
7642
  pixelThreshold: this.pixelCullThreshold,
7453
- maxVisibleCount: this.maxVisibleSplats
7643
+ maxVisibleCount: this.maxVisibleSplats,
7644
+ depthRangeLimit: this.depthRangeLimit
7454
7645
  });
7455
7646
  this.sorter.sort();
7456
7647
  }
@@ -7544,7 +7735,9 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7544
7735
  const h = this.renderer.height;
7545
7736
  this.ensureDepthNormalTextures(w, h);
7546
7737
  const device = this.renderer.device;
7547
- const encoder = device.createCommandEncoder({ label: "depth-normal-encoder" });
7738
+ const encoder = device.createCommandEncoder({
7739
+ label: "depth-normal-encoder"
7740
+ });
7548
7741
  const pass = encoder.beginRenderPass({
7549
7742
  colorAttachments: [
7550
7743
  {
@@ -7602,7 +7795,22 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7602
7795
  const raw = new Uint16Array(buf.getMappedRange().slice(0));
7603
7796
  buf.unmap();
7604
7797
  buf.destroy();
7605
- this.dnResult = { raw, vm, proj, cam, w, h, px: ipx, py: ipy, copyX: x0, copyY: y0, copyW, copyH, cx, cy };
7798
+ this.dnResult = {
7799
+ raw,
7800
+ vm,
7801
+ proj,
7802
+ cam,
7803
+ w,
7804
+ h,
7805
+ px: ipx,
7806
+ py: ipy,
7807
+ copyX: x0,
7808
+ copyY: y0,
7809
+ copyW,
7810
+ copyH,
7811
+ cx,
7812
+ cy
7813
+ };
7606
7814
  }).catch(() => {
7607
7815
  buf.destroy();
7608
7816
  });
@@ -7762,14 +7970,33 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7762
7970
  }
7763
7971
  createEditorPipeline() {
7764
7972
  const device = this.renderer.device;
7765
- const editorShaderCode = this.buildEditorShader();
7973
+ const editorShaderCode = this.buildEditorShader().replace(
7974
+ "const SH_LEVEL: u32 = 3u; // @SH_LEVEL_INJECT@",
7975
+ `const SH_LEVEL: u32 = ${this.shMode}u;`
7976
+ );
7766
7977
  const shaderModule = device.createShaderModule({ code: editorShaderCode });
7767
7978
  this.editorBindGroupLayout = device.createBindGroupLayout({
7768
7979
  entries: [
7769
- { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
7770
- { binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
7771
- { binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
7772
- { binding: 3, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } }
7980
+ {
7981
+ binding: 0,
7982
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
7983
+ buffer: { type: "uniform" }
7984
+ },
7985
+ {
7986
+ binding: 1,
7987
+ visibility: GPUShaderStage.VERTEX,
7988
+ buffer: { type: "read-only-storage" }
7989
+ },
7990
+ {
7991
+ binding: 2,
7992
+ visibility: GPUShaderStage.VERTEX,
7993
+ buffer: { type: "read-only-storage" }
7994
+ },
7995
+ {
7996
+ binding: 3,
7997
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
7998
+ buffer: { type: "read-only-storage" }
7999
+ }
7773
8000
  ]
7774
8001
  });
7775
8002
  const pipelineLayout = device.createPipelineLayout({
@@ -7781,13 +8008,23 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7781
8008
  fragment: {
7782
8009
  module: shaderModule,
7783
8010
  entryPoint: "fs_editor",
7784
- targets: [{
7785
- format: this.renderer.format,
7786
- blend: {
7787
- color: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
7788
- alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" }
8011
+ targets: [
8012
+ {
8013
+ format: this.renderer.format,
8014
+ blend: {
8015
+ color: {
8016
+ srcFactor: "one",
8017
+ dstFactor: "one-minus-src-alpha",
8018
+ operation: "add"
8019
+ },
8020
+ alpha: {
8021
+ srcFactor: "one",
8022
+ dstFactor: "one-minus-src-alpha",
8023
+ operation: "add"
8024
+ }
8025
+ }
7789
8026
  }
7790
- }]
8027
+ ]
7791
8028
  },
7792
8029
  primitive: { topology: "triangle-strip" },
7793
8030
  depthStencil: {
@@ -7798,7 +8035,8 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7798
8035
  });
7799
8036
  }
7800
8037
  rebuildEditorBindGroup() {
7801
- if (!this.editorBindGroupLayout || !this.splatBuffer || !this.sorter || !this.editorStateBuffer) return;
8038
+ if (!this.editorBindGroupLayout || !this.splatBuffer || !this.sorter || !this.editorStateBuffer)
8039
+ return;
7802
8040
  this.editorBindGroup = this.renderer.device.createBindGroup({
7803
8041
  layout: this.editorBindGroupLayout,
7804
8042
  entries: [
@@ -7845,7 +8083,8 @@ struct Uniforms {
7845
8083
  cameraPos: vec3<f32>,
7846
8084
  _pad: f32,
7847
8085
  screenSize: vec2<f32>,
7848
- _pad2: vec2<f32>,
8086
+ shMode: u32,
8087
+ _pad2: f32,
7849
8088
  }
7850
8089
 
7851
8090
  struct Splat {
@@ -7860,12 +8099,19 @@ struct Splat {
7860
8099
  _pad2: array<f32, 3>,
7861
8100
  }
7862
8101
 
8102
+ const SH_LEVEL: u32 = 3u; // @SH_LEVEL_INJECT@
8103
+
7863
8104
  fn evalSH(splat: Splat, dir: vec3<f32>) -> vec3<f32> {
8105
+ if SH_LEVEL == 0u { return vec3<f32>(0.0); }
8106
+
7864
8107
  let x = dir.x; let y = dir.y; let z = dir.z;
7865
8108
  var result = vec3<f32>(0.0);
7866
8109
  result += (-SH_C1 * y) * vec3<f32>(splat.sh1[0], splat.sh1[1], splat.sh1[2]);
7867
8110
  result += ( SH_C1 * z) * vec3<f32>(splat.sh1[3], splat.sh1[4], splat.sh1[5]);
7868
8111
  result += (-SH_C1 * x) * vec3<f32>(splat.sh1[6], splat.sh1[7], splat.sh1[8]);
8112
+
8113
+ if SH_LEVEL == 1u { return result; }
8114
+
7869
8115
  let xx = x * x; let yy = y * y; let zz = z * z;
7870
8116
  let xy = x * y; let yz = y * z; let xz = x * z;
7871
8117
  result += (SH_C2_0 * xy) * vec3<f32>(splat.sh2[0], splat.sh2[1], splat.sh2[2]);
@@ -7873,6 +8119,9 @@ fn evalSH(splat: Splat, dir: vec3<f32>) -> vec3<f32> {
7873
8119
  result += (SH_C2_2 * (2.0 * zz - xx - yy)) * vec3<f32>(splat.sh2[6], splat.sh2[7], splat.sh2[8]);
7874
8120
  result += (SH_C2_3 * xz) * vec3<f32>(splat.sh2[9], splat.sh2[10], splat.sh2[11]);
7875
8121
  result += (SH_C2_4 * (xx - yy)) * vec3<f32>(splat.sh2[12], splat.sh2[13], splat.sh2[14]);
8122
+
8123
+ if SH_LEVEL == 2u { return result; }
8124
+
7876
8125
  result += (SH_C3_0 * y * (3.0 * xx - yy)) * vec3<f32>(splat.sh3[0], splat.sh3[1], splat.sh3[2]);
7877
8126
  result += (SH_C3_1 * xy * z) * vec3<f32>(splat.sh3[3], splat.sh3[4], splat.sh3[5]);
7878
8127
  result += (SH_C3_2 * y * (4.0 * zz - xx - yy)) * vec3<f32>(splat.sh3[6], splat.sh3[7], splat.sh3[8]);
@@ -15657,6 +15906,216 @@ class EyedropperSelection {
15657
15906
  this.pointerId = null;
15658
15907
  }
15659
15908
  }
15909
+ class SphereSelection {
15910
+ constructor(parent, onSelect) {
15911
+ __publicField(this, "parent");
15912
+ __publicField(this, "svg");
15913
+ __publicField(this, "circle");
15914
+ __publicField(this, "onSelect");
15915
+ __publicField(this, "center", { x: 0, y: 0 });
15916
+ __publicField(this, "radiusPx", 0);
15917
+ __publicField(this, "dragId");
15918
+ this.parent = parent;
15919
+ this.onSelect = onSelect;
15920
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
15921
+ this.svg.classList.add("tool-svg");
15922
+ this.svg.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;display:none;z-index:10;";
15923
+ parent.appendChild(this.svg);
15924
+ this.circle = document.createElementNS(
15925
+ this.svg.namespaceURI,
15926
+ "circle"
15927
+ );
15928
+ this.circle.setAttribute("fill", "rgba(0,150,255,0.12)");
15929
+ this.circle.setAttribute("stroke", "#09f");
15930
+ this.circle.setAttribute("stroke-width", "1.5");
15931
+ this.circle.setAttribute("stroke-dasharray", "6 3");
15932
+ this.circle.style.display = "none";
15933
+ this.svg.appendChild(this.circle);
15934
+ this.pointerdown = this.pointerdown.bind(this);
15935
+ this.pointermove = this.pointermove.bind(this);
15936
+ this.pointerup = this.pointerup.bind(this);
15937
+ }
15938
+ activate() {
15939
+ this.svg.style.display = "block";
15940
+ this.parent.style.cursor = "crosshair";
15941
+ this.parent.addEventListener("pointerdown", this.pointerdown);
15942
+ this.parent.addEventListener("pointermove", this.pointermove);
15943
+ this.parent.addEventListener("pointerup", this.pointerup);
15944
+ }
15945
+ deactivate() {
15946
+ if (this.dragId !== void 0) this.dragEnd();
15947
+ this.svg.style.display = "none";
15948
+ this.parent.style.cursor = "";
15949
+ this.parent.removeEventListener("pointerdown", this.pointerdown);
15950
+ this.parent.removeEventListener("pointermove", this.pointermove);
15951
+ this.parent.removeEventListener("pointerup", this.pointerup);
15952
+ }
15953
+ updateCircle() {
15954
+ this.circle.setAttribute("cx", this.center.x.toString());
15955
+ this.circle.setAttribute("cy", this.center.y.toString());
15956
+ this.circle.setAttribute("r", Math.max(1, this.radiusPx).toString());
15957
+ }
15958
+ pointerdown(e) {
15959
+ if (this.dragId !== void 0) return;
15960
+ if (e.pointerType === "mouse" ? e.button !== 0 : !e.isPrimary) return;
15961
+ e.preventDefault();
15962
+ e.stopPropagation();
15963
+ this.dragId = e.pointerId;
15964
+ this.parent.setPointerCapture(this.dragId);
15965
+ this.center.x = e.offsetX;
15966
+ this.center.y = e.offsetY;
15967
+ this.radiusPx = 0;
15968
+ this.updateCircle();
15969
+ this.circle.style.display = "block";
15970
+ }
15971
+ pointermove(e) {
15972
+ if (e.pointerId !== this.dragId) return;
15973
+ e.preventDefault();
15974
+ e.stopPropagation();
15975
+ const dx = e.offsetX - this.center.x;
15976
+ const dy = e.offsetY - this.center.y;
15977
+ this.radiusPx = Math.sqrt(dx * dx + dy * dy);
15978
+ this.updateCircle();
15979
+ }
15980
+ dragEnd() {
15981
+ if (this.dragId !== void 0) {
15982
+ this.parent.releasePointerCapture(this.dragId);
15983
+ this.dragId = void 0;
15984
+ }
15985
+ this.circle.style.display = "none";
15986
+ }
15987
+ pointerup(e) {
15988
+ if (e.pointerId !== this.dragId) return;
15989
+ e.preventDefault();
15990
+ e.stopPropagation();
15991
+ const selectOp = e.shiftKey ? "add" : e.ctrlKey ? "remove" : "set";
15992
+ if (this.radiusPx < 3) this.radiusPx = 20;
15993
+ this.onSelect(selectOp, { x: this.center.x, y: this.center.y }, this.radiusPx);
15994
+ this.dragEnd();
15995
+ }
15996
+ }
15997
+ class BoxSelection {
15998
+ constructor(parent, onSelect) {
15999
+ __publicField(this, "parent");
16000
+ __publicField(this, "svg");
16001
+ __publicField(this, "rect");
16002
+ __publicField(this, "crossV");
16003
+ __publicField(this, "crossH");
16004
+ __publicField(this, "onSelect");
16005
+ __publicField(this, "center", { x: 0, y: 0 });
16006
+ __publicField(this, "halfW", 0);
16007
+ __publicField(this, "halfH", 0);
16008
+ __publicField(this, "dragId");
16009
+ this.parent = parent;
16010
+ this.onSelect = onSelect;
16011
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
16012
+ this.svg.classList.add("tool-svg");
16013
+ this.svg.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;display:none;z-index:10;";
16014
+ parent.appendChild(this.svg);
16015
+ this.rect = document.createElementNS(
16016
+ this.svg.namespaceURI,
16017
+ "rect"
16018
+ );
16019
+ this.rect.setAttribute("fill", "rgba(0,200,100,0.12)");
16020
+ this.rect.setAttribute("stroke", "#0c6");
16021
+ this.rect.setAttribute("stroke-width", "1.5");
16022
+ this.rect.setAttribute("stroke-dasharray", "6 3");
16023
+ this.rect.style.display = "none";
16024
+ this.svg.appendChild(this.rect);
16025
+ const mkLine = () => {
16026
+ const l = document.createElementNS(this.svg.namespaceURI, "line");
16027
+ l.setAttribute("stroke", "#0c6");
16028
+ l.setAttribute("stroke-width", "1");
16029
+ l.setAttribute("stroke-dasharray", "3 3");
16030
+ l.style.display = "none";
16031
+ this.svg.appendChild(l);
16032
+ return l;
16033
+ };
16034
+ this.crossV = mkLine();
16035
+ this.crossH = mkLine();
16036
+ this.pointerdown = this.pointerdown.bind(this);
16037
+ this.pointermove = this.pointermove.bind(this);
16038
+ this.pointerup = this.pointerup.bind(this);
16039
+ }
16040
+ activate() {
16041
+ this.svg.style.display = "block";
16042
+ this.parent.style.cursor = "crosshair";
16043
+ this.parent.addEventListener("pointerdown", this.pointerdown);
16044
+ this.parent.addEventListener("pointermove", this.pointermove);
16045
+ this.parent.addEventListener("pointerup", this.pointerup);
16046
+ }
16047
+ deactivate() {
16048
+ if (this.dragId !== void 0) this.dragEnd();
16049
+ this.svg.style.display = "none";
16050
+ this.parent.style.cursor = "";
16051
+ this.parent.removeEventListener("pointerdown", this.pointerdown);
16052
+ this.parent.removeEventListener("pointermove", this.pointermove);
16053
+ this.parent.removeEventListener("pointerup", this.pointerup);
16054
+ }
16055
+ updateVisuals() {
16056
+ const x = this.center.x - this.halfW;
16057
+ const y = this.center.y - this.halfH;
16058
+ const w = this.halfW * 2;
16059
+ const h = this.halfH * 2;
16060
+ this.rect.setAttribute("x", x.toString());
16061
+ this.rect.setAttribute("y", y.toString());
16062
+ this.rect.setAttribute("width", Math.max(1, w).toString());
16063
+ this.rect.setAttribute("height", Math.max(1, h).toString());
16064
+ this.crossV.setAttribute("x1", this.center.x.toString());
16065
+ this.crossV.setAttribute("y1", y.toString());
16066
+ this.crossV.setAttribute("x2", this.center.x.toString());
16067
+ this.crossV.setAttribute("y2", (y + h).toString());
16068
+ this.crossH.setAttribute("x1", x.toString());
16069
+ this.crossH.setAttribute("y1", this.center.y.toString());
16070
+ this.crossH.setAttribute("x2", (x + w).toString());
16071
+ this.crossH.setAttribute("y2", this.center.y.toString());
16072
+ }
16073
+ pointerdown(e) {
16074
+ if (this.dragId !== void 0) return;
16075
+ if (e.pointerType === "mouse" ? e.button !== 0 : !e.isPrimary) return;
16076
+ e.preventDefault();
16077
+ e.stopPropagation();
16078
+ this.dragId = e.pointerId;
16079
+ this.parent.setPointerCapture(this.dragId);
16080
+ this.center.x = e.offsetX;
16081
+ this.center.y = e.offsetY;
16082
+ this.halfW = 0;
16083
+ this.halfH = 0;
16084
+ this.updateVisuals();
16085
+ this.rect.style.display = "block";
16086
+ this.crossV.style.display = "block";
16087
+ this.crossH.style.display = "block";
16088
+ }
16089
+ pointermove(e) {
16090
+ if (e.pointerId !== this.dragId) return;
16091
+ e.preventDefault();
16092
+ e.stopPropagation();
16093
+ this.halfW = Math.abs(e.offsetX - this.center.x);
16094
+ this.halfH = Math.abs(e.offsetY - this.center.y);
16095
+ this.updateVisuals();
16096
+ }
16097
+ dragEnd() {
16098
+ if (this.dragId !== void 0) {
16099
+ this.parent.releasePointerCapture(this.dragId);
16100
+ this.dragId = void 0;
16101
+ }
16102
+ this.rect.style.display = "none";
16103
+ this.crossV.style.display = "none";
16104
+ this.crossH.style.display = "none";
16105
+ }
16106
+ pointerup(e) {
16107
+ if (e.pointerId !== this.dragId) return;
16108
+ e.preventDefault();
16109
+ e.stopPropagation();
16110
+ const selectOp = e.shiftKey ? "add" : e.ctrlKey ? "remove" : "set";
16111
+ if (this.halfW < 3 && this.halfH < 3) {
16112
+ this.halfW = 20;
16113
+ this.halfH = 20;
16114
+ }
16115
+ this.onSelect(selectOp, { x: this.center.x, y: this.center.y }, this.halfW, this.halfH);
16116
+ this.dragEnd();
16117
+ }
16118
+ }
15660
16119
  function exportEditedPLY(positions, scales, rotations, colors, opacities, shCoeffs, state) {
15661
16120
  const totalCount = state.count;
15662
16121
  let keepCount = 0;
@@ -15867,6 +16326,14 @@ class SplatEditor {
15867
16326
  this.toolOverlay,
15868
16327
  (op, pt, threshold) => this.selectByColor(op, pt, threshold)
15869
16328
  ));
16329
+ this.toolManager.register("sphere", new SphereSelection(
16330
+ this.toolOverlay,
16331
+ (op, centerPx, radiusPx) => this.selectBySphere(op, centerPx, radiusPx)
16332
+ ));
16333
+ this.toolManager.register("box", new BoxSelection(
16334
+ this.toolOverlay,
16335
+ (op, centerPx, halfW, halfH) => this.selectByBox(op, centerPx, halfW, halfH)
16336
+ ));
15870
16337
  this.gsRenderer.setEditorState(this.splatState.data);
15871
16338
  this._keyHandler = this._onKeyDown.bind(this);
15872
16339
  window.addEventListener("keydown", this._keyHandler);
@@ -16069,6 +16536,106 @@ class SplatEditor {
16069
16536
  });
16070
16537
  this.editHistory.add(editOp);
16071
16538
  }
16539
+ /**
16540
+ * 球选择:以点击处最近 splat 为中心,拖拽半径定义世界空间球体
16541
+ */
16542
+ selectBySphere(op, centerPx, radiusPx) {
16543
+ const pick = this.pickWorldPosition(centerPx.x, centerPx.y);
16544
+ if (!pick) return;
16545
+ const worldRadius = radiusPx * pick.pixelToWorld;
16546
+ const cpuPos = this.gsRenderer.getCPUPositions();
16547
+ if (!cpuPos) return;
16548
+ const modelMat = this.gsRenderer.getModelMatrix();
16549
+ const cx = pick.worldX, cy = pick.worldY, cz = pick.worldZ;
16550
+ const r2 = worldRadius * worldRadius;
16551
+ const editOp = new SelectOp(this.splatState, op, (i) => {
16552
+ if (this.splatState.data[i] & (State.hidden | State.deleted)) return false;
16553
+ const i3 = i * 3;
16554
+ const lx = cpuPos[i3], ly = cpuPos[i3 + 1], lz = cpuPos[i3 + 2];
16555
+ const wx = modelMat[0] * lx + modelMat[4] * ly + modelMat[8] * lz + modelMat[12];
16556
+ const wy = modelMat[1] * lx + modelMat[5] * ly + modelMat[9] * lz + modelMat[13];
16557
+ const wz = modelMat[2] * lx + modelMat[6] * ly + modelMat[10] * lz + modelMat[14];
16558
+ const dx = wx - cx, dy = wy - cy, dz = wz - cz;
16559
+ return dx * dx + dy * dy + dz * dz < r2;
16560
+ });
16561
+ this.editHistory.add(editOp);
16562
+ }
16563
+ /**
16564
+ * 盒选择:以点击处最近 splat 为中心,拖拽定义世界空间 AABB
16565
+ * X/Y 半宽由屏幕拖拽距离转换,Z 半深度取 max(halfW, halfH)
16566
+ */
16567
+ selectByBox(op, centerPx, halfWPx, halfHPx) {
16568
+ const pick = this.pickWorldPosition(centerPx.x, centerPx.y);
16569
+ if (!pick) return;
16570
+ const s = pick.pixelToWorld;
16571
+ const hx = halfWPx * s;
16572
+ const hy = halfHPx * s;
16573
+ const hz = Math.max(hx, hy);
16574
+ const cpuPos = this.gsRenderer.getCPUPositions();
16575
+ if (!cpuPos) return;
16576
+ const modelMat = this.gsRenderer.getModelMatrix();
16577
+ const cx = pick.worldX, cy = pick.worldY, cz = pick.worldZ;
16578
+ const editOp = new SelectOp(this.splatState, op, (i) => {
16579
+ if (this.splatState.data[i] & (State.hidden | State.deleted)) return false;
16580
+ const i3 = i * 3;
16581
+ const lx = cpuPos[i3], ly = cpuPos[i3 + 1], lz = cpuPos[i3 + 2];
16582
+ const wx = modelMat[0] * lx + modelMat[4] * ly + modelMat[8] * lz + modelMat[12];
16583
+ const wy = modelMat[1] * lx + modelMat[5] * ly + modelMat[9] * lz + modelMat[13];
16584
+ const wz = modelMat[2] * lx + modelMat[6] * ly + modelMat[10] * lz + modelMat[14];
16585
+ return Math.abs(wx - cx) < hx && Math.abs(wy - cy) < hy && Math.abs(wz - cz) < hz;
16586
+ });
16587
+ this.editHistory.add(editOp);
16588
+ }
16589
+ /**
16590
+ * 根据屏幕像素坐标,找到最近的可见 splat 并返回其世界坐标及深度换算系数
16591
+ */
16592
+ pickWorldPosition(px, py) {
16593
+ this.ensureProjection();
16594
+ const proj = this.projectedPositions;
16595
+ if (!proj) return null;
16596
+ const cpuPos = this.gsRenderer.getCPUPositions();
16597
+ if (!cpuPos) return null;
16598
+ const w = this.container.clientWidth;
16599
+ const h = this.container.clientHeight;
16600
+ const nx = px / w;
16601
+ const ny = py / h;
16602
+ let bestIdx = -1;
16603
+ let bestDist = Infinity;
16604
+ for (let i = 0; i < this.splatState.count; i++) {
16605
+ const b = i * 3;
16606
+ const sz = proj[b + 2];
16607
+ if (sz <= 0 || sz >= 1) continue;
16608
+ if (this.splatState.data[i] & (State.hidden | State.deleted)) continue;
16609
+ const dx = proj[b] - nx;
16610
+ const dy = proj[b + 1] - ny;
16611
+ const d = dx * dx + dy * dy;
16612
+ if (d < bestDist) {
16613
+ bestDist = d;
16614
+ bestIdx = i;
16615
+ }
16616
+ }
16617
+ if (bestIdx < 0) return null;
16618
+ const modelMat = this.gsRenderer.getModelMatrix();
16619
+ const viewMat = this.camera.viewMatrix;
16620
+ const projMat = this.camera.projectionMatrix;
16621
+ const i3 = bestIdx * 3;
16622
+ const lx = cpuPos[i3], ly = cpuPos[i3 + 1], lz = cpuPos[i3 + 2];
16623
+ const wx = modelMat[0] * lx + modelMat[4] * ly + modelMat[8] * lz + modelMat[12];
16624
+ const wy = modelMat[1] * lx + modelMat[5] * ly + modelMat[9] * lz + modelMat[13];
16625
+ const wz = modelMat[2] * lx + modelMat[6] * ly + modelMat[10] * lz + modelMat[14];
16626
+ const vz = viewMat[2] * wx + viewMat[6] * wy + viewMat[10] * wz + viewMat[14];
16627
+ const depth = Math.abs(vz);
16628
+ const focalX = Math.abs(projMat[0]) * 0.5 * w;
16629
+ const focalY = Math.abs(projMat[5]) * 0.5 * h;
16630
+ const focal = (focalX + focalY) * 0.5;
16631
+ return {
16632
+ worldX: wx,
16633
+ worldY: wy,
16634
+ worldZ: wz,
16635
+ viewDepth: depth,
16636
+ pixelToWorld: depth / focal
16637
+ };
16638
+ }
16072
16639
  // ============================================
16073
16640
  // 投影
16074
16641
  // ============================================
@@ -16178,6 +16745,21 @@ class SplatEditor {
16178
16745
  }
16179
16746
  }
16180
16747
  }
16748
+ const DEFAULT_ADAPTIVE_CONFIG = {
16749
+ farThreshold: 2.5,
16750
+ nearThreshold: 0.3,
16751
+ minRenderScale: 1,
16752
+ maxPixelThreshold: 1.2,
16753
+ minPixelThreshold: 0.7,
16754
+ nearVisibleRatio: 1,
16755
+ farUnlimited: true,
16756
+ enableDepthRangeLimit: true,
16757
+ nearDepthRangeRatio: 2.5
16758
+ };
16759
+ function smoothstep(edge0, edge1, x) {
16760
+ const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
16761
+ return t * t * (3 - 2 * t);
16762
+ }
16181
16763
  class App {
16182
16764
  constructor(canvas) {
16183
16765
  __publicField(this, "canvas");
@@ -16198,6 +16780,13 @@ class App {
16198
16780
  __publicField(this, "useMobileRenderer", false);
16199
16781
  // 最近加载的 CompactSplatData(用于编辑器导出)
16200
16782
  __publicField(this, "lastCompactData", null);
16783
+ // 自适应性能控制
16784
+ __publicField(this, "adaptivePerformanceEnabled", false);
16785
+ __publicField(this, "adaptiveConfig", {
16786
+ ...DEFAULT_ADAPTIVE_CONFIG
16787
+ });
16788
+ __publicField(this, "lastAppliedRenderScale", 1);
16789
+ __publicField(this, "baseRenderScale", 1);
16201
16790
  // 绑定的事件处理函数
16202
16791
  __publicField(this, "boundOnResize");
16203
16792
  this.canvas = canvas;
@@ -16231,6 +16820,36 @@ class App {
16231
16820
  );
16232
16821
  this.sceneAids = new SceneAidsRenderer(this.renderer, this.camera);
16233
16822
  window.addEventListener("resize", this.boundOnResize);
16823
+ if (this.renderer.isAppleGPU) {
16824
+ this.applyAppleGPUDefaults();
16825
+ }
16826
+ }
16827
+ /**
16828
+ * Apple GPU (M1/M2/M3 等) 自动优化配置
16829
+ * TBDR 架构特点:片段着色器开销高、带宽敏感、compute pass 较慢
16830
+ * 策略:降低 SH 等级、更积极的自适应参数、禁用无效深度写入
16831
+ */
16832
+ applyAppleGPUDefaults() {
16833
+ this.adaptiveConfig = {
16834
+ ...this.adaptiveConfig,
16835
+ maxPixelThreshold: 2,
16836
+ enableDepthRangeLimit: true,
16837
+ nearDepthRangeRatio: 2
16838
+ };
16839
+ console.log(
16840
+ `[3DGS] Apple GPU detected (${this.renderer.gpuVendor}/${this.renderer.gpuArchitecture}), applying TBDR optimizations: SH→L1, depthWrite off, aggressive culling`
16841
+ );
16842
+ }
16843
+ /**
16844
+ * 创建桌面端 GSSplatRenderer 并自动应用平台优化
16845
+ */
16846
+ createDesktopGSRenderer() {
16847
+ const renderer = new GSSplatRenderer(this.renderer, this.camera);
16848
+ if (this.renderer.isAppleGPU) {
16849
+ renderer.setSHMode(SHMode.L1);
16850
+ renderer.setDepthWriteEnabled(false);
16851
+ }
16852
+ return renderer;
16234
16853
  }
16235
16854
  // ============================================
16236
16855
  // 模型加载
@@ -16277,11 +16896,14 @@ class App {
16277
16896
  const isMobile = isMobileDevice();
16278
16897
  let buffer;
16279
16898
  if (typeof urlOrBuffer === "string") {
16280
- buffer = await this.fetchWithProgress(urlOrBuffer, (downloadProgress) => {
16281
- if (onProgress) {
16282
- onProgress(downloadProgress * 0.5, "download");
16899
+ buffer = await this.fetchWithProgress(
16900
+ urlOrBuffer,
16901
+ (downloadProgress) => {
16902
+ if (onProgress) {
16903
+ onProgress(downloadProgress * 0.5, "download");
16904
+ }
16283
16905
  }
16284
- });
16906
+ );
16285
16907
  } else {
16286
16908
  buffer = urlOrBuffer;
16287
16909
  if (onProgress && isLocalFile) {
@@ -16312,7 +16934,7 @@ class App {
16312
16934
  this.hotspotManager.setGSRenderer(gsRenderer);
16313
16935
  return compactData.count;
16314
16936
  } else {
16315
- gsRenderer = new GSSplatRenderer(this.renderer, this.camera);
16937
+ gsRenderer = this.createDesktopGSRenderer();
16316
16938
  this.useMobileRenderer = false;
16317
16939
  const compactData = await this.parsePLYBuffer(buffer, {
16318
16940
  maxSplats: Infinity,
@@ -16341,11 +16963,14 @@ class App {
16341
16963
  const isMobile = isMobileDevice();
16342
16964
  let buffer;
16343
16965
  if (typeof urlOrBuffer === "string") {
16344
- buffer = await this.fetchWithProgress(urlOrBuffer, (downloadProgress) => {
16345
- if (onProgress) {
16346
- onProgress(downloadProgress * 0.5, "download");
16966
+ buffer = await this.fetchWithProgress(
16967
+ urlOrBuffer,
16968
+ (downloadProgress) => {
16969
+ if (onProgress) {
16970
+ onProgress(downloadProgress * 0.5, "download");
16971
+ }
16347
16972
  }
16348
- });
16973
+ );
16349
16974
  } else {
16350
16975
  buffer = urlOrBuffer;
16351
16976
  if (onProgress && isLocalFile) {
@@ -16368,14 +16993,17 @@ class App {
16368
16993
  if (onProgress) onProgress(90, "upload");
16369
16994
  let gsRenderer;
16370
16995
  if (isMobile) {
16371
- const mobileRenderer = new GSSplatRendererMobile(this.renderer, this.camera);
16996
+ const mobileRenderer = new GSSplatRendererMobile(
16997
+ this.renderer,
16998
+ this.camera
16999
+ );
16372
17000
  this.useMobileRenderer = true;
16373
17001
  const compactData = App.splatCpuToCompactData(splats);
16374
17002
  mobileRenderer.setCompactData(compactData);
16375
17003
  this.lastCompactData = compactData;
16376
17004
  gsRenderer = mobileRenderer;
16377
17005
  } else {
16378
- const desktopRenderer = new GSSplatRenderer(this.renderer, this.camera);
17006
+ const desktopRenderer = this.createDesktopGSRenderer();
16379
17007
  this.useMobileRenderer = false;
16380
17008
  desktopRenderer.setData(splats);
16381
17009
  gsRenderer = desktopRenderer;
@@ -16427,11 +17055,14 @@ class App {
16427
17055
  const isMobile = isMobileDevice();
16428
17056
  let buffer;
16429
17057
  if (typeof urlOrBuffer === "string") {
16430
- buffer = await this.fetchWithProgress(urlOrBuffer, (downloadProgress) => {
16431
- if (onProgress) {
16432
- onProgress(downloadProgress * 0.5, "download");
17058
+ buffer = await this.fetchWithProgress(
17059
+ urlOrBuffer,
17060
+ (downloadProgress) => {
17061
+ if (onProgress) {
17062
+ onProgress(downloadProgress * 0.5, "download");
17063
+ }
16433
17064
  }
16434
- });
17065
+ );
16435
17066
  } else {
16436
17067
  buffer = urlOrBuffer;
16437
17068
  if (onProgress && isLocalFile) {
@@ -16468,7 +17099,7 @@ class App {
16468
17099
  gsRenderer = new GSSplatRendererMobile(this.renderer, this.camera);
16469
17100
  this.useMobileRenderer = true;
16470
17101
  } else {
16471
- gsRenderer = new GSSplatRenderer(this.renderer, this.camera);
17102
+ gsRenderer = this.createDesktopGSRenderer();
16472
17103
  this.useMobileRenderer = false;
16473
17104
  }
16474
17105
  gsRenderer.setCompactData(compactData);
@@ -16521,9 +17152,69 @@ class App {
16521
17152
  this.render();
16522
17153
  this.animationId = requestAnimationFrame(this.animate.bind(this));
16523
17154
  }
17155
+ updateAdaptivePerformance() {
17156
+ if (!this.adaptivePerformanceEnabled) return;
17157
+ const gsRenderer = this.getGSRenderer();
17158
+ if (!gsRenderer) return;
17159
+ const bbox = gsRenderer.getBoundingBox();
17160
+ if (!bbox || bbox.radius < 1e-6) return;
17161
+ const cam = this.camera.position;
17162
+ const model = gsRenderer.getModelMatrix();
17163
+ const cx = bbox.center[0] * model[0] + bbox.center[1] * model[4] + bbox.center[2] * model[8] + model[12];
17164
+ const cy = bbox.center[0] * model[1] + bbox.center[1] * model[5] + bbox.center[2] * model[9] + model[13];
17165
+ const cz = bbox.center[0] * model[2] + bbox.center[1] * model[6] + bbox.center[2] * model[10] + model[14];
17166
+ const modelScale = Math.max(
17167
+ Math.sqrt(
17168
+ model[0] * model[0] + model[1] * model[1] + model[2] * model[2]
17169
+ ),
17170
+ Math.sqrt(
17171
+ model[4] * model[4] + model[5] * model[5] + model[6] * model[6]
17172
+ ),
17173
+ Math.sqrt(
17174
+ model[8] * model[8] + model[9] * model[9] + model[10] * model[10]
17175
+ )
17176
+ );
17177
+ const scaledRadius = bbox.radius * modelScale;
17178
+ const dx = cam[0] - cx;
17179
+ const dy = cam[1] - cy;
17180
+ const dz = cam[2] - cz;
17181
+ const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
17182
+ const distanceRatio = distance / scaledRadius;
17183
+ const cfg = this.adaptiveConfig;
17184
+ const t = smoothstep(cfg.nearThreshold, cfg.farThreshold, distanceRatio);
17185
+ const renderScale = cfg.minRenderScale + (this.baseRenderScale - cfg.minRenderScale) * t;
17186
+ const quantizedScale = Math.round(renderScale * 20) / 20;
17187
+ if (quantizedScale !== this.lastAppliedRenderScale) {
17188
+ this.renderer.setRenderScale(quantizedScale);
17189
+ this.lastAppliedRenderScale = quantizedScale;
17190
+ }
17191
+ const pixelThreshold = cfg.maxPixelThreshold + (cfg.minPixelThreshold - cfg.maxPixelThreshold) * t;
17192
+ gsRenderer.setPixelCullThreshold(pixelThreshold);
17193
+ const splatCount = gsRenderer.getSplatCount();
17194
+ if (cfg.farUnlimited && t >= 0.99) {
17195
+ gsRenderer.setMaxVisibleSplats(0);
17196
+ } else {
17197
+ const ratio = cfg.nearVisibleRatio + (1 - cfg.nearVisibleRatio) * t;
17198
+ gsRenderer.setMaxVisibleSplats(Math.round(splatCount * ratio));
17199
+ }
17200
+ if (cfg.enableDepthRangeLimit) {
17201
+ if (t >= 0.99) {
17202
+ gsRenderer.setDepthRangeLimit(0);
17203
+ } else {
17204
+ const depthRange = scaledRadius * (cfg.nearDepthRangeRatio + (4 - cfg.nearDepthRangeRatio) * t);
17205
+ gsRenderer.setDepthRangeLimit(depthRange);
17206
+ }
17207
+ }
17208
+ if (t < 0.3) {
17209
+ gsRenderer.setSortFrequency(2);
17210
+ } else {
17211
+ gsRenderer.setSortFrequency(1);
17212
+ }
17213
+ }
16524
17214
  render() {
16525
17215
  this.camera.setAspect(this.renderer.getAspectRatio());
16526
17216
  this.controls.update();
17217
+ this.updateAdaptivePerformance();
16527
17218
  this.hotspotManager.updateBillboards();
16528
17219
  const pass = this.renderer.beginFrame();
16529
17220
  const gsRenderer = this.sceneManager.getGSRenderer();
@@ -16534,10 +17225,12 @@ class App {
16534
17225
  this.sceneAids.render(pass);
16535
17226
  this.gizmoManager.render(pass);
16536
17227
  this.renderer.endFrame();
16537
- this.hotspotManager.consumeGPUResult();
16538
- if (gsRenderer == null ? void 0 : gsRenderer.prepareDepthNormalPass) {
16539
- const [px, py] = this.hotspotManager.getPickPixel();
16540
- gsRenderer.prepareDepthNormalPass(px, py);
17228
+ if (this.hotspotManager.isActive()) {
17229
+ this.hotspotManager.consumeGPUResult();
17230
+ if (gsRenderer == null ? void 0 : gsRenderer.prepareDepthNormalPass) {
17231
+ const [px, py] = this.hotspotManager.getPickPixel();
17232
+ gsRenderer.prepareDepthNormalPass(px, py);
17233
+ }
16541
17234
  }
16542
17235
  }
16543
17236
  onResize() {
@@ -16642,7 +17335,14 @@ class App {
16642
17335
  return this.sceneManager.getOverlayMeshColor(index);
16643
17336
  }
16644
17337
  setOverlayMeshRangeColor(startIndex, count, r, g, b, a = 1) {
16645
- return this.sceneManager.setOverlayMeshRangeColor(startIndex, count, r, g, b, a);
17338
+ return this.sceneManager.setOverlayMeshRangeColor(
17339
+ startIndex,
17340
+ count,
17341
+ r,
17342
+ g,
17343
+ b,
17344
+ a
17345
+ );
16646
17346
  }
16647
17347
  createOverlayMeshGroupProxy(startIndex, count) {
16648
17348
  const meshes = this.sceneManager.getOverlayMeshRange(startIndex, count);
@@ -16779,16 +17479,28 @@ class App {
16779
17479
  return this.hotspotManager.getHotspotBillboard(hotspotIndex);
16780
17480
  }
16781
17481
  setHotspotPlacedScale(hotspotIndex, newPlacedScale) {
16782
- return this.hotspotManager.setHotspotPlacedScale(hotspotIndex, newPlacedScale);
17482
+ return this.hotspotManager.setHotspotPlacedScale(
17483
+ hotspotIndex,
17484
+ newPlacedScale
17485
+ );
16783
17486
  }
16784
17487
  getHotspotCount() {
16785
17488
  return this.hotspotManager.getHotspotCount();
16786
17489
  }
16787
17490
  findHotspotIndexByMeshStart(overlayMeshStartIndex) {
16788
- return this.hotspotManager.findHotspotIndexByMeshStart(overlayMeshStartIndex);
17491
+ return this.hotspotManager.findHotspotIndexByMeshStart(
17492
+ overlayMeshStartIndex
17493
+ );
16789
17494
  }
16790
17495
  async placeHotspotAt(objUrl, position, normal, visualDiameter, normalOffset, overrideScale) {
16791
- return this.hotspotManager.placeHotspotAt(objUrl, position, normal, visualDiameter, normalOffset, overrideScale);
17496
+ return this.hotspotManager.placeHotspotAt(
17497
+ objUrl,
17498
+ position,
17499
+ normal,
17500
+ visualDiameter,
17501
+ normalOffset,
17502
+ overrideScale
17503
+ );
16792
17504
  }
16793
17505
  getOverlayMeshByIndex(index) {
16794
17506
  return this.sceneManager.getOverlayMeshByIndex(index);
@@ -16819,7 +17531,13 @@ class App {
16819
17531
  * 设置热点文字标签
16820
17532
  */
16821
17533
  setHotspotLabel(hotspotIndex, text, position, fontSize, visible) {
16822
- return this.hotspotManager.setHotspotLabel(hotspotIndex, text, position, fontSize, visible);
17534
+ return this.hotspotManager.setHotspotLabel(
17535
+ hotspotIndex,
17536
+ text,
17537
+ position,
17538
+ fontSize,
17539
+ visible
17540
+ );
16823
17541
  }
16824
17542
  /**
16825
17543
  * 设置热点标签可见性
@@ -16900,7 +17618,10 @@ class App {
16900
17618
  * 适用于移动端提质或桌面端降负载
16901
17619
  */
16902
17620
  setRenderScale(scale) {
16903
- this.renderer.setRenderScale(scale);
17621
+ this.baseRenderScale = scale;
17622
+ if (!this.adaptivePerformanceEnabled) {
17623
+ this.renderer.setRenderScale(scale);
17624
+ }
16904
17625
  }
16905
17626
  getRenderScale() {
16906
17627
  return this.renderer.getRenderScale();
@@ -16948,6 +17669,49 @@ class App {
16948
17669
  mobileRenderer.setSortFrequency(frequency);
16949
17670
  }
16950
17671
  }
17672
+ /**
17673
+ * 是否检测到 Apple GPU(M1/M2/M3 等)
17674
+ */
17675
+ get isAppleGPU() {
17676
+ var _a2;
17677
+ return ((_a2 = this.renderer) == null ? void 0 : _a2.isAppleGPU) ?? false;
17678
+ }
17679
+ // ============================================
17680
+ // 自适应性能控制
17681
+ // ============================================
17682
+ /**
17683
+ * 启用/禁用自适应性能优化
17684
+ * 开启后系统会根据相机与模型的距离自动调节:
17685
+ * - 渲染分辨率(renderScale)
17686
+ * - 亚像素剔除阈值(pixelThreshold)
17687
+ * - 最大可见 splat 数量(maxVisibleSplats)
17688
+ */
17689
+ setAdaptivePerformance(enabled) {
17690
+ this.adaptivePerformanceEnabled = enabled;
17691
+ if (!enabled) {
17692
+ this.renderer.setRenderScale(this.baseRenderScale);
17693
+ this.lastAppliedRenderScale = this.baseRenderScale;
17694
+ const gsRenderer = this.getGSRenderer();
17695
+ if (gsRenderer) {
17696
+ gsRenderer.setPixelCullThreshold(1);
17697
+ gsRenderer.setMaxVisibleSplats(0);
17698
+ gsRenderer.setDepthRangeLimit(0);
17699
+ gsRenderer.setSortFrequency(1);
17700
+ }
17701
+ }
17702
+ }
17703
+ getAdaptivePerformance() {
17704
+ return this.adaptivePerformanceEnabled;
17705
+ }
17706
+ /**
17707
+ * 设置自适应性能配置参数
17708
+ */
17709
+ setAdaptivePerformanceConfig(config) {
17710
+ this.adaptiveConfig = { ...this.adaptiveConfig, ...config };
17711
+ }
17712
+ getAdaptivePerformanceConfig() {
17713
+ return { ...this.adaptiveConfig };
17714
+ }
16951
17715
  /**
16952
17716
  * 销毁应用及所有资源
16953
17717
  */