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