@d5techs/3dgs-lib 1.4.13 → 1.4.15

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) {
@@ -7416,31 +7616,20 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7416
7616
  if (this.splatCount === 0 || !this.bindGroup || !this.sorter) {
7417
7617
  return;
7418
7618
  }
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
- );
7619
+ const ud = this.uniformData;
7620
+ ud.set(this.camera.viewMatrix, 0);
7621
+ ud.set(this.camera.projectionMatrix, 16);
7622
+ ud.set(this.modelMatrix, 32);
7623
+ const pos = this.camera.position;
7624
+ ud[48] = pos[0];
7625
+ ud[49] = pos[1];
7626
+ ud[50] = pos[2];
7627
+ ud[51] = 0;
7628
+ ud[52] = this.renderer.width;
7629
+ ud[53] = this.renderer.height;
7630
+ ud[54] = 0;
7631
+ ud[55] = 0;
7632
+ this.renderer.device.queue.writeBuffer(this.uniformBuffer, 0, ud);
7444
7633
  const changed = this.needsSort();
7445
7634
  this.frameCounter++;
7446
7635
  const shouldSort = changed || this.sortFrequency > 1 && this.frameCounter % this.sortFrequency === 0;
@@ -7450,7 +7639,8 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7450
7639
  nearPlane: this.camera.near,
7451
7640
  farPlane: this.camera.far,
7452
7641
  pixelThreshold: this.pixelCullThreshold,
7453
- maxVisibleCount: this.maxVisibleSplats
7642
+ maxVisibleCount: this.maxVisibleSplats,
7643
+ depthRangeLimit: this.depthRangeLimit
7454
7644
  });
7455
7645
  this.sorter.sort();
7456
7646
  }
@@ -7544,7 +7734,9 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7544
7734
  const h = this.renderer.height;
7545
7735
  this.ensureDepthNormalTextures(w, h);
7546
7736
  const device = this.renderer.device;
7547
- const encoder = device.createCommandEncoder({ label: "depth-normal-encoder" });
7737
+ const encoder = device.createCommandEncoder({
7738
+ label: "depth-normal-encoder"
7739
+ });
7548
7740
  const pass = encoder.beginRenderPass({
7549
7741
  colorAttachments: [
7550
7742
  {
@@ -7602,7 +7794,22 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7602
7794
  const raw = new Uint16Array(buf.getMappedRange().slice(0));
7603
7795
  buf.unmap();
7604
7796
  buf.destroy();
7605
- this.dnResult = { raw, vm, proj, cam, w, h, px: ipx, py: ipy, copyX: x0, copyY: y0, copyW, copyH, cx, cy };
7797
+ this.dnResult = {
7798
+ raw,
7799
+ vm,
7800
+ proj,
7801
+ cam,
7802
+ w,
7803
+ h,
7804
+ px: ipx,
7805
+ py: ipy,
7806
+ copyX: x0,
7807
+ copyY: y0,
7808
+ copyW,
7809
+ copyH,
7810
+ cx,
7811
+ cy
7812
+ };
7606
7813
  }).catch(() => {
7607
7814
  buf.destroy();
7608
7815
  });
@@ -7762,14 +7969,33 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7762
7969
  }
7763
7970
  createEditorPipeline() {
7764
7971
  const device = this.renderer.device;
7765
- const editorShaderCode = this.buildEditorShader();
7972
+ const editorShaderCode = this.buildEditorShader().replace(
7973
+ "const SH_LEVEL: u32 = 3u; // @SH_LEVEL_INJECT@",
7974
+ `const SH_LEVEL: u32 = ${this.shMode}u;`
7975
+ );
7766
7976
  const shaderModule = device.createShaderModule({ code: editorShaderCode });
7767
7977
  this.editorBindGroupLayout = device.createBindGroupLayout({
7768
7978
  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" } }
7979
+ {
7980
+ binding: 0,
7981
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
7982
+ buffer: { type: "uniform" }
7983
+ },
7984
+ {
7985
+ binding: 1,
7986
+ visibility: GPUShaderStage.VERTEX,
7987
+ buffer: { type: "read-only-storage" }
7988
+ },
7989
+ {
7990
+ binding: 2,
7991
+ visibility: GPUShaderStage.VERTEX,
7992
+ buffer: { type: "read-only-storage" }
7993
+ },
7994
+ {
7995
+ binding: 3,
7996
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
7997
+ buffer: { type: "read-only-storage" }
7998
+ }
7773
7999
  ]
7774
8000
  });
7775
8001
  const pipelineLayout = device.createPipelineLayout({
@@ -7781,13 +8007,23 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7781
8007
  fragment: {
7782
8008
  module: shaderModule,
7783
8009
  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" }
8010
+ targets: [
8011
+ {
8012
+ format: this.renderer.format,
8013
+ blend: {
8014
+ color: {
8015
+ srcFactor: "one",
8016
+ dstFactor: "one-minus-src-alpha",
8017
+ operation: "add"
8018
+ },
8019
+ alpha: {
8020
+ srcFactor: "one",
8021
+ dstFactor: "one-minus-src-alpha",
8022
+ operation: "add"
8023
+ }
8024
+ }
7789
8025
  }
7790
- }]
8026
+ ]
7791
8027
  },
7792
8028
  primitive: { topology: "triangle-strip" },
7793
8029
  depthStencil: {
@@ -7798,7 +8034,8 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7798
8034
  });
7799
8035
  }
