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