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