s3m-three 0.4.18 → 0.4.20

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.
Files changed (169) hide show
  1. package/dist/browser/assets/s3mWorker-DvAUA8NJ.js +1 -0
  2. package/dist/browser/s3m-three.iife.ext-three.js +619 -304
  3. package/dist/browser/s3m-three.iife.js +647 -332
  4. package/dist/core/Draco.d.ts.map +1 -1
  5. package/dist/core/Draco.js +0 -1
  6. package/dist/core/Draco.js.map +1 -1
  7. package/dist/core/GaussianSplat.d.ts +64 -0
  8. package/dist/core/GaussianSplat.d.ts.map +1 -0
  9. package/dist/core/GaussianSplat.js +139 -0
  10. package/dist/core/GaussianSplat.js.map +1 -0
  11. package/dist/core/S3M1Parser.d.ts +1 -1
  12. package/dist/core/S3M1Parser.d.ts.map +1 -1
  13. package/dist/core/S3M1Parser.js +320 -25
  14. package/dist/core/S3M1Parser.js.map +1 -1
  15. package/dist/core/S3MBParser.d.ts.map +1 -1
  16. package/dist/core/S3MBParser.js +13 -0
  17. package/dist/core/S3MBParser.js.map +1 -1
  18. package/dist/core/S3MBlockParser.d.ts.map +1 -1
  19. package/dist/core/S3MBlockParser.js +83 -28
  20. package/dist/core/S3MBlockParser.js.map +1 -1
  21. package/dist/core/SplatLod.d.ts +36 -0
  22. package/dist/core/SplatLod.d.ts.map +1 -0
  23. package/dist/core/SplatLod.js +54 -0
  24. package/dist/core/SplatLod.js.map +1 -0
  25. package/dist/core/SplatProjection.d.ts +75 -0
  26. package/dist/core/SplatProjection.d.ts.map +1 -0
  27. package/dist/core/SplatProjection.js +162 -0
  28. package/dist/core/SplatProjection.js.map +1 -0
  29. package/dist/core/SplatSort.d.ts +26 -0
  30. package/dist/core/SplatSort.d.ts.map +1 -0
  31. package/dist/core/SplatSort.js +61 -0
  32. package/dist/core/SplatSort.js.map +1 -0
  33. package/dist/core/TexCoordZ.d.ts +94 -0
  34. package/dist/core/TexCoordZ.d.ts.map +1 -0
  35. package/dist/core/TexCoordZ.js +125 -0
  36. package/dist/core/TexCoordZ.js.map +1 -0
  37. package/dist/core/TextureDecode.d.ts +19 -0
  38. package/dist/core/TextureDecode.d.ts.map +1 -1
  39. package/dist/core/TextureDecode.js +53 -11
  40. package/dist/core/TextureDecode.js.map +1 -1
  41. package/dist/core/TileGeometry.d.ts +39 -0
  42. package/dist/core/TileGeometry.d.ts.map +1 -1
  43. package/dist/core/TileGeometry.js +133 -14
  44. package/dist/core/TileGeometry.js.map +1 -1
  45. package/dist/core/degradations.d.ts +37 -0
  46. package/dist/core/degradations.d.ts.map +1 -0
  47. package/dist/core/degradations.js +32 -0
  48. package/dist/core/degradations.js.map +1 -0
  49. package/dist/core/types.d.ts +36 -0
  50. package/dist/core/types.d.ts.map +1 -1
  51. package/dist/index.d.ts +7 -1
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +4 -1
  54. package/dist/index.js.map +1 -1
  55. package/dist/render/DatasetManager.d.ts +11 -0
  56. package/dist/render/DatasetManager.d.ts.map +1 -1
  57. package/dist/render/DatasetManager.js +2 -0
  58. package/dist/render/DatasetManager.js.map +1 -1
  59. package/dist/render/FeatureStyleManager.d.ts.map +1 -1
  60. package/dist/render/FeatureStyleManager.js +7 -5
  61. package/dist/render/FeatureStyleManager.js.map +1 -1
  62. package/dist/render/GeometryBuilder.d.ts +31 -0
  63. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  64. package/dist/render/GeometryBuilder.js +229 -6
  65. package/dist/render/GeometryBuilder.js.map +1 -1
  66. package/dist/render/Picker.d.ts.map +1 -1
  67. package/dist/render/Picker.js +19 -5
  68. package/dist/render/Picker.js.map +1 -1
  69. package/dist/render/S3MViewer.d.ts +106 -379
  70. package/dist/render/S3MViewer.d.ts.map +1 -1
  71. package/dist/render/S3MViewer.js +324 -1494
  72. package/dist/render/S3MViewer.js.map +1 -1
  73. package/dist/render/SplatMaterial.d.ts +103 -0
  74. package/dist/render/SplatMaterial.d.ts.map +1 -0
  75. package/dist/render/SplatMaterial.js +388 -0
  76. package/dist/render/SplatMaterial.js.map +1 -0
  77. package/dist/render/SplatSorter.d.ts +14 -0
  78. package/dist/render/SplatSorter.d.ts.map +1 -0
  79. package/dist/render/SplatSorter.js +66 -0
  80. package/dist/render/SplatSorter.js.map +1 -0
  81. package/dist/render/StyleMaterial.d.ts +48 -0
  82. package/dist/render/StyleMaterial.d.ts.map +1 -1
  83. package/dist/render/StyleMaterial.js +292 -69
  84. package/dist/render/StyleMaterial.js.map +1 -1
  85. package/dist/render/TextureFactory.d.ts +10 -0
  86. package/dist/render/TextureFactory.d.ts.map +1 -1
  87. package/dist/render/TextureFactory.js +35 -5
  88. package/dist/render/TextureFactory.js.map +1 -1
  89. package/dist/render/lightingPreset.d.ts +53 -0
  90. package/dist/render/lightingPreset.d.ts.map +1 -0
  91. package/dist/render/lightingPreset.js +36 -0
  92. package/dist/render/lightingPreset.js.map +1 -0
  93. package/dist/render/materialAlpha.d.ts +51 -0
  94. package/dist/render/materialAlpha.d.ts.map +1 -0
  95. package/dist/render/materialAlpha.js +68 -0
  96. package/dist/render/materialAlpha.js.map +1 -0
  97. package/dist/render/viewer/EdgeDecorator.d.ts +206 -0
  98. package/dist/render/viewer/EdgeDecorator.d.ts.map +1 -0
  99. package/dist/render/viewer/EdgeDecorator.js +628 -0
  100. package/dist/render/viewer/EdgeDecorator.js.map +1 -0
  101. package/dist/render/viewer/HeightIndex.d.ts +36 -0
  102. package/dist/render/viewer/HeightIndex.d.ts.map +1 -0
  103. package/dist/render/viewer/HeightIndex.js +98 -0
  104. package/dist/render/viewer/HeightIndex.js.map +1 -0
  105. package/dist/render/viewer/LightRig.d.ts +88 -0
  106. package/dist/render/viewer/LightRig.d.ts.map +1 -0
  107. package/dist/render/viewer/LightRig.js +229 -0
  108. package/dist/render/viewer/LightRig.js.map +1 -0
  109. package/dist/render/viewer/PickService.d.ts +109 -0
  110. package/dist/render/viewer/PickService.d.ts.map +1 -0
  111. package/dist/render/viewer/PickService.js +591 -0
  112. package/dist/render/viewer/PickService.js.map +1 -0
  113. package/dist/render/viewer/SelectionDecorator.d.ts +84 -0
  114. package/dist/render/viewer/SelectionDecorator.d.ts.map +1 -0
  115. package/dist/render/viewer/SelectionDecorator.js +339 -0
  116. package/dist/render/viewer/SelectionDecorator.js.map +1 -0
  117. package/dist/render/viewer/SplatLayer.d.ts +26 -0
  118. package/dist/render/viewer/SplatLayer.d.ts.map +1 -0
  119. package/dist/render/viewer/SplatLayer.js +44 -0
  120. package/dist/render/viewer/SplatLayer.js.map +1 -0
  121. package/dist/render/viewer/types.d.ts +250 -0
  122. package/dist/render/viewer/types.d.ts.map +1 -0
  123. package/dist/render/viewer/types.js +3 -0
  124. package/dist/render/viewer/types.js.map +1 -0
  125. package/dist/tileset/S3MTileset.d.ts +42 -1
  126. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  127. package/dist/tileset/S3MTileset.js +52 -2
  128. package/dist/tileset/S3MTileset.js.map +1 -1
  129. package/dist/vendor/browser-node-stub.js +10 -0
  130. package/dist/worker/s3mWorker.js +1166 -979
  131. package/dist/worker/s3mWorker.js.map +1 -1
  132. package/docs/API.md +55 -1
  133. package/docs/FORMAT-NOTES.md +362 -0
  134. package/package.json +1 -1
  135. package/src/core/Draco.ts +1 -2
  136. package/src/core/GaussianSplat.ts +175 -0
  137. package/src/core/S3M1Parser.ts +330 -24
  138. package/src/core/S3MBParser.ts +18 -0
  139. package/src/core/S3MBlockParser.ts +83 -27
  140. package/src/core/SplatLod.ts +83 -0
  141. package/src/core/SplatProjection.ts +231 -0
  142. package/src/core/SplatSort.ts +92 -0
  143. package/src/core/TexCoordZ.ts +160 -0
  144. package/src/core/TextureDecode.ts +63 -11
  145. package/src/core/TileGeometry.ts +151 -14
  146. package/src/core/degradations.ts +74 -0
  147. package/src/core/types.ts +32 -0
  148. package/src/index.ts +8 -1
  149. package/src/render/DatasetManager.ts +10 -0
  150. package/src/render/FeatureStyleManager.ts +8 -6
  151. package/src/render/GeometryBuilder.ts +271 -7
  152. package/src/render/Picker.ts +21 -5
  153. package/src/render/S3MViewer.ts +358 -1727
  154. package/src/render/SplatMaterial.ts +433 -0
  155. package/src/render/SplatSorter.ts +66 -0
  156. package/src/render/StyleMaterial.ts +339 -69
  157. package/src/render/TextureFactory.ts +39 -5
  158. package/src/render/lightingPreset.ts +76 -0
  159. package/src/render/materialAlpha.ts +75 -0
  160. package/src/render/viewer/EdgeDecorator.ts +722 -0
  161. package/src/render/viewer/HeightIndex.ts +94 -0
  162. package/src/render/viewer/LightRig.ts +267 -0
  163. package/src/render/viewer/PickService.ts +651 -0
  164. package/src/render/viewer/SelectionDecorator.ts +377 -0
  165. package/src/render/viewer/SplatLayer.ts +44 -0
  166. package/src/render/viewer/types.ts +258 -0
  167. package/src/tileset/S3MTileset.ts +78 -2
  168. package/src/vendor/browser-node-stub.js +10 -0
  169. package/dist/browser/assets/s3mWorker-BjPOUgjg.js +0 -1
