s3m-three 0.1.0 → 0.2.0
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/LICENSE +202 -202
- package/dist/render/DatasetManager.d.ts +10 -3
- package/dist/render/DatasetManager.d.ts.map +1 -1
- package/dist/render/DatasetManager.js +14 -5
- package/dist/render/DatasetManager.js.map +1 -1
- package/dist/render/EdgeLines.js +46 -46
- package/dist/render/GroundManager.d.ts +9 -0
- package/dist/render/GroundManager.d.ts.map +1 -1
- package/dist/render/GroundManager.js +13 -1
- package/dist/render/GroundManager.js.map +1 -1
- package/dist/render/Picker.js +66 -66
- package/dist/render/S3MViewer.d.ts +11 -3
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +18 -3
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/StyleMaterial.js +265 -265
- package/dist/vendor/meshopt_decoder.module.js +118 -118
- package/docs/API.md +20 -0
- package/package.json +1 -1
- package/src/core/BinaryReader.ts +116 -116
- package/src/core/DXTTextureDecode.ts +260 -260
- package/src/core/FeatureInfo.ts +150 -150
- package/src/core/MeshOpt.ts +401 -401
- package/src/core/S3MBParser.ts +590 -590
- package/src/core/types.ts +222 -222
- package/src/render/DatasetManager.ts +17 -6
- package/src/render/EdgeLines.ts +343 -343
- package/src/render/GeometryBuilder.ts +840 -840
- package/src/render/GroundManager.ts +14 -1
- package/src/render/Picker.ts +293 -293
- package/src/render/S3MViewer.ts +18 -3
- package/src/render/StyleMaterial.ts +602 -602
- package/src/render/TextureFactory.ts +134 -134
- package/src/tileset/S3MTileset.ts +552 -552
- package/src/vendor/meshopt_decoder.module.d.ts +31 -31
- package/src/vendor/meshopt_decoder.module.js +118 -118
- package/dist/render/CoordinateHelper.d.ts +0 -90
- package/dist/render/CoordinateHelper.d.ts.map +0 -1
- package/dist/render/CoordinateHelper.js +0 -85
- package/dist/render/CoordinateHelper.js.map +0 -1
- package/dist/render/S3MLayer.d.ts +0 -457
- package/dist/render/S3MLayer.d.ts.map +0 -1
- package/dist/render/S3MLayer.js +0 -1860
- package/dist/render/S3MLayer.js.map +0 -1
|
@@ -38,6 +38,8 @@ export class GroundManager {
|
|
|
38
38
|
gridSize: number;
|
|
39
39
|
excavationBounds: [number, number, number, number] | null = null;
|
|
40
40
|
size: number;
|
|
41
|
+
/** 物理地表网格的高度(米,场景 ENU Z 坐标),默认 0 */
|
|
42
|
+
groundHeight = 0;
|
|
41
43
|
|
|
42
44
|
readonly group: THREE.Group;
|
|
43
45
|
private _mesh: THREE.Mesh | null = null;
|
|
@@ -153,7 +155,18 @@ export class GroundManager {
|
|
|
153
155
|
*/
|
|
154
156
|
update(camera?: THREE.Camera | null): void {
|
|
155
157
|
if (!camera || this.mode === 'none') return;
|
|
156
|
-
this.group.position.set(camera.position.x, camera.position.y,
|
|
158
|
+
this.group.position.set(camera.position.x, camera.position.y, this.groundHeight);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 设置物理地表网格的高度(米,场景 ENU Z 坐标)
|
|
163
|
+
*
|
|
164
|
+
* 数据集被抬到真实地面高程后,地表网格需要同步抬高,
|
|
165
|
+
* 否则「地表覆盖 / 地下透视」的物理关系会与被抬升的模型脱节。
|
|
166
|
+
*/
|
|
167
|
+
setGroundHeight(height: number): void {
|
|
168
|
+
this.groundHeight = height;
|
|
169
|
+
this.group.position.z = height;
|
|
157
170
|
}
|
|
158
171
|
|
|
159
172
|
setMode(mode: GroundMode): void {
|
package/src/render/Picker.ts
CHANGED
|
@@ -1,293 +1,293 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 单体级 GPU picking
|
|
3
|
-
*
|
|
4
|
-
* 思路:把「物体编号 + batchId」编码成 RGBA 渲染到离屏目标,点击时读那一个像素。
|
|
5
|
-
* 相比射线求交,代价与三角面数无关——30 万面和 3 千面一样快,且天然只命中
|
|
6
|
-
* 最前面的可见面(深度测试在 GPU 上完成了)。
|
|
7
|
-
*
|
|
8
|
-
* 编码约定(每个通道一个字节):
|
|
9
|
-
* R,G = 物体编号 + 1
|
|
10
|
-
* B,A = batchId + 1 (+1 是为了让「什么都没点到」全 0)
|
|
11
|
-
*
|
|
12
|
-
* 渲染目标只有 size×size 个像素,靠平移 viewport 把「点击点那一格」挪到目标里:
|
|
13
|
-
* viewport 原点设成 (0 - px, 0 - (h-1-py))、尺寸仍是整块画布,于是光栅化结果和全屏
|
|
14
|
-
* 那一趟逐像素一致,只是只有一个像素落在目标上。原来那句「每次点击全屏渲染一趟」
|
|
15
|
-
* 的代价(移动端尤其明显)就没了。
|
|
16
|
-
*/
|
|
17
|
-
import * as THREE from 'three';
|
|
18
|
-
|
|
19
|
-
const PICK_VERT = /* glsl */ `
|
|
20
|
-
attribute float aBatchId;
|
|
21
|
-
#ifdef USE_INSTANCING
|
|
22
|
-
// 实例化:单体粒度在实例上,顶点属性里没有可用的 batchId
|
|
23
|
-
attribute float aInstanceBatchId;
|
|
24
|
-
attribute float aInstanceIndex;
|
|
25
|
-
#endif
|
|
26
|
-
uniform float uObjectId;
|
|
27
|
-
uniform sampler2D uBatchTex;
|
|
28
|
-
uniform float uBatchTexWidth;
|
|
29
|
-
uniform int uBatchTexActive;
|
|
30
|
-
uniform float uSelBatch;
|
|
31
|
-
uniform float uSelWhole;
|
|
32
|
-
uniform float uSelElevation;
|
|
33
|
-
varying float vBatch;
|
|
34
|
-
|
|
35
|
-
void main() {
|
|
36
|
-
float s3mBatch;
|
|
37
|
-
#ifdef USE_INSTANCING
|
|
38
|
-
s3mBatch = aInstanceBatchId;
|
|
39
|
-
// 编码「实例序号」而不是控制位:CPU 侧要靠它拿到实例矩阵(描边 / 中心点)
|
|
40
|
-
vBatch = aInstanceIndex;
|
|
41
|
-
#else
|
|
42
|
-
s3mBatch = aBatchId;
|
|
43
|
-
vBatch = aBatchId;
|
|
44
|
-
#endif
|
|
45
|
-
|
|
46
|
-
// 单体状态(平移 / 隐藏)必须和主着色器用同一份数据、同一套算法:
|
|
47
|
-
// 平移只发生在顶点着色器里,拾取这一趟如果还按原始坐标画,移位的楼就点不中。
|
|
48
|
-
vec3 p = position;
|
|
49
|
-
if ( uBatchTexActive > 0 && s3mBatch >= 0.0 ) {
|
|
50
|
-
vec4 state = texture2D( uBatchTex, vec2( ( s3mBatch + 0.5 ) / uBatchTexWidth, 0.25 ) );
|
|
51
|
-
if ( state.a < -0.5 ) {
|
|
52
|
-
p = vec3( 0.0 );
|
|
53
|
-
} else {
|
|
54
|
-
vec4 offset = texture2D( uBatchTex, vec2( ( s3mBatch + 0.5 ) / uBatchTexWidth, 0.75 ) );
|
|
55
|
-
p += offset.xyz;
|
|
56
|
-
if ( offset.z == 0.0 && state.a > 0.0 ) {
|
|
57
|
-
p.z += state.a;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 选中单体自身的拔高(selectElevation)
|
|
63
|
-
float sel = max( step( 0.0, uSelBatch ) * step( abs( s3mBatch - uSelBatch ), 0.5 ), uSelWhole );
|
|
64
|
-
p.z += sel * uSelElevation;
|
|
65
|
-
|
|
66
|
-
#ifdef USE_INSTANCING
|
|
67
|
-
gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4( p, 1.0 );
|
|
68
|
-
#else
|
|
69
|
-
gl_Position = projectionMatrix * modelViewMatrix * vec4( p, 1.0 );
|
|
70
|
-
#endif
|
|
71
|
-
}
|
|
72
|
-
`;
|
|
73
|
-
|
|
74
|
-
const PICK_FRAG = /* glsl */ `
|
|
75
|
-
uniform float uObjectId;
|
|
76
|
-
varying float vBatch;
|
|
77
|
-
void main() {
|
|
78
|
-
float o = uObjectId + 1.0;
|
|
79
|
-
float b = vBatch + 1.0;
|
|
80
|
-
gl_FragColor = vec4(
|
|
81
|
-
mod( o, 256.0 ) / 255.0,
|
|
82
|
-
floor( o / 256.0 ) / 255.0,
|
|
83
|
-
mod( b, 256.0 ) / 255.0,
|
|
84
|
-
floor( b / 256.0 ) / 255.0
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
`;
|
|
88
|
-
|
|
89
|
-
export interface PickHit {
|
|
90
|
-
/** 由 S3MLayer 分配的网格编号 */
|
|
91
|
-
objectId: number;
|
|
92
|
-
/** 非实例化几何:单体 batchId;实例化几何这里是 -1(改看 instanceIndex) */
|
|
93
|
-
batchId: number;
|
|
94
|
-
/** 实例化几何命中到第几个实例(非实例化为 undefined) */
|
|
95
|
-
instanceIndex?: number;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** S3MLayer 挂到 mesh.userData 上的字段,拾取时会读它 */
|
|
99
|
-
interface PickUserData {
|
|
100
|
-
pickId?: number;
|
|
101
|
-
pickMaterial?: THREE.Material;
|
|
102
|
-
[key: string]: unknown;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** 没有单体状态纹理时用的占位(全 0 = 不隐藏、不平移) */
|
|
106
|
-
const DUMMY_STATE_TEX = (() => {
|
|
107
|
-
const tex = new THREE.DataTexture(new Float32Array([-1, -1, -1, 0, 0, 0, 0, 0]), 1, 2, THREE.RGBAFormat, THREE.FloatType);
|
|
108
|
-
tex.minFilter = THREE.NearestFilter;
|
|
109
|
-
tex.magFilter = THREE.NearestFilter;
|
|
110
|
-
tex.needsUpdate = true;
|
|
111
|
-
return tex;
|
|
112
|
-
})();
|
|
113
|
-
|
|
114
|
-
function createPickMaterial(objectId: number): THREE.ShaderMaterial {
|
|
115
|
-
return new THREE.ShaderMaterial({
|
|
116
|
-
uniforms: {
|
|
117
|
-
uObjectId: { value: objectId },
|
|
118
|
-
uBatchTex: { value: DUMMY_STATE_TEX },
|
|
119
|
-
uBatchTexWidth: { value: 1 },
|
|
120
|
-
uBatchTexActive: { value: 0 },
|
|
121
|
-
uSelBatch: { value: -1 },
|
|
122
|
-
uSelWhole: { value: 0 },
|
|
123
|
-
uSelElevation: { value: 0 },
|
|
124
|
-
},
|
|
125
|
-
vertexShader: PICK_VERT,
|
|
126
|
-
fragmentShader: PICK_FRAG,
|
|
127
|
-
side: THREE.DoubleSide,
|
|
128
|
-
depthTest: true,
|
|
129
|
-
depthWrite: true,
|
|
130
|
-
toneMapped: false,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export class Picker {
|
|
135
|
-
readonly renderer: THREE.WebGLRenderer;
|
|
136
|
-
/** 拾取用的渲染目标边长(像素),默认 1 —— 只要点击点那一格 */
|
|
137
|
-
readonly size: number;
|
|
138
|
-
private target: THREE.WebGLRenderTarget | null = null;
|
|
139
|
-
private readonly buf = new Uint8Array(4);
|
|
140
|
-
|
|
141
|
-
constructor(renderer: THREE.WebGLRenderer, options: { size?: number } = {}) {
|
|
142
|
-
this.renderer = renderer;
|
|
143
|
-
this.size = Math.max(1, Math.floor(options.size ?? 1));
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
private _ensureTarget(size: number): THREE.WebGLRenderTarget {
|
|
147
|
-
if (this.target && this.target.width === size && this.target.height === size) return this.target;
|
|
148
|
-
this.target?.dispose();
|
|
149
|
-
this.target = new THREE.WebGLRenderTarget(size, size, {
|
|
150
|
-
minFilter: THREE.NearestFilter,
|
|
151
|
-
magFilter: THREE.NearestFilter,
|
|
152
|
-
format: THREE.RGBAFormat,
|
|
153
|
-
type: THREE.UnsignedByteType,
|
|
154
|
-
depthBuffer: true,
|
|
155
|
-
stencilBuffer: false,
|
|
156
|
-
});
|
|
157
|
-
// 关掉色彩空间转换,否则 ACES / sRGB 会把编码好的 ID 洗掉
|
|
158
|
-
this.target.texture.colorSpace = THREE.LinearSRGBColorSpace;
|
|
159
|
-
return this.target;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* @param clientX 相对 canvas 左上角的 CSS 像素
|
|
164
|
-
* @param clientY 同上
|
|
165
|
-
*/
|
|
166
|
-
pick(scene: THREE.Scene, camera: THREE.Camera, clientX: number, clientY: number): PickHit | null {
|
|
167
|
-
const renderer = this.renderer;
|
|
168
|
-
const canvas = renderer.domElement;
|
|
169
|
-
const w = canvas.width;
|
|
170
|
-
const h = canvas.height;
|
|
171
|
-
if (!w || !h) return null;
|
|
172
|
-
|
|
173
|
-
const px = Math.floor((clientX / canvas.clientWidth) * w);
|
|
174
|
-
const py = Math.floor((clientY / canvas.clientHeight) * h);
|
|
175
|
-
if (px < 0 || py < 0 || px >= w || py >= h) return null;
|
|
176
|
-
|
|
177
|
-
const target = this._ensureTarget(this.size);
|
|
178
|
-
|
|
179
|
-
// 点击点落在整块画布的第几行(WebGL 的 y 轴朝上)
|
|
180
|
-
const ygl = h - 1 - py;
|
|
181
|
-
// 目标里我们读的那一格(size 为偶数时取偏左上那格)
|
|
182
|
-
const cx = Math.floor(this.size / 2);
|
|
183
|
-
const cy = Math.floor(this.size / 2);
|
|
184
|
-
// viewport 尺寸仍是整块画布、原点平移,把 (px, ygl) 挪到目标的 (cx, cy)
|
|
185
|
-
target.viewport.set(cx - px, cy - ygl, w, h);
|
|
186
|
-
|
|
187
|
-
// ---- 保存渲染器状态
|
|
188
|
-
const prevTarget = renderer.getRenderTarget();
|
|
189
|
-
const prevViewport = renderer.getViewport(new THREE.Vector4());
|
|
190
|
-
const prevTone = renderer.toneMapping;
|
|
191
|
-
const prevColorSpace = renderer.outputColorSpace;
|
|
192
|
-
const prevAutoClear = renderer.autoClear;
|
|
193
|
-
const prevClearColor = renderer.getClearColor(new THREE.Color());
|
|
194
|
-
const prevClearAlpha = renderer.getClearAlpha();
|
|
195
|
-
|
|
196
|
-
// ---- 换材质
|
|
197
|
-
const swapped: Array<[THREE.Mesh, THREE.Material | THREE.Material[]]> = [];
|
|
198
|
-
/** 这次拾取里哪些网格是实例化的(它们的 vBatch 编码的是实例序号) */
|
|
199
|
-
const instancedPickIds = new Set<number>();
|
|
200
|
-
scene.traverseVisible((o) => {
|
|
201
|
-
const mesh = o as THREE.Mesh;
|
|
202
|
-
if (!mesh.isMesh) return;
|
|
203
|
-
const ud = mesh.userData as PickUserData;
|
|
204
|
-
if (ud.pickId === undefined) return;
|
|
205
|
-
if (!ud.pickMaterial) ud.pickMaterial = createPickMaterial(ud.pickId);
|
|
206
|
-
if ((mesh as THREE.InstancedMesh).isInstancedMesh) instancedPickIds.add(ud.pickId);
|
|
207
|
-
|
|
208
|
-
// 把当前的「单体状态纹理」抄给拾取材质:抬高量是逐单体写在里面的,
|
|
209
|
-
// 抄的是同一个纹理对象,主材质那边更新后这里自动跟着变。
|
|
210
|
-
const prev = mesh.material;
|
|
211
|
-
const pm = ud.pickMaterial as THREE.ShaderMaterial;
|
|
212
|
-
const src = (Array.isArray(prev) ? prev[0] : prev) as THREE.Material & {
|
|
213
|
-
userData?: Record<string, THREE.IUniform>;
|
|
214
|
-
};
|
|
215
|
-
const batchTex = src?.userData?.uBatchTex;
|
|
216
|
-
if (batchTex) {
|
|
217
|
-
pm.uniforms.uBatchTex.value = batchTex.value;
|
|
218
|
-
pm.uniforms.uBatchTexWidth.value = (src?.userData?.uBatchTexWidth?.value as number) ?? 1;
|
|
219
|
-
pm.uniforms.uBatchTexActive.value = (src?.userData?.uBatchTexActive?.value as number) ?? 0;
|
|
220
|
-
} else {
|
|
221
|
-
pm.uniforms.uBatchTex.value = DUMMY_STATE_TEX;
|
|
222
|
-
pm.uniforms.uBatchTexWidth.value = 1;
|
|
223
|
-
pm.uniforms.uBatchTexActive.value = 0;
|
|
224
|
-
}
|
|
225
|
-
// 选中那栋自身的拔高也要算,否则选中后再点它会点空
|
|
226
|
-
pm.uniforms.uSelBatch.value = (src?.userData?.uSelBatch?.value as number) ?? -1;
|
|
227
|
-
pm.uniforms.uSelWhole.value = (src?.userData?.uSelWhole?.value as number) ?? 0;
|
|
228
|
-
// uSelElevation 是共享 uniform,挂在材质 uniforms 上而不是 userData
|
|
229
|
-
pm.uniforms.uSelElevation.value =
|
|
230
|
-
((src as THREE.ShaderMaterial | undefined)?.uniforms?.uSelElevation?.value as number) ?? 0;
|
|
231
|
-
|
|
232
|
-
swapped.push([mesh, prev]);
|
|
233
|
-
mesh.material = pm;
|
|
234
|
-
});
|
|
235
|
-
// 描边 / 棱线不参与拾取
|
|
236
|
-
const hidden: THREE.Object3D[] = [];
|
|
237
|
-
scene.traverseVisible((o) => {
|
|
238
|
-
const mesh = o as THREE.Mesh;
|
|
239
|
-
if (mesh.isMesh && (mesh.userData as PickUserData).pickId === undefined) {
|
|
240
|
-
hidden.push(mesh);
|
|
241
|
-
mesh.visible = false;
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
let hit: PickHit | null = null;
|
|
246
|
-
try {
|
|
247
|
-
// 关键:拾取是在「正常渲染帧」之外另开的一趟渲染,
|
|
248
|
-
// MapLibre / Mapbox 在上一次 draw 之后可能已经改过 GL 状态(深度测试、scissor 等),
|
|
249
|
-
// 而 three 的状态缓存还停留在自己的上一次 render。
|
|
250
|
-
// 不重置就会出现深度测试失效 → 命中的是「最后画的」而不是「最前面的」。
|
|
251
|
-
renderer.resetState();
|
|
252
|
-
|
|
253
|
-
renderer.setRenderTarget(target);
|
|
254
|
-
renderer.toneMapping = THREE.NoToneMapping;
|
|
255
|
-
renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
|
|
256
|
-
renderer.autoClear = true;
|
|
257
|
-
renderer.setClearColor(0x000000, 0);
|
|
258
|
-
renderer.clear();
|
|
259
|
-
renderer.render(scene, camera);
|
|
260
|
-
|
|
261
|
-
renderer.readRenderTargetPixels(target, cx, cy, 1, 1, this.buf);
|
|
262
|
-
|
|
263
|
-
const r = this.buf[0];
|
|
264
|
-
const g = this.buf[1];
|
|
265
|
-
const b = this.buf[2];
|
|
266
|
-
const a = this.buf[3];
|
|
267
|
-
const objectId = r + g * 256 - 1;
|
|
268
|
-
const rawId = b + a * 256 - 1;
|
|
269
|
-
if (objectId >= 0) {
|
|
270
|
-
hit = instancedPickIds.has(objectId)
|
|
271
|
-
? { objectId, batchId: -1, instanceIndex: Math.max(0, rawId) }
|
|
272
|
-
: { objectId, batchId: rawId };
|
|
273
|
-
}
|
|
274
|
-
} finally {
|
|
275
|
-
for (const [mesh, mat] of swapped) mesh.material = mat;
|
|
276
|
-
for (const o of hidden) o.visible = true;
|
|
277
|
-
renderer.setRenderTarget(prevTarget);
|
|
278
|
-
renderer.setViewport(prevViewport);
|
|
279
|
-
renderer.toneMapping = prevTone;
|
|
280
|
-
renderer.outputColorSpace = prevColorSpace;
|
|
281
|
-
renderer.autoClear = prevAutoClear;
|
|
282
|
-
renderer.setClearColor(prevClearColor, prevClearAlpha);
|
|
283
|
-
renderer.resetState();
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return hit;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
dispose(): void {
|
|
290
|
-
this.target?.dispose();
|
|
291
|
-
this.target = null;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 单体级 GPU picking
|
|
3
|
+
*
|
|
4
|
+
* 思路:把「物体编号 + batchId」编码成 RGBA 渲染到离屏目标,点击时读那一个像素。
|
|
5
|
+
* 相比射线求交,代价与三角面数无关——30 万面和 3 千面一样快,且天然只命中
|
|
6
|
+
* 最前面的可见面(深度测试在 GPU 上完成了)。
|
|
7
|
+
*
|
|
8
|
+
* 编码约定(每个通道一个字节):
|
|
9
|
+
* R,G = 物体编号 + 1
|
|
10
|
+
* B,A = batchId + 1 (+1 是为了让「什么都没点到」全 0)
|
|
11
|
+
*
|
|
12
|
+
* 渲染目标只有 size×size 个像素,靠平移 viewport 把「点击点那一格」挪到目标里:
|
|
13
|
+
* viewport 原点设成 (0 - px, 0 - (h-1-py))、尺寸仍是整块画布,于是光栅化结果和全屏
|
|
14
|
+
* 那一趟逐像素一致,只是只有一个像素落在目标上。原来那句「每次点击全屏渲染一趟」
|
|
15
|
+
* 的代价(移动端尤其明显)就没了。
|
|
16
|
+
*/
|
|
17
|
+
import * as THREE from 'three';
|
|
18
|
+
|
|
19
|
+
const PICK_VERT = /* glsl */ `
|
|
20
|
+
attribute float aBatchId;
|
|
21
|
+
#ifdef USE_INSTANCING
|
|
22
|
+
// 实例化:单体粒度在实例上,顶点属性里没有可用的 batchId
|
|
23
|
+
attribute float aInstanceBatchId;
|
|
24
|
+
attribute float aInstanceIndex;
|
|
25
|
+
#endif
|
|
26
|
+
uniform float uObjectId;
|
|
27
|
+
uniform sampler2D uBatchTex;
|
|
28
|
+
uniform float uBatchTexWidth;
|
|
29
|
+
uniform int uBatchTexActive;
|
|
30
|
+
uniform float uSelBatch;
|
|
31
|
+
uniform float uSelWhole;
|
|
32
|
+
uniform float uSelElevation;
|
|
33
|
+
varying float vBatch;
|
|
34
|
+
|
|
35
|
+
void main() {
|
|
36
|
+
float s3mBatch;
|
|
37
|
+
#ifdef USE_INSTANCING
|
|
38
|
+
s3mBatch = aInstanceBatchId;
|
|
39
|
+
// 编码「实例序号」而不是控制位:CPU 侧要靠它拿到实例矩阵(描边 / 中心点)
|
|
40
|
+
vBatch = aInstanceIndex;
|
|
41
|
+
#else
|
|
42
|
+
s3mBatch = aBatchId;
|
|
43
|
+
vBatch = aBatchId;
|
|
44
|
+
#endif
|
|
45
|
+
|
|
46
|
+
// 单体状态(平移 / 隐藏)必须和主着色器用同一份数据、同一套算法:
|
|
47
|
+
// 平移只发生在顶点着色器里,拾取这一趟如果还按原始坐标画,移位的楼就点不中。
|
|
48
|
+
vec3 p = position;
|
|
49
|
+
if ( uBatchTexActive > 0 && s3mBatch >= 0.0 ) {
|
|
50
|
+
vec4 state = texture2D( uBatchTex, vec2( ( s3mBatch + 0.5 ) / uBatchTexWidth, 0.25 ) );
|
|
51
|
+
if ( state.a < -0.5 ) {
|
|
52
|
+
p = vec3( 0.0 );
|
|
53
|
+
} else {
|
|
54
|
+
vec4 offset = texture2D( uBatchTex, vec2( ( s3mBatch + 0.5 ) / uBatchTexWidth, 0.75 ) );
|
|
55
|
+
p += offset.xyz;
|
|
56
|
+
if ( offset.z == 0.0 && state.a > 0.0 ) {
|
|
57
|
+
p.z += state.a;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 选中单体自身的拔高(selectElevation)
|
|
63
|
+
float sel = max( step( 0.0, uSelBatch ) * step( abs( s3mBatch - uSelBatch ), 0.5 ), uSelWhole );
|
|
64
|
+
p.z += sel * uSelElevation;
|
|
65
|
+
|
|
66
|
+
#ifdef USE_INSTANCING
|
|
67
|
+
gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4( p, 1.0 );
|
|
68
|
+
#else
|
|
69
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4( p, 1.0 );
|
|
70
|
+
#endif
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
const PICK_FRAG = /* glsl */ `
|
|
75
|
+
uniform float uObjectId;
|
|
76
|
+
varying float vBatch;
|
|
77
|
+
void main() {
|
|
78
|
+
float o = uObjectId + 1.0;
|
|
79
|
+
float b = vBatch + 1.0;
|
|
80
|
+
gl_FragColor = vec4(
|
|
81
|
+
mod( o, 256.0 ) / 255.0,
|
|
82
|
+
floor( o / 256.0 ) / 255.0,
|
|
83
|
+
mod( b, 256.0 ) / 255.0,
|
|
84
|
+
floor( b / 256.0 ) / 255.0
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
export interface PickHit {
|
|
90
|
+
/** 由 S3MLayer 分配的网格编号 */
|
|
91
|
+
objectId: number;
|
|
92
|
+
/** 非实例化几何:单体 batchId;实例化几何这里是 -1(改看 instanceIndex) */
|
|
93
|
+
batchId: number;
|
|
94
|
+
/** 实例化几何命中到第几个实例(非实例化为 undefined) */
|
|
95
|
+
instanceIndex?: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** S3MLayer 挂到 mesh.userData 上的字段,拾取时会读它 */
|
|
99
|
+
interface PickUserData {
|
|
100
|
+
pickId?: number;
|
|
101
|
+
pickMaterial?: THREE.Material;
|
|
102
|
+
[key: string]: unknown;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 没有单体状态纹理时用的占位(全 0 = 不隐藏、不平移) */
|
|
106
|
+
const DUMMY_STATE_TEX = (() => {
|
|
107
|
+
const tex = new THREE.DataTexture(new Float32Array([-1, -1, -1, 0, 0, 0, 0, 0]), 1, 2, THREE.RGBAFormat, THREE.FloatType);
|
|
108
|
+
tex.minFilter = THREE.NearestFilter;
|
|
109
|
+
tex.magFilter = THREE.NearestFilter;
|
|
110
|
+
tex.needsUpdate = true;
|
|
111
|
+
return tex;
|
|
112
|
+
})();
|
|
113
|
+
|
|
114
|
+
function createPickMaterial(objectId: number): THREE.ShaderMaterial {
|
|
115
|
+
return new THREE.ShaderMaterial({
|
|
116
|
+
uniforms: {
|
|
117
|
+
uObjectId: { value: objectId },
|
|
118
|
+
uBatchTex: { value: DUMMY_STATE_TEX },
|
|
119
|
+
uBatchTexWidth: { value: 1 },
|
|
120
|
+
uBatchTexActive: { value: 0 },
|
|
121
|
+
uSelBatch: { value: -1 },
|
|
122
|
+
uSelWhole: { value: 0 },
|
|
123
|
+
uSelElevation: { value: 0 },
|
|
124
|
+
},
|
|
125
|
+
vertexShader: PICK_VERT,
|
|
126
|
+
fragmentShader: PICK_FRAG,
|
|
127
|
+
side: THREE.DoubleSide,
|
|
128
|
+
depthTest: true,
|
|
129
|
+
depthWrite: true,
|
|
130
|
+
toneMapped: false,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export class Picker {
|
|
135
|
+
readonly renderer: THREE.WebGLRenderer;
|
|
136
|
+
/** 拾取用的渲染目标边长(像素),默认 1 —— 只要点击点那一格 */
|
|
137
|
+
readonly size: number;
|
|
138
|
+
private target: THREE.WebGLRenderTarget | null = null;
|
|
139
|
+
private readonly buf = new Uint8Array(4);
|
|
140
|
+
|
|
141
|
+
constructor(renderer: THREE.WebGLRenderer, options: { size?: number } = {}) {
|
|
142
|
+
this.renderer = renderer;
|
|
143
|
+
this.size = Math.max(1, Math.floor(options.size ?? 1));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private _ensureTarget(size: number): THREE.WebGLRenderTarget {
|
|
147
|
+
if (this.target && this.target.width === size && this.target.height === size) return this.target;
|
|
148
|
+
this.target?.dispose();
|
|
149
|
+
this.target = new THREE.WebGLRenderTarget(size, size, {
|
|
150
|
+
minFilter: THREE.NearestFilter,
|
|
151
|
+
magFilter: THREE.NearestFilter,
|
|
152
|
+
format: THREE.RGBAFormat,
|
|
153
|
+
type: THREE.UnsignedByteType,
|
|
154
|
+
depthBuffer: true,
|
|
155
|
+
stencilBuffer: false,
|
|
156
|
+
});
|
|
157
|
+
// 关掉色彩空间转换,否则 ACES / sRGB 会把编码好的 ID 洗掉
|
|
158
|
+
this.target.texture.colorSpace = THREE.LinearSRGBColorSpace;
|
|
159
|
+
return this.target;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param clientX 相对 canvas 左上角的 CSS 像素
|
|
164
|
+
* @param clientY 同上
|
|
165
|
+
*/
|
|
166
|
+
pick(scene: THREE.Scene, camera: THREE.Camera, clientX: number, clientY: number): PickHit | null {
|
|
167
|
+
const renderer = this.renderer;
|
|
168
|
+
const canvas = renderer.domElement;
|
|
169
|
+
const w = canvas.width;
|
|
170
|
+
const h = canvas.height;
|
|
171
|
+
if (!w || !h) return null;
|
|
172
|
+
|
|
173
|
+
const px = Math.floor((clientX / canvas.clientWidth) * w);
|
|
174
|
+
const py = Math.floor((clientY / canvas.clientHeight) * h);
|
|
175
|
+
if (px < 0 || py < 0 || px >= w || py >= h) return null;
|
|
176
|
+
|
|
177
|
+
const target = this._ensureTarget(this.size);
|
|
178
|
+
|
|
179
|
+
// 点击点落在整块画布的第几行(WebGL 的 y 轴朝上)
|
|
180
|
+
const ygl = h - 1 - py;
|
|
181
|
+
// 目标里我们读的那一格(size 为偶数时取偏左上那格)
|
|
182
|
+
const cx = Math.floor(this.size / 2);
|
|
183
|
+
const cy = Math.floor(this.size / 2);
|
|
184
|
+
// viewport 尺寸仍是整块画布、原点平移,把 (px, ygl) 挪到目标的 (cx, cy)
|
|
185
|
+
target.viewport.set(cx - px, cy - ygl, w, h);
|
|
186
|
+
|
|
187
|
+
// ---- 保存渲染器状态
|
|
188
|
+
const prevTarget = renderer.getRenderTarget();
|
|
189
|
+
const prevViewport = renderer.getViewport(new THREE.Vector4());
|
|
190
|
+
const prevTone = renderer.toneMapping;
|
|
191
|
+
const prevColorSpace = renderer.outputColorSpace;
|
|
192
|
+
const prevAutoClear = renderer.autoClear;
|
|
193
|
+
const prevClearColor = renderer.getClearColor(new THREE.Color());
|
|
194
|
+
const prevClearAlpha = renderer.getClearAlpha();
|
|
195
|
+
|
|
196
|
+
// ---- 换材质
|
|
197
|
+
const swapped: Array<[THREE.Mesh, THREE.Material | THREE.Material[]]> = [];
|
|
198
|
+
/** 这次拾取里哪些网格是实例化的(它们的 vBatch 编码的是实例序号) */
|
|
199
|
+
const instancedPickIds = new Set<number>();
|
|
200
|
+
scene.traverseVisible((o) => {
|
|
201
|
+
const mesh = o as THREE.Mesh;
|
|
202
|
+
if (!mesh.isMesh) return;
|
|
203
|
+
const ud = mesh.userData as PickUserData;
|
|
204
|
+
if (ud.pickId === undefined) return;
|
|
205
|
+
if (!ud.pickMaterial) ud.pickMaterial = createPickMaterial(ud.pickId);
|
|
206
|
+
if ((mesh as THREE.InstancedMesh).isInstancedMesh) instancedPickIds.add(ud.pickId);
|
|
207
|
+
|
|
208
|
+
// 把当前的「单体状态纹理」抄给拾取材质:抬高量是逐单体写在里面的,
|
|
209
|
+
// 抄的是同一个纹理对象,主材质那边更新后这里自动跟着变。
|
|
210
|
+
const prev = mesh.material;
|
|
211
|
+
const pm = ud.pickMaterial as THREE.ShaderMaterial;
|
|
212
|
+
const src = (Array.isArray(prev) ? prev[0] : prev) as THREE.Material & {
|
|
213
|
+
userData?: Record<string, THREE.IUniform>;
|
|
214
|
+
};
|
|
215
|
+
const batchTex = src?.userData?.uBatchTex;
|
|
216
|
+
if (batchTex) {
|
|
217
|
+
pm.uniforms.uBatchTex.value = batchTex.value;
|
|
218
|
+
pm.uniforms.uBatchTexWidth.value = (src?.userData?.uBatchTexWidth?.value as number) ?? 1;
|
|
219
|
+
pm.uniforms.uBatchTexActive.value = (src?.userData?.uBatchTexActive?.value as number) ?? 0;
|
|
220
|
+
} else {
|
|
221
|
+
pm.uniforms.uBatchTex.value = DUMMY_STATE_TEX;
|
|
222
|
+
pm.uniforms.uBatchTexWidth.value = 1;
|
|
223
|
+
pm.uniforms.uBatchTexActive.value = 0;
|
|
224
|
+
}
|
|
225
|
+
// 选中那栋自身的拔高也要算,否则选中后再点它会点空
|
|
226
|
+
pm.uniforms.uSelBatch.value = (src?.userData?.uSelBatch?.value as number) ?? -1;
|
|
227
|
+
pm.uniforms.uSelWhole.value = (src?.userData?.uSelWhole?.value as number) ?? 0;
|
|
228
|
+
// uSelElevation 是共享 uniform,挂在材质 uniforms 上而不是 userData
|
|
229
|
+
pm.uniforms.uSelElevation.value =
|
|
230
|
+
((src as THREE.ShaderMaterial | undefined)?.uniforms?.uSelElevation?.value as number) ?? 0;
|
|
231
|
+
|
|
232
|
+
swapped.push([mesh, prev]);
|
|
233
|
+
mesh.material = pm;
|
|
234
|
+
});
|
|
235
|
+
// 描边 / 棱线不参与拾取
|
|
236
|
+
const hidden: THREE.Object3D[] = [];
|
|
237
|
+
scene.traverseVisible((o) => {
|
|
238
|
+
const mesh = o as THREE.Mesh;
|
|
239
|
+
if (mesh.isMesh && (mesh.userData as PickUserData).pickId === undefined) {
|
|
240
|
+
hidden.push(mesh);
|
|
241
|
+
mesh.visible = false;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
let hit: PickHit | null = null;
|
|
246
|
+
try {
|
|
247
|
+
// 关键:拾取是在「正常渲染帧」之外另开的一趟渲染,
|
|
248
|
+
// MapLibre / Mapbox 在上一次 draw 之后可能已经改过 GL 状态(深度测试、scissor 等),
|
|
249
|
+
// 而 three 的状态缓存还停留在自己的上一次 render。
|
|
250
|
+
// 不重置就会出现深度测试失效 → 命中的是「最后画的」而不是「最前面的」。
|
|
251
|
+
renderer.resetState();
|
|
252
|
+
|
|
253
|
+
renderer.setRenderTarget(target);
|
|
254
|
+
renderer.toneMapping = THREE.NoToneMapping;
|
|
255
|
+
renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
|
|
256
|
+
renderer.autoClear = true;
|
|
257
|
+
renderer.setClearColor(0x000000, 0);
|
|
258
|
+
renderer.clear();
|
|
259
|
+
renderer.render(scene, camera);
|
|
260
|
+
|
|
261
|
+
renderer.readRenderTargetPixels(target, cx, cy, 1, 1, this.buf);
|
|
262
|
+
|
|
263
|
+
const r = this.buf[0];
|
|
264
|
+
const g = this.buf[1];
|
|
265
|
+
const b = this.buf[2];
|
|
266
|
+
const a = this.buf[3];
|
|
267
|
+
const objectId = r + g * 256 - 1;
|
|
268
|
+
const rawId = b + a * 256 - 1;
|
|
269
|
+
if (objectId >= 0) {
|
|
270
|
+
hit = instancedPickIds.has(objectId)
|
|
271
|
+
? { objectId, batchId: -1, instanceIndex: Math.max(0, rawId) }
|
|
272
|
+
: { objectId, batchId: rawId };
|
|
273
|
+
}
|
|
274
|
+
} finally {
|
|
275
|
+
for (const [mesh, mat] of swapped) mesh.material = mat;
|
|
276
|
+
for (const o of hidden) o.visible = true;
|
|
277
|
+
renderer.setRenderTarget(prevTarget);
|
|
278
|
+
renderer.setViewport(prevViewport);
|
|
279
|
+
renderer.toneMapping = prevTone;
|
|
280
|
+
renderer.outputColorSpace = prevColorSpace;
|
|
281
|
+
renderer.autoClear = prevAutoClear;
|
|
282
|
+
renderer.setClearColor(prevClearColor, prevClearAlpha);
|
|
283
|
+
renderer.resetState();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return hit;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
dispose(): void {
|
|
290
|
+
this.target?.dispose();
|
|
291
|
+
this.target = null;
|
|
292
|
+
}
|
|
293
|
+
}
|
package/src/render/S3MViewer.ts
CHANGED
|
@@ -508,15 +508,18 @@ export class S3MViewer {
|
|
|
508
508
|
}
|
|
509
509
|
|
|
510
510
|
/**
|
|
511
|
-
*
|
|
512
|
-
* 若不传 id
|
|
511
|
+
* 获取指定数据集的应用层附加垂直偏移(米,不含 align 基准)。
|
|
512
|
+
* 若不传 id,默认获取第一个(或默认)数据集的偏移。
|
|
513
513
|
*/
|
|
514
514
|
getDatasetHeight(id?: string): number {
|
|
515
515
|
return this._datasetManager.getDatasetHeight(id);
|
|
516
516
|
}
|
|
517
517
|
|
|
518
518
|
/**
|
|
519
|
-
*
|
|
519
|
+
* 设置指定数据集的应用层附加垂直偏移(米),正数抬升,负数下降。
|
|
520
|
+
*
|
|
521
|
+
* 语义为「在当前 align 基准之上再抬升多少米」:贴地(把模型抬到真实地面高程)
|
|
522
|
+
* 不会破坏 align='top' 等对齐语义。
|
|
520
523
|
*/
|
|
521
524
|
setDatasetHeight(idOrHeight: string | number, heightOffset?: number): void {
|
|
522
525
|
let id: string;
|
|
@@ -529,10 +532,22 @@ export class S3MViewer {
|
|
|
529
532
|
h = heightOffset ?? 0;
|
|
530
533
|
}
|
|
531
534
|
this._datasetManager.setDatasetHeight(id, h, this.geo);
|
|
535
|
+
this._syncGroundHeight();
|
|
532
536
|
this._refreshAllHeights();
|
|
533
537
|
this._requestRepaint();
|
|
534
538
|
}
|
|
535
539
|
|
|
540
|
+
/**
|
|
541
|
+
* 物理地表网格的高度跟随「主数据集的应用层抬升量」,
|
|
542
|
+
* 保证贴地后地表覆盖 / 地下透视的物理关系与被抬升的模型一致。
|
|
543
|
+
*/
|
|
544
|
+
private _syncGroundHeight(): void {
|
|
545
|
+
const primaryId = this.datasets.keys().next().value;
|
|
546
|
+
if (!primaryId) return;
|
|
547
|
+
const userOffset = this._datasetManager.getDatasetHeight(primaryId);
|
|
548
|
+
this.groundManager.setGroundHeight(userOffset);
|
|
549
|
+
}
|
|
550
|
+
|
|
536
551
|
/**
|
|
537
552
|
* 动态设置指定数据集的垂直对齐基准('origin' | 'bottom' | 'top' | 'center')。
|
|
538
553
|
*/
|