@d5techs/3dgs-lib 1.1.0 → 1.1.1
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 +2948 -349
- package/dist/3dgs-lib.cjs.map +1 -1
- package/dist/3dgs-lib.js +2948 -349
- package/dist/3dgs-lib.js.map +1 -1
- package/dist/App.d.ts +28 -7
- package/dist/core/BoundingBoxRenderer.d.ts +1 -3
- package/dist/core/OrbitControls.d.ts +21 -41
- package/dist/core/gizmo/{TransformGizmoV2.d.ts → TransformGizmo.d.ts} +2 -2
- package/dist/core/{ViewportGizmo.d.ts → gizmo/ViewportGizmo.d.ts} +2 -2
- package/dist/core/gizmo/index.d.ts +3 -2
- package/dist/core/index.d.ts +7 -0
- package/dist/gs/GSSplatRenderer.d.ts +48 -2
- package/dist/gs/GSSplatRendererMobile.d.ts +2 -1
- package/dist/gs/GSSplatSorter.d.ts +3 -0
- package/dist/gs/IGSSplatRenderer.d.ts +31 -2
- package/dist/gs/SOGLoader.d.ts +25 -0
- package/dist/index.d.ts +10 -9
- package/dist/interaction/GizmoManager.d.ts +6 -6
- package/dist/interaction/HotspotManager.d.ts +142 -0
- package/dist/mesh/Mesh.d.ts +4 -8
- package/dist/mesh/MeshRenderer.d.ts +30 -14
- package/dist/scene/SceneManager.d.ts +8 -2
- package/package.json +4 -1
package/dist/3dgs-lib.js
CHANGED
|
@@ -53,7 +53,7 @@ function computeBoundingBox$1(positions) {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
const min = [positions[0], positions[1], positions[2]];
|
|
56
|
-
const
|
|
56
|
+
const max2 = [positions[0], positions[1], positions[2]];
|
|
57
57
|
for (let i = 3; i < positions.length; i += 3) {
|
|
58
58
|
const x = positions[i];
|
|
59
59
|
const y = positions[i + 1];
|
|
@@ -61,20 +61,20 @@ function computeBoundingBox$1(positions) {
|
|
|
61
61
|
min[0] = Math.min(min[0], x);
|
|
62
62
|
min[1] = Math.min(min[1], y);
|
|
63
63
|
min[2] = Math.min(min[2], z);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
max2[0] = Math.max(max2[0], x);
|
|
65
|
+
max2[1] = Math.max(max2[1], y);
|
|
66
|
+
max2[2] = Math.max(max2[2], z);
|
|
67
67
|
}
|
|
68
68
|
const center = [
|
|
69
|
-
(min[0] +
|
|
70
|
-
(min[1] +
|
|
71
|
-
(min[2] +
|
|
69
|
+
(min[0] + max2[0]) / 2,
|
|
70
|
+
(min[1] + max2[1]) / 2,
|
|
71
|
+
(min[2] + max2[2]) / 2
|
|
72
72
|
];
|
|
73
|
-
const dx =
|
|
74
|
-
const dy =
|
|
75
|
-
const dz =
|
|
73
|
+
const dx = max2[0] - min[0];
|
|
74
|
+
const dy = max2[1] - min[1];
|
|
75
|
+
const dz = max2[2] - min[2];
|
|
76
76
|
const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
|
|
77
|
-
return { min, max, center, radius };
|
|
77
|
+
return { min, max: max2, center, radius };
|
|
78
78
|
}
|
|
79
79
|
function mergeBoundingBoxes(boxes) {
|
|
80
80
|
if (boxes.length === 0) return null;
|
|
@@ -100,17 +100,17 @@ function mergeBoundingBoxes(boxes) {
|
|
|
100
100
|
const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
|
|
101
101
|
return { min: combinedMin, max: combinedMax, center, radius };
|
|
102
102
|
}
|
|
103
|
-
function createBoundingBoxFromMinMax(min,
|
|
103
|
+
function createBoundingBoxFromMinMax(min, max2) {
|
|
104
104
|
const center = [
|
|
105
|
-
(min[0] +
|
|
106
|
-
(min[1] +
|
|
107
|
-
(min[2] +
|
|
105
|
+
(min[0] + max2[0]) / 2,
|
|
106
|
+
(min[1] + max2[1]) / 2,
|
|
107
|
+
(min[2] + max2[2]) / 2
|
|
108
108
|
];
|
|
109
|
-
const dx =
|
|
110
|
-
const dy =
|
|
111
|
-
const dz =
|
|
109
|
+
const dx = max2[0] - min[0];
|
|
110
|
+
const dy = max2[1] - min[1];
|
|
111
|
+
const dz = max2[2] - min[2];
|
|
112
112
|
const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
|
|
113
|
-
return { min, max, center, radius };
|
|
113
|
+
return { min, max: max2, center, radius };
|
|
114
114
|
}
|
|
115
115
|
function transformBoundingBox(bbox, modelMatrix) {
|
|
116
116
|
const corners = [
|
|
@@ -438,10 +438,8 @@ class Camera {
|
|
|
438
438
|
__publicField(this, "fov", Math.PI / 4);
|
|
439
439
|
// 45度
|
|
440
440
|
__publicField(this, "aspect", 1);
|
|
441
|
-
__publicField(this, "near", 0.
|
|
442
|
-
|
|
443
|
-
__publicField(this, "far", 1e3);
|
|
444
|
-
// 减小远平面以提高深度精度
|
|
441
|
+
__publicField(this, "near", 0.01);
|
|
442
|
+
__publicField(this, "far", 9e3);
|
|
445
443
|
// 矩阵
|
|
446
444
|
__publicField(this, "viewMatrix", new Float32Array(16));
|
|
447
445
|
__publicField(this, "projectionMatrix", new Float32Array(16));
|
|
@@ -550,9 +548,7 @@ class OrbitControls {
|
|
|
550
548
|
// 球坐标参数
|
|
551
549
|
__publicField(this, "distance", 5);
|
|
552
550
|
__publicField(this, "theta", 0);
|
|
553
|
-
// 水平角 (绕Y轴)
|
|
554
551
|
__publicField(this, "phi", Math.PI / 4);
|
|
555
|
-
// 垂直角 (从Y轴向下)
|
|
556
552
|
// 限制
|
|
557
553
|
__publicField(this, "minDistance", 1e-3);
|
|
558
554
|
__publicField(this, "maxDistance", Infinity);
|
|
@@ -565,10 +561,20 @@ class OrbitControls {
|
|
|
565
561
|
// 移动端触摸灵敏度
|
|
566
562
|
__publicField(this, "touchZoomSpeed", 0.01);
|
|
567
563
|
__publicField(this, "touchPanSpeed", 3e-3);
|
|
564
|
+
// 阻尼(值越大越灵敏,越小越丝滑;0.12 约 5 帧消化 50% 增量)
|
|
565
|
+
__publicField(this, "enableDamping", true);
|
|
566
|
+
__publicField(this, "dampingFactor", 0.12);
|
|
568
567
|
// 状态
|
|
569
568
|
__publicField(this, "isDragging", false);
|
|
570
569
|
__publicField(this, "lastX", 0);
|
|
571
570
|
__publicField(this, "lastY", 0);
|
|
571
|
+
// 阻尼速度
|
|
572
|
+
__publicField(this, "velocityTheta", 0);
|
|
573
|
+
__publicField(this, "velocityPhi", 0);
|
|
574
|
+
__publicField(this, "velocityDistance", 0);
|
|
575
|
+
__publicField(this, "velocityPanX", 0);
|
|
576
|
+
__publicField(this, "velocityPanY", 0);
|
|
577
|
+
__publicField(this, "velocityPanZ", 0);
|
|
572
578
|
// 触摸手势状态
|
|
573
579
|
__publicField(this, "touchMode", "none");
|
|
574
580
|
__publicField(this, "lastTouchDistance", 0);
|
|
@@ -595,11 +601,8 @@ class OrbitControls {
|
|
|
595
601
|
this.boundOnTouchEnd = this.onTouchEnd.bind(this);
|
|
596
602
|
this.boundOnContextMenu = (e) => e.preventDefault();
|
|
597
603
|
this.setupEventListeners();
|
|
598
|
-
this.
|
|
604
|
+
this.applySpherical();
|
|
599
605
|
}
|
|
600
|
-
/**
|
|
601
|
-
* 设置事件监听
|
|
602
|
-
*/
|
|
603
606
|
setupEventListeners() {
|
|
604
607
|
this.canvas.addEventListener("mousedown", this.boundOnMouseDown);
|
|
605
608
|
this.canvas.addEventListener("mousemove", this.boundOnMouseMove);
|
|
@@ -617,9 +620,6 @@ class OrbitControls {
|
|
|
617
620
|
this.canvas.addEventListener("touchend", this.boundOnTouchEnd);
|
|
618
621
|
this.canvas.addEventListener("contextmenu", this.boundOnContextMenu);
|
|
619
622
|
}
|
|
620
|
-
/**
|
|
621
|
-
* 移除事件监听
|
|
622
|
-
*/
|
|
623
623
|
removeEventListeners() {
|
|
624
624
|
this.canvas.removeEventListener("mousedown", this.boundOnMouseDown);
|
|
625
625
|
this.canvas.removeEventListener("mousemove", this.boundOnMouseMove);
|
|
@@ -631,12 +631,37 @@ class OrbitControls {
|
|
|
631
631
|
this.canvas.removeEventListener("touchend", this.boundOnTouchEnd);
|
|
632
632
|
this.canvas.removeEventListener("contextmenu", this.boundOnContextMenu);
|
|
633
633
|
}
|
|
634
|
-
/**
|
|
635
|
-
* 销毁控制器
|
|
636
|
-
*/
|
|
637
634
|
destroy() {
|
|
638
635
|
this.removeEventListeners();
|
|
639
636
|
}
|
|
637
|
+
/**
|
|
638
|
+
* 从视图矩阵提取相机的 right 和 up 向量,用于屏幕空间平移
|
|
639
|
+
*/
|
|
640
|
+
getCameraAxes() {
|
|
641
|
+
const m = this.camera.viewMatrix;
|
|
642
|
+
return {
|
|
643
|
+
right: [m[0], m[4], m[8]],
|
|
644
|
+
up: [m[1], m[5], m[9]]
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* 屏幕空间平移:将屏幕 delta 映射到相机 right/up 方向
|
|
649
|
+
*/
|
|
650
|
+
panByScreenDelta(deltaX, deltaY) {
|
|
651
|
+
const { right, up } = this.getCameraAxes();
|
|
652
|
+
const scale = this.panSpeed * this.distance;
|
|
653
|
+
const dx = -deltaX * scale;
|
|
654
|
+
const dy = deltaY * scale;
|
|
655
|
+
if (this.enableDamping) {
|
|
656
|
+
this.velocityPanX += dx * right[0] + dy * up[0];
|
|
657
|
+
this.velocityPanY += dx * right[1] + dy * up[1];
|
|
658
|
+
this.velocityPanZ += dx * right[2] + dy * up[2];
|
|
659
|
+
} else {
|
|
660
|
+
this.camera.target[0] += dx * right[0] + dy * up[0];
|
|
661
|
+
this.camera.target[1] += dx * right[1] + dy * up[1];
|
|
662
|
+
this.camera.target[2] += dx * right[2] + dy * up[2];
|
|
663
|
+
}
|
|
664
|
+
}
|
|
640
665
|
onMouseDown(e) {
|
|
641
666
|
if (!this.enabled) return;
|
|
642
667
|
this.isDragging = true;
|
|
@@ -650,19 +675,20 @@ class OrbitControls {
|
|
|
650
675
|
this.lastX = e.clientX;
|
|
651
676
|
this.lastY = e.clientY;
|
|
652
677
|
if (e.buttons === 1) {
|
|
653
|
-
this.
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
this.
|
|
663
|
-
|
|
678
|
+
if (this.enableDamping) {
|
|
679
|
+
this.velocityTheta += -deltaX * this.rotateSpeed;
|
|
680
|
+
this.velocityPhi += -deltaY * this.rotateSpeed;
|
|
681
|
+
} else {
|
|
682
|
+
this.theta -= deltaX * this.rotateSpeed;
|
|
683
|
+
this.phi -= deltaY * this.rotateSpeed;
|
|
684
|
+
this.phi = Math.max(this.minPhi, Math.min(this.maxPhi, this.phi));
|
|
685
|
+
}
|
|
686
|
+
} else if (e.buttons === 2 || e.buttons === 4) {
|
|
687
|
+
this.panByScreenDelta(deltaX, deltaY);
|
|
688
|
+
}
|
|
689
|
+
if (!this.enableDamping) {
|
|
690
|
+
this.applySpherical();
|
|
664
691
|
}
|
|
665
|
-
this.update();
|
|
666
692
|
}
|
|
667
693
|
onMouseUp() {
|
|
668
694
|
this.isDragging = false;
|
|
@@ -670,12 +696,17 @@ class OrbitControls {
|
|
|
670
696
|
onWheel(e) {
|
|
671
697
|
e.preventDefault();
|
|
672
698
|
if (!this.enabled) return;
|
|
673
|
-
|
|
674
|
-
this.
|
|
675
|
-
this.
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
699
|
+
const zoomDelta = e.deltaY * this.zoomSpeed;
|
|
700
|
+
if (this.enableDamping) {
|
|
701
|
+
this.velocityDistance += zoomDelta;
|
|
702
|
+
} else {
|
|
703
|
+
this.distance *= Math.exp(zoomDelta);
|
|
704
|
+
this.distance = Math.max(
|
|
705
|
+
this.minDistance,
|
|
706
|
+
Math.min(this.maxDistance, this.distance)
|
|
707
|
+
);
|
|
708
|
+
this.applySpherical();
|
|
709
|
+
}
|
|
679
710
|
}
|
|
680
711
|
onTouchStart(e) {
|
|
681
712
|
e.preventDefault();
|
|
@@ -700,33 +731,51 @@ class OrbitControls {
|
|
|
700
731
|
const deltaY = e.touches[0].clientY - this.lastY;
|
|
701
732
|
this.lastX = e.touches[0].clientX;
|
|
702
733
|
this.lastY = e.touches[0].clientY;
|
|
703
|
-
this.
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
734
|
+
if (this.enableDamping) {
|
|
735
|
+
this.velocityTheta += -deltaX * this.rotateSpeed;
|
|
736
|
+
this.velocityPhi += -deltaY * this.rotateSpeed;
|
|
737
|
+
} else {
|
|
738
|
+
this.theta -= deltaX * this.rotateSpeed;
|
|
739
|
+
this.phi -= deltaY * this.rotateSpeed;
|
|
740
|
+
this.phi = Math.max(this.minPhi, Math.min(this.maxPhi, this.phi));
|
|
741
|
+
this.applySpherical();
|
|
742
|
+
}
|
|
707
743
|
} else if (e.touches.length === 2) {
|
|
708
744
|
const currentDistance = this.getTouchDistance(e.touches);
|
|
709
745
|
const currentCenter = this.getTouchCenter(e.touches);
|
|
710
746
|
if (this.lastTouchDistance > 0) {
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
this.
|
|
714
|
-
this.
|
|
715
|
-
|
|
716
|
-
|
|
747
|
+
const ratio = currentDistance / this.lastTouchDistance;
|
|
748
|
+
const zoomDelta = -Math.log(ratio) / (this.touchZoomSpeed * 100);
|
|
749
|
+
if (this.enableDamping) {
|
|
750
|
+
this.velocityDistance += zoomDelta;
|
|
751
|
+
} else {
|
|
752
|
+
this.distance *= Math.exp(zoomDelta);
|
|
753
|
+
this.distance = Math.max(
|
|
754
|
+
this.minDistance,
|
|
755
|
+
Math.min(this.maxDistance, this.distance)
|
|
756
|
+
);
|
|
757
|
+
}
|
|
717
758
|
}
|
|
718
759
|
const deltaX = currentCenter.x - this.lastTouchCenter.x;
|
|
719
760
|
const deltaY = currentCenter.y - this.lastTouchCenter.y;
|
|
720
|
-
const
|
|
721
|
-
const
|
|
722
|
-
const
|
|
723
|
-
const
|
|
724
|
-
this.
|
|
725
|
-
|
|
726
|
-
|
|
761
|
+
const { right, up } = this.getCameraAxes();
|
|
762
|
+
const scale = this.touchPanSpeed * this.distance;
|
|
763
|
+
const dx = -deltaX * scale;
|
|
764
|
+
const dy = deltaY * scale;
|
|
765
|
+
if (this.enableDamping) {
|
|
766
|
+
this.velocityPanX += dx * right[0] + dy * up[0];
|
|
767
|
+
this.velocityPanY += dx * right[1] + dy * up[1];
|
|
768
|
+
this.velocityPanZ += dx * right[2] + dy * up[2];
|
|
769
|
+
} else {
|
|
770
|
+
this.camera.target[0] += dx * right[0] + dy * up[0];
|
|
771
|
+
this.camera.target[1] += dx * right[1] + dy * up[1];
|
|
772
|
+
this.camera.target[2] += dx * right[2] + dy * up[2];
|
|
773
|
+
}
|
|
727
774
|
this.lastTouchDistance = currentDistance;
|
|
728
775
|
this.lastTouchCenter = currentCenter;
|
|
729
|
-
this.
|
|
776
|
+
if (!this.enableDamping) {
|
|
777
|
+
this.applySpherical();
|
|
778
|
+
}
|
|
730
779
|
}
|
|
731
780
|
}
|
|
732
781
|
onTouchEnd(e) {
|
|
@@ -740,17 +789,11 @@ class OrbitControls {
|
|
|
740
789
|
this.lastY = e.touches[0].clientY;
|
|
741
790
|
}
|
|
742
791
|
}
|
|
743
|
-
/**
|
|
744
|
-
* 计算双指之间的距离
|
|
745
|
-
*/
|
|
746
792
|
getTouchDistance(touches) {
|
|
747
793
|
const dx = touches[0].clientX - touches[1].clientX;
|
|
748
794
|
const dy = touches[0].clientY - touches[1].clientY;
|
|
749
795
|
return Math.sqrt(dx * dx + dy * dy);
|
|
750
796
|
}
|
|
751
|
-
/**
|
|
752
|
-
* 计算双指的中心点
|
|
753
|
-
*/
|
|
754
797
|
getTouchCenter(touches) {
|
|
755
798
|
return {
|
|
756
799
|
x: (touches[0].clientX + touches[1].clientX) / 2,
|
|
@@ -758,9 +801,9 @@ class OrbitControls {
|
|
|
758
801
|
};
|
|
759
802
|
}
|
|
760
803
|
/**
|
|
761
|
-
*
|
|
804
|
+
* 将球坐标写入相机位置(内部方法,不处理阻尼)
|
|
762
805
|
*/
|
|
763
|
-
|
|
806
|
+
applySpherical() {
|
|
764
807
|
const sinPhi = Math.sin(this.phi);
|
|
765
808
|
const cosPhi = Math.cos(this.phi);
|
|
766
809
|
const sinTheta = Math.sin(this.theta);
|
|
@@ -771,12 +814,42 @@ class OrbitControls {
|
|
|
771
814
|
this.camera.updateMatrix();
|
|
772
815
|
}
|
|
773
816
|
/**
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
* @param positive 是否正向
|
|
777
|
-
* @param animate 是否动画过渡
|
|
817
|
+
* 每帧调用:应用阻尼衰减并更新相机
|
|
818
|
+
* 在渲染循环中调用此方法以获得平滑惯性效果
|
|
778
819
|
*/
|
|
820
|
+
update() {
|
|
821
|
+
if (this.enableDamping) {
|
|
822
|
+
const EPS = 1e-6;
|
|
823
|
+
const f = this.dampingFactor;
|
|
824
|
+
this.theta += this.velocityTheta * f;
|
|
825
|
+
this.phi += this.velocityPhi * f;
|
|
826
|
+
this.phi = Math.max(this.minPhi, Math.min(this.maxPhi, this.phi));
|
|
827
|
+
this.distance *= Math.exp(this.velocityDistance * f);
|
|
828
|
+
this.distance = Math.max(
|
|
829
|
+
this.minDistance,
|
|
830
|
+
Math.min(this.maxDistance, this.distance)
|
|
831
|
+
);
|
|
832
|
+
this.camera.target[0] += this.velocityPanX * f;
|
|
833
|
+
this.camera.target[1] += this.velocityPanY * f;
|
|
834
|
+
this.camera.target[2] += this.velocityPanZ * f;
|
|
835
|
+
const decay = 1 - f;
|
|
836
|
+
this.velocityTheta *= decay;
|
|
837
|
+
this.velocityPhi *= decay;
|
|
838
|
+
this.velocityDistance *= decay;
|
|
839
|
+
this.velocityPanX *= decay;
|
|
840
|
+
this.velocityPanY *= decay;
|
|
841
|
+
this.velocityPanZ *= decay;
|
|
842
|
+
if (Math.abs(this.velocityTheta) < EPS) this.velocityTheta = 0;
|
|
843
|
+
if (Math.abs(this.velocityPhi) < EPS) this.velocityPhi = 0;
|
|
844
|
+
if (Math.abs(this.velocityDistance) < EPS) this.velocityDistance = 0;
|
|
845
|
+
if (Math.abs(this.velocityPanX) < EPS) this.velocityPanX = 0;
|
|
846
|
+
if (Math.abs(this.velocityPanY) < EPS) this.velocityPanY = 0;
|
|
847
|
+
if (Math.abs(this.velocityPanZ) < EPS) this.velocityPanZ = 0;
|
|
848
|
+
}
|
|
849
|
+
this.applySpherical();
|
|
850
|
+
}
|
|
779
851
|
setViewAxis(axis, positive, animate = true) {
|
|
852
|
+
this.clearVelocity();
|
|
780
853
|
let targetTheta = this.theta;
|
|
781
854
|
let targetPhi = this.phi;
|
|
782
855
|
switch (axis) {
|
|
@@ -797,12 +870,9 @@ class OrbitControls {
|
|
|
797
870
|
} else {
|
|
798
871
|
this.theta = targetTheta;
|
|
799
872
|
this.phi = targetPhi;
|
|
800
|
-
this.
|
|
873
|
+
this.applySpherical();
|
|
801
874
|
}
|
|
802
875
|
}
|
|
803
|
-
/**
|
|
804
|
-
* 动画过渡到目标视图
|
|
805
|
-
*/
|
|
806
876
|
animateToView(targetTheta, targetPhi) {
|
|
807
877
|
const startTheta = this.theta;
|
|
808
878
|
const startPhi = this.phi;
|
|
@@ -817,28 +887,20 @@ class OrbitControls {
|
|
|
817
887
|
const eased = 1 - Math.pow(1 - progress, 3);
|
|
818
888
|
this.theta = startTheta + deltaTheta * eased;
|
|
819
889
|
this.phi = startPhi + (targetPhi - startPhi) * eased;
|
|
820
|
-
this.
|
|
890
|
+
this.applySpherical();
|
|
821
891
|
if (progress < 1) {
|
|
822
892
|
requestAnimationFrame(animate);
|
|
823
893
|
}
|
|
824
894
|
};
|
|
825
895
|
requestAnimationFrame(animate);
|
|
826
896
|
}
|
|
827
|
-
/**
|
|
828
|
-
* 设置相机目标点(控制器旋转中心)
|
|
829
|
-
* @param x X 坐标
|
|
830
|
-
* @param y Y 坐标
|
|
831
|
-
* @param z Z 坐标
|
|
832
|
-
*/
|
|
833
897
|
setTarget(x, y, z) {
|
|
898
|
+
this.clearVelocity();
|
|
834
899
|
this.camera.target[0] = x;
|
|
835
900
|
this.camera.target[1] = y;
|
|
836
901
|
this.camera.target[2] = z;
|
|
837
|
-
this.
|
|
902
|
+
this.applySpherical();
|
|
838
903
|
}
|
|
839
|
-
/**
|
|
840
|
-
* 获取当前目标点
|
|
841
|
-
*/
|
|
842
904
|
getTarget() {
|
|
843
905
|
return [
|
|
844
906
|
this.camera.target[0],
|
|
@@ -846,13 +908,8 @@ class OrbitControls {
|
|
|
846
908
|
this.camera.target[2]
|
|
847
909
|
];
|
|
848
910
|
}
|
|
849
|
-
/**
|
|
850
|
-
* 根据模型参数自动调整相机位置和参数
|
|
851
|
-
* @param center 模型中心点
|
|
852
|
-
* @param radius 模型包围球半径
|
|
853
|
-
* @param animate 是否使用动画过渡
|
|
854
|
-
*/
|
|
855
911
|
frameModel(center, radius, animate = true) {
|
|
912
|
+
this.clearVelocity();
|
|
856
913
|
const fovRad = this.camera.fov;
|
|
857
914
|
const halfFov = fovRad / 2;
|
|
858
915
|
const marginFactor = 1.5;
|
|
@@ -865,12 +922,9 @@ class OrbitControls {
|
|
|
865
922
|
this.camera.target[1] = center[1];
|
|
866
923
|
this.camera.target[2] = center[2];
|
|
867
924
|
this.distance = clampedDistance;
|
|
868
|
-
this.
|
|
925
|
+
this.applySpherical();
|
|
869
926
|
}
|
|
870
927
|
}
|
|
871
|
-
/**
|
|
872
|
-
* 动画过渡到目标帧(包含目标点和距离)
|
|
873
|
-
*/
|
|
874
928
|
animateToFrame(targetCenter, targetDistance) {
|
|
875
929
|
const startTarget = [
|
|
876
930
|
this.camera.target[0],
|
|
@@ -888,13 +942,21 @@ class OrbitControls {
|
|
|
888
942
|
this.camera.target[1] = startTarget[1] + (targetCenter[1] - startTarget[1]) * eased;
|
|
889
943
|
this.camera.target[2] = startTarget[2] + (targetCenter[2] - startTarget[2]) * eased;
|
|
890
944
|
this.distance = startDistance + (targetDistance - startDistance) * eased;
|
|
891
|
-
this.
|
|
945
|
+
this.applySpherical();
|
|
892
946
|
if (progress < 1) {
|
|
893
947
|
requestAnimationFrame(animate);
|
|
894
948
|
}
|
|
895
949
|
};
|
|
896
950
|
requestAnimationFrame(animate);
|
|
897
951
|
}
|
|
952
|
+
clearVelocity() {
|
|
953
|
+
this.velocityTheta = 0;
|
|
954
|
+
this.velocityPhi = 0;
|
|
955
|
+
this.velocityDistance = 0;
|
|
956
|
+
this.velocityPanX = 0;
|
|
957
|
+
this.velocityPanY = 0;
|
|
958
|
+
this.velocityPanZ = 0;
|
|
959
|
+
}
|
|
898
960
|
}
|
|
899
961
|
const gizmoShaderCode = (
|
|
900
962
|
/* wgsl */
|
|
@@ -1557,11 +1619,11 @@ class BoundingBoxRenderer {
|
|
|
1557
1619
|
* 生成顶点数据
|
|
1558
1620
|
*/
|
|
1559
1621
|
generateVertices(box) {
|
|
1560
|
-
const { min, max } = box;
|
|
1622
|
+
const { min, max: max2 } = box;
|
|
1561
1623
|
const [r, g, b] = this.lineColor;
|
|
1562
|
-
const dx =
|
|
1563
|
-
const dy =
|
|
1564
|
-
const dz =
|
|
1624
|
+
const dx = max2[0] - min[0];
|
|
1625
|
+
const dy = max2[1] - min[1];
|
|
1626
|
+
const dz = max2[2] - min[2];
|
|
1565
1627
|
const lx = dx * this.cornerRatio;
|
|
1566
1628
|
const ly = dy * this.cornerRatio;
|
|
1567
1629
|
const lz = dz * this.cornerRatio;
|
|
@@ -1573,27 +1635,27 @@ class BoundingBoxRenderer {
|
|
|
1573
1635
|
addLine(min[0], min[1], min[2], min[0] + lx, min[1], min[2]);
|
|
1574
1636
|
addLine(min[0], min[1], min[2], min[0], min[1] + ly, min[2]);
|
|
1575
1637
|
addLine(min[0], min[1], min[2], min[0], min[1], min[2] + lz);
|
|
1576
|
-
addLine(
|
|
1577
|
-
addLine(
|
|
1578
|
-
addLine(
|
|
1579
|
-
addLine(min[0],
|
|
1580
|
-
addLine(min[0],
|
|
1581
|
-
addLine(min[0],
|
|
1582
|
-
addLine(
|
|
1583
|
-
addLine(
|
|
1584
|
-
addLine(
|
|
1585
|
-
addLine(min[0], min[1],
|
|
1586
|
-
addLine(min[0], min[1],
|
|
1587
|
-
addLine(min[0], min[1],
|
|
1588
|
-
addLine(
|
|
1589
|
-
addLine(
|
|
1590
|
-
addLine(
|
|
1591
|
-
addLine(min[0],
|
|
1592
|
-
addLine(min[0],
|
|
1593
|
-
addLine(min[0],
|
|
1594
|
-
addLine(
|
|
1595
|
-
addLine(
|
|
1596
|
-
addLine(
|
|
1638
|
+
addLine(max2[0], min[1], min[2], max2[0] - lx, min[1], min[2]);
|
|
1639
|
+
addLine(max2[0], min[1], min[2], max2[0], min[1] + ly, min[2]);
|
|
1640
|
+
addLine(max2[0], min[1], min[2], max2[0], min[1], min[2] + lz);
|
|
1641
|
+
addLine(min[0], max2[1], min[2], min[0] + lx, max2[1], min[2]);
|
|
1642
|
+
addLine(min[0], max2[1], min[2], min[0], max2[1] - ly, min[2]);
|
|
1643
|
+
addLine(min[0], max2[1], min[2], min[0], max2[1], min[2] + lz);
|
|
1644
|
+
addLine(max2[0], max2[1], min[2], max2[0] - lx, max2[1], min[2]);
|
|
1645
|
+
addLine(max2[0], max2[1], min[2], max2[0], max2[1] - ly, min[2]);
|
|
1646
|
+
addLine(max2[0], max2[1], min[2], max2[0], max2[1], min[2] + lz);
|
|
1647
|
+
addLine(min[0], min[1], max2[2], min[0] + lx, min[1], max2[2]);
|
|
1648
|
+
addLine(min[0], min[1], max2[2], min[0], min[1] + ly, max2[2]);
|
|
1649
|
+
addLine(min[0], min[1], max2[2], min[0], min[1], max2[2] - lz);
|
|
1650
|
+
addLine(max2[0], min[1], max2[2], max2[0] - lx, min[1], max2[2]);
|
|
1651
|
+
addLine(max2[0], min[1], max2[2], max2[0], min[1] + ly, max2[2]);
|
|
1652
|
+
addLine(max2[0], min[1], max2[2], max2[0], min[1], max2[2] - lz);
|
|
1653
|
+
addLine(min[0], max2[1], max2[2], min[0] + lx, max2[1], max2[2]);
|
|
1654
|
+
addLine(min[0], max2[1], max2[2], min[0], max2[1] - ly, max2[2]);
|
|
1655
|
+
addLine(min[0], max2[1], max2[2], min[0], max2[1], max2[2] - lz);
|
|
1656
|
+
addLine(max2[0], max2[1], max2[2], max2[0] - lx, max2[1], max2[2]);
|
|
1657
|
+
addLine(max2[0], max2[1], max2[2], max2[0], max2[1] - ly, max2[2]);
|
|
1658
|
+
addLine(max2[0], max2[1], max2[2], max2[0], max2[1], max2[2] - lz);
|
|
1597
1659
|
return new Float32Array(vertices);
|
|
1598
1660
|
}
|
|
1599
1661
|
/**
|
|
@@ -1906,6 +1968,7 @@ class MeshRenderer {
|
|
|
1906
1968
|
__publicField(this, "renderer");
|
|
1907
1969
|
__publicField(this, "camera");
|
|
1908
1970
|
__publicField(this, "items", []);
|
|
1971
|
+
__publicField(this, "overlayItems", []);
|
|
1909
1972
|
// 有纹理的管线
|
|
1910
1973
|
__publicField(this, "pipelineTextured");
|
|
1911
1974
|
__publicField(this, "pipelineTexturedDoubleSided");
|
|
@@ -1914,6 +1977,11 @@ class MeshRenderer {
|
|
|
1914
1977
|
__publicField(this, "pipelineUntextured");
|
|
1915
1978
|
__publicField(this, "pipelineUntexturedDoubleSided");
|
|
1916
1979
|
__publicField(this, "bindGroupLayoutUntextured");
|
|
1980
|
+
// 覆盖层管线(带 depthBias,用于贴面热点/指示器)
|
|
1981
|
+
__publicField(this, "overlayPipelineTextured");
|
|
1982
|
+
__publicField(this, "overlayPipelineTexturedDoubleSided");
|
|
1983
|
+
__publicField(this, "overlayPipelineUntextured");
|
|
1984
|
+
__publicField(this, "overlayPipelineUntexturedDoubleSided");
|
|
1917
1985
|
__publicField(this, "sampler");
|
|
1918
1986
|
__publicField(this, "defaultTexture");
|
|
1919
1987
|
// 光照方向
|
|
@@ -1967,6 +2035,19 @@ class MeshRenderer {
|
|
|
1967
2035
|
{ shaderLocation: 2, offset: 24, format: "float32x2" }
|
|
1968
2036
|
]
|
|
1969
2037
|
};
|
|
2038
|
+
const depthStencilNormal = {
|
|
2039
|
+
format: this.renderer.depthFormat,
|
|
2040
|
+
depthWriteEnabled: true,
|
|
2041
|
+
depthCompare: "less"
|
|
2042
|
+
};
|
|
2043
|
+
const depthStencilOverlay = {
|
|
2044
|
+
format: this.renderer.depthFormat,
|
|
2045
|
+
depthWriteEnabled: false,
|
|
2046
|
+
depthCompare: "less",
|
|
2047
|
+
depthBias: -2e3,
|
|
2048
|
+
depthBiasSlopeScale: -4,
|
|
2049
|
+
depthBiasClamp: 0
|
|
2050
|
+
};
|
|
1970
2051
|
const basePipelineDescTextured = {
|
|
1971
2052
|
layout: pipelineLayoutTextured,
|
|
1972
2053
|
vertex: {
|
|
@@ -1980,7 +2061,7 @@ class MeshRenderer {
|
|
|
1980
2061
|
targets: [{ format: this.renderer.format }]
|
|
1981
2062
|
},
|
|
1982
2063
|
primitive: { topology: "triangle-list", frontFace: "ccw" },
|
|
1983
|
-
depthStencil:
|
|
2064
|
+
depthStencil: depthStencilNormal
|
|
1984
2065
|
};
|
|
1985
2066
|
this.pipelineTextured = device.createRenderPipeline({
|
|
1986
2067
|
...basePipelineDescTextured,
|
|
@@ -1990,6 +2071,15 @@ class MeshRenderer {
|
|
|
1990
2071
|
...basePipelineDescTextured,
|
|
1991
2072
|
primitive: { ...basePipelineDescTextured.primitive, cullMode: "none" }
|
|
1992
2073
|
});
|
|
2074
|
+
const overlayDescTextured = { ...basePipelineDescTextured, depthStencil: depthStencilOverlay };
|
|
2075
|
+
this.overlayPipelineTextured = device.createRenderPipeline({
|
|
2076
|
+
...overlayDescTextured,
|
|
2077
|
+
primitive: { ...overlayDescTextured.primitive, cullMode: "back" }
|
|
2078
|
+
});
|
|
2079
|
+
this.overlayPipelineTexturedDoubleSided = device.createRenderPipeline({
|
|
2080
|
+
...overlayDescTextured,
|
|
2081
|
+
primitive: { ...overlayDescTextured.primitive, cullMode: "none" }
|
|
2082
|
+
});
|
|
1993
2083
|
const shaderModuleUntextured = device.createShaderModule({ code: shaderCodeUntextured });
|
|
1994
2084
|
this.bindGroupLayoutUntextured = device.createBindGroupLayout({
|
|
1995
2085
|
entries: [
|
|
@@ -2019,7 +2109,7 @@ class MeshRenderer {
|
|
|
2019
2109
|
targets: [{ format: this.renderer.format }]
|
|
2020
2110
|
},
|
|
2021
2111
|
primitive: { topology: "triangle-list", frontFace: "ccw" },
|
|
2022
|
-
depthStencil:
|
|
2112
|
+
depthStencil: depthStencilNormal
|
|
2023
2113
|
};
|
|
2024
2114
|
this.pipelineUntextured = device.createRenderPipeline({
|
|
2025
2115
|
...basePipelineDescUntextured,
|
|
@@ -2029,6 +2119,15 @@ class MeshRenderer {
|
|
|
2029
2119
|
...basePipelineDescUntextured,
|
|
2030
2120
|
primitive: { ...basePipelineDescUntextured.primitive, cullMode: "none" }
|
|
2031
2121
|
});
|
|
2122
|
+
const overlayDescUntextured = { ...basePipelineDescUntextured, depthStencil: depthStencilOverlay };
|
|
2123
|
+
this.overlayPipelineUntextured = device.createRenderPipeline({
|
|
2124
|
+
...overlayDescUntextured,
|
|
2125
|
+
primitive: { ...overlayDescUntextured.primitive, cullMode: "back" }
|
|
2126
|
+
});
|
|
2127
|
+
this.overlayPipelineUntexturedDoubleSided = device.createRenderPipeline({
|
|
2128
|
+
...overlayDescUntextured,
|
|
2129
|
+
primitive: { ...overlayDescUntextured.primitive, cullMode: "none" }
|
|
2130
|
+
});
|
|
2032
2131
|
}
|
|
2033
2132
|
/**
|
|
2034
2133
|
* 添加网格(带材质)- 每个 mesh 创建独立的 uniform buffer
|
|
@@ -2068,7 +2167,7 @@ class MeshRenderer {
|
|
|
2068
2167
|
this.items.push({ mesh, material: mat, uniformBuffer, bindGroup });
|
|
2069
2168
|
}
|
|
2070
2169
|
/**
|
|
2071
|
-
*
|
|
2170
|
+
* 移除网格(销毁 GPU 资源)
|
|
2072
2171
|
*/
|
|
2073
2172
|
removeMesh(mesh) {
|
|
2074
2173
|
const index = this.items.findIndex((item) => item.mesh === mesh);
|
|
@@ -2079,6 +2178,65 @@ class MeshRenderer {
|
|
|
2079
2178
|
this.items.splice(index, 1);
|
|
2080
2179
|
}
|
|
2081
2180
|
}
|
|
2181
|
+
/**
|
|
2182
|
+
* 从渲染列表中分离网格(不销毁 GPU 资源,可重新添加)
|
|
2183
|
+
*/
|
|
2184
|
+
detachMesh(mesh) {
|
|
2185
|
+
const index = this.items.findIndex((item) => item.mesh === mesh);
|
|
2186
|
+
if (index !== -1) {
|
|
2187
|
+
const item = this.items[index];
|
|
2188
|
+
item.uniformBuffer.destroy();
|
|
2189
|
+
this.items.splice(index, 1);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* 添加覆盖层网格(使用带 depthBias 的管线,贴面不被 3DGS 深度遮挡)
|
|
2194
|
+
*/
|
|
2195
|
+
addOverlayMesh(mesh, material) {
|
|
2196
|
+
const device = this.renderer.device;
|
|
2197
|
+
const mat = material || {
|
|
2198
|
+
baseColorFactor: [0.8, 0.8, 0.8, 1],
|
|
2199
|
+
baseColorTexture: null,
|
|
2200
|
+
metallicFactor: 0,
|
|
2201
|
+
roughnessFactor: 0.5,
|
|
2202
|
+
doubleSided: false
|
|
2203
|
+
};
|
|
2204
|
+
const uniformBuffer = device.createBuffer({
|
|
2205
|
+
size: UNIFORM_BUFFER_SIZE,
|
|
2206
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
2207
|
+
});
|
|
2208
|
+
let bindGroup;
|
|
2209
|
+
if (mesh.hasUV) {
|
|
2210
|
+
const texture = mat.baseColorTexture || this.defaultTexture;
|
|
2211
|
+
bindGroup = device.createBindGroup({
|
|
2212
|
+
layout: this.bindGroupLayoutTextured,
|
|
2213
|
+
entries: [
|
|
2214
|
+
{ binding: 0, resource: { buffer: uniformBuffer } },
|
|
2215
|
+
{ binding: 1, resource: this.sampler },
|
|
2216
|
+
{ binding: 2, resource: texture.createView() }
|
|
2217
|
+
]
|
|
2218
|
+
});
|
|
2219
|
+
} else {
|
|
2220
|
+
bindGroup = device.createBindGroup({
|
|
2221
|
+
layout: this.bindGroupLayoutUntextured,
|
|
2222
|
+
entries: [
|
|
2223
|
+
{ binding: 0, resource: { buffer: uniformBuffer } }
|
|
2224
|
+
]
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
this.overlayItems.push({ mesh, material: mat, uniformBuffer, bindGroup });
|
|
2228
|
+
}
|
|
2229
|
+
/**
|
|
2230
|
+
* 从覆盖层列表中分离网格(不销毁 vertex/index buffer)
|
|
2231
|
+
*/
|
|
2232
|
+
detachOverlayMesh(mesh) {
|
|
2233
|
+
const index = this.overlayItems.findIndex((item) => item.mesh === mesh);
|
|
2234
|
+
if (index !== -1) {
|
|
2235
|
+
const item = this.overlayItems[index];
|
|
2236
|
+
item.uniformBuffer.destroy();
|
|
2237
|
+
this.overlayItems.splice(index, 1);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2082
2240
|
/**
|
|
2083
2241
|
* 按索引移除网格
|
|
2084
2242
|
*/
|
|
@@ -2093,7 +2251,7 @@ class MeshRenderer {
|
|
|
2093
2251
|
return false;
|
|
2094
2252
|
}
|
|
2095
2253
|
/**
|
|
2096
|
-
*
|
|
2254
|
+
* 清空所有网格(含覆盖层)
|
|
2097
2255
|
*/
|
|
2098
2256
|
clear() {
|
|
2099
2257
|
for (const item of this.items) {
|
|
@@ -2101,6 +2259,11 @@ class MeshRenderer {
|
|
|
2101
2259
|
item.uniformBuffer.destroy();
|
|
2102
2260
|
}
|
|
2103
2261
|
this.items = [];
|
|
2262
|
+
for (const item of this.overlayItems) {
|
|
2263
|
+
item.mesh.destroy();
|
|
2264
|
+
item.uniformBuffer.destroy();
|
|
2265
|
+
}
|
|
2266
|
+
this.overlayItems = [];
|
|
2104
2267
|
}
|
|
2105
2268
|
/**
|
|
2106
2269
|
* 设置光照方向
|
|
@@ -2124,10 +2287,9 @@ class MeshRenderer {
|
|
|
2124
2287
|
return this.ambientIntensity;
|
|
2125
2288
|
}
|
|
2126
2289
|
/**
|
|
2127
|
-
*
|
|
2290
|
+
* 渲染所有网格(普通 + 覆盖层)
|
|
2128
2291
|
*/
|
|
2129
2292
|
render(pass) {
|
|
2130
|
-
if (this.items.length === 0) return;
|
|
2131
2293
|
const device = this.renderer.device;
|
|
2132
2294
|
const vpMatrix = new Float32Array(this.camera.viewProjectionMatrix);
|
|
2133
2295
|
const lightData = new Float32Array([
|
|
@@ -2136,7 +2298,12 @@ class MeshRenderer {
|
|
|
2136
2298
|
this.lightDir[2],
|
|
2137
2299
|
this.ambientIntensity
|
|
2138
2300
|
]);
|
|
2139
|
-
|
|
2301
|
+
this.renderItems(pass, this.items, device, vpMatrix, lightData, false);
|
|
2302
|
+
this.renderItems(pass, this.overlayItems, device, vpMatrix, lightData, true);
|
|
2303
|
+
}
|
|
2304
|
+
renderItems(pass, items, device, vpMatrix, lightData, overlay) {
|
|
2305
|
+
if (items.length === 0) return;
|
|
2306
|
+
for (const item of items) {
|
|
2140
2307
|
const { mesh, material, uniformBuffer, bindGroup } = item;
|
|
2141
2308
|
device.queue.writeBuffer(uniformBuffer, 0, vpMatrix.buffer);
|
|
2142
2309
|
device.queue.writeBuffer(uniformBuffer, 64, mesh.modelMatrix.buffer);
|
|
@@ -2144,10 +2311,18 @@ class MeshRenderer {
|
|
|
2144
2311
|
device.queue.writeBuffer(uniformBuffer, 128, colorData.buffer);
|
|
2145
2312
|
device.queue.writeBuffer(uniformBuffer, 144, lightData.buffer);
|
|
2146
2313
|
let pipeline;
|
|
2147
|
-
if (
|
|
2148
|
-
|
|
2314
|
+
if (overlay) {
|
|
2315
|
+
if (mesh.hasUV) {
|
|
2316
|
+
pipeline = material.doubleSided ? this.overlayPipelineTexturedDoubleSided : this.overlayPipelineTextured;
|
|
2317
|
+
} else {
|
|
2318
|
+
pipeline = material.doubleSided ? this.overlayPipelineUntexturedDoubleSided : this.overlayPipelineUntextured;
|
|
2319
|
+
}
|
|
2149
2320
|
} else {
|
|
2150
|
-
|
|
2321
|
+
if (mesh.hasUV) {
|
|
2322
|
+
pipeline = material.doubleSided ? this.pipelineTexturedDoubleSided : this.pipelineTextured;
|
|
2323
|
+
} else {
|
|
2324
|
+
pipeline = material.doubleSided ? this.pipelineUntexturedDoubleSided : this.pipelineUntextured;
|
|
2325
|
+
}
|
|
2151
2326
|
}
|
|
2152
2327
|
pass.setPipeline(pipeline);
|
|
2153
2328
|
pass.setBindGroup(0, bindGroup);
|
|
@@ -2169,18 +2344,12 @@ class MeshRenderer {
|
|
|
2169
2344
|
}
|
|
2170
2345
|
return null;
|
|
2171
2346
|
}
|
|
2172
|
-
/**
|
|
2173
|
-
* 获取指定索引网格的材质颜色
|
|
2174
|
-
*/
|
|
2175
2347
|
getMeshColor(index) {
|
|
2176
2348
|
if (index >= 0 && index < this.items.length) {
|
|
2177
2349
|
return [...this.items[index].material.baseColorFactor];
|
|
2178
2350
|
}
|
|
2179
2351
|
return null;
|
|
2180
2352
|
}
|
|
2181
|
-
/**
|
|
2182
|
-
* 设置指定索引网格的材质颜色
|
|
2183
|
-
*/
|
|
2184
2353
|
setMeshColor(index, r, g, b, a = 1) {
|
|
2185
2354
|
if (index >= 0 && index < this.items.length) {
|
|
2186
2355
|
this.items[index].material.baseColorFactor = [r, g, b, a];
|
|
@@ -2188,9 +2357,6 @@ class MeshRenderer {
|
|
|
2188
2357
|
}
|
|
2189
2358
|
return false;
|
|
2190
2359
|
}
|
|
2191
|
-
/**
|
|
2192
|
-
* 设置指定范围内所有网格的材质颜色
|
|
2193
|
-
*/
|
|
2194
2360
|
setMeshRangeColor(startIndex, count, r, g, b, a = 1) {
|
|
2195
2361
|
let modified = 0;
|
|
2196
2362
|
for (let i = 0; i < count; i++) {
|
|
@@ -2200,6 +2366,50 @@ class MeshRenderer {
|
|
|
2200
2366
|
}
|
|
2201
2367
|
return modified;
|
|
2202
2368
|
}
|
|
2369
|
+
// ============================================
|
|
2370
|
+
// 覆盖层网格查询 / 颜色 / 删除
|
|
2371
|
+
// ============================================
|
|
2372
|
+
getOverlayMeshCount() {
|
|
2373
|
+
return this.overlayItems.length;
|
|
2374
|
+
}
|
|
2375
|
+
getOverlayMeshByIndex(index) {
|
|
2376
|
+
if (index >= 0 && index < this.overlayItems.length) {
|
|
2377
|
+
return this.overlayItems[index].mesh;
|
|
2378
|
+
}
|
|
2379
|
+
return null;
|
|
2380
|
+
}
|
|
2381
|
+
removeOverlayMeshByIndex(index) {
|
|
2382
|
+
if (index >= 0 && index < this.overlayItems.length) {
|
|
2383
|
+
const item = this.overlayItems[index];
|
|
2384
|
+
item.mesh.destroy();
|
|
2385
|
+
item.uniformBuffer.destroy();
|
|
2386
|
+
this.overlayItems.splice(index, 1);
|
|
2387
|
+
return true;
|
|
2388
|
+
}
|
|
2389
|
+
return false;
|
|
2390
|
+
}
|
|
2391
|
+
getOverlayMeshColor(index) {
|
|
2392
|
+
if (index >= 0 && index < this.overlayItems.length) {
|
|
2393
|
+
return [...this.overlayItems[index].material.baseColorFactor];
|
|
2394
|
+
}
|
|
2395
|
+
return null;
|
|
2396
|
+
}
|
|
2397
|
+
setOverlayMeshColor(index, r, g, b, a = 1) {
|
|
2398
|
+
if (index >= 0 && index < this.overlayItems.length) {
|
|
2399
|
+
this.overlayItems[index].material.baseColorFactor = [r, g, b, a];
|
|
2400
|
+
return true;
|
|
2401
|
+
}
|
|
2402
|
+
return false;
|
|
2403
|
+
}
|
|
2404
|
+
setOverlayMeshRangeColor(startIndex, count, r, g, b, a = 1) {
|
|
2405
|
+
let modified = 0;
|
|
2406
|
+
for (let i = 0; i < count; i++) {
|
|
2407
|
+
if (this.setOverlayMeshColor(startIndex + i, r, g, b, a)) {
|
|
2408
|
+
modified++;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
return modified;
|
|
2412
|
+
}
|
|
2203
2413
|
getCombinedBoundingBox() {
|
|
2204
2414
|
if (this.items.length === 0) return null;
|
|
2205
2415
|
let combinedMin = null;
|
|
@@ -3187,12 +3397,12 @@ class OBJParser {
|
|
|
3187
3397
|
* 解析材质引用 (usemtl name)
|
|
3188
3398
|
*/
|
|
3189
3399
|
parseUseMaterial(parts) {
|
|
3190
|
-
var
|
|
3400
|
+
var _a2;
|
|
3191
3401
|
if (parts.length > 1) {
|
|
3192
3402
|
this.currentMaterial = parts.slice(1).join(" ");
|
|
3193
3403
|
if (this.currentObject && this.currentObject.indices.length > 0) {
|
|
3194
3404
|
this.finalizeCurrentObject();
|
|
3195
|
-
this.createNewObject(((
|
|
3405
|
+
this.createNewObject(((_a2 = this.currentObject) == null ? void 0 : _a2.name) || "default");
|
|
3196
3406
|
}
|
|
3197
3407
|
if (this.currentObject) {
|
|
3198
3408
|
this.currentObject.materialName = this.currentMaterial;
|
|
@@ -3519,11 +3729,14 @@ class OBJLoader {
|
|
|
3519
3729
|
this.device.queue.writeBuffer(indexBuffer, 0, indexData);
|
|
3520
3730
|
} else {
|
|
3521
3731
|
const indexData = new Uint16Array(obj.indices);
|
|
3732
|
+
const alignedSize = Math.ceil(indexData.byteLength / 4) * 4;
|
|
3733
|
+
const alignedBuffer = new Uint8Array(alignedSize);
|
|
3734
|
+
alignedBuffer.set(new Uint8Array(indexData.buffer, indexData.byteOffset, indexData.byteLength));
|
|
3522
3735
|
indexBuffer = this.device.createBuffer({
|
|
3523
|
-
size:
|
|
3736
|
+
size: alignedSize,
|
|
3524
3737
|
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST
|
|
3525
3738
|
});
|
|
3526
|
-
this.device.queue.writeBuffer(indexBuffer, 0,
|
|
3739
|
+
this.device.queue.writeBuffer(indexBuffer, 0, alignedBuffer);
|
|
3527
3740
|
}
|
|
3528
3741
|
const boundingBox = this.computeBoundingBoxFromPositions(obj.positions);
|
|
3529
3742
|
const mesh = new Mesh(vertexBuffer, vertexCount, indexBuffer, indexCount, boundingBox);
|
|
@@ -4009,7 +4222,7 @@ function createSeededRandom(seed) {
|
|
|
4009
4222
|
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
4010
4223
|
};
|
|
4011
4224
|
}
|
|
4012
|
-
const SH_C0 = 0.28209479177387814;
|
|
4225
|
+
const SH_C0$1 = 0.28209479177387814;
|
|
4013
4226
|
function computeImportanceSampling(buffer, dataOffset, stride, totalCount, sampleCount, opacityOffset, opacityType, scale0Offset, scale0Type, scale1Offset, scale1Type, scale2Offset, scale2Type, littleEndian, seed) {
|
|
4014
4227
|
const dataView = new DataView(buffer, dataOffset);
|
|
4015
4228
|
const random = createSeededRandom(seed);
|
|
@@ -4084,12 +4297,12 @@ async function parsePLYBuffer(buffer, options = {}) {
|
|
|
4084
4297
|
propMap.set(prop.name, prop);
|
|
4085
4298
|
}
|
|
4086
4299
|
const getOffset = (name) => {
|
|
4087
|
-
var
|
|
4088
|
-
return ((
|
|
4300
|
+
var _a2;
|
|
4301
|
+
return ((_a2 = propMap.get(name)) == null ? void 0 : _a2.byteOffset) ?? -1;
|
|
4089
4302
|
};
|
|
4090
4303
|
const getType = (name) => {
|
|
4091
|
-
var
|
|
4092
|
-
return ((
|
|
4304
|
+
var _a2;
|
|
4305
|
+
return ((_a2 = propMap.get(name)) == null ? void 0 : _a2.type) ?? "float";
|
|
4093
4306
|
};
|
|
4094
4307
|
const offsets = {
|
|
4095
4308
|
x: getOffset("x"),
|
|
@@ -4184,9 +4397,9 @@ async function parsePLYBuffer(buffer, options = {}) {
|
|
|
4184
4397
|
const f_dc_0 = offsets.f_dc_0 >= 0 ? readProperty(dataView, base + offsets.f_dc_0, types.f_dc_0, littleEndian) : 0;
|
|
4185
4398
|
const f_dc_1 = offsets.f_dc_1 >= 0 ? readProperty(dataView, base + offsets.f_dc_1, types.f_dc_1, littleEndian) : 0;
|
|
4186
4399
|
const f_dc_2 = offsets.f_dc_2 >= 0 ? readProperty(dataView, base + offsets.f_dc_2, types.f_dc_2, littleEndian) : 0;
|
|
4187
|
-
colors[outputIdx * 3 + 0] = 0.5 + SH_C0 * f_dc_0;
|
|
4188
|
-
colors[outputIdx * 3 + 1] = 0.5 + SH_C0 * f_dc_1;
|
|
4189
|
-
colors[outputIdx * 3 + 2] = 0.5 + SH_C0 * f_dc_2;
|
|
4400
|
+
colors[outputIdx * 3 + 0] = 0.5 + SH_C0$1 * f_dc_0;
|
|
4401
|
+
colors[outputIdx * 3 + 1] = 0.5 + SH_C0$1 * f_dc_1;
|
|
4402
|
+
colors[outputIdx * 3 + 2] = 0.5 + SH_C0$1 * f_dc_2;
|
|
4190
4403
|
const rawOpacity = offsets.opacity >= 0 ? readProperty(dataView, base + offsets.opacity, types.opacity, littleEndian) : 0;
|
|
4191
4404
|
opacities[outputIdx] = sigmoid(rawOpacity);
|
|
4192
4405
|
if (shCoeffs && shProps.length > 0) {
|
|
@@ -4375,6 +4588,605 @@ function deserializeSplat(data) {
|
|
|
4375
4588
|
}
|
|
4376
4589
|
return splats;
|
|
4377
4590
|
}
|
|
4591
|
+
var u8 = Uint8Array, u16 = Uint16Array, i32 = Int32Array;
|
|
4592
|
+
var fleb = new u8([
|
|
4593
|
+
0,
|
|
4594
|
+
0,
|
|
4595
|
+
0,
|
|
4596
|
+
0,
|
|
4597
|
+
0,
|
|
4598
|
+
0,
|
|
4599
|
+
0,
|
|
4600
|
+
0,
|
|
4601
|
+
1,
|
|
4602
|
+
1,
|
|
4603
|
+
1,
|
|
4604
|
+
1,
|
|
4605
|
+
2,
|
|
4606
|
+
2,
|
|
4607
|
+
2,
|
|
4608
|
+
2,
|
|
4609
|
+
3,
|
|
4610
|
+
3,
|
|
4611
|
+
3,
|
|
4612
|
+
3,
|
|
4613
|
+
4,
|
|
4614
|
+
4,
|
|
4615
|
+
4,
|
|
4616
|
+
4,
|
|
4617
|
+
5,
|
|
4618
|
+
5,
|
|
4619
|
+
5,
|
|
4620
|
+
5,
|
|
4621
|
+
0,
|
|
4622
|
+
/* unused */
|
|
4623
|
+
0,
|
|
4624
|
+
0,
|
|
4625
|
+
/* impossible */
|
|
4626
|
+
0
|
|
4627
|
+
]);
|
|
4628
|
+
var fdeb = new u8([
|
|
4629
|
+
0,
|
|
4630
|
+
0,
|
|
4631
|
+
0,
|
|
4632
|
+
0,
|
|
4633
|
+
1,
|
|
4634
|
+
1,
|
|
4635
|
+
2,
|
|
4636
|
+
2,
|
|
4637
|
+
3,
|
|
4638
|
+
3,
|
|
4639
|
+
4,
|
|
4640
|
+
4,
|
|
4641
|
+
5,
|
|
4642
|
+
5,
|
|
4643
|
+
6,
|
|
4644
|
+
6,
|
|
4645
|
+
7,
|
|
4646
|
+
7,
|
|
4647
|
+
8,
|
|
4648
|
+
8,
|
|
4649
|
+
9,
|
|
4650
|
+
9,
|
|
4651
|
+
10,
|
|
4652
|
+
10,
|
|
4653
|
+
11,
|
|
4654
|
+
11,
|
|
4655
|
+
12,
|
|
4656
|
+
12,
|
|
4657
|
+
13,
|
|
4658
|
+
13,
|
|
4659
|
+
/* unused */
|
|
4660
|
+
0,
|
|
4661
|
+
0
|
|
4662
|
+
]);
|
|
4663
|
+
var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
|
|
4664
|
+
var freb = function(eb, start) {
|
|
4665
|
+
var b = new u16(31);
|
|
4666
|
+
for (var i = 0; i < 31; ++i) {
|
|
4667
|
+
b[i] = start += 1 << eb[i - 1];
|
|
4668
|
+
}
|
|
4669
|
+
var r = new i32(b[30]);
|
|
4670
|
+
for (var i = 1; i < 30; ++i) {
|
|
4671
|
+
for (var j = b[i]; j < b[i + 1]; ++j) {
|
|
4672
|
+
r[j] = j - b[i] << 5 | i;
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
return { b, r };
|
|
4676
|
+
};
|
|
4677
|
+
var _a = freb(fleb, 2), fl = _a.b, revfl = _a.r;
|
|
4678
|
+
fl[28] = 258, revfl[258] = 28;
|
|
4679
|
+
var _b = freb(fdeb, 0), fd = _b.b;
|
|
4680
|
+
var rev = new u16(32768);
|
|
4681
|
+
for (var i = 0; i < 32768; ++i) {
|
|
4682
|
+
var x = (i & 43690) >> 1 | (i & 21845) << 1;
|
|
4683
|
+
x = (x & 52428) >> 2 | (x & 13107) << 2;
|
|
4684
|
+
x = (x & 61680) >> 4 | (x & 3855) << 4;
|
|
4685
|
+
rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1;
|
|
4686
|
+
}
|
|
4687
|
+
var hMap = function(cd, mb, r) {
|
|
4688
|
+
var s = cd.length;
|
|
4689
|
+
var i = 0;
|
|
4690
|
+
var l = new u16(mb);
|
|
4691
|
+
for (; i < s; ++i) {
|
|
4692
|
+
if (cd[i])
|
|
4693
|
+
++l[cd[i] - 1];
|
|
4694
|
+
}
|
|
4695
|
+
var le = new u16(mb);
|
|
4696
|
+
for (i = 1; i < mb; ++i) {
|
|
4697
|
+
le[i] = le[i - 1] + l[i - 1] << 1;
|
|
4698
|
+
}
|
|
4699
|
+
var co;
|
|
4700
|
+
if (r) {
|
|
4701
|
+
co = new u16(1 << mb);
|
|
4702
|
+
var rvb = 15 - mb;
|
|
4703
|
+
for (i = 0; i < s; ++i) {
|
|
4704
|
+
if (cd[i]) {
|
|
4705
|
+
var sv = i << 4 | cd[i];
|
|
4706
|
+
var r_1 = mb - cd[i];
|
|
4707
|
+
var v = le[cd[i] - 1]++ << r_1;
|
|
4708
|
+
for (var m = v | (1 << r_1) - 1; v <= m; ++v) {
|
|
4709
|
+
co[rev[v] >> rvb] = sv;
|
|
4710
|
+
}
|
|
4711
|
+
}
|
|
4712
|
+
}
|
|
4713
|
+
} else {
|
|
4714
|
+
co = new u16(s);
|
|
4715
|
+
for (i = 0; i < s; ++i) {
|
|
4716
|
+
if (cd[i]) {
|
|
4717
|
+
co[i] = rev[le[cd[i] - 1]++] >> 15 - cd[i];
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
return co;
|
|
4722
|
+
};
|
|
4723
|
+
var flt = new u8(288);
|
|
4724
|
+
for (var i = 0; i < 144; ++i)
|
|
4725
|
+
flt[i] = 8;
|
|
4726
|
+
for (var i = 144; i < 256; ++i)
|
|
4727
|
+
flt[i] = 9;
|
|
4728
|
+
for (var i = 256; i < 280; ++i)
|
|
4729
|
+
flt[i] = 7;
|
|
4730
|
+
for (var i = 280; i < 288; ++i)
|
|
4731
|
+
flt[i] = 8;
|
|
4732
|
+
var fdt = new u8(32);
|
|
4733
|
+
for (var i = 0; i < 32; ++i)
|
|
4734
|
+
fdt[i] = 5;
|
|
4735
|
+
var flrm = /* @__PURE__ */ hMap(flt, 9, 1);
|
|
4736
|
+
var fdrm = /* @__PURE__ */ hMap(fdt, 5, 1);
|
|
4737
|
+
var max = function(a) {
|
|
4738
|
+
var m = a[0];
|
|
4739
|
+
for (var i = 1; i < a.length; ++i) {
|
|
4740
|
+
if (a[i] > m)
|
|
4741
|
+
m = a[i];
|
|
4742
|
+
}
|
|
4743
|
+
return m;
|
|
4744
|
+
};
|
|
4745
|
+
var bits = function(d, p, m) {
|
|
4746
|
+
var o = p / 8 | 0;
|
|
4747
|
+
return (d[o] | d[o + 1] << 8) >> (p & 7) & m;
|
|
4748
|
+
};
|
|
4749
|
+
var bits16 = function(d, p) {
|
|
4750
|
+
var o = p / 8 | 0;
|
|
4751
|
+
return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7);
|
|
4752
|
+
};
|
|
4753
|
+
var shft = function(p) {
|
|
4754
|
+
return (p + 7) / 8 | 0;
|
|
4755
|
+
};
|
|
4756
|
+
var slc = function(v, s, e) {
|
|
4757
|
+
if (s == null || s < 0)
|
|
4758
|
+
s = 0;
|
|
4759
|
+
if (e == null || e > v.length)
|
|
4760
|
+
e = v.length;
|
|
4761
|
+
return new u8(v.subarray(s, e));
|
|
4762
|
+
};
|
|
4763
|
+
var ec = [
|
|
4764
|
+
"unexpected EOF",
|
|
4765
|
+
"invalid block type",
|
|
4766
|
+
"invalid length/literal",
|
|
4767
|
+
"invalid distance",
|
|
4768
|
+
"stream finished",
|
|
4769
|
+
"no stream handler",
|
|
4770
|
+
,
|
|
4771
|
+
"no callback",
|
|
4772
|
+
"invalid UTF-8 data",
|
|
4773
|
+
"extra field too long",
|
|
4774
|
+
"date not in range 1980-2099",
|
|
4775
|
+
"filename too long",
|
|
4776
|
+
"stream finishing",
|
|
4777
|
+
"invalid zip data"
|
|
4778
|
+
// determined by unknown compression method
|
|
4779
|
+
];
|
|
4780
|
+
var err = function(ind, msg, nt) {
|
|
4781
|
+
var e = new Error(msg || ec[ind]);
|
|
4782
|
+
e.code = ind;
|
|
4783
|
+
if (Error.captureStackTrace)
|
|
4784
|
+
Error.captureStackTrace(e, err);
|
|
4785
|
+
if (!nt)
|
|
4786
|
+
throw e;
|
|
4787
|
+
return e;
|
|
4788
|
+
};
|
|
4789
|
+
var inflt = function(dat, st, buf, dict) {
|
|
4790
|
+
var sl = dat.length, dl = dict ? dict.length : 0;
|
|
4791
|
+
if (!sl || st.f && !st.l)
|
|
4792
|
+
return buf || new u8(0);
|
|
4793
|
+
var noBuf = !buf;
|
|
4794
|
+
var resize = noBuf || st.i != 2;
|
|
4795
|
+
var noSt = st.i;
|
|
4796
|
+
if (noBuf)
|
|
4797
|
+
buf = new u8(sl * 3);
|
|
4798
|
+
var cbuf = function(l2) {
|
|
4799
|
+
var bl = buf.length;
|
|
4800
|
+
if (l2 > bl) {
|
|
4801
|
+
var nbuf = new u8(Math.max(bl * 2, l2));
|
|
4802
|
+
nbuf.set(buf);
|
|
4803
|
+
buf = nbuf;
|
|
4804
|
+
}
|
|
4805
|
+
};
|
|
4806
|
+
var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n;
|
|
4807
|
+
var tbts = sl * 8;
|
|
4808
|
+
do {
|
|
4809
|
+
if (!lm) {
|
|
4810
|
+
final = bits(dat, pos, 1);
|
|
4811
|
+
var type = bits(dat, pos + 1, 3);
|
|
4812
|
+
pos += 3;
|
|
4813
|
+
if (!type) {
|
|
4814
|
+
var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l;
|
|
4815
|
+
if (t > sl) {
|
|
4816
|
+
if (noSt)
|
|
4817
|
+
err(0);
|
|
4818
|
+
break;
|
|
4819
|
+
}
|
|
4820
|
+
if (resize)
|
|
4821
|
+
cbuf(bt + l);
|
|
4822
|
+
buf.set(dat.subarray(s, t), bt);
|
|
4823
|
+
st.b = bt += l, st.p = pos = t * 8, st.f = final;
|
|
4824
|
+
continue;
|
|
4825
|
+
} else if (type == 1)
|
|
4826
|
+
lm = flrm, dm = fdrm, lbt = 9, dbt = 5;
|
|
4827
|
+
else if (type == 2) {
|
|
4828
|
+
var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4;
|
|
4829
|
+
var tl = hLit + bits(dat, pos + 5, 31) + 1;
|
|
4830
|
+
pos += 14;
|
|
4831
|
+
var ldt = new u8(tl);
|
|
4832
|
+
var clt = new u8(19);
|
|
4833
|
+
for (var i = 0; i < hcLen; ++i) {
|
|
4834
|
+
clt[clim[i]] = bits(dat, pos + i * 3, 7);
|
|
4835
|
+
}
|
|
4836
|
+
pos += hcLen * 3;
|
|
4837
|
+
var clb = max(clt), clbmsk = (1 << clb) - 1;
|
|
4838
|
+
var clm = hMap(clt, clb, 1);
|
|
4839
|
+
for (var i = 0; i < tl; ) {
|
|
4840
|
+
var r = clm[bits(dat, pos, clbmsk)];
|
|
4841
|
+
pos += r & 15;
|
|
4842
|
+
var s = r >> 4;
|
|
4843
|
+
if (s < 16) {
|
|
4844
|
+
ldt[i++] = s;
|
|
4845
|
+
} else {
|
|
4846
|
+
var c = 0, n = 0;
|
|
4847
|
+
if (s == 16)
|
|
4848
|
+
n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1];
|
|
4849
|
+
else if (s == 17)
|
|
4850
|
+
n = 3 + bits(dat, pos, 7), pos += 3;
|
|
4851
|
+
else if (s == 18)
|
|
4852
|
+
n = 11 + bits(dat, pos, 127), pos += 7;
|
|
4853
|
+
while (n--)
|
|
4854
|
+
ldt[i++] = c;
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit);
|
|
4858
|
+
lbt = max(lt);
|
|
4859
|
+
dbt = max(dt);
|
|
4860
|
+
lm = hMap(lt, lbt, 1);
|
|
4861
|
+
dm = hMap(dt, dbt, 1);
|
|
4862
|
+
} else
|
|
4863
|
+
err(1);
|
|
4864
|
+
if (pos > tbts) {
|
|
4865
|
+
if (noSt)
|
|
4866
|
+
err(0);
|
|
4867
|
+
break;
|
|
4868
|
+
}
|
|
4869
|
+
}
|
|
4870
|
+
if (resize)
|
|
4871
|
+
cbuf(bt + 131072);
|
|
4872
|
+
var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1;
|
|
4873
|
+
var lpos = pos;
|
|
4874
|
+
for (; ; lpos = pos) {
|
|
4875
|
+
var c = lm[bits16(dat, pos) & lms], sym = c >> 4;
|
|
4876
|
+
pos += c & 15;
|
|
4877
|
+
if (pos > tbts) {
|
|
4878
|
+
if (noSt)
|
|
4879
|
+
err(0);
|
|
4880
|
+
break;
|
|
4881
|
+
}
|
|
4882
|
+
if (!c)
|
|
4883
|
+
err(2);
|
|
4884
|
+
if (sym < 256)
|
|
4885
|
+
buf[bt++] = sym;
|
|
4886
|
+
else if (sym == 256) {
|
|
4887
|
+
lpos = pos, lm = null;
|
|
4888
|
+
break;
|
|
4889
|
+
} else {
|
|
4890
|
+
var add = sym - 254;
|
|
4891
|
+
if (sym > 264) {
|
|
4892
|
+
var i = sym - 257, b = fleb[i];
|
|
4893
|
+
add = bits(dat, pos, (1 << b) - 1) + fl[i];
|
|
4894
|
+
pos += b;
|
|
4895
|
+
}
|
|
4896
|
+
var d = dm[bits16(dat, pos) & dms], dsym = d >> 4;
|
|
4897
|
+
if (!d)
|
|
4898
|
+
err(3);
|
|
4899
|
+
pos += d & 15;
|
|
4900
|
+
var dt = fd[dsym];
|
|
4901
|
+
if (dsym > 3) {
|
|
4902
|
+
var b = fdeb[dsym];
|
|
4903
|
+
dt += bits16(dat, pos) & (1 << b) - 1, pos += b;
|
|
4904
|
+
}
|
|
4905
|
+
if (pos > tbts) {
|
|
4906
|
+
if (noSt)
|
|
4907
|
+
err(0);
|
|
4908
|
+
break;
|
|
4909
|
+
}
|
|
4910
|
+
if (resize)
|
|
4911
|
+
cbuf(bt + 131072);
|
|
4912
|
+
var end = bt + add;
|
|
4913
|
+
if (bt < dt) {
|
|
4914
|
+
var shift = dl - dt, dend = Math.min(dt, end);
|
|
4915
|
+
if (shift + bt < 0)
|
|
4916
|
+
err(3);
|
|
4917
|
+
for (; bt < dend; ++bt)
|
|
4918
|
+
buf[bt] = dict[shift + bt];
|
|
4919
|
+
}
|
|
4920
|
+
for (; bt < end; ++bt)
|
|
4921
|
+
buf[bt] = buf[bt - dt];
|
|
4922
|
+
}
|
|
4923
|
+
}
|
|
4924
|
+
st.l = lm, st.p = lpos, st.b = bt, st.f = final;
|
|
4925
|
+
if (lm)
|
|
4926
|
+
final = 1, st.m = lbt, st.d = dm, st.n = dbt;
|
|
4927
|
+
} while (!final);
|
|
4928
|
+
return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt);
|
|
4929
|
+
};
|
|
4930
|
+
var et = /* @__PURE__ */ new u8(0);
|
|
4931
|
+
var b2 = function(d, b) {
|
|
4932
|
+
return d[b] | d[b + 1] << 8;
|
|
4933
|
+
};
|
|
4934
|
+
var b4 = function(d, b) {
|
|
4935
|
+
return (d[b] | d[b + 1] << 8 | d[b + 2] << 16 | d[b + 3] << 24) >>> 0;
|
|
4936
|
+
};
|
|
4937
|
+
var b8 = function(d, b) {
|
|
4938
|
+
return b4(d, b) + b4(d, b + 4) * 4294967296;
|
|
4939
|
+
};
|
|
4940
|
+
function inflateSync(data, opts) {
|
|
4941
|
+
return inflt(data, { i: 2 }, opts && opts.out, opts && opts.dictionary);
|
|
4942
|
+
}
|
|
4943
|
+
var td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder();
|
|
4944
|
+
var tds = 0;
|
|
4945
|
+
try {
|
|
4946
|
+
td.decode(et, { stream: true });
|
|
4947
|
+
tds = 1;
|
|
4948
|
+
} catch (e) {
|
|
4949
|
+
}
|
|
4950
|
+
var dutf8 = function(d) {
|
|
4951
|
+
for (var r = "", i = 0; ; ) {
|
|
4952
|
+
var c = d[i++];
|
|
4953
|
+
var eb = (c > 127) + (c > 223) + (c > 239);
|
|
4954
|
+
if (i + eb > d.length)
|
|
4955
|
+
return { s: r, r: slc(d, i - 1) };
|
|
4956
|
+
if (!eb)
|
|
4957
|
+
r += String.fromCharCode(c);
|
|
4958
|
+
else if (eb == 3) {
|
|
4959
|
+
c = ((c & 15) << 18 | (d[i++] & 63) << 12 | (d[i++] & 63) << 6 | d[i++] & 63) - 65536, r += String.fromCharCode(55296 | c >> 10, 56320 | c & 1023);
|
|
4960
|
+
} else if (eb & 1)
|
|
4961
|
+
r += String.fromCharCode((c & 31) << 6 | d[i++] & 63);
|
|
4962
|
+
else
|
|
4963
|
+
r += String.fromCharCode((c & 15) << 12 | (d[i++] & 63) << 6 | d[i++] & 63);
|
|
4964
|
+
}
|
|
4965
|
+
};
|
|
4966
|
+
function strFromU8(dat, latin1) {
|
|
4967
|
+
if (latin1) {
|
|
4968
|
+
var r = "";
|
|
4969
|
+
for (var i = 0; i < dat.length; i += 16384)
|
|
4970
|
+
r += String.fromCharCode.apply(null, dat.subarray(i, i + 16384));
|
|
4971
|
+
return r;
|
|
4972
|
+
} else if (td) {
|
|
4973
|
+
return td.decode(dat);
|
|
4974
|
+
} else {
|
|
4975
|
+
var _a2 = dutf8(dat), s = _a2.s, r = _a2.r;
|
|
4976
|
+
if (r.length)
|
|
4977
|
+
err(8);
|
|
4978
|
+
return s;
|
|
4979
|
+
}
|
|
4980
|
+
}
|
|
4981
|
+
var slzh = function(d, b) {
|
|
4982
|
+
return b + 30 + b2(d, b + 26) + b2(d, b + 28);
|
|
4983
|
+
};
|
|
4984
|
+
var zh = function(d, b, z) {
|
|
4985
|
+
var fnl = b2(d, b + 28), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl, bs = b4(d, b + 20);
|
|
4986
|
+
var _a2 = z && bs == 4294967295 ? z64e(d, es) : [bs, b4(d, b + 24), b4(d, b + 42)], sc = _a2[0], su = _a2[1], off = _a2[2];
|
|
4987
|
+
return [b2(d, b + 10), sc, su, fn, es + b2(d, b + 30) + b2(d, b + 32), off];
|
|
4988
|
+
};
|
|
4989
|
+
var z64e = function(d, b) {
|
|
4990
|
+
for (; b2(d, b) != 1; b += 4 + b2(d, b + 2))
|
|
4991
|
+
;
|
|
4992
|
+
return [b8(d, b + 12), b8(d, b + 4), b8(d, b + 20)];
|
|
4993
|
+
};
|
|
4994
|
+
function unzipSync(data, opts) {
|
|
4995
|
+
var files = {};
|
|
4996
|
+
var e = data.length - 22;
|
|
4997
|
+
for (; b4(data, e) != 101010256; --e) {
|
|
4998
|
+
if (!e || data.length - e > 65558)
|
|
4999
|
+
err(13);
|
|
5000
|
+
}
|
|
5001
|
+
var c = b2(data, e + 8);
|
|
5002
|
+
if (!c)
|
|
5003
|
+
return {};
|
|
5004
|
+
var o = b4(data, e + 16);
|
|
5005
|
+
var z = o == 4294967295 || c == 65535;
|
|
5006
|
+
if (z) {
|
|
5007
|
+
var ze = b4(data, e - 12);
|
|
5008
|
+
z = b4(data, ze) == 101075792;
|
|
5009
|
+
if (z) {
|
|
5010
|
+
c = b4(data, ze + 32);
|
|
5011
|
+
o = b4(data, ze + 48);
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
for (var i = 0; i < c; ++i) {
|
|
5015
|
+
var _a2 = zh(data, o, z), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
|
|
5016
|
+
o = no;
|
|
5017
|
+
{
|
|
5018
|
+
if (!c_2)
|
|
5019
|
+
files[fn] = slc(data, b, b + sc);
|
|
5020
|
+
else if (c_2 == 8)
|
|
5021
|
+
files[fn] = inflateSync(data.subarray(b, b + sc), { out: new u8(su) });
|
|
5022
|
+
else
|
|
5023
|
+
err(14, "unknown compression type " + c_2);
|
|
5024
|
+
}
|
|
5025
|
+
}
|
|
5026
|
+
return files;
|
|
5027
|
+
}
|
|
5028
|
+
const SH_C0 = 0.28209479177387814;
|
|
5029
|
+
const COEFFS_PER_BAND = [3, 8, 15];
|
|
5030
|
+
function lerp(a, b, t) {
|
|
5031
|
+
return a + (b - a) * t;
|
|
5032
|
+
}
|
|
5033
|
+
function symUnlog(n) {
|
|
5034
|
+
return Math.sign(n) * (Math.exp(Math.abs(n)) - 1);
|
|
5035
|
+
}
|
|
5036
|
+
async function decodeWebP(bytes) {
|
|
5037
|
+
const blob = new Blob([bytes], { type: "image/webp" });
|
|
5038
|
+
const bitmap = await createImageBitmap(blob, {
|
|
5039
|
+
colorSpaceConversion: "none",
|
|
5040
|
+
premultiplyAlpha: "none"
|
|
5041
|
+
});
|
|
5042
|
+
const w = bitmap.width;
|
|
5043
|
+
const h = bitmap.height;
|
|
5044
|
+
const canvas = new OffscreenCanvas(w, h);
|
|
5045
|
+
const ctx = canvas.getContext("2d");
|
|
5046
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
5047
|
+
bitmap.close();
|
|
5048
|
+
return { width: w, height: h, data: ctx.getImageData(0, 0, w, h).data };
|
|
5049
|
+
}
|
|
5050
|
+
function decodeSHPalette(centroidsImg, codebook, paletteSize, bands) {
|
|
5051
|
+
const numCoeffs = COEFFS_PER_BAND[bands - 1];
|
|
5052
|
+
const palette = new Array(paletteSize);
|
|
5053
|
+
for (let n = 0; n < paletteSize; n++) {
|
|
5054
|
+
const entry = new Float32Array(numCoeffs * 3);
|
|
5055
|
+
for (let c = 0; c < numCoeffs; c++) {
|
|
5056
|
+
const u = n % 64 * numCoeffs + c;
|
|
5057
|
+
const v = Math.floor(n / 64);
|
|
5058
|
+
const off = (v * centroidsImg.width + u) * 4;
|
|
5059
|
+
entry[c * 3 + 0] = codebook[centroidsImg.data[off + 0]];
|
|
5060
|
+
entry[c * 3 + 1] = codebook[centroidsImg.data[off + 1]];
|
|
5061
|
+
entry[c * 3 + 2] = codebook[centroidsImg.data[off + 2]];
|
|
5062
|
+
}
|
|
5063
|
+
palette[n] = entry;
|
|
5064
|
+
}
|
|
5065
|
+
return palette;
|
|
5066
|
+
}
|
|
5067
|
+
async function loadSOG(url, onProgress) {
|
|
5068
|
+
const response = await fetch(url);
|
|
5069
|
+
if (!response.ok) throw new Error(`无法加载 SOG 文件: ${url}`);
|
|
5070
|
+
const buffer = await response.arrayBuffer();
|
|
5071
|
+
return deserializeSOG(buffer, onProgress);
|
|
5072
|
+
}
|
|
5073
|
+
async function deserializeSOG(data, onProgress) {
|
|
5074
|
+
if (onProgress) onProgress(0, "parse");
|
|
5075
|
+
const zipEntries = unzipSync(new Uint8Array(data));
|
|
5076
|
+
const metaKey = Object.keys(zipEntries).find((k) => k.endsWith("meta.json"));
|
|
5077
|
+
if (!metaKey) throw new Error("无效的 SOG 文件: 缺少 meta.json");
|
|
5078
|
+
const meta = JSON.parse(new TextDecoder().decode(zipEntries[metaKey]));
|
|
5079
|
+
if (meta.version !== 2) {
|
|
5080
|
+
throw new Error(`不支持的 SOG 版本: ${meta.version},仅支持版本 2`);
|
|
5081
|
+
}
|
|
5082
|
+
const prefix = metaKey.includes("/") ? metaKey.substring(0, metaKey.lastIndexOf("/") + 1) : "";
|
|
5083
|
+
const findFile = (name) => {
|
|
5084
|
+
const entry = zipEntries[prefix + name] ?? zipEntries[name];
|
|
5085
|
+
if (!entry) throw new Error(`SOG 缺少文件: ${name}`);
|
|
5086
|
+
return entry;
|
|
5087
|
+
};
|
|
5088
|
+
if (onProgress) onProgress(5, "parse");
|
|
5089
|
+
const decodeList = [
|
|
5090
|
+
decodeWebP(findFile(meta.means.files[0])),
|
|
5091
|
+
// 0: means_l
|
|
5092
|
+
decodeWebP(findFile(meta.means.files[1])),
|
|
5093
|
+
// 1: means_u
|
|
5094
|
+
decodeWebP(findFile(meta.scales.files[0])),
|
|
5095
|
+
// 2: scales
|
|
5096
|
+
decodeWebP(findFile(meta.quats.files[0])),
|
|
5097
|
+
// 3: quats
|
|
5098
|
+
decodeWebP(findFile(meta.sh0.files[0]))
|
|
5099
|
+
// 4: sh0
|
|
5100
|
+
];
|
|
5101
|
+
const hasHigherSH = !!meta.shN;
|
|
5102
|
+
if (hasHigherSH) {
|
|
5103
|
+
decodeList.push(
|
|
5104
|
+
decodeWebP(findFile(meta.shN.files[0])),
|
|
5105
|
+
// 5: shN_centroids
|
|
5106
|
+
decodeWebP(findFile(meta.shN.files[1]))
|
|
5107
|
+
// 6: shN_labels
|
|
5108
|
+
);
|
|
5109
|
+
}
|
|
5110
|
+
const imgs = await Promise.all(decodeList);
|
|
5111
|
+
const [meansL, meansU, scalesImg, quatsImg, sh0Img] = imgs;
|
|
5112
|
+
const centroidsImg = hasHigherSH ? imgs[5] : null;
|
|
5113
|
+
const labelsImg = hasHigherSH ? imgs[6] : null;
|
|
5114
|
+
if (onProgress) onProgress(35, "parse");
|
|
5115
|
+
let shPalette = null;
|
|
5116
|
+
let shBandCoeffs = 0;
|
|
5117
|
+
if (meta.shN && centroidsImg) {
|
|
5118
|
+
shBandCoeffs = COEFFS_PER_BAND[meta.shN.bands - 1];
|
|
5119
|
+
shPalette = decodeSHPalette(
|
|
5120
|
+
centroidsImg,
|
|
5121
|
+
meta.shN.codebook,
|
|
5122
|
+
meta.shN.count,
|
|
5123
|
+
meta.shN.bands
|
|
5124
|
+
);
|
|
5125
|
+
}
|
|
5126
|
+
if (onProgress) onProgress(45, "parse");
|
|
5127
|
+
const count = meta.count;
|
|
5128
|
+
const positions = new Float32Array(count * 3);
|
|
5129
|
+
const scales = new Float32Array(count * 3);
|
|
5130
|
+
const rotations = new Float32Array(count * 4);
|
|
5131
|
+
const colors = new Float32Array(count * 3);
|
|
5132
|
+
const opacities = new Float32Array(count);
|
|
5133
|
+
const shCoeffs = shPalette ? new Float32Array(count * 45) : void 0;
|
|
5134
|
+
const scCBExp = meta.scales.codebook.map((x) => Math.exp(x));
|
|
5135
|
+
const dcCB = meta.sh0.codebook;
|
|
5136
|
+
const SQRT2 = Math.SQRT2;
|
|
5137
|
+
const quatLookup = new Float64Array(256);
|
|
5138
|
+
for (let i = 0; i < 256; i++) {
|
|
5139
|
+
quatLookup[i] = (i / 255 - 0.5) * SQRT2;
|
|
5140
|
+
}
|
|
5141
|
+
for (let i = 0; i < count; i++) {
|
|
5142
|
+
const px = i % meansL.width;
|
|
5143
|
+
const py = Math.floor(i / meansL.width);
|
|
5144
|
+
const off = (py * meansL.width + px) * 4;
|
|
5145
|
+
const qx = meansU.data[off + 0] << 8 | meansL.data[off + 0];
|
|
5146
|
+
const qy = meansU.data[off + 1] << 8 | meansL.data[off + 1];
|
|
5147
|
+
const qz = meansU.data[off + 2] << 8 | meansL.data[off + 2];
|
|
5148
|
+
positions[i * 3 + 0] = symUnlog(lerp(meta.means.mins[0], meta.means.maxs[0], qx / 65535));
|
|
5149
|
+
positions[i * 3 + 1] = symUnlog(lerp(meta.means.mins[1], meta.means.maxs[1], qy / 65535));
|
|
5150
|
+
positions[i * 3 + 2] = symUnlog(lerp(meta.means.mins[2], meta.means.maxs[2], qz / 65535));
|
|
5151
|
+
scales[i * 3 + 0] = scCBExp[scalesImg.data[off + 0]];
|
|
5152
|
+
scales[i * 3 + 1] = scCBExp[scalesImg.data[off + 1]];
|
|
5153
|
+
scales[i * 3 + 2] = scCBExp[scalesImg.data[off + 2]];
|
|
5154
|
+
const r0 = quatLookup[quatsImg.data[off + 0]];
|
|
5155
|
+
const r1 = quatLookup[quatsImg.data[off + 1]];
|
|
5156
|
+
const r2 = quatLookup[quatsImg.data[off + 2]];
|
|
5157
|
+
const rr = Math.sqrt(Math.max(0, 1 - r0 * r0 - r1 * r1 - r2 * r2));
|
|
5158
|
+
const rOrder = quatsImg.data[off + 3] - 252;
|
|
5159
|
+
const qX = rOrder === 0 ? r0 : rOrder === 1 ? rr : r1;
|
|
5160
|
+
const qY = rOrder <= 1 ? r1 : rOrder === 2 ? rr : r2;
|
|
5161
|
+
const qZ = rOrder <= 2 ? r2 : rr;
|
|
5162
|
+
const qW = rOrder === 0 ? rr : r0;
|
|
5163
|
+
rotations[i * 4 + 0] = qW;
|
|
5164
|
+
rotations[i * 4 + 1] = qX;
|
|
5165
|
+
rotations[i * 4 + 2] = qY;
|
|
5166
|
+
rotations[i * 4 + 3] = qZ;
|
|
5167
|
+
colors[i * 3 + 0] = 0.5 + dcCB[sh0Img.data[off + 0]] * SH_C0;
|
|
5168
|
+
colors[i * 3 + 1] = 0.5 + dcCB[sh0Img.data[off + 1]] * SH_C0;
|
|
5169
|
+
colors[i * 3 + 2] = 0.5 + dcCB[sh0Img.data[off + 2]] * SH_C0;
|
|
5170
|
+
opacities[i] = sh0Img.data[off + 3] / 255;
|
|
5171
|
+
if (shCoeffs && shPalette && labelsImg) {
|
|
5172
|
+
const lOff = (py * labelsImg.width + px) * 4;
|
|
5173
|
+
const label = labelsImg.data[lOff + 0] | labelsImg.data[lOff + 1] << 8;
|
|
5174
|
+
const entry = shPalette[label];
|
|
5175
|
+
if (entry) {
|
|
5176
|
+
const base = i * 45;
|
|
5177
|
+
const len = shBandCoeffs * 3;
|
|
5178
|
+
for (let j = 0; j < len; j++) {
|
|
5179
|
+
shCoeffs[base + j] = entry[j];
|
|
5180
|
+
}
|
|
5181
|
+
}
|
|
5182
|
+
}
|
|
5183
|
+
if (onProgress && (i & 131071) === 0) {
|
|
5184
|
+
onProgress(45 + i / count * 50, "parse");
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
if (onProgress) onProgress(95, "parse");
|
|
5188
|
+
return { count, positions, scales, rotations, colors, opacities, shCoeffs };
|
|
5189
|
+
}
|
|
4378
5190
|
const WORKGROUP_SIZE$1 = 256;
|
|
4379
5191
|
const RADIX_BITS = 8;
|
|
4380
5192
|
const RADIX_SIZE = 256;
|
|
@@ -4419,7 +5231,7 @@ struct CullingParams {
|
|
|
4419
5231
|
screenHeight: f32,
|
|
4420
5232
|
frustumDilation: f32,
|
|
4421
5233
|
pixelThreshold: f32,
|
|
4422
|
-
|
|
5234
|
+
maxVisibleCount: u32,
|
|
4423
5235
|
}
|
|
4424
5236
|
|
|
4425
5237
|
@group(0) @binding(0) var<storage, read> splats: array<Splat>;
|
|
@@ -4482,6 +5294,15 @@ fn projectAndCull(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
4482
5294
|
// 视锥剔除
|
|
4483
5295
|
if !isInFrustum(clipPos, params.frustumDilation) { return; }
|
|
4484
5296
|
|
|
5297
|
+
// 亚像素剔除:Gaussian 可见范围小于阈值的 splat 跳过渲染和排序
|
|
5298
|
+
// scale 是 σ(标准差),Gaussian 可见范围约 3σ(覆盖 99.7%)
|
|
5299
|
+
if params.pixelThreshold > 0.0 {
|
|
5300
|
+
let splatSigma = maxScale(splat.scale) * getModelMaxScale(camera.model);
|
|
5301
|
+
let focalY = abs(camera.proj[1][1]) * params.screenHeight * 0.5;
|
|
5302
|
+
let projectedExtent = splatSigma * 3.0 * focalY / max(abs(viewPos.z), 0.001);
|
|
5303
|
+
if projectedExtent < params.pixelThreshold { return; }
|
|
5304
|
+
}
|
|
5305
|
+
|
|
4485
5306
|
// 深度编码 (viewPos.z 是负数)
|
|
4486
5307
|
let depth = viewPos.z;
|
|
4487
5308
|
let sortableDepth = encodeDepthKey(depth);
|
|
@@ -4503,6 +5324,18 @@ fn initIndirectBuffer() {
|
|
|
4503
5324
|
atomicStore(&indirectBuffer[2], 0u);
|
|
4504
5325
|
atomicStore(&indirectBuffer[3], 0u);
|
|
4505
5326
|
}
|
|
5327
|
+
|
|
5328
|
+
// 排序后截断:只保留最近的 maxVisibleCount 个 splat
|
|
5329
|
+
// 因为 radix sort 是按深度从近到远排序,截断尾部等于丢弃被遮挡的远处 splat
|
|
5330
|
+
@compute @workgroup_size(1)
|
|
5331
|
+
fn clampDrawCount() {
|
|
5332
|
+
let maxCount = params.maxVisibleCount;
|
|
5333
|
+
if maxCount == 0u { return; }
|
|
5334
|
+
let count = atomicLoad(&indirectBuffer[1]);
|
|
5335
|
+
if count > maxCount {
|
|
5336
|
+
atomicStore(&indirectBuffer[1], maxCount);
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
4506
5339
|
`
|
|
4507
5340
|
);
|
|
4508
5341
|
}
|
|
@@ -4717,7 +5550,9 @@ fn spine(
|
|
|
4717
5550
|
}
|
|
4718
5551
|
|
|
4719
5552
|
// ============================================================================
|
|
4720
|
-
// Pass 3: Downsweep -
|
|
5553
|
+
// Pass 3: Downsweep - 并行稳定散射
|
|
5554
|
+
// 所有 256 个线程同时工作,通过局部排名计算保持稳定性
|
|
5555
|
+
// 相比原始单线程版本,workgroup 完成时间提升约 20-40 倍
|
|
4721
5556
|
// ============================================================================
|
|
4722
5557
|
|
|
4723
5558
|
@group(0) @binding(0) var<uniform> downsweepParams: SortParams;
|
|
@@ -4732,6 +5567,7 @@ fn spine(
|
|
|
4732
5567
|
var<workgroup> localKeys: array<u32, BLOCK_SIZE>;
|
|
4733
5568
|
var<workgroup> localValues: array<u32, BLOCK_SIZE>;
|
|
4734
5569
|
var<workgroup> localBins: array<u32, BLOCK_SIZE>;
|
|
5570
|
+
var<workgroup> binBasePos: array<u32, RADIX_SIZE>;
|
|
4735
5571
|
|
|
4736
5572
|
@compute @workgroup_size(256, 1, 1)
|
|
4737
5573
|
fn downsweep(
|
|
@@ -4747,8 +5583,10 @@ fn downsweep(
|
|
|
4747
5583
|
let tid = localId.x;
|
|
4748
5584
|
let partitionStart = partitionId * BLOCK_SIZE;
|
|
4749
5585
|
let shift = downsweepParams.bitShift;
|
|
5586
|
+
let partitionEnd = min(partitionStart + BLOCK_SIZE, numKeys);
|
|
5587
|
+
let elemsInPartition = partitionEnd - partitionStart;
|
|
4750
5588
|
|
|
4751
|
-
//
|
|
5589
|
+
// Phase 1: 所有线程并行加载元素到共享内存
|
|
4752
5590
|
for (var j = 0u; j < ELEMENTS_PER_THREAD; j++) {
|
|
4753
5591
|
let keyIdx = partitionStart + tid * ELEMENTS_PER_THREAD + j;
|
|
4754
5592
|
let localIdx = tid * ELEMENTS_PER_THREAD + j;
|
|
@@ -4763,36 +5601,35 @@ fn downsweep(
|
|
|
4763
5601
|
}
|
|
4764
5602
|
}
|
|
4765
5603
|
|
|
5604
|
+
// Phase 2: 初始化 bin 基础写入位置(利用 256 线程并行)
|
|
5605
|
+
if tid < RADIX_SIZE {
|
|
5606
|
+
let passIdx = downsweepParams.passIndex;
|
|
5607
|
+
binBasePos[tid] = globalHistogramDownsweep[RADIX_SIZE * passIdx + tid] +
|
|
5608
|
+
partitionHistogramDownsweep[RADIX_SIZE * partitionId + tid];
|
|
5609
|
+
}
|
|
5610
|
+
|
|
4766
5611
|
workgroupBarrier();
|
|
4767
5612
|
|
|
4768
|
-
//
|
|
4769
|
-
//
|
|
4770
|
-
|
|
4771
|
-
|
|
5613
|
+
// Phase 3: 并行计算排名并散射
|
|
5614
|
+
// 稳定性保证:rank = 当前元素之前具有相同 bin 的元素数量
|
|
5615
|
+
// 每个线程处理自己的 4 个元素,通过扫描 localBins 确定排名
|
|
5616
|
+
for (var j = 0u; j < ELEMENTS_PER_THREAD; j++) {
|
|
5617
|
+
let localIdx = tid * ELEMENTS_PER_THREAD + j;
|
|
5618
|
+
if localIdx >= elemsInPartition { break; }
|
|
4772
5619
|
|
|
4773
|
-
let
|
|
5620
|
+
let b = localBins[localIdx];
|
|
5621
|
+
if b == 0xFFFFFFFFu { continue; }
|
|
4774
5622
|
|
|
4775
|
-
//
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
5623
|
+
// 计算 rank:扫描本分区中在当前元素之前、且属于同一 bin 的元素数
|
|
5624
|
+
var rank = 0u;
|
|
5625
|
+
for (var p = 0u; p < localIdx; p++) {
|
|
5626
|
+
if localBins[p] == b { rank++; }
|
|
4779
5627
|
}
|
|
4780
5628
|
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
if keyIdx < partitionEnd {
|
|
4786
|
-
let b = localBins[k];
|
|
4787
|
-
if b != 0xFFFFFFFFu {
|
|
4788
|
-
let writePos = binWritePos[b];
|
|
4789
|
-
if writePos < numKeys {
|
|
4790
|
-
downsweepKeysOut[writePos] = localKeys[k];
|
|
4791
|
-
downsweepValuesOut[writePos] = localValues[k];
|
|
4792
|
-
binWritePos[b]++;
|
|
4793
|
-
}
|
|
4794
|
-
}
|
|
4795
|
-
}
|
|
5629
|
+
let writePos = binBasePos[b] + rank;
|
|
5630
|
+
if writePos < numKeys {
|
|
5631
|
+
downsweepKeysOut[writePos] = localKeys[localIdx];
|
|
5632
|
+
downsweepValuesOut[writePos] = localValues[localIdx];
|
|
4796
5633
|
}
|
|
4797
5634
|
}
|
|
4798
5635
|
}
|
|
@@ -4820,6 +5657,7 @@ class GSSplatSorter {
|
|
|
4820
5657
|
// Culling Pipelines
|
|
4821
5658
|
__publicField(this, "initIndirectPipeline");
|
|
4822
5659
|
__publicField(this, "projectCullPipeline");
|
|
5660
|
+
__publicField(this, "clampDrawCountPipeline");
|
|
4823
5661
|
__publicField(this, "cullingBindGroupLayout");
|
|
4824
5662
|
__publicField(this, "cullingBindGroup");
|
|
4825
5663
|
// Radix Sort Pipelines
|
|
@@ -4939,6 +5777,11 @@ class GSSplatSorter {
|
|
|
4939
5777
|
compute: { module: cullingModule, entryPoint: "projectAndCull" },
|
|
4940
5778
|
label: "project-cull-pipeline"
|
|
4941
5779
|
});
|
|
5780
|
+
this.clampDrawCountPipeline = device.createComputePipeline({
|
|
5781
|
+
layout: cullingPipelineLayout,
|
|
5782
|
+
compute: { module: cullingModule, entryPoint: "clampDrawCount" },
|
|
5783
|
+
label: "clamp-draw-count-pipeline"
|
|
5784
|
+
});
|
|
4942
5785
|
this.cullingBindGroup = device.createBindGroup({
|
|
4943
5786
|
layout: this.cullingBindGroupLayout,
|
|
4944
5787
|
entries: [
|
|
@@ -5088,15 +5931,10 @@ class GSSplatSorter {
|
|
|
5088
5931
|
view.setFloat32(16, this.screenHeight, true);
|
|
5089
5932
|
view.setFloat32(20, this.cullingOptions.frustumDilation ?? 0.2, true);
|
|
5090
5933
|
view.setFloat32(24, this.cullingOptions.pixelThreshold, true);
|
|
5091
|
-
view.
|
|
5934
|
+
view.setUint32(28, this.cullingOptions.maxVisibleCount ?? 0, true);
|
|
5092
5935
|
this.device.queue.writeBuffer(this.cullingParamsBuffer, 0, cullingParamsData);
|
|
5093
5936
|
const encoder = this.device.createCommandEncoder({ label: "splat-sort-encoder" });
|
|
5094
|
-
encoder.clearBuffer(this.depthKeysBuffer);
|
|
5095
|
-
encoder.clearBuffer(this.visibleIndicesBuffer);
|
|
5096
|
-
encoder.clearBuffer(this.keysTempBuffer);
|
|
5097
|
-
encoder.clearBuffer(this.valuesTempBuffer);
|
|
5098
5937
|
encoder.clearBuffer(this.globalHistogramBuffer);
|
|
5099
|
-
encoder.clearBuffer(this.partitionHistogramBuffer);
|
|
5100
5938
|
{
|
|
5101
5939
|
const pass = encoder.beginComputePass({ label: "init-indirect" });
|
|
5102
5940
|
pass.setPipeline(this.initIndirectPipeline);
|
|
@@ -5134,6 +5972,13 @@ class GSSplatSorter {
|
|
|
5134
5972
|
pass.end();
|
|
5135
5973
|
}
|
|
5136
5974
|
}
|
|
5975
|
+
{
|
|
5976
|
+
const pass = encoder.beginComputePass({ label: "clamp-draw-count" });
|
|
5977
|
+
pass.setPipeline(this.clampDrawCountPipeline);
|
|
5978
|
+
pass.setBindGroup(0, this.cullingBindGroup);
|
|
5979
|
+
pass.dispatchWorkgroups(1);
|
|
5980
|
+
pass.end();
|
|
5981
|
+
}
|
|
5137
5982
|
this.device.queue.submit([encoder.finish()]);
|
|
5138
5983
|
}
|
|
5139
5984
|
/**
|
|
@@ -5177,18 +6022,38 @@ const gsOptimizedShader = (
|
|
|
5177
6022
|
`
|
|
5178
6023
|
/**
|
|
5179
6024
|
* 优化的 3D Gaussian Splatting Shader
|
|
5180
|
-
*
|
|
6025
|
+
* 支持完整 L3 球谐函数(SH)视角相关颜色
|
|
6026
|
+
* 参考 rfs-gsplat-render / PlayCanvas SuperSplat 实现
|
|
5181
6027
|
*/
|
|
5182
6028
|
|
|
5183
6029
|
const SQRT_8: f32 = 2.82842712475;
|
|
5184
6030
|
const SH_C0: f32 = 0.28209479177387814;
|
|
5185
6031
|
const SH_C1: f32 = 0.4886025119029199;
|
|
5186
|
-
//
|
|
5187
|
-
const
|
|
5188
|
-
const
|
|
5189
|
-
|
|
6032
|
+
// L2 SH 系数
|
|
6033
|
+
const SH_C2_0: f32 = 1.0925484305920792;
|
|
6034
|
+
const SH_C2_1: f32 = -1.0925484305920792;
|
|
6035
|
+
const SH_C2_2: f32 = 0.31539156525252005;
|
|
6036
|
+
const SH_C2_3: f32 = -1.0925484305920792;
|
|
6037
|
+
const SH_C2_4: f32 = 0.5462742152960396;
|
|
6038
|
+
// L3 SH 系数
|
|
6039
|
+
const SH_C3_0: f32 = -0.5900435899266435;
|
|
6040
|
+
const SH_C3_1: f32 = 2.890611442640554;
|
|
6041
|
+
const SH_C3_2: f32 = -0.4570457994644658;
|
|
6042
|
+
const SH_C3_3: f32 = 0.3731763325901154;
|
|
6043
|
+
const SH_C3_4: f32 = -0.4570457994644658;
|
|
6044
|
+
const SH_C3_5: f32 = 1.4453057213202769;
|
|
6045
|
+
const SH_C3_6: f32 = -0.5900435899266435;
|
|
6046
|
+
// 低通滤波器 (正则化协方差矩阵,避免数值问题)
|
|
5190
6047
|
const LOW_PASS_FILTER: f32 = 0.3;
|
|
6048
|
+
// Alpha 剔除阈值 (1/255)
|
|
5191
6049
|
const ALPHA_CULL_THRESHOLD: f32 = 0.00392156863;
|
|
6050
|
+
// Normalized Gaussian 常量 (用于消除边缘雾化)
|
|
6051
|
+
// 使用 k=4 的 Gaussian: exp(-4*A)
|
|
6052
|
+
// EXP_NEG_K = exp(-4) ≈ 0.0183
|
|
6053
|
+
// INV_ONE_MINUS_EXP_NEG_K = 1 / (1 - exp(-4)) ≈ 1.0187
|
|
6054
|
+
const GAUSSIAN_K: f32 = 4.0;
|
|
6055
|
+
const EXP_NEG_K: f32 = 0.01831563888873418;
|
|
6056
|
+
const INV_ONE_MINUS_EXP_NEG_K: f32 = 1.01865736036377408;
|
|
5192
6057
|
|
|
5193
6058
|
struct Uniforms {
|
|
5194
6059
|
view: mat4x4<f32>,
|
|
@@ -5212,14 +6077,51 @@ struct Splat {
|
|
|
5212
6077
|
_pad2: array<f32, 3>,
|
|
5213
6078
|
}
|
|
5214
6079
|
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
6080
|
+
// 完整 L3 球谐函数求值 (匹配原始 3DGS Python 实现)
|
|
6081
|
+
// SH 系数以 interleaved 格式存储: [R0,G0,B0, R1,G1,B1, ...]
|
|
6082
|
+
// dir: 从相机指向 splat 的归一化方向向量(模型空间)
|
|
6083
|
+
fn evalSH(splat: Splat, dir: vec3<f32>) -> vec3<f32> {
|
|
6084
|
+
let x = dir.x;
|
|
6085
|
+
let y = dir.y;
|
|
6086
|
+
let z = dir.z;
|
|
5218
6087
|
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
6088
|
+
var result = vec3<f32>(0.0);
|
|
6089
|
+
|
|
6090
|
+
// L1: 3 个基函数
|
|
6091
|
+
result += (-SH_C1 * y) * vec3<f32>(splat.sh1[0], splat.sh1[1], splat.sh1[2]);
|
|
6092
|
+
result += ( SH_C1 * z) * vec3<f32>(splat.sh1[3], splat.sh1[4], splat.sh1[5]);
|
|
6093
|
+
result += (-SH_C1 * x) * vec3<f32>(splat.sh1[6], splat.sh1[7], splat.sh1[8]);
|
|
6094
|
+
|
|
6095
|
+
// L2: 5 个基函数
|
|
6096
|
+
let xx = x * x; let yy = y * y; let zz = z * z;
|
|
6097
|
+
let xy = x * y; let yz = y * z; let xz = x * z;
|
|
6098
|
+
|
|
6099
|
+
result += (SH_C2_0 * xy) * vec3<f32>(splat.sh2[0], splat.sh2[1], splat.sh2[2]);
|
|
6100
|
+
result += (SH_C2_1 * yz) * vec3<f32>(splat.sh2[3], splat.sh2[4], splat.sh2[5]);
|
|
6101
|
+
result += (SH_C2_2 * (2.0 * zz - xx - yy)) * vec3<f32>(splat.sh2[6], splat.sh2[7], splat.sh2[8]);
|
|
6102
|
+
result += (SH_C2_3 * xz) * vec3<f32>(splat.sh2[9], splat.sh2[10], splat.sh2[11]);
|
|
6103
|
+
result += (SH_C2_4 * (xx - yy)) * vec3<f32>(splat.sh2[12], splat.sh2[13], splat.sh2[14]);
|
|
6104
|
+
|
|
6105
|
+
// L3: 7 个基函数
|
|
6106
|
+
result += (SH_C3_0 * y * (3.0 * xx - yy)) * vec3<f32>(splat.sh3[0], splat.sh3[1], splat.sh3[2]);
|
|
6107
|
+
result += (SH_C3_1 * xy * z) * vec3<f32>(splat.sh3[3], splat.sh3[4], splat.sh3[5]);
|
|
6108
|
+
result += (SH_C3_2 * y * (4.0 * zz - xx - yy)) * vec3<f32>(splat.sh3[6], splat.sh3[7], splat.sh3[8]);
|
|
6109
|
+
result += (SH_C3_3 * z * (2.0 * zz - 3.0 * xx - 3.0 * yy)) * vec3<f32>(splat.sh3[9], splat.sh3[10], splat.sh3[11]);
|
|
6110
|
+
result += (SH_C3_4 * x * (4.0 * zz - xx - yy)) * vec3<f32>(splat.sh3[12], splat.sh3[13], splat.sh3[14]);
|
|
6111
|
+
result += (SH_C3_5 * z * (xx - yy)) * vec3<f32>(splat.sh3[15], splat.sh3[16], splat.sh3[17]);
|
|
6112
|
+
result += (SH_C3_6 * x * (xx - 3.0 * yy)) * vec3<f32>(splat.sh3[18], splat.sh3[19], splat.sh3[20]);
|
|
6113
|
+
|
|
6114
|
+
return result;
|
|
6115
|
+
}
|
|
6116
|
+
|
|
6117
|
+
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
6118
|
+
@group(0) @binding(1) var<storage, read> splats: array<Splat>;
|
|
6119
|
+
@group(0) @binding(2) var<storage, read> sortedIndices: array<u32>;
|
|
6120
|
+
|
|
6121
|
+
struct VertexOutput {
|
|
6122
|
+
@builtin(position) position: vec4<f32>,
|
|
6123
|
+
@location(0) fragPos: vec2<f32>,
|
|
6124
|
+
@location(1) color: vec3<f32>,
|
|
5223
6125
|
@location(2) opacity: f32,
|
|
5224
6126
|
}
|
|
5225
6127
|
|
|
@@ -5228,16 +6130,24 @@ const QUAD_POSITIONS = array<vec2<f32>, 4>(
|
|
|
5228
6130
|
vec2<f32>(1.0, -1.0), vec2<f32>(1.0, 1.0),
|
|
5229
6131
|
);
|
|
5230
6132
|
|
|
5231
|
-
// ClipCorner
|
|
5232
|
-
//
|
|
5233
|
-
//
|
|
6133
|
+
// ClipCorner 优化:根据透明度缩小 quad,排除低于阈值的 Gaussian 区域
|
|
6134
|
+
// 使用 Normalized Gaussian: weight = (exp(-k*A) - exp(-k)) / (1 - exp(-k))
|
|
6135
|
+
// 需要找到 A 使得 alpha * weight >= 1/255
|
|
6136
|
+
// 即 weight >= 1/(255*alpha)
|
|
6137
|
+
// (exp(-k*A) - exp(-k)) / (1 - exp(-k)) >= 1/(255*alpha)
|
|
6138
|
+
// exp(-k*A) >= 1/(255*alpha) * (1 - exp(-k)) + exp(-k)
|
|
6139
|
+
// -k*A >= ln(1/(255*alpha) * (1 - exp(-k)) + exp(-k))
|
|
6140
|
+
// A <= -ln(1/(255*alpha) * (1 - exp(-k)) + exp(-k)) / k
|
|
5234
6141
|
fn computeClipFactor(alpha: f32) -> f32 {
|
|
5235
|
-
// 保护非常小的 alpha 值
|
|
5236
|
-
// 当 alpha <= 1/255 时,splat 不可见
|
|
5237
6142
|
if alpha <= ALPHA_CULL_THRESHOLD { return 0.0; }
|
|
5238
|
-
|
|
5239
|
-
//
|
|
5240
|
-
|
|
6143
|
+
let threshold = 1.0 / (255.0 * alpha);
|
|
6144
|
+
// 如果 threshold >= 1.0,整个 splat 都低于可见阈值
|
|
6145
|
+
if threshold >= 1.0 { return 0.0; }
|
|
6146
|
+
// 计算 clip 因子:找到 normalized gaussian = threshold 的位置
|
|
6147
|
+
let targetExp = threshold * (1.0 - EXP_NEG_K) + EXP_NEG_K;
|
|
6148
|
+
if targetExp >= 1.0 { return 0.0; }
|
|
6149
|
+
let A = -log(targetExp) / GAUSSIAN_K;
|
|
6150
|
+
return min(1.0, sqrt(A));
|
|
5241
6151
|
}
|
|
5242
6152
|
|
|
5243
6153
|
// 四元数转旋转矩阵 (PLY 格式: w, x, y, z)
|
|
@@ -5310,9 +6220,9 @@ fn computeExtentBasisAA(cov2dIn: vec3<f32>, opacity: f32, viewportSize: vec2<f32
|
|
|
5310
6220
|
let radius = length(vec2<f32>((a - d) * 0.5, b));
|
|
5311
6221
|
|
|
5312
6222
|
let lambda1 = mid + radius;
|
|
5313
|
-
let lambda2 =
|
|
6223
|
+
let lambda2 = mid - radius;
|
|
5314
6224
|
|
|
5315
|
-
//
|
|
6225
|
+
// 非正定协方差矩阵直接剔除
|
|
5316
6226
|
if lambda2 <= 0.0 {
|
|
5317
6227
|
result.basis = vec4<f32>(0.0);
|
|
5318
6228
|
result.adjustedOpacity = 0.0;
|
|
@@ -5322,14 +6232,15 @@ fn computeExtentBasisAA(cov2dIn: vec3<f32>, opacity: f32, viewportSize: vec2<f32
|
|
|
5322
6232
|
// 使用基于视口的最大限制 (匹配 PlayCanvas)
|
|
5323
6233
|
let vmin = min(1024.0, min(viewportSize.x, viewportSize.y));
|
|
5324
6234
|
|
|
5325
|
-
// 计算轴长度: l = 2
|
|
5326
|
-
//
|
|
5327
|
-
|
|
5328
|
-
|
|
6235
|
+
// 计算轴长度: l = 2 * sqrt(2 * lambda) ≈ 2.83 * sqrt(lambda)
|
|
6236
|
+
// 这与 GAUSSIAN_K=4 配套使用:
|
|
6237
|
+
// 在 UV=1 边界,对应 2*sqrt(2) 个标准差的位置
|
|
6238
|
+
// exp(-4 * 1) = exp(-4) ≈ 0.018,Normalized 后精确为 0
|
|
6239
|
+
let l1 = min(2.0 * sqrt(2.0 * lambda1), vmin);
|
|
6240
|
+
let l2 = min(2.0 * sqrt(2.0 * lambda2), vmin);
|
|
5329
6241
|
|
|
5330
|
-
//
|
|
5331
|
-
|
|
5332
|
-
if l1 < 2.0 && l2 < 2.0 {
|
|
6242
|
+
// 剔除小于 2 像素的 Gaussian(消除亚像素 splat 造成的雾化)
|
|
6243
|
+
if l1 < 0.5 && l2 < 0.5 {
|
|
5333
6244
|
result.basis = vec4<f32>(0.0);
|
|
5334
6245
|
result.adjustedOpacity = 0.0;
|
|
5335
6246
|
return result;
|
|
@@ -5410,7 +6321,7 @@ fn vs_main(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) ins
|
|
|
5410
6321
|
|
|
5411
6322
|
// 视锥边缘剔除 (匹配 PlayCanvas)
|
|
5412
6323
|
let maxExtentPixels = max(length(basis.xy), length(basis.zw));
|
|
5413
|
-
let pixelToClip = vec2<f32>(clipPos.w, clipPos.w) / uniforms.screenSize;
|
|
6324
|
+
let pixelToClip = 2.0 * vec2<f32>(clipPos.w, clipPos.w) / uniforms.screenSize;
|
|
5414
6325
|
let splatExtentClip = vec2<f32>(maxExtentPixels, maxExtentPixels) * pixelToClip;
|
|
5415
6326
|
if any((abs(clipPos.xy) - splatExtentClip) > vec2<f32>(clipPos.w, clipPos.w)) {
|
|
5416
6327
|
output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output;
|
|
@@ -5437,9 +6348,18 @@ fn vs_main(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) ins
|
|
|
5437
6348
|
// UV 输出 - 用 clipFactor 缩放以获得正确的 Gaussian 权重
|
|
5438
6349
|
output.fragPos = quadPos * clipFactor;
|
|
5439
6350
|
|
|
5440
|
-
//
|
|
5441
|
-
//
|
|
5442
|
-
|
|
6351
|
+
// 球谐函数颜色计算:
|
|
6352
|
+
// colorDC 已在 CPU 端预处理为 (dc * SH_C0 + 0.5)
|
|
6353
|
+
// evalSH() 计算 L1~L3 的视角相关色彩贡献
|
|
6354
|
+
// 方向需要在模型空间计算(SH 系数在模型空间定义)
|
|
6355
|
+
let shDir = normalize(
|
|
6356
|
+
vec3<f32>(
|
|
6357
|
+
dot(worldPos.xyz - uniforms.cameraPos, uniforms.model[0].xyz),
|
|
6358
|
+
dot(worldPos.xyz - uniforms.cameraPos, uniforms.model[1].xyz),
|
|
6359
|
+
dot(worldPos.xyz - uniforms.cameraPos, uniforms.model[2].xyz)
|
|
6360
|
+
)
|
|
6361
|
+
);
|
|
6362
|
+
output.color = splat.colorDC + evalSH(splat, shDir);
|
|
5443
6363
|
output.opacity = adjustedOpacity;
|
|
5444
6364
|
return output;
|
|
5445
6365
|
}
|
|
@@ -5449,35 +6369,284 @@ fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
|
5449
6369
|
if input.opacity <= 0.0 { discard; }
|
|
5450
6370
|
|
|
5451
6371
|
// A = 到中心的平方距离,在 UV 空间中
|
|
5452
|
-
// 由于 clipCorner 优化,fragPos 在 [-clip, clip] 范围内
|
|
5453
6372
|
let A = dot(input.fragPos, input.fragPos);
|
|
5454
6373
|
|
|
5455
6374
|
// 丢弃单位圆外的片段
|
|
5456
6375
|
if A > 1.0 { discard; }
|
|
5457
6376
|
|
|
5458
|
-
// Normalized Gaussian 衰减 (
|
|
5459
|
-
//
|
|
5460
|
-
//
|
|
5461
|
-
// 在 A=
|
|
5462
|
-
let weight = (exp(-
|
|
6377
|
+
// Normalized Gaussian 衰减 (关键:消除边缘雾化)
|
|
6378
|
+
// 标准 Gaussian exp(-k*A) 在边界 A=1 时不为 0,会产生累积雾化
|
|
6379
|
+
// Normalized 公式: (exp(-k*A) - exp(-k)) / (1 - exp(-k))
|
|
6380
|
+
// 在 A=0 时 = 1.0,在 A=1 时 = 精确的 0.0
|
|
6381
|
+
let weight = (exp(-GAUSSIAN_K * A) - EXP_NEG_K) * INV_ONE_MINUS_EXP_NEG_K;
|
|
5463
6382
|
|
|
5464
6383
|
// 组合 splat 透明度
|
|
5465
6384
|
let opacity = weight * input.opacity;
|
|
5466
6385
|
|
|
5467
|
-
// Alpha 阈值丢弃
|
|
6386
|
+
// Alpha 阈值丢弃
|
|
5468
6387
|
if opacity < ALPHA_CULL_THRESHOLD { discard; }
|
|
5469
6388
|
|
|
5470
6389
|
// 颜色 clamp 到有效范围 (防止负值)
|
|
5471
6390
|
let color = max(input.color, vec3<f32>(0.0));
|
|
5472
6391
|
|
|
5473
6392
|
// 预乘 alpha 输出 (匹配 blend mode: src=ONE, dst=ONE_MINUS_SRC_ALPHA)
|
|
5474
|
-
// 这是 3DGS 渲染的标准混合模式
|
|
5475
6393
|
return vec4<f32>(color * opacity, opacity);
|
|
5476
6394
|
}
|
|
5477
6395
|
`
|
|
5478
6396
|
);
|
|
6397
|
+
const gsDepthNormalShader = (
|
|
6398
|
+
/* wgsl */
|
|
6399
|
+
`
|
|
6400
|
+
const SQRT_8: f32 = 2.82842712475;
|
|
6401
|
+
const LOW_PASS_FILTER: f32 = 0.3;
|
|
6402
|
+
const ALPHA_CULL_THRESHOLD: f32 = 0.00392156863;
|
|
6403
|
+
const GAUSSIAN_K: f32 = 4.0;
|
|
6404
|
+
const EXP_NEG_K: f32 = 0.01831563888873418;
|
|
6405
|
+
const INV_ONE_MINUS_EXP_NEG_K: f32 = 1.01865736036377408;
|
|
6406
|
+
|
|
6407
|
+
struct Uniforms {
|
|
6408
|
+
view: mat4x4<f32>,
|
|
6409
|
+
proj: mat4x4<f32>,
|
|
6410
|
+
model: mat4x4<f32>,
|
|
6411
|
+
cameraPos: vec3<f32>,
|
|
6412
|
+
_pad: f32,
|
|
6413
|
+
screenSize: vec2<f32>,
|
|
6414
|
+
_pad2: vec2<f32>,
|
|
6415
|
+
}
|
|
6416
|
+
|
|
6417
|
+
struct Splat {
|
|
6418
|
+
mean: vec3<f32>, _pad0: f32,
|
|
6419
|
+
scale: vec3<f32>, _pad1: f32,
|
|
6420
|
+
rotation: vec4<f32>,
|
|
6421
|
+
colorDC: vec3<f32>,
|
|
6422
|
+
opacity: f32,
|
|
6423
|
+
sh1: array<f32, 9>,
|
|
6424
|
+
sh2: array<f32, 15>,
|
|
6425
|
+
sh3: array<f32, 21>,
|
|
6426
|
+
_pad2: array<f32, 3>,
|
|
6427
|
+
}
|
|
6428
|
+
|
|
6429
|
+
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
6430
|
+
@group(0) @binding(1) var<storage, read> splats: array<Splat>;
|
|
6431
|
+
@group(0) @binding(2) var<storage, read> sortedIndices: array<u32>;
|
|
6432
|
+
|
|
6433
|
+
struct VertexOutput {
|
|
6434
|
+
@builtin(position) position: vec4<f32>,
|
|
6435
|
+
@location(0) fragPos: vec2<f32>,
|
|
6436
|
+
@location(1) opacity: f32,
|
|
6437
|
+
@location(2) viewDepth: f32,
|
|
6438
|
+
@location(3) worldNormal: vec3<f32>,
|
|
6439
|
+
}
|
|
6440
|
+
|
|
6441
|
+
const QUAD_POSITIONS = array<vec2<f32>, 4>(
|
|
6442
|
+
vec2<f32>(-1.0, -1.0), vec2<f32>(-1.0, 1.0),
|
|
6443
|
+
vec2<f32>(1.0, -1.0), vec2<f32>(1.0, 1.0),
|
|
6444
|
+
);
|
|
6445
|
+
|
|
6446
|
+
fn computeClipFactor(alpha: f32) -> f32 {
|
|
6447
|
+
if alpha <= ALPHA_CULL_THRESHOLD { return 0.0; }
|
|
6448
|
+
let threshold = 1.0 / (255.0 * alpha);
|
|
6449
|
+
if threshold >= 1.0 { return 0.0; }
|
|
6450
|
+
let targetExp = threshold * (1.0 - EXP_NEG_K) + EXP_NEG_K;
|
|
6451
|
+
if targetExp >= 1.0 { return 0.0; }
|
|
6452
|
+
let A = -log(targetExp) / GAUSSIAN_K;
|
|
6453
|
+
return min(1.0, sqrt(A));
|
|
6454
|
+
}
|
|
6455
|
+
|
|
6456
|
+
fn quatToMat3(q: vec4<f32>) -> mat3x3<f32> {
|
|
6457
|
+
let r = q.x; let x = q.y; let y = q.z; let z = q.w;
|
|
6458
|
+
return mat3x3<f32>(
|
|
6459
|
+
vec3<f32>(1.0 - 2.0 * (y * y + z * z), 2.0 * (x * y + r * z), 2.0 * (x * z - r * y)),
|
|
6460
|
+
vec3<f32>(2.0 * (x * y - r * z), 1.0 - 2.0 * (x * x + z * z), 2.0 * (y * z + r * x)),
|
|
6461
|
+
vec3<f32>(2.0 * (x * z + r * y), 2.0 * (y * z - r * x), 1.0 - 2.0 * (x * x + y * y))
|
|
6462
|
+
);
|
|
6463
|
+
}
|
|
6464
|
+
|
|
6465
|
+
fn computeCovariance3D(scale: vec3<f32>, rotation: vec4<f32>) -> mat3x3<f32> {
|
|
6466
|
+
let R = quatToMat3(rotation);
|
|
6467
|
+
let S = mat3x3<f32>(vec3<f32>(scale.x, 0.0, 0.0), vec3<f32>(0.0, scale.y, 0.0), vec3<f32>(0.0, 0.0, scale.z));
|
|
6468
|
+
let M = R * S;
|
|
6469
|
+
return M * transpose(M);
|
|
6470
|
+
}
|
|
6471
|
+
|
|
6472
|
+
fn projectCovariance(cov3d: mat3x3<f32>, viewCenter: vec4<f32>, focal: vec2<f32>, modelViewMat: mat4x4<f32>) -> vec3<f32> {
|
|
6473
|
+
let v = viewCenter.xyz;
|
|
6474
|
+
let s = 1.0 / (v.z * v.z);
|
|
6475
|
+
let J = mat3x3<f32>(
|
|
6476
|
+
vec3<f32>(focal.x / v.z, 0.0, 0.0),
|
|
6477
|
+
vec3<f32>(0.0, focal.y / v.z, 0.0),
|
|
6478
|
+
vec3<f32>(-(focal.x * v.x) * s, -(focal.y * v.y) * s, 0.0)
|
|
6479
|
+
);
|
|
6480
|
+
let W = mat3x3<f32>(
|
|
6481
|
+
vec3<f32>(modelViewMat[0][0], modelViewMat[0][1], modelViewMat[0][2]),
|
|
6482
|
+
vec3<f32>(modelViewMat[1][0], modelViewMat[1][1], modelViewMat[1][2]),
|
|
6483
|
+
vec3<f32>(modelViewMat[2][0], modelViewMat[2][1], modelViewMat[2][2])
|
|
6484
|
+
);
|
|
6485
|
+
let T = J * W;
|
|
6486
|
+
let cov2d = T * cov3d * transpose(T);
|
|
6487
|
+
return vec3<f32>(cov2d[0][0], cov2d[0][1], cov2d[1][1]);
|
|
6488
|
+
}
|
|
6489
|
+
|
|
6490
|
+
struct ExtentResult {
|
|
6491
|
+
basis: vec4<f32>,
|
|
6492
|
+
adjustedOpacity: f32,
|
|
6493
|
+
}
|
|
6494
|
+
|
|
6495
|
+
fn computeExtentBasisAA(cov2dIn: vec3<f32>, opacity: f32, viewportSize: vec2<f32>) -> ExtentResult {
|
|
6496
|
+
var result: ExtentResult;
|
|
6497
|
+
var cov2d = cov2dIn;
|
|
6498
|
+
var alpha = opacity;
|
|
6499
|
+
cov2d.x += LOW_PASS_FILTER;
|
|
6500
|
+
cov2d.z += LOW_PASS_FILTER;
|
|
6501
|
+
let a = cov2d.x;
|
|
6502
|
+
let d = cov2d.z;
|
|
6503
|
+
let b = cov2d.y;
|
|
6504
|
+
let mid = 0.5 * (a + d);
|
|
6505
|
+
let radius = length(vec2<f32>((a - d) * 0.5, b));
|
|
6506
|
+
let lambda1 = mid + radius;
|
|
6507
|
+
let lambda2 = mid - radius;
|
|
6508
|
+
if lambda2 <= 0.0 {
|
|
6509
|
+
result.basis = vec4<f32>(0.0);
|
|
6510
|
+
result.adjustedOpacity = 0.0;
|
|
6511
|
+
return result;
|
|
6512
|
+
}
|
|
6513
|
+
let vmin = min(1024.0, min(viewportSize.x, viewportSize.y));
|
|
6514
|
+
let l1 = min(2.0 * sqrt(2.0 * lambda1), vmin);
|
|
6515
|
+
let l2 = min(2.0 * sqrt(2.0 * lambda2), vmin);
|
|
6516
|
+
if l1 < 0.5 && l2 < 0.5 {
|
|
6517
|
+
result.basis = vec4<f32>(0.0);
|
|
6518
|
+
result.adjustedOpacity = 0.0;
|
|
6519
|
+
return result;
|
|
6520
|
+
}
|
|
6521
|
+
let diagVec = normalize(vec2<f32>(b, lambda1 - a));
|
|
6522
|
+
let eigenvector1 = diagVec;
|
|
6523
|
+
let eigenvector2 = vec2<f32>(diagVec.y, -diagVec.x);
|
|
6524
|
+
result.basis = vec4<f32>(eigenvector1 * l1, eigenvector2 * l2);
|
|
6525
|
+
result.adjustedOpacity = alpha;
|
|
6526
|
+
return result;
|
|
6527
|
+
}
|
|
6528
|
+
|
|
6529
|
+
// Extract the normal of a gaussian ellipsoid from its scale and rotation.
|
|
6530
|
+
// The normal is the axis corresponding to the smallest scale (shortest axis).
|
|
6531
|
+
// After rotation, this gives the world-space normal of the local surface.
|
|
6532
|
+
fn computeSplatNormal(scale: vec3<f32>, rotation: vec4<f32>, modelMat: mat4x4<f32>) -> vec3<f32> {
|
|
6533
|
+
let R = quatToMat3(rotation);
|
|
6534
|
+
// Find the axis with smallest scale
|
|
6535
|
+
var minAxis: vec3<f32>;
|
|
6536
|
+
if scale.x <= scale.y && scale.x <= scale.z {
|
|
6537
|
+
minAxis = R[0]; // column 0
|
|
6538
|
+
} else if scale.y <= scale.z {
|
|
6539
|
+
minAxis = R[1]; // column 1
|
|
6540
|
+
} else {
|
|
6541
|
+
minAxis = R[2]; // column 2
|
|
6542
|
+
}
|
|
6543
|
+
// Transform to world space via model matrix 3x3 part
|
|
6544
|
+
let worldNormal = vec3<f32>(
|
|
6545
|
+
modelMat[0][0] * minAxis.x + modelMat[1][0] * minAxis.y + modelMat[2][0] * minAxis.z,
|
|
6546
|
+
modelMat[0][1] * minAxis.x + modelMat[1][1] * minAxis.y + modelMat[2][1] * minAxis.z,
|
|
6547
|
+
modelMat[0][2] * minAxis.x + modelMat[1][2] * minAxis.y + modelMat[2][2] * minAxis.z
|
|
6548
|
+
);
|
|
6549
|
+
let len = length(worldNormal);
|
|
6550
|
+
if len < 1e-8 { return vec3<f32>(0.0, 1.0, 0.0); }
|
|
6551
|
+
return worldNormal / len;
|
|
6552
|
+
}
|
|
6553
|
+
|
|
6554
|
+
@vertex
|
|
6555
|
+
fn vs_main(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> VertexOutput {
|
|
6556
|
+
var output: VertexOutput;
|
|
6557
|
+
let splatIndex = sortedIndices[instanceIndex];
|
|
6558
|
+
let splat = splats[splatIndex];
|
|
6559
|
+
let quadPos = QUAD_POSITIONS[vertexIndex];
|
|
6560
|
+
|
|
6561
|
+
if splat.opacity < ALPHA_CULL_THRESHOLD { output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output; }
|
|
6562
|
+
let quatNormSqr = dot(splat.rotation, splat.rotation);
|
|
6563
|
+
if quatNormSqr < 1e-6 { output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output; }
|
|
6564
|
+
|
|
6565
|
+
let worldPos = uniforms.model * vec4<f32>(splat.mean, 1.0);
|
|
6566
|
+
let viewPos = uniforms.view * worldPos;
|
|
6567
|
+
let clipPos = uniforms.proj * viewPos;
|
|
6568
|
+
|
|
6569
|
+
if viewPos.z >= 0.0 { output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output; }
|
|
6570
|
+
|
|
6571
|
+
let pW = 1.0 / (clipPos.w + 0.0000001);
|
|
6572
|
+
let ndcPos = clipPos * pW;
|
|
6573
|
+
|
|
6574
|
+
let clipBound = 1.3;
|
|
6575
|
+
if abs(ndcPos.x) > clipBound || abs(ndcPos.y) > clipBound || ndcPos.z < -0.2 || ndcPos.z > 1.0 {
|
|
6576
|
+
output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output;
|
|
6577
|
+
}
|
|
6578
|
+
|
|
6579
|
+
let cov3d = computeCovariance3D(splat.scale, splat.rotation);
|
|
6580
|
+
let focal = vec2<f32>(
|
|
6581
|
+
abs(uniforms.proj[0][0]) * 0.5 * uniforms.screenSize.x,
|
|
6582
|
+
abs(uniforms.proj[1][1]) * 0.5 * uniforms.screenSize.y
|
|
6583
|
+
);
|
|
6584
|
+
let modelViewMat = uniforms.view * uniforms.model;
|
|
6585
|
+
let cov2d = projectCovariance(cov3d, viewPos, focal, modelViewMat);
|
|
6586
|
+
|
|
6587
|
+
let extentResult = computeExtentBasisAA(cov2d, splat.opacity, uniforms.screenSize);
|
|
6588
|
+
let basis = extentResult.basis;
|
|
6589
|
+
let adjustedOpacity = extentResult.adjustedOpacity;
|
|
6590
|
+
|
|
6591
|
+
if basis.x == 0.0 && basis.y == 0.0 && basis.z == 0.0 && basis.w == 0.0 {
|
|
6592
|
+
output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output;
|
|
6593
|
+
}
|
|
6594
|
+
|
|
6595
|
+
let maxExtentPixels = max(length(basis.xy), length(basis.zw));
|
|
6596
|
+
let pixelToClip = 2.0 * vec2<f32>(clipPos.w, clipPos.w) / uniforms.screenSize;
|
|
6597
|
+
let splatExtentClip = vec2<f32>(maxExtentPixels, maxExtentPixels) * pixelToClip;
|
|
6598
|
+
if any((abs(clipPos.xy) - splatExtentClip) > vec2<f32>(clipPos.w, clipPos.w)) {
|
|
6599
|
+
output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output;
|
|
6600
|
+
}
|
|
6601
|
+
|
|
6602
|
+
let clipFactor = computeClipFactor(adjustedOpacity);
|
|
6603
|
+
if clipFactor <= 0.0 { output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output; }
|
|
6604
|
+
|
|
6605
|
+
let basisViewport = vec2<f32>(1.0 / uniforms.screenSize.x, 1.0 / uniforms.screenSize.y);
|
|
6606
|
+
let basisVector1 = basis.xy * clipFactor;
|
|
6607
|
+
let basisVector2 = basis.zw * clipFactor;
|
|
6608
|
+
let ndcOffset = (quadPos.x * basisVector1 + quadPos.y * basisVector2) * basisViewport * 2.0;
|
|
6609
|
+
output.position = vec4<f32>(ndcPos.xy + ndcOffset, ndcPos.z, 1.0);
|
|
6610
|
+
|
|
6611
|
+
output.fragPos = quadPos * clipFactor;
|
|
6612
|
+
output.opacity = adjustedOpacity;
|
|
6613
|
+
output.viewDepth = -viewPos.z;
|
|
6614
|
+
|
|
6615
|
+
// Normal from shortest ellipsoid axis, oriented toward camera
|
|
6616
|
+
var normal = computeSplatNormal(splat.scale, splat.rotation, uniforms.model);
|
|
6617
|
+
let toCamera = uniforms.cameraPos - worldPos.xyz;
|
|
6618
|
+
if dot(normal, toCamera) < 0.0 {
|
|
6619
|
+
normal = -normal;
|
|
6620
|
+
}
|
|
6621
|
+
output.worldNormal = normal;
|
|
6622
|
+
return output;
|
|
6623
|
+
}
|
|
6624
|
+
|
|
6625
|
+
struct FragOutput {
|
|
6626
|
+
@location(0) depthOut: vec4<f32>,
|
|
6627
|
+
@location(1) normalOut: vec4<f32>,
|
|
6628
|
+
}
|
|
6629
|
+
|
|
6630
|
+
@fragment
|
|
6631
|
+
fn fs_depth_normal(input: VertexOutput) -> FragOutput {
|
|
6632
|
+
if input.opacity <= 0.0 { discard; }
|
|
6633
|
+
let A = dot(input.fragPos, input.fragPos);
|
|
6634
|
+
if A > 1.0 { discard; }
|
|
6635
|
+
let weight = (exp(-GAUSSIAN_K * A) - EXP_NEG_K) * INV_ONE_MINUS_EXP_NEG_K;
|
|
6636
|
+
let opacity = weight * input.opacity;
|
|
6637
|
+
if opacity < ALPHA_CULL_THRESHOLD { discard; }
|
|
6638
|
+
|
|
6639
|
+
var out: FragOutput;
|
|
6640
|
+
// RT0: depth + opacity — RGB blend 1·src+(1-α)·dst accumulates depth, Alpha blend 0·src+(1-α)·dst accumulates transmittance
|
|
6641
|
+
out.depthOut = vec4<f32>(input.viewDepth * opacity, 0.0, 0.0, opacity);
|
|
6642
|
+
// RT1: normal — same blend, RGB accumulates weighted normal, Alpha accumulates transmittance
|
|
6643
|
+
out.normalOut = vec4<f32>(input.worldNormal * opacity, opacity);
|
|
6644
|
+
return out;
|
|
6645
|
+
}
|
|
6646
|
+
`
|
|
6647
|
+
);
|
|
5479
6648
|
const SPLAT_FLOAT_COUNT = 64;
|
|
5480
|
-
class
|
|
6649
|
+
const _GSSplatRenderer = class _GSSplatRenderer {
|
|
5481
6650
|
constructor(renderer, camera) {
|
|
5482
6651
|
__publicField(this, "renderer");
|
|
5483
6652
|
__publicField(this, "camera");
|
|
@@ -5488,8 +6657,9 @@ class GSSplatRenderer {
|
|
|
5488
6657
|
__publicField(this, "splatCount", 0);
|
|
5489
6658
|
__publicField(this, "bindGroup", null);
|
|
5490
6659
|
__publicField(this, "sorter", null);
|
|
5491
|
-
__publicField(this, "shMode", SHMode.
|
|
6660
|
+
__publicField(this, "shMode", SHMode.L3);
|
|
5492
6661
|
__publicField(this, "boundingBox", null);
|
|
6662
|
+
__publicField(this, "cpuPositions", null);
|
|
5493
6663
|
// Transform
|
|
5494
6664
|
__publicField(this, "position", [0, 0, 0]);
|
|
5495
6665
|
__publicField(this, "rotation", [0, 0, 0]);
|
|
@@ -5498,6 +6668,28 @@ class GSSplatRenderer {
|
|
|
5498
6668
|
__publicField(this, "modelMatrix", new Float32Array(16));
|
|
5499
6669
|
// 剔除选项
|
|
5500
6670
|
__publicField(this, "pixelCullThreshold", 1);
|
|
6671
|
+
__publicField(this, "maxVisibleSplats", 0);
|
|
6672
|
+
// 排序优化:相机变化检测 + 频率控制
|
|
6673
|
+
__publicField(this, "lastSortViewMatrix", new Float32Array(16));
|
|
6674
|
+
__publicField(this, "lastSortProjMatrix", new Float32Array(16));
|
|
6675
|
+
__publicField(this, "lastSortModelMatrix", new Float32Array(16));
|
|
6676
|
+
__publicField(this, "lastSortWidth", 0);
|
|
6677
|
+
__publicField(this, "lastSortHeight", 0);
|
|
6678
|
+
__publicField(this, "lastSortPixelThreshold", -1);
|
|
6679
|
+
__publicField(this, "lastSortMaxVisible", -1);
|
|
6680
|
+
__publicField(this, "sortStateInitialized", false);
|
|
6681
|
+
__publicField(this, "sortFrequency", 1);
|
|
6682
|
+
__publicField(this, "frameCounter", 0);
|
|
6683
|
+
// 深度法线Pass依赖资源
|
|
6684
|
+
__publicField(this, "depthNormalPipeline", null);
|
|
6685
|
+
__publicField(this, "depthRT", null);
|
|
6686
|
+
__publicField(this, "depthRTView", null);
|
|
6687
|
+
__publicField(this, "normalRT", null);
|
|
6688
|
+
__publicField(this, "normalRTView", null);
|
|
6689
|
+
__publicField(this, "dnRTWidth", 0);
|
|
6690
|
+
__publicField(this, "dnRTHeight", 0);
|
|
6691
|
+
// 这一帧的深度法线回读结果,返回指定像素的3*3区域的深度值和法线
|
|
6692
|
+
__publicField(this, "dnResult", null);
|
|
5501
6693
|
this.renderer = renderer;
|
|
5502
6694
|
this.camera = camera;
|
|
5503
6695
|
this.createPipeline();
|
|
@@ -5511,9 +6703,21 @@ class GSSplatRenderer {
|
|
|
5511
6703
|
});
|
|
5512
6704
|
this.bindGroupLayout = device.createBindGroupLayout({
|
|
5513
6705
|
entries: [
|
|
5514
|
-
{
|
|
5515
|
-
|
|
5516
|
-
|
|
6706
|
+
{
|
|
6707
|
+
binding: 0,
|
|
6708
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
6709
|
+
buffer: { type: "uniform" }
|
|
6710
|
+
},
|
|
6711
|
+
{
|
|
6712
|
+
binding: 1,
|
|
6713
|
+
visibility: GPUShaderStage.VERTEX,
|
|
6714
|
+
buffer: { type: "read-only-storage" }
|
|
6715
|
+
},
|
|
6716
|
+
{
|
|
6717
|
+
binding: 2,
|
|
6718
|
+
visibility: GPUShaderStage.VERTEX,
|
|
6719
|
+
buffer: { type: "read-only-storage" }
|
|
6720
|
+
}
|
|
5517
6721
|
]
|
|
5518
6722
|
});
|
|
5519
6723
|
const pipelineLayout = device.createPipelineLayout({
|
|
@@ -5529,28 +6733,30 @@ class GSSplatRenderer {
|
|
|
5529
6733
|
fragment: {
|
|
5530
6734
|
module: shaderModule,
|
|
5531
6735
|
entryPoint: "fs_main",
|
|
5532
|
-
targets: [
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
6736
|
+
targets: [
|
|
6737
|
+
{
|
|
6738
|
+
format: this.renderer.format,
|
|
6739
|
+
blend: {
|
|
6740
|
+
color: {
|
|
6741
|
+
srcFactor: "one",
|
|
6742
|
+
dstFactor: "one-minus-src-alpha",
|
|
6743
|
+
operation: "add"
|
|
6744
|
+
},
|
|
6745
|
+
alpha: {
|
|
6746
|
+
srcFactor: "one",
|
|
6747
|
+
dstFactor: "one-minus-src-alpha",
|
|
6748
|
+
operation: "add"
|
|
6749
|
+
}
|
|
5544
6750
|
}
|
|
5545
6751
|
}
|
|
5546
|
-
|
|
6752
|
+
]
|
|
5547
6753
|
},
|
|
5548
6754
|
primitive: {
|
|
5549
6755
|
topology: "triangle-strip"
|
|
5550
6756
|
},
|
|
5551
6757
|
depthStencil: {
|
|
5552
6758
|
format: this.renderer.depthFormat,
|
|
5553
|
-
depthWriteEnabled:
|
|
6759
|
+
depthWriteEnabled: true,
|
|
5554
6760
|
depthCompare: "always"
|
|
5555
6761
|
}
|
|
5556
6762
|
});
|
|
@@ -5561,6 +6767,65 @@ class GSSplatRenderer {
|
|
|
5561
6767
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
5562
6768
|
});
|
|
5563
6769
|
}
|
|
6770
|
+
// 创建深度和法线渲染管线,这个管线用于渲染深度和法线信息,用于后续的拾取操作
|
|
6771
|
+
// RGB采用"one add one-minus-src-alpha"的混合模式对深度和法线做AlphaBlending
|
|
6772
|
+
// A采用"zero add one-minus-src-alpha"的混合模式计算Transmittance
|
|
6773
|
+
createDepthNormalPipeline() {
|
|
6774
|
+
const device = this.renderer.device;
|
|
6775
|
+
const shaderModule = device.createShaderModule({ code: gsDepthNormalShader });
|
|
6776
|
+
const pipelineLayout = device.createPipelineLayout({
|
|
6777
|
+
bindGroupLayouts: [this.bindGroupLayout]
|
|
6778
|
+
});
|
|
6779
|
+
this.depthNormalPipeline = device.createRenderPipeline({
|
|
6780
|
+
layout: pipelineLayout,
|
|
6781
|
+
vertex: {
|
|
6782
|
+
module: shaderModule,
|
|
6783
|
+
entryPoint: "vs_main",
|
|
6784
|
+
buffers: []
|
|
6785
|
+
},
|
|
6786
|
+
fragment: {
|
|
6787
|
+
module: shaderModule,
|
|
6788
|
+
entryPoint: "fs_depth_normal",
|
|
6789
|
+
targets: [
|
|
6790
|
+
{
|
|
6791
|
+
format: "rgba16float",
|
|
6792
|
+
blend: {
|
|
6793
|
+
color: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
|
|
6794
|
+
alpha: { srcFactor: "zero", dstFactor: "one-minus-src-alpha", operation: "add" }
|
|
6795
|
+
}
|
|
6796
|
+
},
|
|
6797
|
+
{
|
|
6798
|
+
format: "rgba16float",
|
|
6799
|
+
blend: {
|
|
6800
|
+
color: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
|
|
6801
|
+
alpha: { srcFactor: "zero", dstFactor: "one-minus-src-alpha", operation: "add" }
|
|
6802
|
+
}
|
|
6803
|
+
}
|
|
6804
|
+
]
|
|
6805
|
+
},
|
|
6806
|
+
primitive: { topology: "triangle-strip" }
|
|
6807
|
+
});
|
|
6808
|
+
}
|
|
6809
|
+
ensureDepthNormalTextures(w, h) {
|
|
6810
|
+
if (this.depthRT && this.dnRTWidth === w && this.dnRTHeight === h) return;
|
|
6811
|
+
const device = this.renderer.device;
|
|
6812
|
+
if (this.depthRT) this.depthRT.destroy();
|
|
6813
|
+
if (this.normalRT) this.normalRT.destroy();
|
|
6814
|
+
this.depthRT = device.createTexture({
|
|
6815
|
+
size: { width: w, height: h },
|
|
6816
|
+
format: "rgba16float",
|
|
6817
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC
|
|
6818
|
+
});
|
|
6819
|
+
this.depthRTView = this.depthRT.createView();
|
|
6820
|
+
this.normalRT = device.createTexture({
|
|
6821
|
+
size: { width: w, height: h },
|
|
6822
|
+
format: "rgba16float",
|
|
6823
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC
|
|
6824
|
+
});
|
|
6825
|
+
this.normalRTView = this.normalRT.createView();
|
|
6826
|
+
this.dnRTWidth = w;
|
|
6827
|
+
this.dnRTHeight = h;
|
|
6828
|
+
}
|
|
5564
6829
|
setPosition(x, y, z) {
|
|
5565
6830
|
this.position = [x, y, z];
|
|
5566
6831
|
this.updateModelMatrix();
|
|
@@ -5644,6 +6909,56 @@ class GSSplatRenderer {
|
|
|
5644
6909
|
setPixelCullThreshold(threshold) {
|
|
5645
6910
|
this.pixelCullThreshold = threshold;
|
|
5646
6911
|
}
|
|
6912
|
+
/**
|
|
6913
|
+
* 设置最大可见 splat 数量
|
|
6914
|
+
* 排序后只保留距离最近的 N 个 splat,远处被遮挡的 splat 被丢弃
|
|
6915
|
+
* 0 = 不限制
|
|
6916
|
+
*/
|
|
6917
|
+
setMaxVisibleSplats(count) {
|
|
6918
|
+
this.maxVisibleSplats = count;
|
|
6919
|
+
}
|
|
6920
|
+
getMaxVisibleSplats() {
|
|
6921
|
+
return this.maxVisibleSplats;
|
|
6922
|
+
}
|
|
6923
|
+
/**
|
|
6924
|
+
* 设置排序频率
|
|
6925
|
+
* 1 = 每帧排序(默认),2 = 每 2 帧排序一次,以此类推
|
|
6926
|
+
* 相机静止时自动跳过排序,不受此参数影响
|
|
6927
|
+
*/
|
|
6928
|
+
setSortFrequency(frequency) {
|
|
6929
|
+
this.sortFrequency = Math.max(1, Math.round(frequency));
|
|
6930
|
+
}
|
|
6931
|
+
getSortFrequency() {
|
|
6932
|
+
return this.sortFrequency;
|
|
6933
|
+
}
|
|
6934
|
+
needsSort() {
|
|
6935
|
+
const view = this.camera.viewMatrix;
|
|
6936
|
+
const proj = this.camera.projectionMatrix;
|
|
6937
|
+
const model = this.modelMatrix;
|
|
6938
|
+
const w = this.renderer.width;
|
|
6939
|
+
const h = this.renderer.height;
|
|
6940
|
+
if (!this.sortStateInitialized || w !== this.lastSortWidth || h !== this.lastSortHeight || this.pixelCullThreshold !== this.lastSortPixelThreshold || this.maxVisibleSplats !== this.lastSortMaxVisible) {
|
|
6941
|
+
this.saveSortState(view, proj, model, w, h);
|
|
6942
|
+
return true;
|
|
6943
|
+
}
|
|
6944
|
+
for (let i = 0; i < 16; i++) {
|
|
6945
|
+
if (view[i] !== this.lastSortViewMatrix[i] || proj[i] !== this.lastSortProjMatrix[i] || model[i] !== this.lastSortModelMatrix[i]) {
|
|
6946
|
+
this.saveSortState(view, proj, model, w, h);
|
|
6947
|
+
return true;
|
|
6948
|
+
}
|
|
6949
|
+
}
|
|
6950
|
+
return false;
|
|
6951
|
+
}
|
|
6952
|
+
saveSortState(view, proj, model, w, h) {
|
|
6953
|
+
this.lastSortViewMatrix.set(view);
|
|
6954
|
+
this.lastSortProjMatrix.set(proj);
|
|
6955
|
+
this.lastSortModelMatrix.set(model);
|
|
6956
|
+
this.lastSortWidth = w;
|
|
6957
|
+
this.lastSortHeight = h;
|
|
6958
|
+
this.lastSortPixelThreshold = this.pixelCullThreshold;
|
|
6959
|
+
this.lastSortMaxVisible = this.maxVisibleSplats;
|
|
6960
|
+
this.sortStateInitialized = true;
|
|
6961
|
+
}
|
|
5647
6962
|
setData(splats) {
|
|
5648
6963
|
const device = this.renderer.device;
|
|
5649
6964
|
if (this.splatBuffer) {
|
|
@@ -5661,10 +6976,14 @@ class GSSplatRenderer {
|
|
|
5661
6976
|
return;
|
|
5662
6977
|
}
|
|
5663
6978
|
this.boundingBox = this.computeBoundingBox(splats);
|
|
6979
|
+
const positions = new Float32Array(this.splatCount * 3);
|
|
5664
6980
|
const data = new Float32Array(this.splatCount * SPLAT_FLOAT_COUNT);
|
|
5665
6981
|
for (let i = 0; i < this.splatCount; i++) {
|
|
5666
6982
|
const splat = splats[i];
|
|
5667
6983
|
const offset = i * SPLAT_FLOAT_COUNT;
|
|
6984
|
+
positions[i * 3 + 0] = splat.mean[0];
|
|
6985
|
+
positions[i * 3 + 1] = splat.mean[1];
|
|
6986
|
+
positions[i * 3 + 2] = splat.mean[2];
|
|
5668
6987
|
data[offset + 0] = splat.mean[0];
|
|
5669
6988
|
data[offset + 1] = splat.mean[1];
|
|
5670
6989
|
data[offset + 2] = splat.mean[2];
|
|
@@ -5700,6 +7019,7 @@ class GSSplatRenderer {
|
|
|
5700
7019
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
5701
7020
|
});
|
|
5702
7021
|
device.queue.writeBuffer(this.splatBuffer, 0, data);
|
|
7022
|
+
this.cpuPositions = positions;
|
|
5703
7023
|
this.sorter = new GSSplatSorter(
|
|
5704
7024
|
device,
|
|
5705
7025
|
this.splatCount,
|
|
@@ -5738,6 +7058,7 @@ class GSSplatRenderer {
|
|
|
5738
7058
|
return;
|
|
5739
7059
|
}
|
|
5740
7060
|
this.boundingBox = this.computeBoundingBoxFromCompact(compactData);
|
|
7061
|
+
this.cpuPositions = new Float32Array(compactData.positions);
|
|
5741
7062
|
const includeSH = compactData.shCoeffs !== void 0;
|
|
5742
7063
|
const gpuData = compactDataToGPUBuffer(compactData, includeSH);
|
|
5743
7064
|
this.splatBuffer = device.createBuffer({
|
|
@@ -5795,13 +7116,19 @@ class GSSplatRenderer {
|
|
|
5795
7116
|
208,
|
|
5796
7117
|
new Float32Array([this.renderer.width, this.renderer.height, 0, 0])
|
|
5797
7118
|
);
|
|
5798
|
-
this.
|
|
5799
|
-
this.
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
7119
|
+
const changed = this.needsSort();
|
|
7120
|
+
this.frameCounter++;
|
|
7121
|
+
const shouldSort = changed || this.sortFrequency > 1 && this.frameCounter % this.sortFrequency === 0;
|
|
7122
|
+
if (shouldSort) {
|
|
7123
|
+
this.sorter.setScreenSize(this.renderer.width, this.renderer.height);
|
|
7124
|
+
this.sorter.setCullingOptions({
|
|
7125
|
+
nearPlane: this.camera.near,
|
|
7126
|
+
farPlane: this.camera.far,
|
|
7127
|
+
pixelThreshold: this.pixelCullThreshold,
|
|
7128
|
+
maxVisibleCount: this.maxVisibleSplats
|
|
7129
|
+
});
|
|
7130
|
+
this.sorter.sort();
|
|
7131
|
+
}
|
|
5805
7132
|
pass.setPipeline(this.pipeline);
|
|
5806
7133
|
pass.setBindGroup(0, this.bindGroup);
|
|
5807
7134
|
pass.drawIndirect(this.sorter.getDrawIndirectBuffer(), 0);
|
|
@@ -5812,31 +7139,42 @@ class GSSplatRenderer {
|
|
|
5812
7139
|
getBoundingBox() {
|
|
5813
7140
|
return this.boundingBox;
|
|
5814
7141
|
}
|
|
7142
|
+
getCPUPositions() {
|
|
7143
|
+
return this.cpuPositions;
|
|
7144
|
+
}
|
|
5815
7145
|
computeBoundingBox(splats) {
|
|
5816
7146
|
if (splats.length === 0) {
|
|
5817
7147
|
return { min: [0, 0, 0], max: [0, 0, 0], center: [0, 0, 0], radius: 0 };
|
|
5818
7148
|
}
|
|
5819
|
-
const min = [
|
|
5820
|
-
|
|
7149
|
+
const min = [
|
|
7150
|
+
splats[0].mean[0],
|
|
7151
|
+
splats[0].mean[1],
|
|
7152
|
+
splats[0].mean[2]
|
|
7153
|
+
];
|
|
7154
|
+
const max2 = [
|
|
7155
|
+
splats[0].mean[0],
|
|
7156
|
+
splats[0].mean[1],
|
|
7157
|
+
splats[0].mean[2]
|
|
7158
|
+
];
|
|
5821
7159
|
for (let i = 1; i < splats.length; i++) {
|
|
5822
7160
|
const [x, y, z] = splats[i].mean;
|
|
5823
7161
|
min[0] = Math.min(min[0], x);
|
|
5824
7162
|
min[1] = Math.min(min[1], y);
|
|
5825
7163
|
min[2] = Math.min(min[2], z);
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
7164
|
+
max2[0] = Math.max(max2[0], x);
|
|
7165
|
+
max2[1] = Math.max(max2[1], y);
|
|
7166
|
+
max2[2] = Math.max(max2[2], z);
|
|
5829
7167
|
}
|
|
5830
7168
|
const center = [
|
|
5831
|
-
(min[0] +
|
|
5832
|
-
(min[1] +
|
|
5833
|
-
(min[2] +
|
|
7169
|
+
(min[0] + max2[0]) / 2,
|
|
7170
|
+
(min[1] + max2[1]) / 2,
|
|
7171
|
+
(min[2] + max2[2]) / 2
|
|
5834
7172
|
];
|
|
5835
|
-
const dx =
|
|
5836
|
-
const dy =
|
|
5837
|
-
const dz =
|
|
7173
|
+
const dx = max2[0] - min[0];
|
|
7174
|
+
const dy = max2[1] - min[1];
|
|
7175
|
+
const dz = max2[2] - min[2];
|
|
5838
7176
|
const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
|
|
5839
|
-
return { min, max, center, radius };
|
|
7177
|
+
return { min, max: max2, center, radius };
|
|
5840
7178
|
}
|
|
5841
7179
|
computeBoundingBoxFromCompact(data) {
|
|
5842
7180
|
if (data.count === 0) {
|
|
@@ -5844,7 +7182,7 @@ class GSSplatRenderer {
|
|
|
5844
7182
|
}
|
|
5845
7183
|
const positions = data.positions;
|
|
5846
7184
|
const min = [positions[0], positions[1], positions[2]];
|
|
5847
|
-
const
|
|
7185
|
+
const max2 = [positions[0], positions[1], positions[2]];
|
|
5848
7186
|
for (let i = 1; i < data.count; i++) {
|
|
5849
7187
|
const x = positions[i * 3 + 0];
|
|
5850
7188
|
const y = positions[i * 3 + 1];
|
|
@@ -5852,20 +7190,183 @@ class GSSplatRenderer {
|
|
|
5852
7190
|
min[0] = Math.min(min[0], x);
|
|
5853
7191
|
min[1] = Math.min(min[1], y);
|
|
5854
7192
|
min[2] = Math.min(min[2], z);
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
7193
|
+
max2[0] = Math.max(max2[0], x);
|
|
7194
|
+
max2[1] = Math.max(max2[1], y);
|
|
7195
|
+
max2[2] = Math.max(max2[2], z);
|
|
7196
|
+
}
|
|
7197
|
+
const center = [
|
|
7198
|
+
(min[0] + max2[0]) / 2,
|
|
7199
|
+
(min[1] + max2[1]) / 2,
|
|
7200
|
+
(min[2] + max2[2]) / 2
|
|
7201
|
+
];
|
|
7202
|
+
const dx = max2[0] - min[0];
|
|
7203
|
+
const dy = max2[1] - min[1];
|
|
7204
|
+
const dz = max2[2] - min[2];
|
|
7205
|
+
const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
|
|
7206
|
+
return { min, max: max2, center, radius };
|
|
7207
|
+
}
|
|
7208
|
+
// 1、提交深度和法线渲染Pass;
|
|
7209
|
+
// 2、如果px和py不为-1,则表示需要回读指定像素周围3*3区域的深度和法线信息,这一帧的回读结果保存在dnResult中,下一帧可以获取
|
|
7210
|
+
prepareDepthNormalPass(px = -1, py = -1) {
|
|
7211
|
+
if (this.splatCount === 0 || !this.bindGroup || !this.sorter) return;
|
|
7212
|
+
if (!this.depthNormalPipeline) this.createDepthNormalPipeline();
|
|
7213
|
+
const w = this.renderer.width;
|
|
7214
|
+
const h = this.renderer.height;
|
|
7215
|
+
this.ensureDepthNormalTextures(w, h);
|
|
7216
|
+
const device = this.renderer.device;
|
|
7217
|
+
const encoder = device.createCommandEncoder({ label: "depth-normal-encoder" });
|
|
7218
|
+
const pass = encoder.beginRenderPass({
|
|
7219
|
+
colorAttachments: [
|
|
7220
|
+
{
|
|
7221
|
+
view: this.depthRTView,
|
|
7222
|
+
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
|
7223
|
+
loadOp: "clear",
|
|
7224
|
+
storeOp: "store"
|
|
7225
|
+
},
|
|
7226
|
+
{
|
|
7227
|
+
view: this.normalRTView,
|
|
7228
|
+
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
|
7229
|
+
loadOp: "clear",
|
|
7230
|
+
storeOp: "store"
|
|
7231
|
+
}
|
|
7232
|
+
]
|
|
7233
|
+
});
|
|
7234
|
+
pass.setPipeline(this.depthNormalPipeline);
|
|
7235
|
+
pass.setBindGroup(0, this.bindGroup);
|
|
7236
|
+
pass.drawIndirect(this.sorter.getDrawIndirectBuffer(), 0);
|
|
7237
|
+
pass.end();
|
|
7238
|
+
const ipx = Math.floor(px);
|
|
7239
|
+
const ipy = Math.floor(py);
|
|
7240
|
+
if (ipx >= 0 && ipx < w && ipy >= 0 && ipy < h) {
|
|
7241
|
+
const x0 = Math.max(0, ipx - 1);
|
|
7242
|
+
const y0 = Math.max(0, ipy - 1);
|
|
7243
|
+
const x1 = Math.min(w, ipx + 2);
|
|
7244
|
+
const y1 = Math.min(h, ipy + 2);
|
|
7245
|
+
const copyW = x1 - x0;
|
|
7246
|
+
const copyH = y1 - y0;
|
|
7247
|
+
const cx = ipx - x0;
|
|
7248
|
+
const cy = ipy - y0;
|
|
7249
|
+
const rawBytesPerRow = copyW * 8;
|
|
7250
|
+
const bytesPerRow = Math.ceil(rawBytesPerRow / 256) * 256;
|
|
7251
|
+
const sliceBytes = bytesPerRow * copyH;
|
|
7252
|
+
const buf = device.createBuffer({
|
|
7253
|
+
size: sliceBytes * 2,
|
|
7254
|
+
// two RT slices (depth + normal)
|
|
7255
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
7256
|
+
});
|
|
7257
|
+
encoder.copyTextureToBuffer(
|
|
7258
|
+
{ texture: this.depthRT, origin: { x: x0, y: y0 } },
|
|
7259
|
+
{ buffer: buf, offset: 0, bytesPerRow },
|
|
7260
|
+
{ width: copyW, height: copyH }
|
|
7261
|
+
);
|
|
7262
|
+
encoder.copyTextureToBuffer(
|
|
7263
|
+
{ texture: this.normalRT, origin: { x: x0, y: y0 } },
|
|
7264
|
+
{ buffer: buf, offset: sliceBytes, bytesPerRow },
|
|
7265
|
+
{ width: copyW, height: copyH }
|
|
7266
|
+
);
|
|
7267
|
+
device.queue.submit([encoder.finish()]);
|
|
7268
|
+
const vm = new Float32Array(this.camera.viewMatrix);
|
|
7269
|
+
const proj = new Float32Array(this.camera.projectionMatrix);
|
|
7270
|
+
const cam = new Float32Array(this.camera.position);
|
|
7271
|
+
buf.mapAsync(GPUMapMode.READ).then(() => {
|
|
7272
|
+
const raw = new Uint16Array(buf.getMappedRange().slice(0));
|
|
7273
|
+
buf.unmap();
|
|
7274
|
+
buf.destroy();
|
|
7275
|
+
this.dnResult = { raw, vm, proj, cam, w, h, px: ipx, py: ipy, copyX: x0, copyY: y0, copyW, copyH, cx, cy };
|
|
7276
|
+
}).catch(() => {
|
|
7277
|
+
buf.destroy();
|
|
7278
|
+
});
|
|
7279
|
+
} else {
|
|
7280
|
+
device.queue.submit([encoder.finish()]);
|
|
7281
|
+
}
|
|
7282
|
+
}
|
|
7283
|
+
static half2Float(h) {
|
|
7284
|
+
const sign = (h & 32768) << 16;
|
|
7285
|
+
const exponent = (h & 31744) >> 10;
|
|
7286
|
+
const mantissa = h & 1023;
|
|
7287
|
+
if (exponent === 0) {
|
|
7288
|
+
if (mantissa === 0) {
|
|
7289
|
+
_GSSplatRenderer._f32[0] = 0;
|
|
7290
|
+
return sign ? -0 : 0;
|
|
7291
|
+
}
|
|
7292
|
+
let e = -1;
|
|
7293
|
+
let m = mantissa;
|
|
7294
|
+
do {
|
|
7295
|
+
e++;
|
|
7296
|
+
m <<= 1;
|
|
7297
|
+
} while ((m & 1024) === 0);
|
|
7298
|
+
_GSSplatRenderer._u32[0] = sign | 127 - 15 - e << 23 | (m & 1023) << 13;
|
|
7299
|
+
} else if (exponent === 31) {
|
|
7300
|
+
_GSSplatRenderer._u32[0] = sign | 2139095040 | mantissa << 13;
|
|
7301
|
+
} else {
|
|
7302
|
+
_GSSplatRenderer._u32[0] = sign | exponent + 127 - 15 << 23 | mantissa << 13;
|
|
7303
|
+
}
|
|
7304
|
+
return _GSSplatRenderer._f32[0];
|
|
7305
|
+
}
|
|
7306
|
+
// 通过上一帧的回读结果dnResult,获取深度和法线信息
|
|
7307
|
+
// 世界位置通过当前像素以及对应深度反向投影计算
|
|
7308
|
+
// 世界法线使用3×3邻域直接平均以降低噪声敏感度
|
|
7309
|
+
getDepthNormal() {
|
|
7310
|
+
if (!this.dnResult) return null;
|
|
7311
|
+
const { raw, vm, proj, cam, w, h, px, py, copyW, copyH, cx, cy } = this.dnResult;
|
|
7312
|
+
const rawBytesPerRow = copyW * 8;
|
|
7313
|
+
const bytesPerRow = Math.ceil(rawBytesPerRow / 256) * 256;
|
|
7314
|
+
const u16PerRow = bytesPerRow / 2;
|
|
7315
|
+
const sliceU16 = u16PerRow * copyH;
|
|
7316
|
+
const h2f = _GSSplatRenderer.half2Float;
|
|
7317
|
+
const cOff = cy * u16PerRow + cx * 4;
|
|
7318
|
+
const accumDepth = h2f(raw[cOff + 0]);
|
|
7319
|
+
const transmittance = h2f(raw[cOff + 3]);
|
|
7320
|
+
const alpha = 1 - transmittance;
|
|
7321
|
+
if (alpha < 1e-6) return null;
|
|
7322
|
+
const viewDepth = accumDepth / alpha;
|
|
7323
|
+
let snx = 0, sny = 0, snz = 0;
|
|
7324
|
+
for (let ky = 0; ky < 3; ky++) {
|
|
7325
|
+
const ry = cy + ky - 1;
|
|
7326
|
+
if (ry < 0 || ry >= copyH) continue;
|
|
7327
|
+
for (let kx = 0; kx < 3; kx++) {
|
|
7328
|
+
const rx = cx + kx - 1;
|
|
7329
|
+
if (rx < 0 || rx >= copyW) continue;
|
|
7330
|
+
const off = sliceU16 + ry * u16PerRow + rx * 4;
|
|
7331
|
+
const a = 1 - h2f(raw[off + 3]);
|
|
7332
|
+
if (a < 1e-6) continue;
|
|
7333
|
+
snx += h2f(raw[off + 0]) / a;
|
|
7334
|
+
sny += h2f(raw[off + 1]) / a;
|
|
7335
|
+
snz += h2f(raw[off + 2]) / a;
|
|
7336
|
+
}
|
|
7337
|
+
}
|
|
7338
|
+
let nx = snx, ny = sny, nz = snz;
|
|
7339
|
+
const nLen = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
7340
|
+
if (nLen > 1e-8) {
|
|
7341
|
+
nx /= nLen;
|
|
7342
|
+
ny /= nLen;
|
|
7343
|
+
nz /= nLen;
|
|
7344
|
+
} else {
|
|
7345
|
+
nx = 0;
|
|
7346
|
+
ny = 1;
|
|
7347
|
+
nz = 0;
|
|
7348
|
+
}
|
|
7349
|
+
const ndcX = (px + 0.5) / w * 2 - 1;
|
|
7350
|
+
const ndcY = 1 - (py + 0.5) / h * 2;
|
|
7351
|
+
const viewX = ndcX * viewDepth / proj[0];
|
|
7352
|
+
const viewY = ndcY * viewDepth / proj[5];
|
|
7353
|
+
const viewZ = -viewDepth;
|
|
7354
|
+
const wx = vm[0] * viewX + vm[1] * viewY + vm[2] * viewZ + cam[0];
|
|
7355
|
+
const wy = vm[4] * viewX + vm[5] * viewY + vm[6] * viewZ + cam[1];
|
|
7356
|
+
const wz = vm[8] * viewX + vm[9] * viewY + vm[10] * viewZ + cam[2];
|
|
7357
|
+
const toCamX = cam[0] - wx;
|
|
7358
|
+
const toCamY = cam[1] - wy;
|
|
7359
|
+
const toCamZ = cam[2] - wz;
|
|
7360
|
+
if (nx * toCamX + ny * toCamY + nz * toCamZ < 0) {
|
|
7361
|
+
nx = -nx;
|
|
7362
|
+
ny = -ny;
|
|
7363
|
+
nz = -nz;
|
|
5858
7364
|
}
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
const dx = max[0] - min[0];
|
|
5865
|
-
const dy = max[1] - min[1];
|
|
5866
|
-
const dz = max[2] - min[2];
|
|
5867
|
-
const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
|
|
5868
|
-
return { min, max, center, radius };
|
|
7365
|
+
return {
|
|
7366
|
+
depth: viewDepth,
|
|
7367
|
+
normal: [nx, ny, nz],
|
|
7368
|
+
worldPos: [wx, wy, wz]
|
|
7369
|
+
};
|
|
5869
7370
|
}
|
|
5870
7371
|
supportsSHMode(mode) {
|
|
5871
7372
|
return mode >= SHMode.L0 && mode <= SHMode.L3;
|
|
@@ -5887,11 +7388,24 @@ class GSSplatRenderer {
|
|
|
5887
7388
|
this.sorter.destroy();
|
|
5888
7389
|
this.sorter = null;
|
|
5889
7390
|
}
|
|
7391
|
+
if (this.depthRT) {
|
|
7392
|
+
this.depthRT.destroy();
|
|
7393
|
+
this.depthRT = null;
|
|
7394
|
+
}
|
|
7395
|
+
if (this.normalRT) {
|
|
7396
|
+
this.normalRT.destroy();
|
|
7397
|
+
this.normalRT = null;
|
|
7398
|
+
}
|
|
7399
|
+
this.dnResult = null;
|
|
5890
7400
|
this.uniformBuffer.destroy();
|
|
5891
7401
|
this.splatCount = 0;
|
|
5892
7402
|
this.bindGroup = null;
|
|
7403
|
+
this.depthNormalPipeline = null;
|
|
5893
7404
|
}
|
|
5894
|
-
}
|
|
7405
|
+
};
|
|
7406
|
+
__publicField(_GSSplatRenderer, "_f32", new Float32Array(1));
|
|
7407
|
+
__publicField(_GSSplatRenderer, "_u32", new Uint32Array(_GSSplatRenderer._f32.buffer));
|
|
7408
|
+
let GSSplatRenderer = _GSSplatRenderer;
|
|
5895
7409
|
function calculateTextureDimensions(count) {
|
|
5896
7410
|
if (count <= 0) {
|
|
5897
7411
|
return { width: 4, height: 4 };
|
|
@@ -5913,7 +7427,7 @@ function computeBoundingBox(positions, count) {
|
|
|
5913
7427
|
};
|
|
5914
7428
|
}
|
|
5915
7429
|
const min = [positions[0], positions[1], positions[2]];
|
|
5916
|
-
const
|
|
7430
|
+
const max2 = [positions[0], positions[1], positions[2]];
|
|
5917
7431
|
for (let i = 1; i < count; i++) {
|
|
5918
7432
|
const x = positions[i * 3 + 0];
|
|
5919
7433
|
const y = positions[i * 3 + 1];
|
|
@@ -5921,11 +7435,11 @@ function computeBoundingBox(positions, count) {
|
|
|
5921
7435
|
min[0] = Math.min(min[0], x);
|
|
5922
7436
|
min[1] = Math.min(min[1], y);
|
|
5923
7437
|
min[2] = Math.min(min[2], z);
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
7438
|
+
max2[0] = Math.max(max2[0], x);
|
|
7439
|
+
max2[1] = Math.max(max2[1], y);
|
|
7440
|
+
max2[2] = Math.max(max2[2], z);
|
|
5927
7441
|
}
|
|
5928
|
-
return { min, max };
|
|
7442
|
+
return { min, max: max2 };
|
|
5929
7443
|
}
|
|
5930
7444
|
function compressSplatsToTextures(device, data) {
|
|
5931
7445
|
const count = data.count;
|
|
@@ -6721,6 +8235,7 @@ class GSSplatRendererMobile {
|
|
|
6721
8235
|
__publicField(this, "positionsBuffer", null);
|
|
6722
8236
|
// Bounding box
|
|
6723
8237
|
__publicField(this, "boundingBox", null);
|
|
8238
|
+
__publicField(this, "cpuPositions", null);
|
|
6724
8239
|
// 帧计数(用于排序频率控制)
|
|
6725
8240
|
__publicField(this, "frameCount", 0);
|
|
6726
8241
|
__publicField(this, "sortEveryNFrames", 1);
|
|
@@ -6937,7 +8452,7 @@ class GSSplatRendererMobile {
|
|
|
6937
8452
|
},
|
|
6938
8453
|
depthStencil: {
|
|
6939
8454
|
format: this.renderer.depthFormat,
|
|
6940
|
-
depthWriteEnabled:
|
|
8455
|
+
depthWriteEnabled: true,
|
|
6941
8456
|
depthCompare: "always"
|
|
6942
8457
|
}
|
|
6943
8458
|
});
|
|
@@ -6967,6 +8482,7 @@ class GSSplatRendererMobile {
|
|
|
6967
8482
|
}
|
|
6968
8483
|
this.compressedTextures = compressSplatsToTextures(device, data);
|
|
6969
8484
|
this.boundingBox = this.computeBoundingBox(data);
|
|
8485
|
+
this.cpuPositions = new Float32Array(data.positions);
|
|
6970
8486
|
this.positionsBuffer = device.createBuffer({
|
|
6971
8487
|
size: data.count * 12,
|
|
6972
8488
|
// 3 floats per position
|
|
@@ -7044,7 +8560,7 @@ class GSSplatRendererMobile {
|
|
|
7044
8560
|
}
|
|
7045
8561
|
const positions = data.positions;
|
|
7046
8562
|
const min = [positions[0], positions[1], positions[2]];
|
|
7047
|
-
const
|
|
8563
|
+
const max2 = [positions[0], positions[1], positions[2]];
|
|
7048
8564
|
for (let i = 1; i < data.count; i++) {
|
|
7049
8565
|
const x = positions[i * 3 + 0];
|
|
7050
8566
|
const y = positions[i * 3 + 1];
|
|
@@ -7052,20 +8568,20 @@ class GSSplatRendererMobile {
|
|
|
7052
8568
|
min[0] = Math.min(min[0], x);
|
|
7053
8569
|
min[1] = Math.min(min[1], y);
|
|
7054
8570
|
min[2] = Math.min(min[2], z);
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
8571
|
+
max2[0] = Math.max(max2[0], x);
|
|
8572
|
+
max2[1] = Math.max(max2[1], y);
|
|
8573
|
+
max2[2] = Math.max(max2[2], z);
|
|
7058
8574
|
}
|
|
7059
8575
|
const center = [
|
|
7060
|
-
(min[0] +
|
|
7061
|
-
(min[1] +
|
|
7062
|
-
(min[2] +
|
|
8576
|
+
(min[0] + max2[0]) / 2,
|
|
8577
|
+
(min[1] + max2[1]) / 2,
|
|
8578
|
+
(min[2] + max2[2]) / 2
|
|
7063
8579
|
];
|
|
7064
|
-
const dx =
|
|
7065
|
-
const dy =
|
|
7066
|
-
const dz =
|
|
8580
|
+
const dx = max2[0] - min[0];
|
|
8581
|
+
const dy = max2[1] - min[1];
|
|
8582
|
+
const dz = max2[2] - min[2];
|
|
7067
8583
|
const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
|
|
7068
|
-
return { min, max, center, radius };
|
|
8584
|
+
return { min, max: max2, center, radius };
|
|
7069
8585
|
}
|
|
7070
8586
|
/**
|
|
7071
8587
|
* 渲染
|
|
@@ -7119,6 +8635,9 @@ class GSSplatRendererMobile {
|
|
|
7119
8635
|
getBoundingBox() {
|
|
7120
8636
|
return this.boundingBox;
|
|
7121
8637
|
}
|
|
8638
|
+
getCPUPositions() {
|
|
8639
|
+
return this.cpuPositions;
|
|
8640
|
+
}
|
|
7122
8641
|
/**
|
|
7123
8642
|
* 设置排序频率
|
|
7124
8643
|
* @param n 每 n 帧排序一次
|
|
@@ -7254,14 +8773,40 @@ class SceneManager {
|
|
|
7254
8773
|
return this.meshRenderer.removeMeshByIndex(index);
|
|
7255
8774
|
}
|
|
7256
8775
|
// ============================================
|
|
8776
|
+
// 覆盖层 Mesh 管理(热点等)
|
|
8777
|
+
// ============================================
|
|
8778
|
+
getOverlayMeshCount() {
|
|
8779
|
+
return this.meshRenderer.getOverlayMeshCount();
|
|
8780
|
+
}
|
|
8781
|
+
getOverlayMeshByIndex(index) {
|
|
8782
|
+
return this.meshRenderer.getOverlayMeshByIndex(index);
|
|
8783
|
+
}
|
|
8784
|
+
getOverlayMeshRange(startIndex, count) {
|
|
8785
|
+
const meshes = [];
|
|
8786
|
+
for (let i = 0; i < count; i++) {
|
|
8787
|
+
const mesh = this.meshRenderer.getOverlayMeshByIndex(startIndex + i);
|
|
8788
|
+
if (mesh) meshes.push(mesh);
|
|
8789
|
+
}
|
|
8790
|
+
return meshes;
|
|
8791
|
+
}
|
|
8792
|
+
removeOverlayMeshByIndex(index) {
|
|
8793
|
+
return this.meshRenderer.removeOverlayMeshByIndex(index);
|
|
8794
|
+
}
|
|
8795
|
+
getOverlayMeshColor(index) {
|
|
8796
|
+
return this.meshRenderer.getOverlayMeshColor(index);
|
|
8797
|
+
}
|
|
8798
|
+
setOverlayMeshRangeColor(startIndex, count, r, g, b, a = 1) {
|
|
8799
|
+
return this.meshRenderer.setOverlayMeshRangeColor(startIndex, count, r, g, b, a);
|
|
8800
|
+
}
|
|
8801
|
+
// ============================================
|
|
7257
8802
|
// Splat 管理
|
|
7258
8803
|
// ============================================
|
|
7259
8804
|
/**
|
|
7260
8805
|
* 获取 Splat 数量
|
|
7261
8806
|
*/
|
|
7262
8807
|
getSplatCount() {
|
|
7263
|
-
var
|
|
7264
|
-
return ((
|
|
8808
|
+
var _a2;
|
|
8809
|
+
return ((_a2 = this.gsRenderer) == null ? void 0 : _a2.getSplatCount()) ?? 0;
|
|
7265
8810
|
}
|
|
7266
8811
|
/**
|
|
7267
8812
|
* 清空 Splats
|
|
@@ -7279,43 +8824,43 @@ class SceneManager {
|
|
|
7279
8824
|
* 设置 Splat 位置
|
|
7280
8825
|
*/
|
|
7281
8826
|
setSplatPosition(x, y, z) {
|
|
7282
|
-
var
|
|
7283
|
-
(
|
|
8827
|
+
var _a2;
|
|
8828
|
+
(_a2 = this.gsRenderer) == null ? void 0 : _a2.setPosition(x, y, z);
|
|
7284
8829
|
}
|
|
7285
8830
|
/**
|
|
7286
8831
|
* 获取 Splat 位置
|
|
7287
8832
|
*/
|
|
7288
8833
|
getSplatPosition() {
|
|
7289
|
-
var
|
|
7290
|
-
return ((
|
|
8834
|
+
var _a2;
|
|
8835
|
+
return ((_a2 = this.gsRenderer) == null ? void 0 : _a2.getPosition()) ?? null;
|
|
7291
8836
|
}
|
|
7292
8837
|
/**
|
|
7293
8838
|
* 设置 Splat 旋转
|
|
7294
8839
|
*/
|
|
7295
8840
|
setSplatRotation(x, y, z) {
|
|
7296
|
-
var
|
|
7297
|
-
(
|
|
8841
|
+
var _a2;
|
|
8842
|
+
(_a2 = this.gsRenderer) == null ? void 0 : _a2.setRotation(x, y, z);
|
|
7298
8843
|
}
|
|
7299
8844
|
/**
|
|
7300
8845
|
* 获取 Splat 旋转
|
|
7301
8846
|
*/
|
|
7302
8847
|
getSplatRotation() {
|
|
7303
|
-
var
|
|
7304
|
-
return ((
|
|
8848
|
+
var _a2;
|
|
8849
|
+
return ((_a2 = this.gsRenderer) == null ? void 0 : _a2.getRotation()) ?? null;
|
|
7305
8850
|
}
|
|
7306
8851
|
/**
|
|
7307
8852
|
* 设置 Splat 缩放
|
|
7308
8853
|
*/
|
|
7309
8854
|
setSplatScale(x, y, z) {
|
|
7310
|
-
var
|
|
7311
|
-
(
|
|
8855
|
+
var _a2;
|
|
8856
|
+
(_a2 = this.gsRenderer) == null ? void 0 : _a2.setScale(x, y, z);
|
|
7312
8857
|
}
|
|
7313
8858
|
/**
|
|
7314
8859
|
* 获取 Splat 缩放
|
|
7315
8860
|
*/
|
|
7316
8861
|
getSplatScale() {
|
|
7317
|
-
var
|
|
7318
|
-
return ((
|
|
8862
|
+
var _a2;
|
|
8863
|
+
return ((_a2 = this.gsRenderer) == null ? void 0 : _a2.getScale()) ?? null;
|
|
7319
8864
|
}
|
|
7320
8865
|
// ============================================
|
|
7321
8866
|
// SH 模式
|
|
@@ -7324,8 +8869,8 @@ class SceneManager {
|
|
|
7324
8869
|
* 设置 SH 模式
|
|
7325
8870
|
*/
|
|
7326
8871
|
setSHMode(mode) {
|
|
7327
|
-
var
|
|
7328
|
-
if ((
|
|
8872
|
+
var _a2;
|
|
8873
|
+
if ((_a2 = this.gsRenderer) == null ? void 0 : _a2.setSHMode) {
|
|
7329
8874
|
this.gsRenderer.setSHMode(mode);
|
|
7330
8875
|
}
|
|
7331
8876
|
}
|
|
@@ -7333,8 +8878,8 @@ class SceneManager {
|
|
|
7333
8878
|
* 获取当前 SH 模式
|
|
7334
8879
|
*/
|
|
7335
8880
|
getSHMode() {
|
|
7336
|
-
var
|
|
7337
|
-
return ((
|
|
8881
|
+
var _a2, _b2;
|
|
8882
|
+
return ((_b2 = (_a2 = this.gsRenderer) == null ? void 0 : _a2.getSHMode) == null ? void 0 : _b2.call(_a2)) ?? 0;
|
|
7338
8883
|
}
|
|
7339
8884
|
// ============================================
|
|
7340
8885
|
// Bounding Box 查询
|
|
@@ -7342,15 +8887,15 @@ class SceneManager {
|
|
|
7342
8887
|
/**
|
|
7343
8888
|
* 获取 Mesh 的组合 bounding box
|
|
7344
8889
|
*/
|
|
7345
|
-
|
|
8890
|
+
getBoundingBox() {
|
|
7346
8891
|
return this.meshRenderer.getCombinedBoundingBox();
|
|
7347
8892
|
}
|
|
7348
8893
|
/**
|
|
7349
8894
|
* 获取 Splat 的 bounding box
|
|
7350
8895
|
*/
|
|
7351
8896
|
getSplatBoundingBox() {
|
|
7352
|
-
var
|
|
7353
|
-
return ((
|
|
8897
|
+
var _a2;
|
|
8898
|
+
return ((_a2 = this.gsRenderer) == null ? void 0 : _a2.getBoundingBox()) ?? null;
|
|
7354
8899
|
}
|
|
7355
8900
|
/**
|
|
7356
8901
|
* 获取指定 Mesh 范围的组合 bounding box
|
|
@@ -7393,16 +8938,16 @@ class SceneManager {
|
|
|
7393
8938
|
getSceneBoundingBox() {
|
|
7394
8939
|
let combinedMin = null;
|
|
7395
8940
|
let combinedMax = null;
|
|
7396
|
-
const meshBBox = this.
|
|
8941
|
+
const meshBBox = this.getBoundingBox();
|
|
7397
8942
|
if (meshBBox) {
|
|
7398
|
-
combinedMin = [
|
|
7399
|
-
combinedMax = [
|
|
8943
|
+
combinedMin = [meshBBox.min[0], meshBBox.min[1], meshBBox.min[2]];
|
|
8944
|
+
combinedMax = [meshBBox.max[0], meshBBox.max[1], meshBBox.max[2]];
|
|
7400
8945
|
}
|
|
7401
8946
|
const splatBBox = this.getSplatBoundingBox();
|
|
7402
8947
|
if (splatBBox) {
|
|
7403
8948
|
if (combinedMin === null || combinedMax === null) {
|
|
7404
|
-
combinedMin = [
|
|
7405
|
-
combinedMax = [
|
|
8949
|
+
combinedMin = [splatBBox.min[0], splatBBox.min[1], splatBBox.min[2]];
|
|
8950
|
+
combinedMax = [splatBBox.max[0], splatBBox.max[1], splatBBox.max[2]];
|
|
7406
8951
|
} else {
|
|
7407
8952
|
combinedMin[0] = Math.min(combinedMin[0], splatBBox.min[0]);
|
|
7408
8953
|
combinedMin[1] = Math.min(combinedMin[1], splatBBox.min[1]);
|
|
@@ -9117,7 +10662,7 @@ class ArcShape extends Shape {
|
|
|
9117
10662
|
const dynRotY = this._dynamicRotation.y * Math.PI / 180;
|
|
9118
10663
|
const dynRotZ = this._dynamicRotation.z * Math.PI / 180;
|
|
9119
10664
|
const dynQuat = Quat.fromEuler(dynRotX, dynRotY, dynRotZ);
|
|
9120
|
-
const finalQuat =
|
|
10665
|
+
const finalQuat = dynQuat.multiply(baseQuat);
|
|
9121
10666
|
return Mat4.compose(this._position, finalQuat, this._scale);
|
|
9122
10667
|
}
|
|
9123
10668
|
/**
|
|
@@ -9612,7 +11157,7 @@ const RING_FACING_EPSILON = 1e-4;
|
|
|
9612
11157
|
const PERS_SCALE_RATIO = 0.3;
|
|
9613
11158
|
const MIN_SCALE = 1e-4;
|
|
9614
11159
|
const RAD_TO_DEG = 180 / Math.PI;
|
|
9615
|
-
class
|
|
11160
|
+
class TransformGizmo {
|
|
9616
11161
|
constructor(config) {
|
|
9617
11162
|
__publicField(this, "renderer");
|
|
9618
11163
|
__publicField(this, "camera");
|
|
@@ -10272,8 +11817,8 @@ class TransformGizmoV2 {
|
|
|
10272
11817
|
const forward = gizmoRot.transformVector(new Vec3(0, 0, 1));
|
|
10273
11818
|
const xArc = this._shapes.get("x");
|
|
10274
11819
|
if (xArc) {
|
|
10275
|
-
const angle = Math.atan2(localFacingDir.
|
|
10276
|
-
xArc.setDynamicRotation(new Vec3(
|
|
11820
|
+
const angle = -Math.atan2(localFacingDir.y, localFacingDir.z) * RAD_TO_DEG;
|
|
11821
|
+
xArc.setDynamicRotation(new Vec3(angle, 0, 0));
|
|
10277
11822
|
const dot = facingDir.dot(right);
|
|
10278
11823
|
if (!this._dragging) {
|
|
10279
11824
|
const showSector = 1 - Math.abs(dot) > RING_FACING_EPSILON;
|
|
@@ -10488,7 +12033,7 @@ class TransformGizmoV2 {
|
|
|
10488
12033
|
}
|
|
10489
12034
|
}
|
|
10490
12035
|
_updateHover(axis, isPlane) {
|
|
10491
|
-
var
|
|
12036
|
+
var _a2, _b2, _c, _d, _e, _f, _g;
|
|
10492
12037
|
if (this._dragging) return;
|
|
10493
12038
|
if (this._hoverAxis !== axis) {
|
|
10494
12039
|
for (const shape of this._shapes.values()) {
|
|
@@ -10498,8 +12043,8 @@ class TransformGizmoV2 {
|
|
|
10498
12043
|
this._hoverIsPlane = isPlane;
|
|
10499
12044
|
if (axis) {
|
|
10500
12045
|
if (axis === "xyz") {
|
|
10501
|
-
(
|
|
10502
|
-
(
|
|
12046
|
+
(_a2 = this._shapes.get("x")) == null ? void 0 : _a2.hover(true);
|
|
12047
|
+
(_b2 = this._shapes.get("y")) == null ? void 0 : _b2.hover(true);
|
|
10503
12048
|
(_c = this._shapes.get("z")) == null ? void 0 : _c.hover(true);
|
|
10504
12049
|
(_d = this._shapes.get("xyz")) == null ? void 0 : _d.hover(true);
|
|
10505
12050
|
} else if (axis === "f") {
|
|
@@ -10833,7 +12378,7 @@ class GizmoManager {
|
|
|
10833
12378
|
this.canvas = canvas;
|
|
10834
12379
|
this.controls = controls;
|
|
10835
12380
|
this.viewportGizmo = new ViewportGizmo(renderer, camera, canvas);
|
|
10836
|
-
this.transformGizmo = new
|
|
12381
|
+
this.transformGizmo = new TransformGizmo({ renderer, camera, canvas });
|
|
10837
12382
|
this.transformGizmo.init();
|
|
10838
12383
|
this.boundingBoxRenderer = new BoundingBoxRenderer(renderer, camera);
|
|
10839
12384
|
this.transformGizmo.setOnDragStateChange((isDragging) => {
|
|
@@ -10973,6 +12518,946 @@ class GizmoManager {
|
|
|
10973
12518
|
this.boundingBoxRenderer.destroy();
|
|
10974
12519
|
}
|
|
10975
12520
|
}
|
|
12521
|
+
class HotspotManager {
|
|
12522
|
+
constructor(renderer, camera, canvas, controls, meshRenderer) {
|
|
12523
|
+
__publicField(this, "camera");
|
|
12524
|
+
__publicField(this, "renderer");
|
|
12525
|
+
__publicField(this, "controls");
|
|
12526
|
+
__publicField(this, "meshRenderer");
|
|
12527
|
+
__publicField(this, "objLoader");
|
|
12528
|
+
__publicField(this, "canvas");
|
|
12529
|
+
__publicField(this, "active", false);
|
|
12530
|
+
__publicField(this, "gsRenderer", null);
|
|
12531
|
+
// 圆圈指示器
|
|
12532
|
+
__publicField(this, "indicatorMesh", null);
|
|
12533
|
+
__publicField(this, "indicatorAdded", false);
|
|
12534
|
+
// 已放置的热点
|
|
12535
|
+
__publicField(this, "hotspots", []);
|
|
12536
|
+
// 当前命中点缓存
|
|
12537
|
+
__publicField(this, "currentHit", null);
|
|
12538
|
+
// 指示器变换状态缓存(点击时用于热点放置)
|
|
12539
|
+
__publicField(this, "indicatorTransform", null);
|
|
12540
|
+
// OBJ 模型 URL
|
|
12541
|
+
__publicField(this, "hotspotOBJUrl", "");
|
|
12542
|
+
// 事件处理函数引用
|
|
12543
|
+
__publicField(this, "boundOnMouseMove");
|
|
12544
|
+
__publicField(this, "boundOnMouseDown");
|
|
12545
|
+
__publicField(this, "boundOnMouseUp");
|
|
12546
|
+
__publicField(this, "boundOnKeyDown");
|
|
12547
|
+
// 拖拽检测
|
|
12548
|
+
__publicField(this, "mouseDownPos", null);
|
|
12549
|
+
__publicField(this, "isDragging", false);
|
|
12550
|
+
__publicField(this, "DRAG_THRESHOLD", 5);
|
|
12551
|
+
// rAF 驱动的流畅更新
|
|
12552
|
+
__publicField(this, "pendingMouseX", 0);
|
|
12553
|
+
__publicField(this, "pendingMouseY", 0);
|
|
12554
|
+
__publicField(this, "hasPendingMove", false);
|
|
12555
|
+
__publicField(this, "rafId", 0);
|
|
12556
|
+
// 法线缓存:如果命中点没有大变化则复用上次法线
|
|
12557
|
+
__publicField(this, "lastHitIdx", -1);
|
|
12558
|
+
__publicField(this, "lastNormal", [0, 1, 0]);
|
|
12559
|
+
// 指示器半径(世界空间基准)
|
|
12560
|
+
__publicField(this, "indicatorBaseRadius", 0.02);
|
|
12561
|
+
// 搜索邻域的屏幕空间半径(像素)
|
|
12562
|
+
__publicField(this, "pickRadiusPx", 20);
|
|
12563
|
+
// 回调
|
|
12564
|
+
__publicField(this, "onHotspotPlaced", null);
|
|
12565
|
+
__publicField(this, "onModeChanged", null);
|
|
12566
|
+
// ============================================
|
|
12567
|
+
// 事件处理
|
|
12568
|
+
// ============================================
|
|
12569
|
+
// Current mouse pixel position in device pixels (for GPU readback request)
|
|
12570
|
+
__publicField(this, "pickPixelX", -1);
|
|
12571
|
+
__publicField(this, "pickPixelY", -1);
|
|
12572
|
+
// Last client coords for CPU fallback
|
|
12573
|
+
__publicField(this, "lastClientX", 0);
|
|
12574
|
+
__publicField(this, "lastClientY", 0);
|
|
12575
|
+
// True when mouse has moved since last consumeGPUResult that produced a result
|
|
12576
|
+
__publicField(this, "pickDirty", false);
|
|
12577
|
+
this.renderer = renderer;
|
|
12578
|
+
this.camera = camera;
|
|
12579
|
+
this.canvas = canvas;
|
|
12580
|
+
this.controls = controls;
|
|
12581
|
+
this.meshRenderer = meshRenderer;
|
|
12582
|
+
this.objLoader = new OBJLoader(renderer.device);
|
|
12583
|
+
this.boundOnMouseMove = this.onMouseMove.bind(this);
|
|
12584
|
+
this.boundOnMouseDown = this.onMouseDown.bind(this);
|
|
12585
|
+
this.boundOnMouseUp = this.onMouseUp.bind(this);
|
|
12586
|
+
this.boundOnKeyDown = this.onKeyDown.bind(this);
|
|
12587
|
+
}
|
|
12588
|
+
setGSRenderer(gsRenderer) {
|
|
12589
|
+
this.gsRenderer = gsRenderer;
|
|
12590
|
+
if (gsRenderer) {
|
|
12591
|
+
const bbox = gsRenderer.getBoundingBox();
|
|
12592
|
+
if (bbox) {
|
|
12593
|
+
this.indicatorBaseRadius = bbox.radius * 8e-3;
|
|
12594
|
+
}
|
|
12595
|
+
}
|
|
12596
|
+
}
|
|
12597
|
+
setHotspotOBJUrl(url) {
|
|
12598
|
+
this.hotspotOBJUrl = url;
|
|
12599
|
+
}
|
|
12600
|
+
enter() {
|
|
12601
|
+
var _a2;
|
|
12602
|
+
if (this.active) return;
|
|
12603
|
+
if (!this.gsRenderer || !this.gsRenderer.getCPUPositions()) {
|
|
12604
|
+
console.warn("HotspotManager: 没有可用的 GS 数据");
|
|
12605
|
+
return;
|
|
12606
|
+
}
|
|
12607
|
+
this.active = true;
|
|
12608
|
+
this.canvas.style.cursor = "none";
|
|
12609
|
+
this.createIndicatorMesh();
|
|
12610
|
+
this.canvas.addEventListener("mousemove", this.boundOnMouseMove);
|
|
12611
|
+
this.canvas.addEventListener("mousedown", this.boundOnMouseDown);
|
|
12612
|
+
this.canvas.addEventListener("mouseup", this.boundOnMouseUp);
|
|
12613
|
+
window.addEventListener("keydown", this.boundOnKeyDown);
|
|
12614
|
+
(_a2 = this.onModeChanged) == null ? void 0 : _a2.call(this, true);
|
|
12615
|
+
}
|
|
12616
|
+
exit() {
|
|
12617
|
+
var _a2;
|
|
12618
|
+
if (!this.active) return;
|
|
12619
|
+
this.active = false;
|
|
12620
|
+
this.canvas.style.cursor = "";
|
|
12621
|
+
this.canvas.removeEventListener("mousemove", this.boundOnMouseMove);
|
|
12622
|
+
this.canvas.removeEventListener("mousedown", this.boundOnMouseDown);
|
|
12623
|
+
this.canvas.removeEventListener("mouseup", this.boundOnMouseUp);
|
|
12624
|
+
window.removeEventListener("keydown", this.boundOnKeyDown);
|
|
12625
|
+
if (this.rafId) {
|
|
12626
|
+
cancelAnimationFrame(this.rafId);
|
|
12627
|
+
this.rafId = 0;
|
|
12628
|
+
}
|
|
12629
|
+
this.hasPendingMove = false;
|
|
12630
|
+
this.hideIndicator();
|
|
12631
|
+
this.currentHit = null;
|
|
12632
|
+
this.mouseDownPos = null;
|
|
12633
|
+
this.isDragging = false;
|
|
12634
|
+
this.lastHitIdx = -1;
|
|
12635
|
+
this.pickPixelX = -1;
|
|
12636
|
+
this.pickPixelY = -1;
|
|
12637
|
+
this.pickDirty = false;
|
|
12638
|
+
(_a2 = this.onModeChanged) == null ? void 0 : _a2.call(this, false);
|
|
12639
|
+
}
|
|
12640
|
+
isActive() {
|
|
12641
|
+
return this.active;
|
|
12642
|
+
}
|
|
12643
|
+
getHotspots() {
|
|
12644
|
+
return [...this.hotspots];
|
|
12645
|
+
}
|
|
12646
|
+
setOnHotspotPlaced(cb) {
|
|
12647
|
+
this.onHotspotPlaced = cb;
|
|
12648
|
+
}
|
|
12649
|
+
setOnModeChanged(cb) {
|
|
12650
|
+
this.onModeChanged = cb;
|
|
12651
|
+
}
|
|
12652
|
+
/**
|
|
12653
|
+
* Returns the current pick pixel coordinates in device pixels.
|
|
12654
|
+
* Called by the render loop to pass to prepareDepthNormalPass.
|
|
12655
|
+
* Returns [-1, -1] when no pick is needed.
|
|
12656
|
+
*/
|
|
12657
|
+
getPickPixel() {
|
|
12658
|
+
return [this.pickPixelX, this.pickPixelY];
|
|
12659
|
+
}
|
|
12660
|
+
onMouseMove(e) {
|
|
12661
|
+
if (!this.active) return;
|
|
12662
|
+
if (this.mouseDownPos) {
|
|
12663
|
+
const dx = e.clientX - this.mouseDownPos.x;
|
|
12664
|
+
const dy = e.clientY - this.mouseDownPos.y;
|
|
12665
|
+
if (dx * dx + dy * dy > this.DRAG_THRESHOLD * this.DRAG_THRESHOLD) {
|
|
12666
|
+
this.isDragging = true;
|
|
12667
|
+
}
|
|
12668
|
+
}
|
|
12669
|
+
this.pendingMouseX = e.clientX;
|
|
12670
|
+
this.pendingMouseY = e.clientY;
|
|
12671
|
+
if (!this.hasPendingMove) {
|
|
12672
|
+
this.hasPendingMove = true;
|
|
12673
|
+
this.rafId = requestAnimationFrame(() => this.processMouseMove());
|
|
12674
|
+
}
|
|
12675
|
+
}
|
|
12676
|
+
processMouseMove() {
|
|
12677
|
+
this.hasPendingMove = false;
|
|
12678
|
+
if (!this.active) return;
|
|
12679
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
12680
|
+
const dpr = this.canvas.width / rect.width;
|
|
12681
|
+
this.pickPixelX = (this.pendingMouseX - rect.left) * dpr;
|
|
12682
|
+
this.pickPixelY = (this.pendingMouseY - rect.top) * dpr;
|
|
12683
|
+
this.lastClientX = this.pendingMouseX;
|
|
12684
|
+
this.lastClientY = this.pendingMouseY;
|
|
12685
|
+
this.pickDirty = true;
|
|
12686
|
+
}
|
|
12687
|
+
/**
|
|
12688
|
+
* 读取上一帧的深度法线GPU回读结果,更新指示器
|
|
12689
|
+
* 每帧在App.Render()中调用
|
|
12690
|
+
* 如果GPU没有回读结果,则使用CPU拾取
|
|
12691
|
+
*/
|
|
12692
|
+
consumeGPUResult() {
|
|
12693
|
+
var _a2;
|
|
12694
|
+
if (!this.active || !this.pickDirty) return;
|
|
12695
|
+
let hit = null;
|
|
12696
|
+
if ((_a2 = this.gsRenderer) == null ? void 0 : _a2.getDepthNormal) {
|
|
12697
|
+
const result = this.gsRenderer.getDepthNormal();
|
|
12698
|
+
if (result) {
|
|
12699
|
+
const dist = Math.sqrt(
|
|
12700
|
+
(result.worldPos[0] - this.camera.position[0]) ** 2 + (result.worldPos[1] - this.camera.position[1]) ** 2 + (result.worldPos[2] - this.camera.position[2]) ** 2
|
|
12701
|
+
);
|
|
12702
|
+
hit = { point: result.worldPos, normal: result.normal, distance: dist };
|
|
12703
|
+
}
|
|
12704
|
+
}
|
|
12705
|
+
if (!hit) {
|
|
12706
|
+
hit = this.raycastSplatsCPU(this.lastClientX, this.lastClientY);
|
|
12707
|
+
}
|
|
12708
|
+
this.pickDirty = false;
|
|
12709
|
+
this.currentHit = hit;
|
|
12710
|
+
if (hit) {
|
|
12711
|
+
this.updateIndicator(hit.point, hit.normal);
|
|
12712
|
+
this.showIndicator();
|
|
12713
|
+
} else {
|
|
12714
|
+
this.hideIndicator();
|
|
12715
|
+
}
|
|
12716
|
+
}
|
|
12717
|
+
onMouseDown(e) {
|
|
12718
|
+
if (!this.active || e.button !== 0) return;
|
|
12719
|
+
this.mouseDownPos = { x: e.clientX, y: e.clientY };
|
|
12720
|
+
this.isDragging = false;
|
|
12721
|
+
}
|
|
12722
|
+
onMouseUp(e) {
|
|
12723
|
+
if (!this.active || e.button !== 0) return;
|
|
12724
|
+
if (!this.isDragging && this.currentHit) {
|
|
12725
|
+
this.placeHotspot(this.currentHit.point, this.currentHit.normal);
|
|
12726
|
+
}
|
|
12727
|
+
this.mouseDownPos = null;
|
|
12728
|
+
this.isDragging = false;
|
|
12729
|
+
}
|
|
12730
|
+
onKeyDown(e) {
|
|
12731
|
+
if (e.key === "Escape") {
|
|
12732
|
+
this.exit();
|
|
12733
|
+
}
|
|
12734
|
+
}
|
|
12735
|
+
// ============================================
|
|
12736
|
+
// 射线拾取
|
|
12737
|
+
// ============================================
|
|
12738
|
+
raycastSplatsCPU(clientX, clientY) {
|
|
12739
|
+
var _a2;
|
|
12740
|
+
const positions = (_a2 = this.gsRenderer) == null ? void 0 : _a2.getCPUPositions();
|
|
12741
|
+
if (!positions) return null;
|
|
12742
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
12743
|
+
const ndcX = (clientX - rect.left) / rect.width * 2 - 1;
|
|
12744
|
+
const ndcY = -((clientY - rect.top) / rect.height * 2 - 1);
|
|
12745
|
+
const rayOrigin = this.camera.position;
|
|
12746
|
+
const modelMatrix = this.gsRenderer.getModelMatrix();
|
|
12747
|
+
const vp = this.camera.viewProjectionMatrix;
|
|
12748
|
+
const count = positions.length / 3;
|
|
12749
|
+
const step = count > 3e5 ? Math.ceil(count / 3e5) : 1;
|
|
12750
|
+
const halfW = rect.width / 2;
|
|
12751
|
+
const halfH = rect.height / 2;
|
|
12752
|
+
const pickRadSq = this.pickRadiusPx * this.pickRadiusPx;
|
|
12753
|
+
let bestCamDistSq = Infinity;
|
|
12754
|
+
let bestIdx = -1;
|
|
12755
|
+
let bestScreenDistSq = Infinity;
|
|
12756
|
+
const m0 = modelMatrix[0], m1 = modelMatrix[1], m2 = modelMatrix[2];
|
|
12757
|
+
const m4 = modelMatrix[4], m5 = modelMatrix[5], m6 = modelMatrix[6];
|
|
12758
|
+
const m8 = modelMatrix[8], m9 = modelMatrix[9], m10 = modelMatrix[10];
|
|
12759
|
+
const m12 = modelMatrix[12], m13 = modelMatrix[13], m14 = modelMatrix[14];
|
|
12760
|
+
const v0 = vp[0], v1 = vp[1], v3 = vp[3];
|
|
12761
|
+
const v4 = vp[4], v5 = vp[5], v7 = vp[7];
|
|
12762
|
+
const v8 = vp[8], v9 = vp[9], v11 = vp[11];
|
|
12763
|
+
const v12 = vp[12], v13 = vp[13], v15 = vp[15];
|
|
12764
|
+
const rox = rayOrigin[0], roy = rayOrigin[1], roz = rayOrigin[2];
|
|
12765
|
+
for (let i = 0; i < count; i += step) {
|
|
12766
|
+
const i3 = i * 3;
|
|
12767
|
+
const lx = positions[i3], ly = positions[i3 + 1], lz = positions[i3 + 2];
|
|
12768
|
+
const tx = m0 * lx + m4 * ly + m8 * lz + m12;
|
|
12769
|
+
const ty = m1 * lx + m5 * ly + m9 * lz + m13;
|
|
12770
|
+
const tz = m2 * lx + m6 * ly + m10 * lz + m14;
|
|
12771
|
+
const cw = v3 * tx + v7 * ty + v11 * tz + v15;
|
|
12772
|
+
if (cw <= 0) continue;
|
|
12773
|
+
const invCw = 1 / cw;
|
|
12774
|
+
const pixX = ((v0 * tx + v4 * ty + v8 * tz + v12) * invCw - ndcX) * halfW;
|
|
12775
|
+
const pixY = ((v1 * tx + v5 * ty + v9 * tz + v13) * invCw - ndcY) * halfH;
|
|
12776
|
+
const screenDistSq = pixX * pixX + pixY * pixY;
|
|
12777
|
+
if (screenDistSq < pickRadSq) {
|
|
12778
|
+
const dx = tx - rox, dy = ty - roy, dz = tz - roz;
|
|
12779
|
+
const camDistSq = dx * dx + dy * dy + dz * dz;
|
|
12780
|
+
if (camDistSq < bestCamDistSq * 0.98 || camDistSq < bestCamDistSq * 1.02 && screenDistSq < bestScreenDistSq) {
|
|
12781
|
+
bestCamDistSq = camDistSq;
|
|
12782
|
+
bestScreenDistSq = screenDistSq;
|
|
12783
|
+
bestIdx = i;
|
|
12784
|
+
}
|
|
12785
|
+
}
|
|
12786
|
+
}
|
|
12787
|
+
if (bestIdx < 0) return null;
|
|
12788
|
+
const hx = positions[bestIdx * 3], hy = positions[bestIdx * 3 + 1], hz = positions[bestIdx * 3 + 2];
|
|
12789
|
+
const hitPoint = [
|
|
12790
|
+
m0 * hx + m4 * hy + m8 * hz + m12,
|
|
12791
|
+
m1 * hx + m5 * hy + m9 * hz + m13,
|
|
12792
|
+
m2 * hx + m6 * hy + m10 * hz + m14
|
|
12793
|
+
];
|
|
12794
|
+
let normal;
|
|
12795
|
+
if (bestIdx === this.lastHitIdx) {
|
|
12796
|
+
normal = [...this.lastNormal];
|
|
12797
|
+
} else {
|
|
12798
|
+
normal = this.estimateNormal(positions, bestIdx, modelMatrix);
|
|
12799
|
+
this.lastHitIdx = bestIdx;
|
|
12800
|
+
this.lastNormal = [...normal];
|
|
12801
|
+
}
|
|
12802
|
+
const toCamX = rox - hitPoint[0], toCamY = roy - hitPoint[1], toCamZ = roz - hitPoint[2];
|
|
12803
|
+
if (normal[0] * toCamX + normal[1] * toCamY + normal[2] * toCamZ < 0) {
|
|
12804
|
+
normal[0] = -normal[0];
|
|
12805
|
+
normal[1] = -normal[1];
|
|
12806
|
+
normal[2] = -normal[2];
|
|
12807
|
+
}
|
|
12808
|
+
return { point: hitPoint, normal, distance: Math.sqrt(bestCamDistSq) };
|
|
12809
|
+
}
|
|
12810
|
+
// ============================================
|
|
12811
|
+
// 法线估计 (局部 PCA)
|
|
12812
|
+
// ============================================
|
|
12813
|
+
estimateNormal(positions, centerIdx, modelMatrix) {
|
|
12814
|
+
var _a2;
|
|
12815
|
+
const count = positions.length / 3;
|
|
12816
|
+
const m = modelMatrix;
|
|
12817
|
+
const cx = positions[centerIdx * 3 + 0];
|
|
12818
|
+
const cy = positions[centerIdx * 3 + 1];
|
|
12819
|
+
const cz = positions[centerIdx * 3 + 2];
|
|
12820
|
+
const wcx = m[0] * cx + m[4] * cy + m[8] * cz + m[12];
|
|
12821
|
+
const wcy = m[1] * cx + m[5] * cy + m[9] * cz + m[13];
|
|
12822
|
+
const wcz = m[2] * cx + m[6] * cy + m[10] * cz + m[14];
|
|
12823
|
+
const bbox = (_a2 = this.gsRenderer) == null ? void 0 : _a2.getBoundingBox();
|
|
12824
|
+
const searchRadius = bbox ? bbox.radius * 0.015 : 0.3;
|
|
12825
|
+
const searchRadiusSq = searchRadius * searchRadius;
|
|
12826
|
+
const neighbors = [];
|
|
12827
|
+
const maxNeighbors = 80;
|
|
12828
|
+
const step = count > 15e4 ? Math.ceil(count / 15e4) : 1;
|
|
12829
|
+
for (let i = 0; i < count; i += step) {
|
|
12830
|
+
const px = positions[i * 3 + 0];
|
|
12831
|
+
const py = positions[i * 3 + 1];
|
|
12832
|
+
const pz = positions[i * 3 + 2];
|
|
12833
|
+
const wpx = m[0] * px + m[4] * py + m[8] * pz + m[12];
|
|
12834
|
+
const wpy = m[1] * px + m[5] * py + m[9] * pz + m[13];
|
|
12835
|
+
const wpz = m[2] * px + m[6] * py + m[10] * pz + m[14];
|
|
12836
|
+
const dx = wpx - wcx;
|
|
12837
|
+
const dy = wpy - wcy;
|
|
12838
|
+
const dz = wpz - wcz;
|
|
12839
|
+
const distSq = dx * dx + dy * dy + dz * dz;
|
|
12840
|
+
if (distSq < searchRadiusSq && distSq > 1e-12) {
|
|
12841
|
+
neighbors.push({ pos: [wpx, wpy, wpz], distSq });
|
|
12842
|
+
}
|
|
12843
|
+
}
|
|
12844
|
+
if (neighbors.length < 6) {
|
|
12845
|
+
const bigRadiusSq = searchRadiusSq * 4;
|
|
12846
|
+
neighbors.length = 0;
|
|
12847
|
+
for (let i = 0; i < count; i += step) {
|
|
12848
|
+
const px = positions[i * 3 + 0];
|
|
12849
|
+
const py = positions[i * 3 + 1];
|
|
12850
|
+
const pz = positions[i * 3 + 2];
|
|
12851
|
+
const wpx = m[0] * px + m[4] * py + m[8] * pz + m[12];
|
|
12852
|
+
const wpy = m[1] * px + m[5] * py + m[9] * pz + m[13];
|
|
12853
|
+
const wpz = m[2] * px + m[6] * py + m[10] * pz + m[14];
|
|
12854
|
+
const dx = wpx - wcx;
|
|
12855
|
+
const dy = wpy - wcy;
|
|
12856
|
+
const dz = wpz - wcz;
|
|
12857
|
+
const distSq = dx * dx + dy * dy + dz * dz;
|
|
12858
|
+
if (distSq < bigRadiusSq && distSq > 1e-12) {
|
|
12859
|
+
neighbors.push({ pos: [wpx, wpy, wpz], distSq });
|
|
12860
|
+
}
|
|
12861
|
+
}
|
|
12862
|
+
}
|
|
12863
|
+
if (neighbors.length < 3) return [0, 1, 0];
|
|
12864
|
+
if (neighbors.length > maxNeighbors) {
|
|
12865
|
+
neighbors.sort((a, b) => a.distSq - b.distSq);
|
|
12866
|
+
neighbors.length = maxNeighbors;
|
|
12867
|
+
}
|
|
12868
|
+
const roughNormal = this.weightedPCA(neighbors, wcx, wcy, wcz);
|
|
12869
|
+
const [rnx, rny, rnz] = roughNormal;
|
|
12870
|
+
const projections = [];
|
|
12871
|
+
for (const nb of neighbors) {
|
|
12872
|
+
const d = (nb.pos[0] - wcx) * rnx + (nb.pos[1] - wcy) * rny + (nb.pos[2] - wcz) * rnz;
|
|
12873
|
+
projections.push(d);
|
|
12874
|
+
}
|
|
12875
|
+
const absDev = projections.map((d) => Math.abs(d));
|
|
12876
|
+
absDev.sort((a, b) => a - b);
|
|
12877
|
+
const mad = absDev[Math.floor(absDev.length / 2)] || 1e-8;
|
|
12878
|
+
const inlierThreshold = Math.max(mad * 3, searchRadius * 0.05);
|
|
12879
|
+
const inliers = neighbors.filter(
|
|
12880
|
+
(_, i) => Math.abs(projections[i]) < inlierThreshold
|
|
12881
|
+
);
|
|
12882
|
+
if (inliers.length < 3) return roughNormal;
|
|
12883
|
+
return this.weightedPCA(inliers, wcx, wcy, wcz);
|
|
12884
|
+
}
|
|
12885
|
+
/**
|
|
12886
|
+
* 距离加权 PCA:离中心越近的点权重越高
|
|
12887
|
+
*/
|
|
12888
|
+
weightedPCA(neighbors, cx, cy, cz) {
|
|
12889
|
+
let totalW = 0;
|
|
12890
|
+
let mx = 0, my = 0, mz = 0;
|
|
12891
|
+
for (const nb of neighbors) {
|
|
12892
|
+
const w = 1 / (nb.distSq + 1e-10);
|
|
12893
|
+
mx += nb.pos[0] * w;
|
|
12894
|
+
my += nb.pos[1] * w;
|
|
12895
|
+
mz += nb.pos[2] * w;
|
|
12896
|
+
totalW += w;
|
|
12897
|
+
}
|
|
12898
|
+
mx /= totalW;
|
|
12899
|
+
my /= totalW;
|
|
12900
|
+
mz /= totalW;
|
|
12901
|
+
let c00 = 0, c01 = 0, c02 = 0;
|
|
12902
|
+
let c11 = 0, c12 = 0, c22 = 0;
|
|
12903
|
+
for (const nb of neighbors) {
|
|
12904
|
+
const w = 1 / (nb.distSq + 1e-10);
|
|
12905
|
+
const dx = nb.pos[0] - mx;
|
|
12906
|
+
const dy = nb.pos[1] - my;
|
|
12907
|
+
const dz = nb.pos[2] - mz;
|
|
12908
|
+
c00 += w * dx * dx;
|
|
12909
|
+
c01 += w * dx * dy;
|
|
12910
|
+
c02 += w * dx * dz;
|
|
12911
|
+
c11 += w * dy * dy;
|
|
12912
|
+
c12 += w * dy * dz;
|
|
12913
|
+
c22 += w * dz * dz;
|
|
12914
|
+
}
|
|
12915
|
+
c00 /= totalW;
|
|
12916
|
+
c01 /= totalW;
|
|
12917
|
+
c02 /= totalW;
|
|
12918
|
+
c11 /= totalW;
|
|
12919
|
+
c12 /= totalW;
|
|
12920
|
+
c22 /= totalW;
|
|
12921
|
+
return this.smallestEigenvector(c00, c01, c02, c11, c12, c22);
|
|
12922
|
+
}
|
|
12923
|
+
/**
|
|
12924
|
+
* 求 3x3 对称矩阵最小特征值对应的特征向量(偏移幂迭代)
|
|
12925
|
+
*/
|
|
12926
|
+
smallestEigenvector(a00, a01, a02, a11, a12, a22) {
|
|
12927
|
+
let vx = 0.577, vy = 0.577, vz = 0.577;
|
|
12928
|
+
for (let iter = 0; iter < 30; iter++) {
|
|
12929
|
+
const nx = a00 * vx + a01 * vy + a02 * vz;
|
|
12930
|
+
const ny = a01 * vx + a11 * vy + a12 * vz;
|
|
12931
|
+
const nz = a02 * vx + a12 * vy + a22 * vz;
|
|
12932
|
+
const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
12933
|
+
if (len < 1e-12) return [0, 1, 0];
|
|
12934
|
+
vx = nx / len;
|
|
12935
|
+
vy = ny / len;
|
|
12936
|
+
vz = nz / len;
|
|
12937
|
+
}
|
|
12938
|
+
const lambdaMax = a00 * vx * vx + a11 * vy * vy + a22 * vz * vz + 2 * a01 * vx * vy + 2 * a02 * vx * vz + 2 * a12 * vy * vz;
|
|
12939
|
+
const b00 = lambdaMax - a00;
|
|
12940
|
+
const b01 = -a01;
|
|
12941
|
+
const b02 = -a02;
|
|
12942
|
+
const b11 = lambdaMax - a11;
|
|
12943
|
+
const b12 = -a12;
|
|
12944
|
+
const b22 = lambdaMax - a22;
|
|
12945
|
+
vx = 0.577;
|
|
12946
|
+
vy = 0.577;
|
|
12947
|
+
vz = 0.577;
|
|
12948
|
+
for (let iter = 0; iter < 30; iter++) {
|
|
12949
|
+
const nx = b00 * vx + b01 * vy + b02 * vz;
|
|
12950
|
+
const ny = b01 * vx + b11 * vy + b12 * vz;
|
|
12951
|
+
const nz = b02 * vx + b12 * vy + b22 * vz;
|
|
12952
|
+
const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
12953
|
+
if (len < 1e-12) return [0, 1, 0];
|
|
12954
|
+
vx = nx / len;
|
|
12955
|
+
vy = ny / len;
|
|
12956
|
+
vz = nz / len;
|
|
12957
|
+
}
|
|
12958
|
+
return [vx, vy, vz];
|
|
12959
|
+
}
|
|
12960
|
+
// ============================================
|
|
12961
|
+
// 圆圈指示器
|
|
12962
|
+
// ============================================
|
|
12963
|
+
createIndicatorMesh() {
|
|
12964
|
+
if (this.indicatorMesh) return;
|
|
12965
|
+
const device = this.renderer.device;
|
|
12966
|
+
const segments = 48;
|
|
12967
|
+
const outerRadius = this.indicatorBaseRadius;
|
|
12968
|
+
const innerRadius = outerRadius * 0.92;
|
|
12969
|
+
const vertexCount = segments * 2;
|
|
12970
|
+
const vertexData = new Float32Array(vertexCount * 6);
|
|
12971
|
+
for (let i = 0; i < segments; i++) {
|
|
12972
|
+
const angle = i / segments * Math.PI * 2;
|
|
12973
|
+
const cos = Math.cos(angle);
|
|
12974
|
+
const sin = Math.sin(angle);
|
|
12975
|
+
const oi = i * 2 * 6;
|
|
12976
|
+
vertexData[oi] = cos * outerRadius;
|
|
12977
|
+
vertexData[oi + 1] = sin * outerRadius;
|
|
12978
|
+
vertexData[oi + 2] = 0;
|
|
12979
|
+
vertexData[oi + 3] = 0;
|
|
12980
|
+
vertexData[oi + 4] = 0;
|
|
12981
|
+
vertexData[oi + 5] = 1;
|
|
12982
|
+
const ii = (i * 2 + 1) * 6;
|
|
12983
|
+
vertexData[ii] = cos * innerRadius;
|
|
12984
|
+
vertexData[ii + 1] = sin * innerRadius;
|
|
12985
|
+
vertexData[ii + 2] = 0;
|
|
12986
|
+
vertexData[ii + 3] = 0;
|
|
12987
|
+
vertexData[ii + 4] = 0;
|
|
12988
|
+
vertexData[ii + 5] = 1;
|
|
12989
|
+
}
|
|
12990
|
+
const indices = [];
|
|
12991
|
+
for (let i = 0; i < segments; i++) {
|
|
12992
|
+
const o0 = i * 2, i0 = i * 2 + 1;
|
|
12993
|
+
const o1 = (i + 1) % segments * 2, i1 = (i + 1) % segments * 2 + 1;
|
|
12994
|
+
indices.push(o0, o1, i0);
|
|
12995
|
+
indices.push(i0, o1, i1);
|
|
12996
|
+
}
|
|
12997
|
+
const vertexBuffer = device.createBuffer({
|
|
12998
|
+
size: vertexData.byteLength,
|
|
12999
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
|
|
13000
|
+
});
|
|
13001
|
+
device.queue.writeBuffer(vertexBuffer, 0, vertexData);
|
|
13002
|
+
const indexData = new Uint16Array(indices);
|
|
13003
|
+
const alignedSize = Math.ceil(indexData.byteLength / 4) * 4;
|
|
13004
|
+
const alignedBuf = new Uint8Array(alignedSize);
|
|
13005
|
+
alignedBuf.set(
|
|
13006
|
+
new Uint8Array(
|
|
13007
|
+
indexData.buffer,
|
|
13008
|
+
indexData.byteOffset,
|
|
13009
|
+
indexData.byteLength
|
|
13010
|
+
)
|
|
13011
|
+
);
|
|
13012
|
+
const indexBuffer = device.createBuffer({
|
|
13013
|
+
size: alignedSize,
|
|
13014
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST
|
|
13015
|
+
});
|
|
13016
|
+
device.queue.writeBuffer(indexBuffer, 0, alignedBuf);
|
|
13017
|
+
const bbox = {
|
|
13018
|
+
min: [-outerRadius, -outerRadius, 0],
|
|
13019
|
+
max: [outerRadius, outerRadius, 0],
|
|
13020
|
+
center: [0, 0, 0],
|
|
13021
|
+
radius: outerRadius
|
|
13022
|
+
};
|
|
13023
|
+
this.indicatorMesh = new Mesh(
|
|
13024
|
+
vertexBuffer,
|
|
13025
|
+
vertexCount,
|
|
13026
|
+
indexBuffer,
|
|
13027
|
+
indices.length,
|
|
13028
|
+
bbox
|
|
13029
|
+
);
|
|
13030
|
+
this.indicatorMesh.hasUV = false;
|
|
13031
|
+
this.indicatorMesh.indexFormat = "uint16";
|
|
13032
|
+
}
|
|
13033
|
+
showIndicator() {
|
|
13034
|
+
if (!this.indicatorMesh || this.indicatorAdded) return;
|
|
13035
|
+
const material = {
|
|
13036
|
+
baseColorFactor: [1, 1, 1, 0.85],
|
|
13037
|
+
baseColorTexture: null,
|
|
13038
|
+
metallicFactor: 0,
|
|
13039
|
+
roughnessFactor: 1,
|
|
13040
|
+
doubleSided: true
|
|
13041
|
+
};
|
|
13042
|
+
this.meshRenderer.addOverlayMesh(this.indicatorMesh, material);
|
|
13043
|
+
this.indicatorAdded = true;
|
|
13044
|
+
}
|
|
13045
|
+
hideIndicator() {
|
|
13046
|
+
if (!this.indicatorMesh || !this.indicatorAdded) return;
|
|
13047
|
+
this.meshRenderer.detachOverlayMesh(this.indicatorMesh);
|
|
13048
|
+
this.indicatorAdded = false;
|
|
13049
|
+
}
|
|
13050
|
+
updateIndicator(point, normal) {
|
|
13051
|
+
if (!this.indicatorMesh) return;
|
|
13052
|
+
const camPos = this.camera.position;
|
|
13053
|
+
const dist = Math.sqrt(
|
|
13054
|
+
(point[0] - camPos[0]) ** 2 + (point[1] - camPos[1]) ** 2 + (point[2] - camPos[2]) ** 2
|
|
13055
|
+
);
|
|
13056
|
+
const targetScreenPx = 40;
|
|
13057
|
+
const halfVFov = this.camera.fov / 2;
|
|
13058
|
+
const halfHFov = Math.atan(Math.tan(halfVFov) * this.camera.aspect);
|
|
13059
|
+
const visualRadius = dist * Math.tan(halfHFov) * (targetScreenPx / this.canvas.clientWidth);
|
|
13060
|
+
const [nx, ny, nz] = normal;
|
|
13061
|
+
const up = Math.abs(ny) < 0.99 ? [0, 1, 0] : [1, 0, 0];
|
|
13062
|
+
let rx = up[1] * nz - up[2] * ny;
|
|
13063
|
+
let ry = up[2] * nx - up[0] * nz;
|
|
13064
|
+
let rz = up[0] * ny - up[1] * nx;
|
|
13065
|
+
let rLen = Math.sqrt(rx * rx + ry * ry + rz * rz);
|
|
13066
|
+
if (rLen < 1e-8) {
|
|
13067
|
+
rx = 1;
|
|
13068
|
+
ry = 0;
|
|
13069
|
+
rz = 0;
|
|
13070
|
+
rLen = 1;
|
|
13071
|
+
}
|
|
13072
|
+
rx /= rLen;
|
|
13073
|
+
ry /= rLen;
|
|
13074
|
+
rz /= rLen;
|
|
13075
|
+
let fx = ny * rz - nz * ry;
|
|
13076
|
+
let fy = nz * rx - nx * rz;
|
|
13077
|
+
let fz = nx * ry - ny * rx;
|
|
13078
|
+
let fLen = Math.sqrt(fx * fx + fy * fy + fz * fz);
|
|
13079
|
+
if (fLen < 1e-8) {
|
|
13080
|
+
fx = 0;
|
|
13081
|
+
fy = 0;
|
|
13082
|
+
fz = 1;
|
|
13083
|
+
fLen = 1;
|
|
13084
|
+
}
|
|
13085
|
+
fx /= fLen;
|
|
13086
|
+
fy /= fLen;
|
|
13087
|
+
fz /= fLen;
|
|
13088
|
+
const scale = visualRadius / this.indicatorBaseRadius;
|
|
13089
|
+
const m = this.indicatorMesh.modelMatrix;
|
|
13090
|
+
m[0] = rx * scale;
|
|
13091
|
+
m[1] = ry * scale;
|
|
13092
|
+
m[2] = rz * scale;
|
|
13093
|
+
m[3] = 0;
|
|
13094
|
+
m[4] = fx * scale;
|
|
13095
|
+
m[5] = fy * scale;
|
|
13096
|
+
m[6] = fz * scale;
|
|
13097
|
+
m[7] = 0;
|
|
13098
|
+
m[8] = nx * scale;
|
|
13099
|
+
m[9] = ny * scale;
|
|
13100
|
+
m[10] = nz * scale;
|
|
13101
|
+
m[11] = 0;
|
|
13102
|
+
const normalOffset = dist * 0.02;
|
|
13103
|
+
m[12] = point[0] + nx * normalOffset;
|
|
13104
|
+
m[13] = point[1] + ny * normalOffset;
|
|
13105
|
+
m[14] = point[2] + nz * normalOffset;
|
|
13106
|
+
m[15] = 1;
|
|
13107
|
+
this.indicatorTransform = {
|
|
13108
|
+
point: [...point],
|
|
13109
|
+
normal: [nx, ny, nz],
|
|
13110
|
+
right: [rx, ry, rz],
|
|
13111
|
+
forward: [fx, fy, fz],
|
|
13112
|
+
visualDiameter: visualRadius * 2,
|
|
13113
|
+
normalOffset
|
|
13114
|
+
};
|
|
13115
|
+
}
|
|
13116
|
+
// ============================================
|
|
13117
|
+
// OBJ 热点放置
|
|
13118
|
+
// ============================================
|
|
13119
|
+
async placeHotspot(point, normal) {
|
|
13120
|
+
var _a2;
|
|
13121
|
+
if (!this.hotspotOBJUrl) {
|
|
13122
|
+
console.warn("HotspotManager: 未设置热点 OBJ 模型 URL");
|
|
13123
|
+
return;
|
|
13124
|
+
}
|
|
13125
|
+
const snap = this.indicatorTransform;
|
|
13126
|
+
if (!snap) {
|
|
13127
|
+
console.warn("HotspotManager: 无指示器状态,跳过放置");
|
|
13128
|
+
return;
|
|
13129
|
+
}
|
|
13130
|
+
this.hideIndicator();
|
|
13131
|
+
try {
|
|
13132
|
+
const loadedMeshes = await this.objLoader.load(this.hotspotOBJUrl);
|
|
13133
|
+
if (loadedMeshes.length === 0) {
|
|
13134
|
+
console.warn("HotspotManager: OBJ 加载无网格");
|
|
13135
|
+
return;
|
|
13136
|
+
}
|
|
13137
|
+
const meshStartIndex = this.meshRenderer.getMeshCount();
|
|
13138
|
+
let placedScale = 1;
|
|
13139
|
+
let placedLocalCenter = [0, 0, 0];
|
|
13140
|
+
const firstBbox = loadedMeshes[0].mesh.getLocalBoundingBox();
|
|
13141
|
+
if (firstBbox) {
|
|
13142
|
+
const modelW = firstBbox.max[0] - firstBbox.min[0];
|
|
13143
|
+
const modelH = firstBbox.max[1] - firstBbox.min[1];
|
|
13144
|
+
const modelD = firstBbox.max[2] - firstBbox.min[2];
|
|
13145
|
+
const modelMaxDim = Math.max(modelW, modelH, modelD, 1e-6);
|
|
13146
|
+
placedScale = snap.visualDiameter / modelMaxDim;
|
|
13147
|
+
placedLocalCenter = [
|
|
13148
|
+
(firstBbox.min[0] + firstBbox.max[0]) / 2,
|
|
13149
|
+
(firstBbox.min[1] + firstBbox.max[1]) / 2,
|
|
13150
|
+
(firstBbox.min[2] + firstBbox.max[2]) / 2
|
|
13151
|
+
];
|
|
13152
|
+
}
|
|
13153
|
+
for (const { mesh, material } of loadedMeshes) {
|
|
13154
|
+
this.applyIndicatorTransform(mesh, snap);
|
|
13155
|
+
this.meshRenderer.addOverlayMesh(mesh, material);
|
|
13156
|
+
}
|
|
13157
|
+
const info = {
|
|
13158
|
+
position: [...snap.point],
|
|
13159
|
+
normal: [...snap.normal],
|
|
13160
|
+
meshStartIndex,
|
|
13161
|
+
meshCount: loadedMeshes.length,
|
|
13162
|
+
billboard: false,
|
|
13163
|
+
placedScale,
|
|
13164
|
+
placedNormalOffset: snap.normalOffset,
|
|
13165
|
+
placedLocalCenter
|
|
13166
|
+
};
|
|
13167
|
+
this.hotspots.push(info);
|
|
13168
|
+
(_a2 = this.onHotspotPlaced) == null ? void 0 : _a2.call(this, info);
|
|
13169
|
+
console.log(
|
|
13170
|
+
`热点已放置: pos=(${snap.point[0].toFixed(3)}, ${snap.point[1].toFixed(3)}, ${snap.point[2].toFixed(3)}) normal=(${snap.normal[0].toFixed(3)}, ${snap.normal[1].toFixed(3)}, ${snap.normal[2].toFixed(3)})`
|
|
13171
|
+
);
|
|
13172
|
+
} catch (error) {
|
|
13173
|
+
console.error("HotspotManager: 放置热点失败:", error);
|
|
13174
|
+
}
|
|
13175
|
+
}
|
|
13176
|
+
/**
|
|
13177
|
+
* 将 OBJ mesh 的变换设置为与指示器完全一致的大小、位置、朝向。
|
|
13178
|
+
*
|
|
13179
|
+
* 使用指示器缓存的基向量 (right, forward, normal) 和 visualDiameter,
|
|
13180
|
+
* 将 OBJ 缩放到与指示器圆圈等大,居中放置在相同位置。
|
|
13181
|
+
*/
|
|
13182
|
+
applyIndicatorTransform(mesh, snap) {
|
|
13183
|
+
const { point, normal, right, forward, visualDiameter, normalOffset } = snap;
|
|
13184
|
+
const [nx, ny, nz] = normal;
|
|
13185
|
+
const [rx, ry, rz] = right;
|
|
13186
|
+
const [fx, fy, fz] = forward;
|
|
13187
|
+
const bbox = mesh.getLocalBoundingBox();
|
|
13188
|
+
let objScale = 1;
|
|
13189
|
+
if (bbox) {
|
|
13190
|
+
const modelW = bbox.max[0] - bbox.min[0];
|
|
13191
|
+
const modelH = bbox.max[1] - bbox.min[1];
|
|
13192
|
+
const modelD = bbox.max[2] - bbox.min[2];
|
|
13193
|
+
const modelMaxDim = Math.max(modelW, modelH, modelD, 1e-6);
|
|
13194
|
+
objScale = visualDiameter / modelMaxDim;
|
|
13195
|
+
}
|
|
13196
|
+
let lcx = 0, lcy = 0, lcz = 0;
|
|
13197
|
+
if (bbox) {
|
|
13198
|
+
lcx = (bbox.min[0] + bbox.max[0]) / 2;
|
|
13199
|
+
lcy = (bbox.min[1] + bbox.max[1]) / 2;
|
|
13200
|
+
lcz = (bbox.min[2] + bbox.max[2]) / 2;
|
|
13201
|
+
}
|
|
13202
|
+
const owx = (rx * lcx + fx * lcy + nx * lcz) * objScale;
|
|
13203
|
+
const owy = (ry * lcx + fy * lcy + ny * lcz) * objScale;
|
|
13204
|
+
const owz = (rz * lcx + fz * lcy + nz * lcz) * objScale;
|
|
13205
|
+
const m = mesh.modelMatrix;
|
|
13206
|
+
m[0] = rx * objScale;
|
|
13207
|
+
m[1] = ry * objScale;
|
|
13208
|
+
m[2] = rz * objScale;
|
|
13209
|
+
m[3] = 0;
|
|
13210
|
+
m[4] = fx * objScale;
|
|
13211
|
+
m[5] = fy * objScale;
|
|
13212
|
+
m[6] = fz * objScale;
|
|
13213
|
+
m[7] = 0;
|
|
13214
|
+
m[8] = nx * objScale;
|
|
13215
|
+
m[9] = ny * objScale;
|
|
13216
|
+
m[10] = nz * objScale;
|
|
13217
|
+
m[11] = 0;
|
|
13218
|
+
m[12] = point[0] + nx * normalOffset - owx;
|
|
13219
|
+
m[13] = point[1] + ny * normalOffset - owy;
|
|
13220
|
+
m[14] = point[2] + nz * normalOffset - owz;
|
|
13221
|
+
m[15] = 1;
|
|
13222
|
+
this.decomposeModelMatrix(mesh);
|
|
13223
|
+
}
|
|
13224
|
+
/**
|
|
13225
|
+
* 从 modelMatrix 反向提取 position / rotation / scale,
|
|
13226
|
+
* 使 Mesh 的分离属性与矩阵保持同步(Gizmo 需要)。
|
|
13227
|
+
*/
|
|
13228
|
+
decomposeModelMatrix(mesh) {
|
|
13229
|
+
const m = mesh.modelMatrix;
|
|
13230
|
+
mesh.position[0] = m[12];
|
|
13231
|
+
mesh.position[1] = m[13];
|
|
13232
|
+
mesh.position[2] = m[14];
|
|
13233
|
+
const sx = Math.sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]);
|
|
13234
|
+
const sy = Math.sqrt(m[4] * m[4] + m[5] * m[5] + m[6] * m[6]);
|
|
13235
|
+
const sz = Math.sqrt(m[8] * m[8] + m[9] * m[9] + m[10] * m[10]);
|
|
13236
|
+
mesh.scale[0] = sx;
|
|
13237
|
+
mesh.scale[1] = sy;
|
|
13238
|
+
mesh.scale[2] = sz;
|
|
13239
|
+
const isx = sx > 1e-8 ? 1 / sx : 0;
|
|
13240
|
+
const isy = sy > 1e-8 ? 1 / sy : 0;
|
|
13241
|
+
const isz = sz > 1e-8 ? 1 / sz : 0;
|
|
13242
|
+
const r00 = m[0] * isx, r10 = m[1] * isx, r20 = m[2] * isx;
|
|
13243
|
+
const r21 = m[6] * isy;
|
|
13244
|
+
const r22 = m[10] * isz;
|
|
13245
|
+
const sinRy = -r20;
|
|
13246
|
+
const ry = Math.asin(Math.max(-1, Math.min(1, sinRy)));
|
|
13247
|
+
const cosRy = Math.cos(ry);
|
|
13248
|
+
let rotX, rotZ;
|
|
13249
|
+
if (Math.abs(cosRy) > 1e-6) {
|
|
13250
|
+
rotX = Math.atan2(r21, r22);
|
|
13251
|
+
rotZ = Math.atan2(r10, r00);
|
|
13252
|
+
} else {
|
|
13253
|
+
rotX = Math.atan2(-m[9] * isz, m[5] * isy);
|
|
13254
|
+
rotZ = 0;
|
|
13255
|
+
}
|
|
13256
|
+
mesh.rotation[0] = rotX;
|
|
13257
|
+
mesh.rotation[1] = ry;
|
|
13258
|
+
mesh.rotation[2] = rotZ;
|
|
13259
|
+
}
|
|
13260
|
+
// ============================================
|
|
13261
|
+
// Billboard 控制
|
|
13262
|
+
// ============================================
|
|
13263
|
+
/**
|
|
13264
|
+
* 设置指定热点的 billboard 模式
|
|
13265
|
+
* @param hotspotIndex 热点索引(在 hotspots 数组中的位置)
|
|
13266
|
+
* @param enabled 是否启用 billboard
|
|
13267
|
+
*/
|
|
13268
|
+
setHotspotBillboard(hotspotIndex, enabled) {
|
|
13269
|
+
if (hotspotIndex < 0 || hotspotIndex >= this.hotspots.length) return false;
|
|
13270
|
+
const info = this.hotspots[hotspotIndex];
|
|
13271
|
+
if (info.billboard === enabled) return true;
|
|
13272
|
+
info.billboard = enabled;
|
|
13273
|
+
if (!enabled) {
|
|
13274
|
+
this.restoreHotspotOrientation(info);
|
|
13275
|
+
}
|
|
13276
|
+
return true;
|
|
13277
|
+
}
|
|
13278
|
+
getHotspotBillboard(hotspotIndex) {
|
|
13279
|
+
if (hotspotIndex < 0 || hotspotIndex >= this.hotspots.length) return false;
|
|
13280
|
+
return this.hotspots[hotspotIndex].billboard;
|
|
13281
|
+
}
|
|
13282
|
+
getHotspotCount() {
|
|
13283
|
+
return this.hotspots.length;
|
|
13284
|
+
}
|
|
13285
|
+
/**
|
|
13286
|
+
* 通过 overlay mesh 起始索引找到对应的热点索引
|
|
13287
|
+
*/
|
|
13288
|
+
findHotspotIndexByMeshStart(overlayMeshStartIndex) {
|
|
13289
|
+
return this.hotspots.findIndex(
|
|
13290
|
+
(h) => h.meshStartIndex === overlayMeshStartIndex
|
|
13291
|
+
);
|
|
13292
|
+
}
|
|
13293
|
+
/**
|
|
13294
|
+
* 每帧调用:更新所有 billboard 热点朝向相机
|
|
13295
|
+
*/
|
|
13296
|
+
updateBillboards() {
|
|
13297
|
+
const camPos = this.camera.position;
|
|
13298
|
+
for (const info of this.hotspots) {
|
|
13299
|
+
if (!info.billboard) continue;
|
|
13300
|
+
const {
|
|
13301
|
+
position,
|
|
13302
|
+
normal,
|
|
13303
|
+
placedScale,
|
|
13304
|
+
placedNormalOffset,
|
|
13305
|
+
placedLocalCenter
|
|
13306
|
+
} = info;
|
|
13307
|
+
const [nx, ny, nz] = normal;
|
|
13308
|
+
const wx = position[0] + nx * placedNormalOffset;
|
|
13309
|
+
const wy = position[1] + ny * placedNormalOffset;
|
|
13310
|
+
const wz = position[2] + nz * placedNormalOffset;
|
|
13311
|
+
let zx = camPos[0] - wx;
|
|
13312
|
+
let zy = camPos[1] - wy;
|
|
13313
|
+
let zz = camPos[2] - wz;
|
|
13314
|
+
const zLen = Math.sqrt(zx * zx + zy * zy + zz * zz);
|
|
13315
|
+
if (zLen < 1e-8) continue;
|
|
13316
|
+
zx /= zLen;
|
|
13317
|
+
zy /= zLen;
|
|
13318
|
+
zz /= zLen;
|
|
13319
|
+
const up = Math.abs(zy) < 0.99 ? [0, 1, 0] : [1, 0, 0];
|
|
13320
|
+
let rx = up[1] * zz - up[2] * zy;
|
|
13321
|
+
let ry = up[2] * zx - up[0] * zz;
|
|
13322
|
+
let rz = up[0] * zy - up[1] * zx;
|
|
13323
|
+
let rLen = Math.sqrt(rx * rx + ry * ry + rz * rz);
|
|
13324
|
+
if (rLen < 1e-8) {
|
|
13325
|
+
rx = 1;
|
|
13326
|
+
ry = 0;
|
|
13327
|
+
rz = 0;
|
|
13328
|
+
rLen = 1;
|
|
13329
|
+
}
|
|
13330
|
+
rx /= rLen;
|
|
13331
|
+
ry /= rLen;
|
|
13332
|
+
rz /= rLen;
|
|
13333
|
+
let fx = zy * rz - zz * ry;
|
|
13334
|
+
let fy = zz * rx - zx * rz;
|
|
13335
|
+
let fz = zx * ry - zy * rx;
|
|
13336
|
+
let fLen = Math.sqrt(fx * fx + fy * fy + fz * fz);
|
|
13337
|
+
if (fLen < 1e-8) {
|
|
13338
|
+
fx = 0;
|
|
13339
|
+
fy = 1;
|
|
13340
|
+
fz = 0;
|
|
13341
|
+
fLen = 1;
|
|
13342
|
+
}
|
|
13343
|
+
fx /= fLen;
|
|
13344
|
+
fy /= fLen;
|
|
13345
|
+
fz /= fLen;
|
|
13346
|
+
const s = placedScale;
|
|
13347
|
+
const [lcx, lcy, lcz] = placedLocalCenter;
|
|
13348
|
+
const owx = (rx * lcx + fx * lcy + zx * lcz) * s;
|
|
13349
|
+
const owy = (ry * lcx + fy * lcy + zy * lcz) * s;
|
|
13350
|
+
const owz = (rz * lcx + fz * lcy + zz * lcz) * s;
|
|
13351
|
+
for (let i = 0; i < info.meshCount; i++) {
|
|
13352
|
+
const mesh = this.meshRenderer.getOverlayMeshByIndex(
|
|
13353
|
+
info.meshStartIndex + i
|
|
13354
|
+
);
|
|
13355
|
+
if (!mesh) continue;
|
|
13356
|
+
const m = mesh.modelMatrix;
|
|
13357
|
+
m[0] = rx * s;
|
|
13358
|
+
m[1] = ry * s;
|
|
13359
|
+
m[2] = rz * s;
|
|
13360
|
+
m[3] = 0;
|
|
13361
|
+
m[4] = fx * s;
|
|
13362
|
+
m[5] = fy * s;
|
|
13363
|
+
m[6] = fz * s;
|
|
13364
|
+
m[7] = 0;
|
|
13365
|
+
m[8] = zx * s;
|
|
13366
|
+
m[9] = zy * s;
|
|
13367
|
+
m[10] = zz * s;
|
|
13368
|
+
m[11] = 0;
|
|
13369
|
+
m[12] = wx - owx;
|
|
13370
|
+
m[13] = wy - owy;
|
|
13371
|
+
m[14] = wz - owz;
|
|
13372
|
+
m[15] = 1;
|
|
13373
|
+
this.decomposeModelMatrix(mesh);
|
|
13374
|
+
}
|
|
13375
|
+
}
|
|
13376
|
+
}
|
|
13377
|
+
/**
|
|
13378
|
+
* 关闭 billboard 时恢复到放置时的法线朝向
|
|
13379
|
+
*/
|
|
13380
|
+
restoreHotspotOrientation(info) {
|
|
13381
|
+
const {
|
|
13382
|
+
position,
|
|
13383
|
+
normal,
|
|
13384
|
+
placedScale,
|
|
13385
|
+
placedNormalOffset,
|
|
13386
|
+
placedLocalCenter
|
|
13387
|
+
} = info;
|
|
13388
|
+
const [nx, ny, nz] = normal;
|
|
13389
|
+
const up = Math.abs(ny) < 0.99 ? [0, 1, 0] : [1, 0, 0];
|
|
13390
|
+
let rx = up[1] * nz - up[2] * ny;
|
|
13391
|
+
let ry = up[2] * nx - up[0] * nz;
|
|
13392
|
+
let rz = up[0] * ny - up[1] * nx;
|
|
13393
|
+
let rLen = Math.sqrt(rx * rx + ry * ry + rz * rz);
|
|
13394
|
+
if (rLen < 1e-8) {
|
|
13395
|
+
rx = 1;
|
|
13396
|
+
ry = 0;
|
|
13397
|
+
rz = 0;
|
|
13398
|
+
rLen = 1;
|
|
13399
|
+
}
|
|
13400
|
+
rx /= rLen;
|
|
13401
|
+
ry /= rLen;
|
|
13402
|
+
rz /= rLen;
|
|
13403
|
+
let fx = ny * rz - nz * ry;
|
|
13404
|
+
let fy = nz * rx - nx * rz;
|
|
13405
|
+
let fz = nx * ry - ny * rx;
|
|
13406
|
+
let fLen = Math.sqrt(fx * fx + fy * fy + fz * fz);
|
|
13407
|
+
if (fLen < 1e-8) {
|
|
13408
|
+
fx = 0;
|
|
13409
|
+
fy = 0;
|
|
13410
|
+
fz = 1;
|
|
13411
|
+
fLen = 1;
|
|
13412
|
+
}
|
|
13413
|
+
fx /= fLen;
|
|
13414
|
+
fy /= fLen;
|
|
13415
|
+
fz /= fLen;
|
|
13416
|
+
const s = placedScale;
|
|
13417
|
+
const [lcx, lcy, lcz] = placedLocalCenter;
|
|
13418
|
+
const owx = (rx * lcx + fx * lcy + nx * lcz) * s;
|
|
13419
|
+
const owy = (ry * lcx + fy * lcy + ny * lcz) * s;
|
|
13420
|
+
const owz = (rz * lcx + fz * lcy + nz * lcz) * s;
|
|
13421
|
+
const wx = position[0] + nx * placedNormalOffset;
|
|
13422
|
+
const wy = position[1] + ny * placedNormalOffset;
|
|
13423
|
+
const wz = position[2] + nz * placedNormalOffset;
|
|
13424
|
+
for (let i = 0; i < info.meshCount; i++) {
|
|
13425
|
+
const mesh = this.meshRenderer.getOverlayMeshByIndex(
|
|
13426
|
+
info.meshStartIndex + i
|
|
13427
|
+
);
|
|
13428
|
+
if (!mesh) continue;
|
|
13429
|
+
const m = mesh.modelMatrix;
|
|
13430
|
+
m[0] = rx * s;
|
|
13431
|
+
m[1] = ry * s;
|
|
13432
|
+
m[2] = rz * s;
|
|
13433
|
+
m[3] = 0;
|
|
13434
|
+
m[4] = fx * s;
|
|
13435
|
+
m[5] = fy * s;
|
|
13436
|
+
m[6] = fz * s;
|
|
13437
|
+
m[7] = 0;
|
|
13438
|
+
m[8] = nx * s;
|
|
13439
|
+
m[9] = ny * s;
|
|
13440
|
+
m[10] = nz * s;
|
|
13441
|
+
m[11] = 0;
|
|
13442
|
+
m[12] = wx - owx;
|
|
13443
|
+
m[13] = wy - owy;
|
|
13444
|
+
m[14] = wz - owz;
|
|
13445
|
+
m[15] = 1;
|
|
13446
|
+
this.decomposeModelMatrix(mesh);
|
|
13447
|
+
}
|
|
13448
|
+
}
|
|
13449
|
+
// ============================================
|
|
13450
|
+
// 生命周期
|
|
13451
|
+
// ============================================
|
|
13452
|
+
destroy() {
|
|
13453
|
+
this.exit();
|
|
13454
|
+
if (this.indicatorMesh && !this.indicatorAdded) {
|
|
13455
|
+
this.indicatorMesh.destroy();
|
|
13456
|
+
}
|
|
13457
|
+
this.indicatorMesh = null;
|
|
13458
|
+
this.hotspots = [];
|
|
13459
|
+
}
|
|
13460
|
+
}
|
|
10976
13461
|
class App {
|
|
10977
13462
|
constructor(canvas) {
|
|
10978
13463
|
__publicField(this, "canvas");
|
|
@@ -10985,6 +13470,7 @@ class App {
|
|
|
10985
13470
|
// 子系统管理器
|
|
10986
13471
|
__publicField(this, "sceneManager");
|
|
10987
13472
|
__publicField(this, "gizmoManager");
|
|
13473
|
+
__publicField(this, "hotspotManager");
|
|
10988
13474
|
__publicField(this, "isRunning", false);
|
|
10989
13475
|
__publicField(this, "animationId", 0);
|
|
10990
13476
|
// 是否使用移动端渲染器
|
|
@@ -11013,6 +13499,13 @@ class App {
|
|
|
11013
13499
|
this.canvas,
|
|
11014
13500
|
this.controls
|
|
11015
13501
|
);
|
|
13502
|
+
this.hotspotManager = new HotspotManager(
|
|
13503
|
+
this.renderer,
|
|
13504
|
+
this.camera,
|
|
13505
|
+
this.canvas,
|
|
13506
|
+
this.controls,
|
|
13507
|
+
this.meshRenderer
|
|
13508
|
+
);
|
|
11016
13509
|
window.addEventListener("resize", this.boundOnResize);
|
|
11017
13510
|
}
|
|
11018
13511
|
// ============================================
|
|
@@ -11086,6 +13579,7 @@ class App {
|
|
|
11086
13579
|
gsRenderer.setCompactData(compactData);
|
|
11087
13580
|
if (onProgress) onProgress(100, "upload");
|
|
11088
13581
|
this.sceneManager.setGSRenderer(gsRenderer);
|
|
13582
|
+
this.hotspotManager.setGSRenderer(gsRenderer);
|
|
11089
13583
|
return compactData.count;
|
|
11090
13584
|
} else {
|
|
11091
13585
|
gsRenderer = new GSSplatRenderer(this.renderer, this.camera);
|
|
@@ -11099,6 +13593,7 @@ class App {
|
|
|
11099
13593
|
gsRenderer.setCompactData(compactData);
|
|
11100
13594
|
if (onProgress) onProgress(100, "upload");
|
|
11101
13595
|
this.sceneManager.setGSRenderer(gsRenderer);
|
|
13596
|
+
this.hotspotManager.setGSRenderer(gsRenderer);
|
|
11102
13597
|
return compactData.count;
|
|
11103
13598
|
}
|
|
11104
13599
|
} catch (error) {
|
|
@@ -11130,6 +13625,7 @@ class App {
|
|
|
11130
13625
|
const gsRenderer = new GSSplatRenderer(this.renderer, this.camera);
|
|
11131
13626
|
gsRenderer.setData(splats);
|
|
11132
13627
|
this.sceneManager.setGSRenderer(gsRenderer);
|
|
13628
|
+
this.hotspotManager.setGSRenderer(gsRenderer);
|
|
11133
13629
|
this.useMobileRenderer = false;
|
|
11134
13630
|
if (onProgress) onProgress(100, "upload");
|
|
11135
13631
|
return splats.length;
|
|
@@ -11137,6 +13633,49 @@ class App {
|
|
|
11137
13633
|
throw error;
|
|
11138
13634
|
}
|
|
11139
13635
|
}
|
|
13636
|
+
/**
|
|
13637
|
+
* 加载 SOG 文件 (Spatially Ordered Gaussians)
|
|
13638
|
+
*/
|
|
13639
|
+
async addSOG(urlOrBuffer, onProgress, isLocalFile = false) {
|
|
13640
|
+
try {
|
|
13641
|
+
const isMobile = isMobileDevice();
|
|
13642
|
+
let buffer;
|
|
13643
|
+
if (typeof urlOrBuffer === "string") {
|
|
13644
|
+
buffer = await this.fetchWithProgress(urlOrBuffer, (downloadProgress) => {
|
|
13645
|
+
if (onProgress) {
|
|
13646
|
+
onProgress(downloadProgress * 0.5, "download");
|
|
13647
|
+
}
|
|
13648
|
+
});
|
|
13649
|
+
} else {
|
|
13650
|
+
buffer = urlOrBuffer;
|
|
13651
|
+
if (onProgress && isLocalFile) {
|
|
13652
|
+
onProgress(50, "download");
|
|
13653
|
+
}
|
|
13654
|
+
}
|
|
13655
|
+
if (onProgress) onProgress(50, "parse");
|
|
13656
|
+
const compactData = await deserializeSOG(buffer, (p, stage) => {
|
|
13657
|
+
if (onProgress) {
|
|
13658
|
+
onProgress(50 + p / 100 * 40, stage);
|
|
13659
|
+
}
|
|
13660
|
+
});
|
|
13661
|
+
if (onProgress) onProgress(90, "upload");
|
|
13662
|
+
let gsRenderer;
|
|
13663
|
+
if (isMobile) {
|
|
13664
|
+
gsRenderer = new GSSplatRendererMobile(this.renderer, this.camera);
|
|
13665
|
+
this.useMobileRenderer = true;
|
|
13666
|
+
} else {
|
|
13667
|
+
gsRenderer = new GSSplatRenderer(this.renderer, this.camera);
|
|
13668
|
+
this.useMobileRenderer = false;
|
|
13669
|
+
}
|
|
13670
|
+
gsRenderer.setCompactData(compactData);
|
|
13671
|
+
this.sceneManager.setGSRenderer(gsRenderer);
|
|
13672
|
+
this.hotspotManager.setGSRenderer(gsRenderer);
|
|
13673
|
+
if (onProgress) onProgress(100, "upload");
|
|
13674
|
+
return compactData.count;
|
|
13675
|
+
} catch (error) {
|
|
13676
|
+
throw error;
|
|
13677
|
+
}
|
|
13678
|
+
}
|
|
11140
13679
|
/**
|
|
11141
13680
|
* 添加测试立方体
|
|
11142
13681
|
*/
|
|
@@ -11179,7 +13718,8 @@ class App {
|
|
|
11179
13718
|
}
|
|
11180
13719
|
render() {
|
|
11181
13720
|
this.camera.setAspect(this.renderer.getAspectRatio());
|
|
11182
|
-
this.
|
|
13721
|
+
this.controls.update();
|
|
13722
|
+
this.hotspotManager.updateBillboards();
|
|
11183
13723
|
const pass = this.renderer.beginFrame();
|
|
11184
13724
|
const gsRenderer = this.sceneManager.getGSRenderer();
|
|
11185
13725
|
if (gsRenderer) {
|
|
@@ -11188,6 +13728,11 @@ class App {
|
|
|
11188
13728
|
this.meshRenderer.render(pass);
|
|
11189
13729
|
this.gizmoManager.render(pass);
|
|
11190
13730
|
this.renderer.endFrame();
|
|
13731
|
+
this.hotspotManager.consumeGPUResult();
|
|
13732
|
+
if (gsRenderer == null ? void 0 : gsRenderer.prepareDepthNormalPass) {
|
|
13733
|
+
const [px, py] = this.hotspotManager.getPickPixel();
|
|
13734
|
+
gsRenderer.prepareDepthNormalPass(px, py);
|
|
13735
|
+
}
|
|
11191
13736
|
}
|
|
11192
13737
|
onResize() {
|
|
11193
13738
|
this.camera.setAspect(this.renderer.getAspectRatio());
|
|
@@ -11217,6 +13762,7 @@ class App {
|
|
|
11217
13762
|
}
|
|
11218
13763
|
clearSplats() {
|
|
11219
13764
|
this.sceneManager.clearSplats();
|
|
13765
|
+
this.hotspotManager.setGSRenderer(null);
|
|
11220
13766
|
this.useMobileRenderer = false;
|
|
11221
13767
|
}
|
|
11222
13768
|
// ============================================
|
|
@@ -11271,6 +13817,25 @@ class App {
|
|
|
11271
13817
|
return this.sceneManager.setMeshRangeColor(startIndex, count, r, g, b, a);
|
|
11272
13818
|
}
|
|
11273
13819
|
// ============================================
|
|
13820
|
+
// 覆盖层 Mesh(热点等)
|
|
13821
|
+
// ============================================
|
|
13822
|
+
getOverlayMeshCount() {
|
|
13823
|
+
return this.sceneManager.getOverlayMeshCount();
|
|
13824
|
+
}
|
|
13825
|
+
removeOverlayMeshByIndex(index) {
|
|
13826
|
+
return this.sceneManager.removeOverlayMeshByIndex(index);
|
|
13827
|
+
}
|
|
13828
|
+
getOverlayMeshColor(index) {
|
|
13829
|
+
return this.sceneManager.getOverlayMeshColor(index);
|
|
13830
|
+
}
|
|
13831
|
+
setOverlayMeshRangeColor(startIndex, count, r, g, b, a = 1) {
|
|
13832
|
+
return this.sceneManager.setOverlayMeshRangeColor(startIndex, count, r, g, b, a);
|
|
13833
|
+
}
|
|
13834
|
+
createOverlayMeshGroupProxy(startIndex, count) {
|
|
13835
|
+
const meshes = this.sceneManager.getOverlayMeshRange(startIndex, count);
|
|
13836
|
+
return this.gizmoManager.createMeshGroupProxy(meshes);
|
|
13837
|
+
}
|
|
13838
|
+
// ============================================
|
|
11274
13839
|
// 相机控制
|
|
11275
13840
|
// ============================================
|
|
11276
13841
|
frameCurrentModel(animate = true) {
|
|
@@ -11364,6 +13929,40 @@ class App {
|
|
|
11364
13929
|
return this.useMobileRenderer;
|
|
11365
13930
|
}
|
|
11366
13931
|
// ============================================
|
|
13932
|
+
// 热点管理
|
|
13933
|
+
// ============================================
|
|
13934
|
+
getHotspotManager() {
|
|
13935
|
+
return this.hotspotManager;
|
|
13936
|
+
}
|
|
13937
|
+
enterHotspotMode(objUrl) {
|
|
13938
|
+
this.hotspotManager.setHotspotOBJUrl(objUrl);
|
|
13939
|
+
this.hotspotManager.enter();
|
|
13940
|
+
}
|
|
13941
|
+
exitHotspotMode() {
|
|
13942
|
+
this.hotspotManager.exit();
|
|
13943
|
+
}
|
|
13944
|
+
isHotspotModeActive() {
|
|
13945
|
+
return this.hotspotManager.isActive();
|
|
13946
|
+
}
|
|
13947
|
+
getHotspots() {
|
|
13948
|
+
return this.hotspotManager.getHotspots();
|
|
13949
|
+
}
|
|
13950
|
+
setHotspotBillboard(hotspotIndex, enabled) {
|
|
13951
|
+
return this.hotspotManager.setHotspotBillboard(hotspotIndex, enabled);
|
|
13952
|
+
}
|
|
13953
|
+
getHotspotBillboard(hotspotIndex) {
|
|
13954
|
+
return this.hotspotManager.getHotspotBillboard(hotspotIndex);
|
|
13955
|
+
}
|
|
13956
|
+
getHotspotCount() {
|
|
13957
|
+
return this.hotspotManager.getHotspotCount();
|
|
13958
|
+
}
|
|
13959
|
+
findHotspotIndexByMeshStart(overlayMeshStartIndex) {
|
|
13960
|
+
return this.hotspotManager.findHotspotIndexByMeshStart(overlayMeshStartIndex);
|
|
13961
|
+
}
|
|
13962
|
+
getOverlayMeshByIndex(index) {
|
|
13963
|
+
return this.sceneManager.getOverlayMeshByIndex(index);
|
|
13964
|
+
}
|
|
13965
|
+
// ============================================
|
|
11367
13966
|
// 内部方法
|
|
11368
13967
|
// ============================================
|
|
11369
13968
|
async fetchWithProgress(url, onProgress) {
|
|
@@ -11411,6 +14010,7 @@ class App {
|
|
|
11411
14010
|
window.removeEventListener("resize", this.boundOnResize);
|
|
11412
14011
|
this.sceneManager.destroy();
|
|
11413
14012
|
this.gizmoManager.destroy();
|
|
14013
|
+
this.hotspotManager.destroy();
|
|
11414
14014
|
if (this.meshRenderer) {
|
|
11415
14015
|
this.meshRenderer.destroy();
|
|
11416
14016
|
}
|
|
@@ -11429,16 +14029,13 @@ export {
|
|
|
11429
14029
|
DEFAULT_MATERIAL,
|
|
11430
14030
|
DEFAULT_OBJ_MATERIAL,
|
|
11431
14031
|
GLBLoader,
|
|
11432
|
-
SHMode as GSSHMode,
|
|
11433
14032
|
GSSplatRenderer,
|
|
11434
14033
|
GSSplatRendererMobile,
|
|
11435
14034
|
GSSplatSorter,
|
|
11436
14035
|
GSSplatSorterMobile,
|
|
11437
14036
|
GizmoManager,
|
|
11438
|
-
MeshGroupProxy as GizmoMeshGroupProxy,
|
|
11439
14037
|
GizmoMode,
|
|
11440
|
-
|
|
11441
|
-
SplatTransformProxy as GizmoSplatTransformProxy,
|
|
14038
|
+
HotspotManager,
|
|
11442
14039
|
MTLParser,
|
|
11443
14040
|
Mesh,
|
|
11444
14041
|
MeshGroupProxy,
|
|
@@ -11452,7 +14049,7 @@ export {
|
|
|
11452
14049
|
SplatBoundingBoxProvider,
|
|
11453
14050
|
SplatTransformProxy,
|
|
11454
14051
|
TextureCache,
|
|
11455
|
-
|
|
14052
|
+
TransformGizmo,
|
|
11456
14053
|
ViewportGizmo,
|
|
11457
14054
|
calculateTextureDimensions,
|
|
11458
14055
|
compactDataToGPUBuffer,
|
|
@@ -11460,6 +14057,7 @@ export {
|
|
|
11460
14057
|
computeBoundingBox$1 as computeBoundingBox,
|
|
11461
14058
|
createBoundingBoxFromMinMax,
|
|
11462
14059
|
createTextureFromImageBitmap,
|
|
14060
|
+
deserializeSOG,
|
|
11463
14061
|
deserializeSplat,
|
|
11464
14062
|
destroyCompressedTextures,
|
|
11465
14063
|
getRecommendedDPR,
|
|
@@ -11467,6 +14065,7 @@ export {
|
|
|
11467
14065
|
isWebGPUSupported,
|
|
11468
14066
|
loadPLY,
|
|
11469
14067
|
loadPLYMobile,
|
|
14068
|
+
loadSOG,
|
|
11470
14069
|
loadSplat,
|
|
11471
14070
|
loadTextureFromBlob,
|
|
11472
14071
|
loadTextureFromBuffer,
|