@@ -0,0 +1,433 @@
1
+ /**
2
+ * 高斯泼溅材质(屏幕空间 EWA 投影)
3
+ *
4
+ * 每个高斯是一个四边形,顶点在 **ndc** 里按自身协方差的两个特征轴展开,
5
+ * 片元里做二维高斯衰减。公式与 core/SplatProjection.ts 逐行对应(那是它的
6
+ * 可单测版本,改公式两边同步):
7
+ *
8
+ * Σ′ = J · (W R S Sᵀ Rᵀ Wᵀ) · Jᵀ
9
+ *
10
+ * J 是 ndc 对视图坐标的雅可比,直接用投影矩阵元素求 —— 本项目的
11
+ * projectionMatrix 是 ENU→clip 的完整矩阵,w 行不是标准针孔的 (0,0,-1,0),
12
+ * 套教科书简式会算错。顶点输出是 clip.xy += ndcOffset * clip.w,与宿主投影
13
+ * 矩阵的具体形式完全解耦。
14
+ *
15
+ * 为什么不是早先的「主轴投到垂直视线平面、丢掉 z 分量」:薄片高斯侧对相机时
16
+ * 被丢的那根轴归零,四边形塌成一条线 → 满墙空洞。EWA 的 2×2 协方差不会退化。
17
+ *
18
+ * **半轴不放大,只剔除。** 官方顶点着色器在展开前有一句
19
+ * `dot(cov.xy,cov.xy) < 4.0 && dot(cov.zw,cov.zw) < 4.0 → discard`,
20
+ * 即两轴都短于 2px 的高斯根本不画。泼溅瓦片细分到亚像素时,一个像素上会压着
21
+ * 几十个高斯,逐个抬成 1px 硬点就是那层彩色马赛克 —— 丢掉它们,画面才干净。
22
+ *
23
+ * 彩色噪点还有另一半原因:alpha 混合顺序相关,实例必须按视深 back-to-front,
24
+ * 见 render/SplatSorter.ts(每帧相机变化时重排)。
25
+ *
26
+ * ⚠️ 改这个文件时注意:GLSL 是用模板字符串写的,**着色器源码里的注释绝不能出现反引号**。
27
+ * 反引号会提前终止模板字符串,剩下的 GLSL 被当成 JS 求值 —— 报错信息通常是
28
+ * "ReferenceError: position is not defined",而且只在运行时炸,很难一眼看出根源。
29
+ */
30
+ import * as THREE from 'three';
31
+ import {
32
+ SPLAT_SIGMA,
33
+ SPLAT_LOW_PASS_PIXELS,
34
+ SPLAT_MIN_AXIS_PIXELS,
35
+ SPLAT_MAX_AXIS_PIXELS,
36
+ } from '../core/SplatProjection.js';
37
+
38
+ /**
39
+ * 全瓦片共享的绘图缓冲尺寸(像素)。
40
+ *
41
+ * 泼溅材质与拾取材质都要把像素换算成 ndc,必须看到**同一个**尺寸;
42
+ * 每帧由 `setSplatViewport()` 就地更新这一个 Vector2,所有引用它的 uniform 自动跟着变。
43
+ */
44
+ export const SPLAT_VIEWPORT = new THREE.Vector2(1000, 1000);
45
+
46
+ /** 拾取时每个高斯至少撑开这么多像素半径(否则细高斯在 1px 采样下点不中) */
47
+ const PICK_MIN_PIXELS = 4;
48
+
49
+ export interface SplatMaterialOptions {
50
+ /** 不透明度整体缩放(0~1),默认 1 */
51
+ opacity?: number;
52
+ /**
53
+ * 两根轴都短于这个像素数就整片丢弃,默认 2(官方口径)。
54
+ * 0 = 不剔除:亚像素高斯会全部绘制,密集区域立刻变成彩色噪点。
55
+ */
56
+ cullPixels?: number;
57
+ /**
58
+ * 半轴像素上限,默认 1024。中心贴近相机平面时 EWA 会数值爆炸(实测能到
59
+ * 几百万像素),巨大 quad 与视锥只相交出一个角 → 锐角三角面 / 放射拉丝。
60
+ * 0 = 不封顶。
61
+ */
62
+ maxPixels?: number;
63
+ }
64
+
65
+ export function createSplatMaterial(options: SplatMaterialOptions = {}): THREE.ShaderMaterial {
66
+ return new THREE.ShaderMaterial({
67
+ uniforms: {
68
+ uOpacity: { value: options.opacity ?? 1 },
69
+ uViewport: { value: SPLAT_VIEWPORT },
70
+ uCullPixels: { value: options.cullPixels ?? 2.0 },
71
+ uMaxPixels: { value: options.maxPixels ?? SPLAT_MAX_AXIS_PIXELS },
72
+ },
73
+ vertexShader: /* glsl */ `
74
+ // position 由 three 的 ShaderMaterial 前缀自动声明(vec3),这里直接取 .xy
75
+ attribute vec3 iCenter;
76
+ attribute vec4 iQuat; // (x, y, z, w)
77
+ attribute vec3 iScale; // 米
78
+ attribute vec4 iColor; // RGBA,已归一化到 0~1
79
+
80
+ uniform vec2 uViewport;
81
+ uniform float uCullPixels;
82
+ uniform float uMaxPixels;
83
+
84
+ varying vec2 vQuad;
85
+ varying vec4 vColor;
86
+
87
+ void main() {
88
+ vec4 centerView = modelViewMatrix * vec4(iCenter, 1.0);
89
+ vec4 clip = projectionMatrix * centerView;
90
+
91
+ // 相机后方 / 退化实例:整个四边形扔到裁剪体外,不做任何透视除法
92
+ if (clip.w <= 0.000001) {
93
+ gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
94
+ vQuad = position.xy;
95
+ vColor = vec4(0.0);
96
+ return;
97
+ }
98
+
99
+ // M = W · R · diag(s),R 由四元数展开(列主序)
100
+ vec4 q = iQuat;
101
+ float xx = q.x * q.x, yy = q.y * q.y, zz = q.z * q.z;
102
+ float xy = q.x * q.y, xz = q.x * q.z, yz = q.y * q.z;
103
+ float wx = q.w * q.x, wy = q.w * q.y, wz = q.w * q.z;
104
+ mat3 R = mat3(
105
+ 1.0 - 2.0 * (yy + zz), 2.0 * (xy + wz), 2.0 * (xz - wy),
106
+ 2.0 * (xy - wz), 1.0 - 2.0 * (xx + zz), 2.0 * (yz + wx),
107
+ 2.0 * (xz + wy), 2.0 * (yz - wx), 1.0 - 2.0 * (xx + yy)
108
+ );
109
+ mat3 M = mat3(modelViewMatrix) * R * mat3(
110
+ iScale.x, 0.0, 0.0,
111
+ 0.0, iScale.y, 0.0,
112
+ 0.0, 0.0, iScale.z
113
+ );
114
+
115
+ // Σ = M Mᵀ,只算对称上三角(行向量点积)
116
+ vec3 r0 = vec3(M[0][0], M[1][0], M[2][0]);
117
+ vec3 r1 = vec3(M[0][1], M[1][1], M[2][1]);
118
+ vec3 r2 = vec3(M[0][2], M[1][2], M[2][2]);
119
+ float v00 = dot(r0, r0), v01 = dot(r0, r1), v02 = dot(r0, r2);
120
+ float v11 = dot(r1, r1), v12 = dot(r1, r2), v22 = dot(r2, r2);
121
+
122
+ // 雅可比:ndc = clip.xy / w 对视图坐标的偏导(通用投影矩阵)
123
+ float w2 = clip.w * clip.w;
124
+ vec3 J0 = (vec3(projectionMatrix[0][0], projectionMatrix[1][0], projectionMatrix[2][0]) * clip.w
125
+ - clip.x * vec3(projectionMatrix[0][3], projectionMatrix[1][3], projectionMatrix[2][3])) / w2;
126
+ vec3 J1 = (vec3(projectionMatrix[0][1], projectionMatrix[1][1], projectionMatrix[2][1]) * clip.w
127
+ - clip.y * vec3(projectionMatrix[0][3], projectionMatrix[1][3], projectionMatrix[2][3])) / w2;
128
+
129
+ // C = J Σ Jᵀ(2×2 对称)
130
+ float c00 = dot(J0, vec3(v00 * J0.x + v01 * J0.y + v02 * J0.z,
131
+ v01 * J0.x + v11 * J0.y + v12 * J0.z,
132
+ v02 * J0.x + v12 * J0.y + v22 * J0.z));
133
+ float c01 = dot(J0, vec3(v00 * J1.x + v01 * J1.y + v02 * J1.z,
134
+ v01 * J1.x + v11 * J1.y + v12 * J1.z,
135
+ v02 * J1.x + v12 * J1.y + v22 * J1.z));
136
+ float c11 = dot(J1, vec3(v00 * J1.x + v01 * J1.y + v02 * J1.z,
137
+ v01 * J1.x + v11 * J1.y + v12 * J1.z,
138
+ v02 * J1.x + v12 * J1.y + v22 * J1.z));
139
+
140
+ // 抗锯齿低通:对角各加 0.3px 的方差(3DGS 论文口径)
141
+ vec2 lp = 2.0 * ${SPLAT_LOW_PASS_PIXELS.toFixed(1)} / uViewport;
142
+ float d00 = c00 + lp.x * lp.x;
143
+ float d11 = c11 + lp.y * lp.y;
144
+
145
+ // 2×2 特征分解(解析解)
146
+ float mid = 0.5 * (d00 + d11);
147
+ float halfDiff = 0.5 * (d00 - d11);
148
+ float disc = sqrt(max(halfDiff * halfDiff + c01 * c01, 0.0));
149
+ float l1 = mid + disc;
150
+ float l2 = max(mid - disc, 0.0);
151
+ vec2 e1 = vec2(c01, l1 - d00);
152
+ float elen = length(e1);
153
+ e1 = elen > 1e-20 ? e1 / elen : vec2(1.0, 0.0);
154
+ vec2 e2 = vec2(-e1.y, e1.x);
155
+
156
+ vec2 A = e1 * (${SPLAT_SIGMA.toFixed(1)} * sqrt(max(l1, 0.0)));
157
+ vec2 B = e2 * (${SPLAT_SIGMA.toFixed(1)} * sqrt(l2));
158
+
159
+ // 两道像素级处理,口径逐字照抄官方顶点着色器:
160
+ //
161
+ // ① 细轴下限:官方 lambda2 = max(mid - radius, 0.1) 对应半轴 0.447px,
162
+ // 只防完全退化,不把 1mm 的薄轴放大成可见的硬点。
163
+ // ② 亚像素剔除:官方 dot(cov.xy,cov.xy) < 4.0 && dot(cov.zw,cov.zw) < 4.0
164
+ // 就 discard。泼溅数据被过度细化时一个像素压着几十个亚像素高斯,
165
+ // 逐个抬成 1px 硬点就是满屏彩色马赛克;丢掉它们,粗层的大高斯接管。
166
+ vec2 pxScale = uViewport * 0.5;
167
+ float la = length(A * pxScale);
168
+ float lb = length(B * pxScale);
169
+
170
+ // ③ 上限封顶:必须在下限之前。中心贴近相机平面时雅可比爆炸,半轴能到
171
+ // 几百万像素;等比缩小两根轴(长宽比不变),quad 不再跨满屏。
172
+ // 不封顶的表现就是散落的锐角三角面与放射状拉丝。
173
+ if (uMaxPixels > 0.0) {
174
+ float biggest = max(la, lb);
175
+ if (biggest > uMaxPixels) {
176
+ float k = uMaxPixels / biggest;
177
+ A *= k;
178
+ B *= k;
179
+ la *= k;
180
+ lb *= k;
181
+ }
182
+ }
183
+
184
+ if (la < ${SPLAT_MIN_AXIS_PIXELS.toFixed(2)}) {
185
+ A = la > 1e-20 ? A * (${SPLAT_MIN_AXIS_PIXELS.toFixed(2)} / la) : vec2(${(SPLAT_MIN_AXIS_PIXELS * 2).toFixed(2)} / uViewport.x, 0.0);
186
+ }
187
+ if (lb < ${SPLAT_MIN_AXIS_PIXELS.toFixed(2)}) {
188
+ B = lb > 1e-20 ? B * (${SPLAT_MIN_AXIS_PIXELS.toFixed(2)} / lb) : vec2(0.0, ${(SPLAT_MIN_AXIS_PIXELS * 2).toFixed(2)} / uViewport.y);
189
+ }
190
+ if (uCullPixels > 0.0
191
+ && length(A * pxScale) < uCullPixels
192
+ && length(B * pxScale) < uCullPixels) {
193
+ gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
194
+ vQuad = vec2(0.0);
195
+ vColor = vec4(0.0);
196
+ return;
197
+ }
198
+
199
+ gl_Position = clip;
200
+ gl_Position.xy += (position.x * A + position.y * B) * clip.w;
201
+
202
+ vQuad = position.xy;
203
+ vColor = iColor;
204
+ }
205
+ `,
206
+ fragmentShader: /* glsl */ `
207
+ uniform float uOpacity;
208
+
209
+ varying vec2 vQuad;
210
+ varying vec4 vColor;
211
+
212
+ void main() {
213
+ float r2 = dot(vQuad, vQuad);
214
+ // 3σ 之外直接丢,配合下面的高斯核就是「椭圆边界」而不是硬切一个方框
215
+ if (r2 > 1.0) discard;
216
+
217
+ // 高斯核:r=0 时为 1,r=1(3σ)时降到 e^-4 ≈ 0.018
218
+ float alpha = exp(-4.0 * r2) * vColor.a * uOpacity;
219
+ if (alpha < 0.002) discard;
220
+
221
+ // 预乘 alpha:混合用 One / OneMinusSrcAlpha
222
+ gl_FragColor = vec4(vColor.rgb * alpha, alpha);
223
+ }
224
+ `,
225
+ transparent: true,
226
+ depthTest: true,
227
+ depthWrite: false,
228
+ // 预乘 alpha 混合:src 已经是 rgb*alpha
229
+ blending: THREE.CustomBlending,
230
+ blendSrc: THREE.OneFactor,
231
+ blendDst: THREE.OneMinusSrcAlphaFactor,
232
+ blendEquation: THREE.AddEquation,
233
+ blendSrcAlpha: THREE.OneFactor,
234
+ blendDstAlpha: THREE.OneMinusSrcAlphaFactor,
235
+ // 泼溅是逐点混合,实例顺序由 SplatSorter 保证 back-to-front;
236
+ // 瓦片之间交给 three 的透明对象排序
237
+ side: THREE.DoubleSide,
238
+ });
239
+ }
240
+
241
+ /**
242
+ * 1×1 的四边形,所有实例共用。
243
+ *
244
+ * ⚠️ 必须是 **6 个顶点(两个三角形)**,不能写成 4 顶点 TRIANGLE_STRIP 那样省两个点:
245
+ * three 的 renderBufferDirect 对 `object.isMesh` 一律 `gl.TRIANGLES`,不看
246
+ * drawMode / 是不是 strip。4 顶点只会画出前 3 个组成的一个三角形 —— 表现就是
247
+ * 满屏三角片、沿对角线的拉丝,而且每个高斯只覆盖了一半面积(叠加起来又是空洞)。
248
+ */
249
+ export function createQuadGeometry(): THREE.InstancedBufferGeometry {
250
+ const geo = new THREE.InstancedBufferGeometry();
251
+ // 必须叫 `position` 且是 vec3:three 的 renderBufferDirect 拿它取 drawCount,
252
+ // 找不到就直接 console.error 后 return —— 表现是「瓦片有请求、画面全空」。
253
+ // ShaderMaterial 的顶点前缀会自己声明 `attribute vec3 position`,shader 里不要再写一遍。
254
+ geo.setAttribute(
255
+ 'position',
256
+ new THREE.BufferAttribute(
257
+ // 左下→右下→左上 | 左上→右下→右上,两个三角形拼满 [-1,1]²
258
+ new Float32Array([
259
+ -1, -1, 0, 1, -1, 0, -1, 1, 0,
260
+ -1, 1, 0, 1, -1, 0, 1, 1, 0,
261
+ ]),
262
+ 3
263
+ )
264
+ );
265
+ // 顶点着色器完全用实例属性定位,包围球无意义;关掉剔除免得整块被误裁
266
+ geo.boundingSphere = new THREE.Sphere(new THREE.Vector3(), Infinity);
267
+ return geo;
268
+ }
269
+
270
+ /**
271
+ * 泼溅网格的 raycast。
272
+ *
273
+ * 原来这里挂的是 `() => {}`(完全不参与射线求交),后果不只是「点不中」:拾取结果里
274
+ * 的精确命中点也拿不到,回退去算 `geometry.boundingBox` —— 而那只是原点处那个
275
+ * 2×2 方块的包围盒,于是返回的经纬度/高程全是错的。
276
+ *
277
+ * 每个高斯近似成一个球,半径取 3σ 里最长的那根轴(跟画面上看到的范围一致)。
278
+ */
279
+ export function splatRaycast(
280
+ this: THREE.Mesh,
281
+ raycaster: THREE.Raycaster,
282
+ intersects: THREE.Intersection[]
283
+ ): void {
284
+ const geo = this.geometry as THREE.InstancedBufferGeometry;
285
+ const center = geo.getAttribute('iCenter');
286
+ const scale = geo.getAttribute('iScale');
287
+ const n = geo.instanceCount ?? 0;
288
+ if (!center || !scale || n <= 0) return;
289
+
290
+ const sphere = geo.boundingSphere;
291
+ if (sphere && Number.isFinite(sphere.radius)) {
292
+ _raySphere.copy(sphere).applyMatrix4(this.matrixWorld);
293
+ if (!raycaster.ray.intersectsSphere(_raySphere)) return;
294
+ }
295
+
296
+ _rayInv.copy(this.matrixWorld).invert();
297
+ _rayLocal.copy(raycaster.ray).applyMatrix4(_rayInv);
298
+ const o = _rayLocal.origin;
299
+ const d = _rayLocal.direction;
300
+
301
+ let bestT = Infinity;
302
+ let bestIdx = -1;
303
+ const ca = center.array as Float32Array;
304
+ const sa = scale.array as Float32Array;
305
+
306
+ for (let i = 0; i < n; i++) {
307
+ const cx = ca[i * 3]!;
308
+ const cy = ca[i * 3 + 1]!;
309
+ const cz = ca[i * 3 + 2]!;
310
+ const vx = cx - o.x;
311
+ const vy = cy - o.y;
312
+ const vz = cz - o.z;
313
+ const t = vx * d.x + vy * d.y + vz * d.z;
314
+ if (t <= 0 || t >= bestT) continue;
315
+ // 垂距² 与球半径² 比较:半径 = 3 × 最长轴
316
+ const r = SPLAT_SIGMA * Math.max(sa[i * 3]!, sa[i * 3 + 1]!, sa[i * 3 + 2]!);
317
+ const perp2 = vx * vx + vy * vy + vz * vz - t * t;
318
+ if (perp2 > r * r) continue;
319
+ bestT = t;
320
+ bestIdx = i;
321
+ }
322
+ if (bestIdx < 0) return;
323
+
324
+ const point = new THREE.Vector3(
325
+ ca[bestIdx * 3]!,
326
+ ca[bestIdx * 3 + 1]!,
327
+ ca[bestIdx * 3 + 2]!
328
+ ).applyMatrix4(this.matrixWorld);
329
+
330
+ intersects.push({
331
+ distance: raycaster.ray.origin.distanceTo(point),
332
+ point,
333
+ object: this,
334
+ instanceId: bestIdx,
335
+ });
336
+ }
337
+
338
+ const _raySphere = new THREE.Sphere();
339
+ const _rayInv = new THREE.Matrix4();
340
+ const _rayLocal = new THREE.Ray();
341
+
342
+ /**
343
+ * 泼溅专用的 **拾取材质**(走 GPU 拾取那一趟渲染)。
344
+ *
345
+ * 为什么必须单独做:拾取通道(render/Picker.ts)换的是它自己的通用拾取着色器,
346
+ * 那个着色器只读 `position`,根本不知道 `iCenter`。把泼溅的几何交给它,每个实例
347
+ * 都会塌在模型原点,画出一个 2×2 的像素块 —— 表现就是「模型完全点不中」。
348
+ *
349
+ * 这里只需要把每个高斯的**中心**摊成一个 `PICK_MIN_PIXELS` 半径的屏幕方块,
350
+ * 不需要复刻 EWA(拾取只关心「这个像素属于哪块瓦片」以及谁更近)。颜色编码
351
+ * 与 Picker 的 PICK_FRAG 逐位一致:R/G 存 objectId + 1,B/A 留 0。
352
+ */
353
+ export function createSplatPickMaterial(objectId: number): THREE.ShaderMaterial {
354
+ return new THREE.ShaderMaterial({
355
+ uniforms: {
356
+ uObjectId: { value: objectId },
357
+ uViewport: { value: SPLAT_VIEWPORT },
358
+ },
359
+ vertexShader: /* glsl */ `
360
+ attribute vec3 iCenter;
361
+
362
+ uniform vec2 uViewport;
363
+
364
+ void main() {
365
+ vec4 clip = projectionMatrix * modelViewMatrix * vec4(iCenter, 1.0);
366
+ // 相机后方:扔出裁剪体,避免透视除法把方块翻到屏幕另一侧
367
+ if (clip.w <= 0.000001) {
368
+ gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
369
+ return;
370
+ }
371
+ // position ∈ [-1,1],乘上「半个方块以 ndc 计」的尺寸
372
+ vec2 half2 = (${(PICK_MIN_PIXELS * 2).toFixed(1)} / uViewport);
373
+ gl_Position = clip;
374
+ gl_Position.xy += position.xy * half2 * clip.w;
375
+ }
376
+ `,
377
+ fragmentShader: /* glsl */ `
378
+ uniform float uObjectId;
379
+
380
+ void main() {
381
+ float o = uObjectId + 1.0;
382
+ gl_FragColor = vec4(mod(o, 256.0) / 255.0, floor(o / 256.0) / 255.0, 0.0, 0.0);
383
+ }
384
+ `,
385
+ side: THREE.DoubleSide,
386
+ depthTest: true,
387
+ depthWrite: true,
388
+ });
389
+ }
390
+
391
+ /** 泼溅实例中心的局部 AABB */
392
+ export interface SplatBounds {
393
+ minX: number;
394
+ minY: number;
395
+ minZ: number;
396
+ maxX: number;
397
+ maxY: number;
398
+ maxZ: number;
399
+ count: number;
400
+ }
401
+
402
+ /**
403
+ * 泼溅的包围盒只能由 `iCenter` 算。
404
+ *
405
+ * geometry 的 `position` 只是 `[-1,1]²` 的四边形角点,真实位置全在实例属性里;
406
+ * 拿 `position` 去 computeBoundingBox 会得到一个跟模型毫无关系的 2m 立方体,
407
+ * 拾取结果里的中心经纬度、包围盒属性就全是错的。
408
+ */
409
+ export function splatCenterBounds(geo: THREE.BufferGeometry): SplatBounds | null {
410
+ const attr = geo.getAttribute('iCenter') as THREE.BufferAttribute | undefined;
411
+ if (!attr) return null;
412
+ const a = attr.array as ArrayLike<number>;
413
+ const n = attr.count;
414
+ if (!n) return null;
415
+ let minX = Infinity;
416
+ let minY = Infinity;
417
+ let minZ = Infinity;
418
+ let maxX = -Infinity;
419
+ let maxY = -Infinity;
420
+ let maxZ = -Infinity;
421
+ for (let i = 0; i < n; i++) {
422
+ const x = a[i * 3]!;
423
+ const y = a[i * 3 + 1]!;
424
+ const z = a[i * 3 + 2]!;
425
+ if (x < minX) minX = x;
426
+ if (x > maxX) maxX = x;
427
+ if (y < minY) minY = y;
428
+ if (y > maxY) maxY = y;
429
+ if (z < minZ) minZ = z;
430
+ if (z > maxZ) maxZ = z;
431
+ }
432
+ return { minX, minY, minZ, maxX, maxY, maxZ, count: n };
433
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 把泼溅实例按视深重排(back-to-front)
3
+ *
4
+ * three 的 InstancedMesh 按 attribute 顺序画实例,所以「排序」在这里就是
5
+ * 用排序索引把 iCenter / iScale / iQuat / iColor 四条流一起重排 —— 必须
6
+ * 四条同步动,只排中心会把颜色和形状排串。
7
+ *
8
+ * 顺序已经正确时直接返回 false,不换 buffer、不触发上传(静止相机 + 静止
9
+ * 数据时零开销)。scratch 数组按 attribute 缓存,避免每帧分配。
10
+ */
11
+ import * as THREE from 'three';
12
+ import { splatDepthOrder } from '../core/SplatSort.js';
13
+
14
+ const SORTED_ATTRS = ['iCenter', 'iScale', 'iQuat', 'iColor'] as const;
15
+
16
+ const _view = new THREE.Matrix4();
17
+ const _scratch = new WeakMap<THREE.BufferAttribute, THREE.TypedArray>();
18
+ /** 排序下标缓冲:按 iCenter attribute 缓存,每帧重排不再分配 */
19
+ const _orderCache = new WeakMap<THREE.BufferAttribute, Uint32Array>();
20
+
21
+ /** @returns 是否真的重排了(false = 顺序本来就对,没动任何数据) */
22
+ export function sortSplatMesh(mesh: THREE.Mesh, viewMatrix: THREE.Matrix4): boolean {
23
+ const geo = mesh.geometry as THREE.InstancedBufferGeometry;
24
+ const center = geo.getAttribute('iCenter') as THREE.InstancedBufferAttribute | undefined;
25
+ if (!center) return false;
26
+ const count = Math.min(geo.instanceCount ?? Infinity, center.count);
27
+ if (count <= 1) return false;
28
+
29
+ _view.multiplyMatrices(viewMatrix, mesh.matrixWorld);
30
+ let order = _orderCache.get(center);
31
+ if (!order || order.length < count) {
32
+ order = new Uint32Array(count);
33
+ _orderCache.set(center, order);
34
+ }
35
+ splatDepthOrder(center.array as Float32Array, count, _view, order);
36
+
37
+ let sorted = true;
38
+ for (let i = 0; i < count; i++) {
39
+ if (order[i] !== i) {
40
+ sorted = false;
41
+ break;
42
+ }
43
+ }
44
+ if (sorted) return false;
45
+
46
+ for (const name of SORTED_ATTRS) {
47
+ const attr = geo.getAttribute(name) as THREE.InstancedBufferAttribute | undefined;
48
+ if (!attr) continue;
49
+ const src = attr.array;
50
+ const itemSize = attr.itemSize;
51
+ const needed = count * itemSize;
52
+ let dst = _scratch.get(attr);
53
+ if (!dst || dst.length < needed) {
54
+ dst = new (src.constructor as new (n: number) => THREE.TypedArray)(needed);
55
+ _scratch.set(attr, dst);
56
+ }
57
+ for (let i = 0; i < count; i++) {
58
+ const from = order[i]! * itemSize;
59
+ const to = i * itemSize;
60
+ for (let k = 0; k < itemSize; k++) dst[to + k] = src[from + k]!;
61
+ }
62
+ src.set(dst.subarray(0, needed));
63
+ attr.needsUpdate = true;
64
+ }
65
+ return true;
66
+ }