7800
8036
  rebuildEditorBindGroup() {
7801
- if (!this.editorBindGroupLayout || !this.splatBuffer || !this.sorter || !this.editorStateBuffer) return;
8037
+ if (!this.editorBindGroupLayout || !this.splatBuffer || !this.sorter || !this.editorStateBuffer)
8038
+ return;
7802
8039
  this.editorBindGroup = this.renderer.device.createBindGroup({
7803
8040
  layout: this.editorBindGroupLayout,
7804
8041
  entries: [
@@ -7845,7 +8082,8 @@ struct Uniforms {
7845
8082
  cameraPos: vec3<f32>,
7846
8083
  _pad: f32,
7847
8084
  screenSize: vec2<f32>,
7848
- _pad2: vec2<f32>,
8085
+ shMode: u32,
8086
+ _pad2: f32,
7849
8087
  }
7850
8088
 
7851
8089
  struct Splat {
@@ -7860,12 +8098,19 @@ struct Splat {
7860
8098
  _pad2: array<f32, 3>,
7861
8099
  }
7862
8100
 
8101
+ const SH_LEVEL: u32 = 3u; // @SH_LEVEL_INJECT@
8102
+
7863
8103
  fn evalSH(splat: Splat, dir: vec3<f32>) -> vec3<f32> {
8104
+ if SH_LEVEL == 0u { return vec3<f32>(0.0); }
8105
+
7864
8106
  let x = dir.x; let y = dir.y; let z = dir.z;
7865
8107
  var result = vec3<f32>(0.0);
7866
8108
  result += (-SH_C1 * y) * vec3<f32>(splat.sh1[0], splat.sh1[1], splat.sh1[2]);
7867
8109
  result += ( SH_C1 * z) * vec3<f32>(splat.sh1[3], splat.sh1[4], splat.sh1[5]);
7868
8110
  result += (-SH_C1 * x) * vec3<f32>(splat.sh1[6], splat.sh1[7], splat.sh1[8]);
8111
+
8112
+ if SH_LEVEL == 1u { return result; }
8113
+
7869
8114
  let xx = x * x; let yy = y * y; let zz = z * z;
7870
8115
  let xy = x * y; let yz = y * z; let xz = x * z;
7871
8116
  result += (SH_C2_0 * xy) * vec3<f32>(splat.sh2[0], splat.sh2[1], splat.sh2[2]);
@@ -7873,6 +8118,9 @@ fn evalSH(splat: Splat, dir: vec3<f32>) -> vec3<f32> {
7873
8118
  result += (SH_C2_2 * (2.0 * zz - xx - yy)) * vec3<f32>(splat.sh2[6], splat.sh2[7], splat.sh2[8]);
7874
8119
  result += (SH_C2_3 * xz) * vec3<f32>(splat.sh2[9], splat.sh2[10], splat.sh2[11]);
7875
8120
  result += (SH_C2_4 * (xx - yy)) * vec3<f32>(splat.sh2[12], splat.sh2[13], splat.sh2[14]);
8121
+
8122
+ if SH_LEVEL == 2u { return result; }
8123
+
7876
8124
  result += (SH_C3_0 * y * (3.0 * xx - yy)) * vec3<f32>(splat.sh3[0], splat.sh3[1], splat.sh3[2]);
7877
8125
  result += (SH_C3_1 * xy * z) * vec3<f32>(splat.sh3[3], splat.sh3[4], splat.sh3[5]);
7878
8126
  result += (SH_C3_2 * y * (4.0 * zz - xx - yy)) * vec3<f32>(splat.sh3[6], splat.sh3[7], splat.sh3[8]);
@@ -13603,7 +13851,16 @@ class HotspotManager {
13603
13851
  let closestIdx = -1;
13604
13852
  for (let i = 0; i < this.hotspots.length; i++) {
13605
13853
  const h = this.hotspots[i];
13606
- const [px, py, pz] = h.position;
13854
+ let px, py, pz;
13855
+ const firstMesh = this.meshRenderer.getOverlayMeshByIndex(h.meshStartIndex);
13856
+ if (firstMesh) {
13857
+ const m = firstMesh.modelMatrix;
13858
+ px = m[12];
13859
+ py = m[13];
13860
+ pz = m[14];
13861
+ } else {
13862
+ [px, py, pz] = h.position;
13863
+ }
13607
13864
  const clipW = vp[3] * px + vp[7] * py + vp[11] * pz + vp[15];
13608
13865
  if (clipW <= 0) continue;
13609
13866
  const clipX = (vp[0] * px + vp[4] * py + vp[8] * pz + vp[12]) / clipW;
@@ -15648,6 +15905,216 @@ class EyedropperSelection {
15648
15905
  this.pointerId = null;
15649
15906
  }
15650
15907
  }
15908
+ class SphereSelection {
15909
+ constructor(parent, onSelect) {
15910
+ __publicField(this, "parent");
15911
+ __publicField(this, "svg");
15912
+ __publicField(this, "circle");
15913
+ __publicField(this, "onSelect");
15914
+ __publicField(this, "center", { x: 0, y: 0 });
15915
+ __publicField(this, "radiusPx", 0);
15916
+ __publicField(this, "dragId");
15917
+ this.parent = parent;
15918
+ this.onSelect = onSelect;
15919
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
15920
+ this.svg.classList.add("tool-svg");
15921
+ this.svg.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;display:none;z-index:10;";
15922
+ parent.appendChild(this.svg);
15923
+ this.circle = document.createElementNS(
15924
+ this.svg.namespaceURI,
15925
+ "circle"
15926
+ );
15927
+ this.circle.setAttribute("fill", "rgba(0,150,255,0.12)");
15928
+ this.circle.setAttribute("stroke", "#09f");
15929
+ this.circle.setAttribute("stroke-width", "1.5");
15930
+ this.circle.setAttribute("stroke-dasharray", "6 3");
15931
+ this.circle.style.display = "none";
15932
+ this.svg.appendChild(this.circle);
15933
+ this.pointerdown = this.pointerdown.bind(this);
15934
+ this.pointermove = this.pointermove.bind(this);
15935
+ this.pointerup = this.pointerup.bind(this);
15936
+ }
15937
+ activate() {
15938
+ this.svg.style.display = "block";
15939
+ this.parent.style.cursor = "crosshair";
15940
+ this.parent.addEventListener("pointerdown", this.pointerdown);
15941
+ this.parent.addEventListener("pointermove", this.pointermove);
15942
+ this.parent.addEventListener("pointerup", this.pointerup);
15943
+ }
15944
+ deactivate() {
15945
+ if (this.dragId !== void 0) this.dragEnd();
15946
+ this.svg.style.display = "none";
15947
+ this.parent.style.cursor = "";
15948
+ this.parent.removeEventListener("pointerdown", this.pointerdown);
15949
+ this.parent.removeEventListener("pointermove", this.pointermove);
15950
+ this.parent.removeEventListener("pointerup", this.pointerup);
15951
+ }
15952
+ updateCircle() {
15953
+ this.circle.setAttribute("cx", this.center.x.toString());
15954
+ this.circle.setAttribute("cy", this.center.y.toString());
15955
+ this.circle.setAttribute("r", Math.max(1, this.radiusPx).toString());
15956
+ }
15957
+ pointerdown(e) {
15958
+ if (this.dragId !== void 0) return;
15959
+ if (e.pointerType === "mouse" ? e.button !== 0 : !e.isPrimary) return;
15960
+ e.preventDefault();
15961
+ e.stopPropagation();
15962
+ this.dragId = e.pointerId;
15963
+ this.parent.setPointerCapture(this.dragId);
15964
+ this.center.x = e.offsetX;
15965
+ this.center.y = e.offsetY;
15966
+ this.radiusPx = 0;
15967
+ this.updateCircle();
15968
+ this.circle.style.display = "block";
15969
+ }
15970
+ pointermove(e) {
15971
+ if (e.pointerId !== this.dragId) return;
15972
+ e.preventDefault();
15973
+ e.stopPropagation();
15974
+ const dx = e.offsetX - this.center.x;
15975
+ const dy = e.offsetY - this.center.y;
15976
+ this.radiusPx = Math.sqrt(dx * dx + dy * dy);
15977
+ this.updateCircle();
15978
+ }
15979
+ dragEnd() {
15980
+ if (this.dragId !== void 0) {
15981
+ this.parent.releasePointerCapture(this.dragId);
15982
+ this.dragId = void 0;
15983
+ }
15984
+ this.circle.style.display = "none";
15985
+ }
15986
+ pointerup(e) {
15987
+ if (e.pointerId !== this.dragId) return;
15988
+ e.preventDefault();
15989
+ e.stopPropagation();
15990
+ const selectOp = e.shiftKey ? "add" : e.ctrlKey ? "remove" : "set";
15991
+ if (this.radiusPx < 3) this.radiusPx = 20;
15992
+ this.onSelect(selectOp, { x: this.center.x, y: this.center.y }, this.radiusPx);
15993
+ this.dragEnd();
15994
+ }
15995
+ }
15996
+ class BoxSelection {
15997
+ constructor(parent, onSelect) {
15998
+ __publicField(this, "parent");
15999
+ __publicField(this, "svg");
16000
+ __publicField(this, "rect");
16001
+ __publicField(this, "crossV");
16002
+ __publicField(this, "crossH");
16003
+ __publicField(this, "onSelect");
16004
+ __publicField(this, "center", { x: 0, y: 0 });
16005
+ __publicField(this, "halfW", 0);
16006
+ __publicField(this, "halfH", 0);
16007
+ __publicField(this, "dragId");
16008
+ this.parent = parent;
16009
+ this.onSelect = onSelect;
16010
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
16011
+ this.svg.classList.add("tool-svg");
16012
+ this.svg.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;display:none;z-index:10;";
16013
+ parent.appendChild(this.svg);
16014
+ this.rect = document.createElementNS(
16015
+ this.svg.namespaceURI,
16016
+ "rect"
16017
+ );
16018
+ this.rect.setAttribute("fill", "rgba(0,200,100,0.12)");
16019
+ this.rect.setAttribute("stroke", "#0c6");
16020
+ this.rect.setAttribute("stroke-width", "1.5");
16021
+ this.rect.setAttribute("stroke-dasharray", "6 3");
16022
+ this.rect.style.display = "none";
16023
+ this.svg.appendChild(this.rect);
16024
+ const mkLine = () => {
16025
+ const l = document.createElementNS(this.svg.namespaceURI, "line");
16026
+ l.setAttribute("stroke", "#0c6");
16027
+ l.setAttribute("stroke-width", "1");
16028
+ l.setAttribute("stroke-dasharray", "3 3");
16029
+ l.style.display = "none";
16030
+ this.svg.appendChild(l);
16031
+ return l;
16032
+ };
16033
+ this.crossV = mkLine();
16034
+ this.crossH = mkLine();
16035
+ this.pointerdown = this.pointerdown.bind(this);
16036
+ this.pointermove = this.pointermove.bind(this);
16037
+ this.pointerup = this.pointerup.bind(this);
16038
+ }
16039
+ activate() {
16040
+ this.svg.style.display = "block";
16041
+ this.parent.style.cursor = "crosshair";
16042
+ this.parent.addEventListener("pointerdown", this.pointerdown);
16043
+ this.parent.addEventListener("pointermove", this.pointermove);
16044
+ this.parent.addEventListener("pointerup", this.pointerup);
16045
+ }
16046
+ deactivate() {
16047
+ if (this.dragId !== void 0) this.dragEnd();
16048
+ this.svg.style.display = "none";
16049
+ this.parent.style.cursor = "";
16050
+ this.parent.removeEventListener("pointerdown", this.pointerdown);
16051
+ this.parent.removeEventListener("pointermove", this.pointermove);
16052
+ this.parent.removeEventListener("pointerup", this.pointerup);
16053
+ }
16054
+ updateVisuals() {
16055
+ const x = this.center.x - this.halfW;
16056
+ const y = this.center.y - this.halfH;
16057
+ const w = this.halfW * 2;
16058
+ const h = this.halfH * 2;
16059
+ this.rect.setAttribute("x", x.toString());
16060
+ this.rect.setAttribute("y", y.toString());
16061
+ this.rect.setAttribute("width", Math.max(1, w).toString());
16062
+ this.rect.setAttribute("height", Math.max(1, h).toString());
16063
+ this.crossV.setAttribute("x1", this.center.x.toString());
16064
+ this.crossV.setAttribute("y1", y.toString());
16065
+ this.crossV.setAttribute("x2", this.center.x.toString());
16066
+ this.crossV.setAttribute("y2", (y + h).toString());
16067
+ this.crossH.setAttribute("x1", x.toString());
16068
+ this.crossH.setAttribute("y1", this.center.y.toString());
16069
+ this.crossH.setAttribute("x2", (x + w).toString());
16070
+ this.crossH.setAttribute("y2", this.center.y.toString());
16071
+ }
16072
+ pointerdown(e) {
16073
+ if (this.dragId !== void 0) return;
16074
+ if (e.pointerType === "mouse" ? e.button !== 0 : !e.isPrimary) return;
16075
+ e.preventDefault();
16076
+ e.stopPropagation();
16077
+ this.dragId = e.pointerId;
16078
+ this.parent.setPointerCapture(this.dragId);
16079
+ this.center.x = e.offsetX;
16080
+ this.center.y = e.offsetY;
16081
+ this.halfW = 0;
16082
+ this.halfH = 0;
16083
+ this.updateVisuals();
16084
+ this.rect.style.display = "block";
16085
+ this.crossV.style.display = "block";
16086
+ this.crossH.style.display = "block";
16087
+ }
16088
+ pointermove(e) {
16089
+ if (e.pointerId !== this.dragId) return;
16090
+ e.preventDefault();
16091
+ e.stopPropagation();
16092
+ this.halfW = Math.abs(e.offsetX - this.center.x);
16093
+ this.halfH = Math.abs(e.offsetY - this.center.y);
16094
+ this.updateVisuals();
16095
+ }
16096
+ dragEnd() {
16097
+ if (this.dragId !== void 0) {
16098
+ this.parent.releasePointerCapture(this.dragId);
16099
+ this.dragId = void 0;
16100
+ }
16101
+ this.rect.style.display = "none";
16102
+ this.crossV.style.display = "none";
16103
+ this.crossH.style.display = "none";
16104
+ }
16105
+ pointerup(e) {
16106
+ if (e.pointerId !== this.dragId) return;
16107
+ e.preventDefault();
16108
+ e.stopPropagation();
16109
+ const selectOp = e.shiftKey ? "add" : e.ctrlKey ? "remove" : "set";
16110
+ if (this.halfW < 3 && this.halfH < 3) {
16111
+ this.halfW = 20;
16112
+ this.halfH = 20;
16113
+ }
16114
+ this.onSelect(selectOp, { x: this.center.x, y: this.center.y }, this.halfW, this.halfH);
16115
+ this.dragEnd();
16116
+ }
16117
+ }
15651
16118
  function exportEditedPLY(positions, scales, rotations, colors, opacities, shCoeffs, state) {
15652
16119
  const totalCount = state.count;
15653
16120
  let keepCount = 0;
@@ -15858,6 +16325,14 @@ class SplatEditor {
15858
16325
  this.toolOverlay,
15859
16326
  (op, pt, threshold) => this.selectByColor(op, pt, threshold)
15860
16327
  ));
16328
+ this.toolManager.register("sphere", new SphereSelection(
16329
+ this.toolOverlay,
16330
+ (op, centerPx, radiusPx) => this.selectBySphere(op, centerPx, radiusPx)
16331
+ ));
16332
+ this.toolManager.register("box", new BoxSelection(
16333
+ this.toolOverlay,
16334
+ (op, centerPx, halfW, halfH) => this.selectByBox(op, centerPx, halfW, halfH)
16335
+ ));
15861
16336
  this.gsRenderer.setEditorState(this.splatState.data);
15862
16337
  this._keyHandler = this._onKeyDown.bind(this);
15863
16338
  window.addEventListener("keydown", this._keyHandler);
@@ -16060,6 +16535,106 @@ class SplatEditor {
16060
16535
  });
16061
16536
  this.editHistory.add(editOp);
16062
16537
  }
16538
+ /**
16539
+ * 球选择:以点击处最近 splat 为中心,拖拽半径定义世界空间球体
16540
+ */
16541
+ selectBySphere(op, centerPx, radiusPx) {
16542
+ const pick = this.pickWorldPosition(centerPx.x, centerPx.y);
16543
+ if (!pick) return;
16544
+ const worldRadius = radiusPx * pick.pixelToWorld;
16545
+ const cpuPos = this.gsRenderer.getCPUPositions();
16546
+ if (!cpuPos) return;
16547
+ const modelMat = this.gsRenderer.getModelMatrix();
16548
+ const cx = pick.worldX, cy = pick.worldY, cz = pick.worldZ;
16549
+ const r2 = worldRadius * worldRadius;
16550
+ const editOp = new SelectOp(this.splatState, op, (i) => {
16551
+ if (this.splatState.data[i] & (State.hidden | State.deleted)) return false;
16552
+ const i3 = i * 3;
16553
+ const lx = cpuPos[i3], ly = cpuPos[i3 + 1], lz = cpuPos[i3 + 2];
16554
+ const wx = modelMat[0] * lx + modelMat[4] * ly + modelMat[8] * lz + modelMat[12];
16555
+ const wy = modelMat[1] * lx + modelMat[5] * ly + modelMat[9] * lz + modelMat[13];
16556
+ const wz = modelMat[2] * lx + modelMat[6] * ly + modelMat[10] * lz + modelMat[14];
16557
+ const dx = wx - cx, dy = wy - cy, dz = wz - cz;
16558
+ return dx * dx + dy * dy + dz * dz < r2;
16559
+ });
16560
+ this.editHistory.add(editOp);
16561
+ }
16562
+ /**
16563
+ * 盒选择:以点击处最近 splat 为中心,拖拽定义世界空间 AABB
16564
+ * X/Y 半宽由屏幕拖拽距离转换,Z 半深度取 max(halfW, halfH)
16565
+ */
16566
+ selectByBox(op, centerPx, halfWPx, halfHPx) {
16567
+ const pick = this.pickWorldPosition(centerPx.x, centerPx.y);
16568
+ if (!pick) return;
16569
+ const s = pick.pixelToWorld;
16570
+ const hx = halfWPx * s;
16571
+ const hy = halfHPx * s;
16572
+ const hz = Math.max(hx, hy);
16573
+ const cpuPos = this.gsRenderer.getCPUPositions();
16574
+ if (!cpuPos) return;
16575
+ const modelMat = this.gsRenderer.getModelMatrix();
16576
+ const cx = pick.worldX, cy = pick.worldY, cz = pick.worldZ;
16577
+ const editOp = new SelectOp(this.splatState, op, (i) => {
16578
+ if (this.splatState.data[i] & (State.hidden | State.deleted)) return false;
16579
+ const i3 = i * 3;
16580
+ const lx = cpuPos[i3], ly = cpuPos[i3 + 1], lz = cpuPos[i3 + 2];
16581
+ const wx = modelMat[0] * lx + modelMat[4] * ly + modelMat[8] * lz + modelMat[12];
16582
+ const wy = modelMat[1] * lx + modelMat[5] * ly + modelMat[9] * lz + modelMat[13];
16583
+ const wz = modelMat[2] * lx + modelMat[6] * ly + modelMat[10] * lz + modelMat[14];
16584
+ return Math.abs(wx - cx) < hx && Math.abs(wy - cy) < hy && Math.abs(wz - cz) < hz;
16585
+ });
16586
+ this.editHistory.add(editOp);
16587
+ }
16588
+ /**
16589
+ * 根据屏幕像素坐标,找到最近的可见 splat 并返回其世界坐标及深度换算系数
16590
+ */
16591
+ pickWorldPosition(px, py) {
16592
+ this.ensureProjection();
16593
+ const proj = this.projectedPositions;
16594
+ if (!proj) return null;
16595
+ const cpuPos = this.gsRenderer.getCPUPositions();
16596
+ if (!cpuPos) return null;
16597
+ const w = this.container.clientWidth;
16598
+ const h = this.container.clientHeight;
16599
+ const nx = px / w;
16600
+ const ny = py / h;
16601
+ let bestIdx = -1;
16602
+ let bestDist = Infinity;
16603
+ for (let i = 0; i < this.splatState.count; i++) {
16604
+ const b = i * 3;
16605
+ const sz = proj[b + 2];
16606
+ if (sz <= 0 || sz >= 1) continue;
16607
+ if (this.splatState.data[i] & (State.hidden | State.deleted)) continue;
16608
+ const dx = proj[b] - nx;
16609
+ const dy = proj[b + 1] - ny;
16610
+ const d = dx * dx + dy * dy;
16611
+ if (d < bestDist) {
16612
+ bestDist = d;
16613
+ bestIdx = i;
16614
+ }
16615
+ }
16616
+ if (bestIdx < 0) return null;
16617
+ const modelMat = this.gsRenderer.getModelMatrix();
16618
+ const viewMat = this.camera.viewMatrix;
16619
+ const projMat = this.camera.projectionMatrix;
16620
+ const i3 = bestIdx * 3;
16621
+ const lx = cpuPos[i3], ly = cpuPos[i3 + 1], lz = cpuPos[i3 + 2];
16622
+ const wx = modelMat[0] * lx + modelMat[4] * ly + modelMat[8] * lz + modelMat[12];
16623
+ const wy = modelMat[1] * lx + modelMat[5] * ly + modelMat[9] * lz + modelMat[13];
16624
+ const wz = modelMat[2] * lx + modelMat[6] * ly + modelMat[10] * lz + modelMat[14];
16625
+ const vz = viewMat[2] * wx + viewMat[6] * wy + viewMat[10] * wz + viewMat[14];
16626
+ const depth = Math.abs(vz);
16627
+ const focalX = Math.abs(projMat[0]) * 0.5 * w;
16628
+ const focalY = Math.abs(projMat[5]) * 0.5 * h;
16629
+ const focal = (focalX + focalY) * 0.5;
16630
+ return {
16631
+ worldX: wx,
16632
+ worldY: wy,
16633
+ worldZ: wz,
16634
+ viewDepth: depth,
16635
+ pixelToWorld: depth / focal
16636
+ };
16637
+ }
16063
16638
  // ============================================
16064
16639
  // 投影
16065
16640
  // ============================================
@@ -16169,6 +16744,21 @@ class SplatEditor {
16169
16744
  }
16170
16745
  }
16171
16746
  }
16747
+ const DEFAULT_ADAPTIVE_CONFIG = {
16748
+ farThreshold: 2.5,
16749
+ nearThreshold: 0.3,
16750
+ minRenderScale: 1,
16751
+ maxPixelThreshold: 1.2,
16752
+ minPixelThreshold: 0.7,
16753
+ nearVisibleRatio: 1,
16754
+ farUnlimited: true,
16755
+ enableDepthRangeLimit: true,
16756
+ nearDepthRangeRatio: 2.5
16757
+ };
16758
+ function smoothstep(edge0, edge1, x) {
16759
+ const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
16760
+ return t * t * (3 - 2 * t);
16761
+ }
16172
16762
  class App {
16173
16763
  constructor(canvas) {
16174
16764
  __publicField(this, "canvas");
@@ -16189,6 +16779,13 @@ class App {
16189
16779
  __publicField(this, "useMobileRenderer", false);
16190
16780
  // 最近加载的 CompactSplatData(用于编辑器导出)
16191
16781
  __publicField(this, "lastCompactData", null);
16782
+ // 自适应性能控制
16783
+ __publicField(this, "adaptivePerformanceEnabled", false);
16784
+ __publicField(this, "adaptiveConfig", {
16785
+ ...DEFAULT_ADAPTIVE_CONFIG
16786
+ });
16787
+ __publicField(this, "lastAppliedRenderScale", 1);
16788
+ __publicField(this, "baseRenderScale", 1);
16192
16789
  // 绑定的事件处理函数
16193
16790
  __publicField(this, "boundOnResize");
16194
16791
  this.canvas = canvas;
@@ -16222,6 +16819,36 @@ class App {
16222
16819
  );
16223
16820
  this.sceneAids = new SceneAidsRenderer(this.renderer, this.camera);
16224
16821
  window.addEventListener("resize", this.boundOnResize);
16822
+ if (this.renderer.isAppleGPU) {
16823
+ this.applyAppleGPUDefaults();
16824
+ }
16825
+ }
16826
+ /**
16827
+ * Apple GPU (M1/M2/M3 等) 自动优化配置
16828
+ * TBDR 架构特点:片段着色器开销高、带宽敏感、compute pass 较慢
16829
+ * 策略:降低 SH 等级、更积极的自适应参数、禁用无效深度写入
16830
+ */
16831
+ applyAppleGPUDefaults() {
16832
+ this.adaptiveConfig = {
16833
+ ...this.adaptiveConfig,
16834
+ maxPixelThreshold: 2,
16835
+ enableDepthRangeLimit: true,
16836
+ nearDepthRangeRatio: 2
16837
+ };
16838
+ console.log(
16839
+ `[3DGS] Apple GPU detected (${this.renderer.gpuVendor}/${this.renderer.gpuArchitecture}), applying TBDR optimizations: SH→L1, depthWrite off, aggressive culling`
16840
+ );
16841
+ }
16842
+ /**
16843
+ * 创建桌面端 GSSplatRenderer 并自动应用平台优化
16844
+ */
16845
+ createDesktopGSRenderer() {
16846
+ const renderer = new GSSplatRenderer(this.renderer, this.camera);
16847
+ if (this.renderer.isAppleGPU) {
16848
+ renderer.setSHMode(SHMode.L1);
16849
+ renderer.setDepthWriteEnabled(false);
16850
+ }
16851
+ return renderer;
16225
16852
  }
16226
16853
  // ============================================
16227
16854
  // 模型加载
@@ -16268,11 +16895,14 @@ class App {
16268
16895
  const isMobile = isMobileDevice();
16269
16896
  let buffer;
16270
16897
  if (typeof urlOrBuffer === "string") {
16271
- buffer = await this.fetchWithProgress(urlOrBuffer, (downloadProgress) => {
16272
- if (onProgress) {
16273
- onProgress(downloadProgress * 0.5, "download");
16898
+ buffer = await this.fetchWithProgress(
16899
+ urlOrBuffer,
16900
+ (downloadProgress) => {
16901
+ if (onProgress) {
16902
+ onProgress(downloadProgress * 0.5, "download");
16903
+ }
16274
16904
  }
16275
- });
16905
+ );
16276
16906
  } else {
16277
16907
  buffer = urlOrBuffer;
16278
16908
  if (onProgress && isLocalFile) {
@@ -16303,7 +16933,7 @@ class App {
16303
16933
  this.hotspotManager.setGSRenderer(gsRenderer);
16304
16934
  return compactData.count;
16305
16935
  } else {
16306
- gsRenderer = new GSSplatRenderer(this.renderer, this.camera);
16936
+ gsRenderer = this.createDesktopGSRenderer();
16307
16937
  this.useMobileRenderer = false;
16308
16938
  const compactData = await this.parsePLYBuffer(buffer, {
16309
16939
  maxSplats: Infinity,
@@ -16332,11 +16962,14 @@ class App {
16332
16962
  const isMobile = isMobileDevice();
16333
16963
  let buffer;
16334
16964
  if (typeof urlOrBuffer === "string") {
16335
- buffer = await this.fetchWithProgress(urlOrBuffer, (downloadProgress) => {
16336
- if (onProgress) {
16337
- onProgress(downloadProgress * 0.5, "download");
16965
+ buffer = await this.fetchWithProgress(
16966
+ urlOrBuffer,
16967
+ (downloadProgress) => {
16968
+ if (onProgress) {
16969
+ onProgress(downloadProgress * 0.5, "download");
16970
+ }
16338
16971
  }
16339
- });
16972
+ );
16340
16973
  } else {
16341
16974
  buffer = urlOrBuffer;
16342
16975
  if (onProgress && isLocalFile) {
@@ -16359,14 +16992,17 @@ class App {
16359
16992
  if (onProgress) onProgress(90, "upload");
16360
16993
  let gsRenderer;
16361
16994
  if (isMobile) {
16362
- const mobileRenderer = new GSSplatRendererMobile(this.renderer, this.camera);
16995
+ const mobileRenderer = new GSSplatRendererMobile(
16996
+ this.renderer,
16997
+ this.camera
16998
+ );
16363
16999
  this.useMobileRenderer = true;
16364
17000
  const compactData = App.splatCpuToCompactData(splats);
16365
17001
  mobileRenderer.setCompactData(compactData);
16366
17002
  this.lastCompactData = compactData;
16367
17003
  gsRenderer = mobileRenderer;
16368
17004
  } else {
16369
- const desktopRenderer = new GSSplatRenderer(this.renderer, this.camera);
17005
+ const desktopRenderer = this.createDesktopGSRenderer();
16370
17006
  this.useMobileRenderer = false;
16371
17007
  desktopRenderer.setData(splats);
16372
17008
  gsRenderer = desktopRenderer;
@@ -16418,11 +17054,14 @@ class App {
16418
17054
  const isMobile = isMobileDevice();
16419
17055
  let buffer;
16420
17056
  if (typeof urlOrBuffer === "string") {
16421
- buffer = await this.fetchWithProgress(urlOrBuffer, (downloadProgress) => {
16422
- if (onProgress) {
16423
- onProgress(downloadProgress * 0.5, "download");
17057
+ buffer = await this.fetchWithProgress(
17058
+ urlOrBuffer,
17059
+ (downloadProgress) => {
17060
+ if (onProgress) {
17061
+ onProgress(downloadProgress * 0.5, "download");
17062
+ }
16424
17063
  }
16425
- });
17064
+ );
16426
17065
  } else {
16427
17066
  buffer = urlOrBuffer;
16428
17067
  if (onProgress && isLocalFile) {
@@ -16459,7 +17098,7 @@ class App {
16459
17098
  gsRenderer = new GSSplatRendererMobile(this.renderer, this.camera);
16460
17099
  this.useMobileRenderer = true;
16461
17100
  } else {
16462
- gsRenderer = new GSSplatRenderer(this.renderer, this.camera);
17101
+ gsRenderer = this.createDesktopGSRenderer();
16463
17102
  this.useMobileRenderer = false;
16464
17103
  }
16465
17104
  gsRenderer.setCompactData(compactData);
@@ -16512,9 +17151,69 @@ class App {
16512
17151
  this.render();
16513
17152
  this.animationId = requestAnimationFrame(this.animate.bind(this));
16514
17153
  }
17154
+ updateAdaptivePerformance() {
17155
+ if (!this.adaptivePerformanceEnabled) return;
17156
+ const gsRenderer = this.getGSRenderer();
17157
+ if (!gsRenderer) return;
17158
+ const bbox = gsRenderer.getBoundingBox();
17159
+ if (!bbox || bbox.radius < 1e-6) return;
17160
+ const cam = this.camera.position;
17161
+ const model = gsRenderer.getModelMatrix();
17162
+ const cx = bbox.center[0] * model[0] + bbox.center[1] * model[4] + bbox.center[2] * model[8] + model[12];
17163
+ const cy = bbox.center[0] * model[1] + bbox.center[1] * model[5] + bbox.center[2] * model[9] + model[13];
17164
+ const cz = bbox.center[0] * model[2] + bbox.center[1] * model[6] + bbox.center[2] * model[10] + model[14];
17165
+ const modelScale = Math.max(
17166
+ Math.sqrt(
17167
+ model[0] * model[0] + model[1] * model[1] + model[2] * model[2]
17168
+ ),
17169
+ Math.sqrt(
17170
+ model[4] * model[4] + model[5] * model[5] + model[6] * model[6]
17171
+ ),
17172
+ Math.sqrt(
17173
+ model[8] * model[8] + model[9] * model[9] + model[10] * model[10]
17174
+ )
17175
+ );
17176
+ const scaledRadius = bbox.radius * modelScale;
17177
+ const dx = cam[0] - cx;
17178
+ const dy = cam[1] - cy;
17179
+ const dz = cam[2] - cz;
17180
+ const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
17181
+ const distanceRatio = distance / scaledRadius;
17182
+ const cfg = this.adaptiveConfig;
17183
+ const t = smoothstep(cfg.nearThreshold, cfg.farThreshold, distanceRatio);
17184
+ const renderScale = cfg.minRenderScale + (this.baseRenderScale - cfg.minRenderScale) * t;
17185
+ const quantizedScale = Math.round(renderScale * 20) / 20;
17186
+ if (quantizedScale !== this.lastAppliedRenderScale) {
17187
+ this.renderer.setRenderScale(quantizedScale);
17188
+ this.lastAppliedRenderScale = quantizedScale;
17189
+ }
17190
+ const pixelThreshold = cfg.maxPixelThreshold + (cfg.minPixelThreshold - cfg.maxPixelThreshold) * t;
17191
+ gsRenderer.setPixelCullThreshold(pixelThreshold);
17192
+ const splatCount = gsRenderer.getSplatCount();
17193
+ if (cfg.farUnlimited && t >= 0.99) {
17194
+ gsRenderer.setMaxVisibleSplats(0);
17195
+ } else {
17196
+ const ratio = cfg.nearVisibleRatio + (1 - cfg.nearVisibleRatio) * t;
17197
+ gsRenderer.setMaxVisibleSplats(Math.round(splatCount * ratio));
17198
+ }
17199
+ if (cfg.enableDepthRangeLimit) {
17200
+ if (t >= 0.99) {
17201
+ gsRenderer.setDepthRangeLimit(0);
17202
+ } else {
17203
+ const depthRange = scaledRadius * (cfg.nearDepthRangeRatio + (4 - cfg.nearDepthRangeRatio) * t);
17204
+ gsRenderer.setDepthRangeLimit(depthRange);
17205
+ }
17206
+ }
17207
+ if (t < 0.3) {
17208
+ gsRenderer.setSortFrequency(2);
17209
+ } else {
17210
+ gsRenderer.setSortFrequency(1);
17211
+ }
17212
+ }
16515
17213
  render() {
16516
17214
  this.camera.setAspect(this.renderer.getAspectRatio());
16517
17215
  this.controls.update();
17216
+ this.updateAdaptivePerformance();
16518
17217
  this.hotspotManager.updateBillboards();
16519
17218
  const pass = this.renderer.beginFrame();
16520
17219
  const gsRenderer = this.sceneManager.getGSRenderer();
@@ -16525,10 +17224,12 @@ class App {
16525
17224
  this.sceneAids.render(pass);
16526
17225
  this.gizmoManager.render(pass);
16527
17226
  this.renderer.endFrame();
16528
- this.hotspotManager.consumeGPUResult();
16529
- if (gsRenderer == null ? void 0 : gsRenderer.prepareDepthNormalPass) {
16530
- const [px, py] = this.hotspotManager.getPickPixel();
16531
- gsRenderer.prepareDepthNormalPass(px, py);
17227
+ if (this.hotspotManager.isActive()) {
17228
+ this.hotspotManager.consumeGPUResult();
17229
+ if (gsRenderer == null ? void 0 : gsRenderer.prepareDepthNormalPass) {
17230
+ const [px, py] = this.hotspotManager.getPickPixel();
17231
+ gsRenderer.prepareDepthNormalPass(px, py);
17232
+ }
16532
17233
  }
16533
17234
  }
16534
17235
  onResize() {
@@ -16633,7 +17334,14 @@ class App {
16633
17334
  return this.sceneManager.getOverlayMeshColor(index);
16634
17335
  }
16635
17336
  setOverlayMeshRangeColor(startIndex, count, r, g, b, a = 1) {
16636
- return this.sceneManager.setOverlayMeshRangeColor(startIndex, count, r, g, b, a);
17337
+ return this.sceneManager.setOverlayMeshRangeColor(
17338
+ startIndex,
17339
+ count,
17340
+ r,
17341
+ g,
17342
+ b,
17343
+ a
17344
+ );
16637
17345
  }
16638
17346
  createOverlayMeshGroupProxy(startIndex, count) {
16639
17347
  const meshes = this.sceneManager.getOverlayMeshRange(startIndex, count);
@@ -16770,16 +17478,28 @@ class App {
16770
17478
  return this.hotspotManager.getHotspotBillboard(hotspotIndex);
16771
17479
  }
16772
17480
  setHotspotPlacedScale(hotspotIndex, newPlacedScale) {
16773
- return this.hotspotManager.setHotspotPlacedScale(hotspotIndex, newPlacedScale);
17481
+ return this.hotspotManager.setHotspotPlacedScale(
17482
+ hotspotIndex,
17483
+ newPlacedScale
17484
+ );
16774
17485
  }
16775
17486
  getHotspotCount() {
16776
17487
  return this.hotspotManager.getHotspotCount();
16777
17488
  }
16778
17489
  findHotspotIndexByMeshStart(overlayMeshStartIndex) {
16779
- return this.hotspotManager.findHotspotIndexByMeshStart(overlayMeshStartIndex);
17490
+ return this.hotspotManager.findHotspotIndexByMeshStart(
17491
+ overlayMeshStartIndex
17492
+ );
16780
17493
  }
16781
17494
  async placeHotspotAt(objUrl, position, normal, visualDiameter, normalOffset, overrideScale) {
16782
- return this.hotspotManager.placeHotspotAt(objUrl, position, normal, visualDiameter, normalOffset, overrideScale);
17495
+ return this.hotspotManager.placeHotspotAt(
17496
+ objUrl,
17497
+ position,
17498
+ normal,
17499
+ visualDiameter,
17500
+ normalOffset,
17501
+ overrideScale
17502
+ );
16783
17503
  }
16784
17504
  getOverlayMeshByIndex(index) {
16785
17505
  return this.sceneManager.getOverlayMeshByIndex(index);
@@ -16810,7 +17530,13 @@ class App {
16810
17530
  * 设置热点文字标签
16811
17531
  */
16812
17532
  setHotspotLabel(hotspotIndex, text, position, fontSize, visible) {
16813
- return this.hotspotManager.setHotspotLabel(hotspotIndex, text, position, fontSize, visible);
17533
+ return this.hotspotManager.setHotspotLabel(
17534
+ hotspotIndex,
17535
+ text,
17536
+ position,
17537
+ fontSize,
17538
+ visible
17539
+ );
16814
17540
  }
16815
17541
  /**
16816
17542
  * 设置热点标签可见性
@@ -16891,7 +17617,10 @@ class App {
16891
17617
  * 适用于移动端提质或桌面端降负载
16892
17618
  */
16893
17619
  setRenderScale(scale) {
16894
- this.renderer.setRenderScale(scale);
17620
+ this.baseRenderScale = scale;
17621
+ if (!this.adaptivePerformanceEnabled) {
17622
+ this.renderer.setRenderScale(scale);
17623
+ }
16895
17624
  }
16896
17625
  getRenderScale() {
16897
17626
  return this.renderer.getRenderScale();
@@ -16939,6 +17668,49 @@ class App {
16939
17668
  mobileRenderer.setSortFrequency(frequency);
16940
17669
  }
16941
17670
  }
17671
+ /**
17672
+ * 是否检测到 Apple GPU(M1/M2/M3 等)
17673
+ */
17674
+ get isAppleGPU() {
17675
+ var _a2;
17676
+ return ((_a2 = this.renderer) == null ? void 0 : _a2.isAppleGPU) ?? false;
17677
+ }
17678
+ // ============================================
17679
+ // 自适应性能控制
17680
+ // ============================================
17681
+ /**
17682
+ * 启用/禁用自适应性能优化
17683
+ * 开启后系统会根据相机与模型的距离自动调节:
17684
+ * - 渲染分辨率(renderScale)
17685
+ * - 亚像素剔除阈值(pixelThreshold)
17686
+ * - 最大可见 splat 数量(maxVisibleSplats)
17687
+ */
17688
+ setAdaptivePerformance(enabled) {
17689
+ this.adaptivePerformanceEnabled = enabled;
17690
+ if (!enabled) {
17691
+ this.renderer.setRenderScale(this.baseRenderScale);
17692
+ this.lastAppliedRenderScale = this.baseRenderScale;
17693
+ const gsRenderer = this.getGSRenderer();
17694
+ if (gsRenderer) {
17695
+ gsRenderer.setPixelCullThreshold(1);
17696
+ gsRenderer.setMaxVisibleSplats(0);
17697
+ gsRenderer.setDepthRangeLimit(0);
17698
+ gsRenderer.setSortFrequency(1);
17699
+ }
17700
+ }
17701
+ }
17702
+ getAdaptivePerformance() {
17703
+ return this.adaptivePerformanceEnabled;
17704
+ }
17705
+ /**
17706
+ * 设置自适应性能配置参数
17707
+ */
17708
+ setAdaptivePerformanceConfig(config) {
17709
+ this.adaptiveConfig = { ...this.adaptiveConfig, ...config };
17710
+ }
17711
+ getAdaptivePerformanceConfig() {
17712
+ return { ...this.adaptiveConfig };
17713
+ }
16942
17714
  /**
16943
17715
  * 销毁应用及所有资源
16944
17716
  */