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