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