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