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